Advertisement
Guest User

osc.lua

a guest
Mar 8th, 2023
59
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 89.21 KB | None | 0 0
  1. local assdraw = require 'mp.assdraw'
  2. local msg = require 'mp.msg'
  3. local opt = require 'mp.options'
  4. local utils = require 'mp.utils'
  5.  
  6. --
  7. -- Parameters
  8. --
  9. -- default user option values
  10. -- do not touch, change them in osc.conf
  11. local user_opts = {
  12.     showwindowed = true, -- show OSC when windowed?
  13.     showfullscreen = true, -- show OSC when fullscreen?
  14.     scalewindowed = 1, -- scaling of the controller when windowed
  15.     scalefullscreen = 1, -- scaling of the controller when fullscreen
  16.     scaleforcedwindow = 2, -- scaling when rendered on a forced window
  17.     vidscale = true, -- scale the controller with the video?
  18.     valign = 0.8, -- vertical alignment, -1 (top) to 1 (bottom)
  19.     halign = 0, -- horizontal alignment, -1 (left) to 1 (right)
  20.     barmargin = 0, -- vertical margin of top/bottombar
  21.     boxalpha = 150, -- alpha of the background box,
  22.     -- 0 (opaque) to 255 (fully transparent)
  23.     hidetimeout = 30000, -- duration in ms until the OSC hides if no
  24.     -- mouse movement. enforced non-negative for the
  25.     -- user, but internally negative is "always-on".
  26.     fadeduration = 200, -- duration of fade out in ms, 0 = no fade
  27.     deadzonesize = 0.9, -- size of deadzone
  28.     minmousemove = 0, -- minimum amount of pixels the mouse has to
  29.     -- move between ticks to make the OSC show up
  30.     iamaprogrammer = false, -- use native mpv values and disable OSC
  31.     -- internal track list management (and some
  32.     -- functions that depend on it)
  33.     layout = "bottombar",
  34.     seekbarstyle = "bar", -- bar, diamond or knob
  35.     seekbarhandlesize = 0.6, -- size ratio of the diamond and knob handle
  36.     seekrangestyle = "inverted", -- bar, line, slider, inverted or none
  37.     seekrangeseparate = true, -- wether the seekranges overlay on the bar-style seekbar
  38.     seekrangealpha = 200, -- transparency of seekranges
  39.     seekbarkeyframes = true, -- use keyframes when dragging the seekbar
  40.     title = "${media-title}", -- string compatible with property-expansion
  41.     -- to be shown as OSC title
  42.     tooltipborder = 1, -- border of tooltip in bottom/topbar
  43.     timetotal = true, -- display total time instead of remaining time?
  44.     timems = false, -- display timecodes with milliseconds?
  45.     visibility = "auto", -- only used at init to set visibility_mode(...)
  46.     boxmaxchars = 80 -- title crop threshold for box layout
  47. }
  48.  
  49. -- read_options may modify hidetimeout, so save the original default value in
  50. -- case the user set hidetimeout < 0 and we need the default instead.
  51. local hidetimeout_def = user_opts.hidetimeout
  52. -- read options from config and command-line
  53. opt.read_options(user_opts, "osc")
  54. if user_opts.hidetimeout < 0 then
  55.     user_opts.hidetimeout = hidetimeout_def
  56.     msg.warn("hidetimeout cannot be negative. Using " .. user_opts.hidetimeout)
  57. end
  58.  
  59. local osc_param = { -- calculated by osc_init()
  60.     playresy = 0, -- canvas size Y
  61.     playresx = 0, -- canvas size X
  62.     display_aspect = 1,
  63.     unscaled_y = 0,
  64.     areas = {}
  65. }
  66.  
  67. local osc_styles = {
  68.     bigButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs50\\fnmpv-osd-symbols}",
  69.     smallButtonsL = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs19\\fnmpv-osd-symbols}",
  70.     smallButtonsLlabel = "{\\fscx105\\fscy105\\fn" ..
  71.         mp.get_property("options/osd-font") .. "}",
  72.     smallButtonsR = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs30\\fnmpv-osd-symbols}",
  73.     topButtons = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\fnmpv-osd-symbols}",
  74.  
  75.     elementDown = "{\\1c&H999999}",
  76.     timecodes = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs20}",
  77.     vidtitle = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs12\\q2}",
  78.     box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  79.  
  80.     topButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
  81.     smallButtonsBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs28\\fnmpv-osd-symbols}",
  82.     smallButtonsBarS = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\fnmpv-osd-symbols}",
  83.     smallButtonsBarF = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs26\\fnmpv-osd-symbols}",
  84.     timecodesBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs22}",
  85.     timecodesBack = "{\\blur0\\bord0\\1c&H979797\\3c&HFFFFFF\\fs18\\q2}",
  86.     timePosBar = "{\\blur0\\bord" .. user_opts.tooltipborder ..
  87.         "\\1c&HFFFFFF\\3c&H000000\\fs22}",
  88.     vidtitleBar = "{\\blur0\\bord0\\1c&HFFFFFF\\3c&HFFFFFF\\fs18\\q2}"
  89. }
  90.  
  91. -- internal states, do not touch
  92. local state = {
  93.     showtime, -- time of last invocation (last mouse move)
  94.     osc_visible = false,
  95.     anistart, -- time when the animation started
  96.     anitype, -- current type of animation
  97.     animation, -- current animation alpha
  98.     mouse_down_counter = 0, -- used for softrepeat
  99.     active_element = nil, -- nil = none, 0 = background, 1+ = see elements[]
  100.     active_event_source = nil, -- the "button" that issued the current event
  101.     rightTC_trem = not user_opts.timetotal, -- if the right timecode should display total or remaining time
  102.     tc_ms = user_opts.timems, -- Should the timecodes display their time with milliseconds
  103.     mp_screen_sizeX,
  104.     mp_screen_sizeY, -- last screen-resolution, to detect resolution changes to issue reINITs
  105.     initREQ = false, -- is a re-init request pending?
  106.     last_mouseX,
  107.     last_mouseY, -- last mouse position, to detect significant mouse movement
  108.     message_text,
  109.     message_timeout,
  110.     fullscreen = false,
  111.     timer = nil,
  112.     cache_idle = false,
  113.     idle = false,
  114.     enabled = true,
  115.     input_enabled = true,
  116.     showhide_enabled = false,
  117.     dmx_cache = 0
  118. }
  119.  
  120. --
  121. -- Helperfunctions
  122. --
  123.  
  124. -- scale factor for translating between real and virtual ASS coordinates
  125. function get_virt_scale_factor()
  126.     local w, h = mp.get_osd_size()
  127.     if w <= 0 or h <= 0 then return 0, 0 end
  128.     return osc_param.playresx / w, osc_param.playresy / h
  129. end
  130. local thumbfast = {
  131.     width = 0,
  132.     height = 0,
  133.     disabled = false
  134. }
  135. -- return mouse position in virtual ASS coordinates (playresx/y)
  136. function get_virt_mouse_pos()
  137.     local sx, sy = get_virt_scale_factor()
  138.     local x, y = mp.get_mouse_pos()
  139.     return x * sx, y * sy
  140. end
  141.  
  142. function set_virt_mouse_area(x0, y0, x1, y1, name)
  143.     local sx, sy = get_virt_scale_factor()
  144.     mp.set_mouse_area(x0 / sx, y0 / sy, x1 / sx, y1 / sy, name)
  145. end
  146.  
  147. function scale_value(x0, x1, y0, y1, val)
  148.     local m = (y1 - y0) / (x1 - x0)
  149.     local b = y0 - (m * x0)
  150.     return (m * val) + b
  151. end
  152.  
  153. -- returns hitbox spanning coordinates (top left, bottom right corner)
  154. -- according to alignment
  155. function get_hitbox_coords(x, y, an, w, h)
  156.  
  157.     local alignments = {
  158.         [1] = function() return x, y - h, x + w, y end,
  159.         [2] = function() return x - (w / 2), y - h, x + (w / 2), y end,
  160.         [3] = function() return x - w, y - h, x, y end,
  161.  
  162.         [4] = function() return x, y - (h / 2), x + w, y + (h / 2) end,
  163.         [5] = function()
  164.             return x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)
  165.         end,
  166.         [6] = function() return x - w, y - (h / 2), x, y + (h / 2) end,
  167.  
  168.         [7] = function() return x, y, x + w, y + h end,
  169.         [8] = function() return x - (w / 2), y, x + (w / 2), y + h end,
  170.         [9] = function() return x - w, y, x, y + h end
  171.     }
  172.  
  173.     return alignments[an]()
  174. end
  175.  
  176. function get_hitbox_coords_geo(geometry)
  177.     return get_hitbox_coords(geometry.x, geometry.y, geometry.an, geometry.w,
  178.                              geometry.h)
  179. end
  180.  
  181. function get_element_hitbox(element)
  182.     return element.hitbox.x1, element.hitbox.y1, element.hitbox.x2,
  183.            element.hitbox.y2
  184. end
  185.  
  186. function mouse_hit(element) return mouse_hit_coords(get_element_hitbox(element)) end
  187.  
  188. function mouse_hit_coords(bX1, bY1, bX2, bY2)
  189.     local mX, mY = get_virt_mouse_pos()
  190.     return (mX >= bX1 and mX <= bX2 and mY >= bY1 and mY <= bY2)
  191. end
  192.  
  193. function limit_range(min, max, val)
  194.     if val > max then
  195.         val = max
  196.     elseif val < min then
  197.         val = min
  198.     end
  199.     return val
  200. end
  201.  
  202. -- translate value into element coordinates
  203. function get_slider_ele_pos_for(element, val)
  204.  
  205.     local ele_pos = scale_value(element.slider.min.value,
  206.                                 element.slider.max.value,
  207.                                 element.slider.min.ele_pos,
  208.                                 element.slider.max.ele_pos, val)
  209.  
  210.     return limit_range(element.slider.min.ele_pos, element.slider.max.ele_pos,
  211.                        ele_pos)
  212. end
  213.  
  214. -- translates global (mouse) coordinates to value
  215. function get_slider_value_at(element, glob_pos)
  216.  
  217.     local val = scale_value(element.slider.min.glob_pos,
  218.                             element.slider.max.glob_pos,
  219.                             element.slider.min.value, element.slider.max.value,
  220.                             glob_pos)
  221.  
  222.     return limit_range(element.slider.min.value, element.slider.max.value, val)
  223. end
  224.  
  225. -- get value at current mouse position
  226. function get_slider_value(element)
  227.     return get_slider_value_at(element, get_virt_mouse_pos())
  228. end
  229.  
  230. function countone(val)
  231.     if not (user_opts.iamaprogrammer) then val = val + 1 end
  232.     return val
  233. end
  234.  
  235. -- align:  -1 .. +1
  236. -- frame:  size of the containing area
  237. -- obj:    size of the object that should be positioned inside the area
  238. -- margin: min. distance from object to frame (as long as -1 <= align <= +1)
  239. function get_align(align, frame, obj, margin)
  240.     return (frame / 2) + (((frame / 2) - margin - (obj / 2)) * align)
  241. end
  242.  
  243. -- multiplies two alpha values, formular can probably be improved
  244. function mult_alpha(alphaA, alphaB)
  245.     return 255 - (((1 - (alphaA / 255)) * (1 - (alphaB / 255))) * 255)
  246. end
  247.  
  248. function add_area(name, x1, y1, x2, y2)
  249.     -- create area if needed
  250.     if (osc_param.areas[name] == nil) then osc_param.areas[name] = {} end
  251.     table.insert(osc_param.areas[name], {x1 = x1, y1 = y1, x2 = x2, y2 = y2})
  252. end
  253.  
  254. function ass_append_alpha(ass, alpha, modifier)
  255.     local ar = {}
  256.  
  257.     for ai, av in pairs(alpha) do
  258.         av = mult_alpha(av, modifier)
  259.         if state.animation then av = mult_alpha(av, state.animation) end
  260.         ar[ai] = av
  261.     end
  262.  
  263.     ass:append(string.format("{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}", ar[1],
  264.                              ar[2], ar[3], ar[4]))
  265. end
  266.  
  267. function ass_draw_rr_h_cw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  268.     if hexagon then
  269.         ass:hexagon_cw(x0, y0, x1, y1, r1, r2)
  270.     else
  271.         ass:round_rect_cw(x0, y0, x1, y1, r1, r2)
  272.     end
  273. end
  274.  
  275. function ass_draw_rr_h_ccw(ass, x0, y0, x1, y1, r1, hexagon, r2)
  276.     if hexagon then
  277.         ass:hexagon_ccw(x0, y0, x1, y1, r1, r2)
  278.     else
  279.         ass:round_rect_ccw(x0, y0, x1, y1, r1, r2)
  280.     end
  281. end
  282.  
  283. --
  284. -- Tracklist Management
  285. --
  286.  
  287. local nicetypes = {video = "Video", audio = "Audio", sub = "Subtitle"}
  288.  
  289. -- updates the OSC internal playlists, should be run each time the track-layout changes
  290. function update_tracklist()
  291.     local tracktable = mp.get_property_native("track-list", {})
  292.  
  293.     -- by osc_id
  294.     tracks_osc = {}
  295.     tracks_osc.video, tracks_osc.audio, tracks_osc.sub = {}, {}, {}
  296.     -- by mpv_id
  297.     tracks_mpv = {}
  298.     tracks_mpv.video, tracks_mpv.audio, tracks_mpv.sub = {}, {}, {}
  299.     for n = 1, #tracktable do
  300.         if not (tracktable[n].type == "unknown") then
  301.             local type = tracktable[n].type
  302.             local mpv_id = tonumber(tracktable[n].id)
  303.  
  304.             -- by osc_id
  305.             table.insert(tracks_osc[type], tracktable[n])
  306.  
  307.             -- by mpv_id
  308.             tracks_mpv[type][mpv_id] = tracktable[n]
  309.             tracks_mpv[type][mpv_id].osc_id = #tracks_osc[type]
  310.         end
  311.     end
  312. end
  313.  
  314. -- return a nice list of tracks of the given type (video, audio, sub)
  315. function get_tracklist(type)
  316.     local msg = "Available " .. nicetypes[type] .. " Tracks: "
  317.     if #tracks_osc[type] == 0 then
  318.         msg = msg .. "none"
  319.     else
  320.         for n = 1, #tracks_osc[type] do
  321.             local track = tracks_osc[type][n]
  322.             local lang, title, selected = "unknown", "", "○"
  323.             if not (track.lang == nil) then lang = track.lang end
  324.             if not (track.title == nil) then title = track.title end
  325.             if (track.id == tonumber(mp.get_property(type))) then
  326.                 selected = "●"
  327.             end
  328.             msg =
  329.                 msg .. "\n" .. selected .. " " .. n .. ": [" .. lang .. "] " ..
  330.                     title
  331.         end
  332.     end
  333.     return msg
  334. end
  335.  
  336. -- relatively change the track of given <type> by <next> tracks
  337. -- (+1 -> next, -1 -> previous)
  338. function set_track(type, next)
  339.     local current_track_mpv, current_track_osc
  340.     if (mp.get_property(type) == "no") then
  341.         current_track_osc = 0
  342.     else
  343.         current_track_mpv = tonumber(mp.get_property(type))
  344.         current_track_osc = tracks_mpv[type][current_track_mpv].osc_id
  345.     end
  346.     local new_track_osc = (current_track_osc + next) % (#tracks_osc[type] + 1)
  347.     local new_track_mpv
  348.     if new_track_osc == 0 then
  349.         new_track_mpv = "no"
  350.     else
  351.         new_track_mpv = tracks_osc[type][new_track_osc].id
  352.     end
  353.  
  354.     mp.commandv("set", type, new_track_mpv)
  355.  
  356.     if (new_track_osc == 0) then
  357.         show_message(nicetypes[type] .. " Track: none")
  358.     else
  359.         show_message(nicetypes[type] .. " Track: " .. new_track_osc .. "/" ..
  360.                          #tracks_osc[type] .. " [" ..
  361.                          (tracks_osc[type][new_track_osc].lang or "unknown") ..
  362.                          "] " .. (tracks_osc[type][new_track_osc].title or ""))
  363.     end
  364. end
  365.  
  366. -- get the currently selected track of <type>, OSC-style counted
  367. function get_track(type)
  368.     local track = mp.get_property(type)
  369.     if track ~= "no" and track ~= nil then
  370.         local tr = tracks_mpv[type][tonumber(track)]
  371.         if tr then return tr.osc_id end
  372.     end
  373.     return 0
  374. end
  375.  
  376. --
  377. -- Element Management
  378. --
  379.  
  380. local elements = {}
  381.  
  382. function prepare_elements()
  383.  
  384.     -- remove elements without layout or invisble
  385.     local elements2 = {}
  386.     for n, element in pairs(elements) do
  387.         if not (element.layout == nil) and (element.visible) then
  388.             table.insert(elements2, element)
  389.         end
  390.     end
  391.     elements = elements2
  392.  
  393.     function elem_compare(a, b) return a.layout.layer < b.layout.layer end
  394.  
  395.     table.sort(elements, elem_compare)
  396.  
  397.     for _, element in pairs(elements) do
  398.  
  399.         local elem_geo = element.layout.geometry
  400.  
  401.         -- Calculate the hitbox
  402.         local bX1, bY1, bX2, bY2 = get_hitbox_coords_geo(elem_geo)
  403.         element.hitbox = {x1 = bX1, y1 = bY1, x2 = bX2, y2 = bY2}
  404.  
  405.         local style_ass = assdraw.ass_new()
  406.  
  407.         -- prepare static elements
  408.         style_ass:append("{}") -- hack to troll new_event into inserting a \n
  409.         style_ass:new_event()
  410.         style_ass:pos(elem_geo.x, elem_geo.y)
  411.         style_ass:an(elem_geo.an)
  412.         style_ass:append(element.layout.style)
  413.  
  414.         element.style_ass = style_ass
  415.  
  416.         local static_ass = assdraw.ass_new()
  417.  
  418.         if (element.type == "box") then
  419.             -- draw box
  420.             static_ass:draw_start()
  421.             ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h,
  422.                              element.layout.box.radius,
  423.                              element.layout.box.hexagon)
  424.             static_ass:draw_stop()
  425.  
  426.         elseif (element.type == "slider") then
  427.             -- draw static slider parts
  428.  
  429.             local r1 = 0
  430.             local r2 = 0
  431.             local slider_lo = element.layout.slider
  432.             -- offset between element outline and drag-area
  433.             local foV = slider_lo.border + slider_lo.gap
  434.  
  435.             -- calculate positions of min and max points
  436.             if (slider_lo.stype ~= "bar") then
  437.                 r1 = elem_geo.h / 2
  438.                 element.slider.min.ele_pos = elem_geo.h / 2
  439.                 element.slider.max.ele_pos = elem_geo.w - (elem_geo.h / 2)
  440.                 if (slider_lo.stype == "diamond") then
  441.                     r2 = (elem_geo.h - 2 * slider_lo.border) / 2
  442.                 elseif (slider_lo.stype == "knob") then
  443.                     r2 = r1
  444.                 end
  445.             else
  446.                 element.slider.min.ele_pos = slider_lo.border + slider_lo.gap
  447.                 element.slider.max.ele_pos =
  448.                     elem_geo.w - (slider_lo.border + slider_lo.gap)
  449.             end
  450.  
  451.             element.slider.min.glob_pos =
  452.                 element.hitbox.x1 + element.slider.min.ele_pos
  453.             element.slider.max.glob_pos =
  454.                 element.hitbox.x1 + element.slider.max.ele_pos
  455.  
  456.             -- -- --
  457.  
  458.             static_ass:draw_start()
  459.  
  460.             -- the box
  461.             ass_draw_rr_h_cw(static_ass, 0, 0, elem_geo.w, elem_geo.h, r1,
  462.                              slider_lo.stype == "diamond")
  463.  
  464.             -- the "hole"
  465.             ass_draw_rr_h_ccw(static_ass, slider_lo.border, slider_lo.border,
  466.                               elem_geo.w - slider_lo.border,
  467.                               elem_geo.h - slider_lo.border, r2,
  468.                               slider_lo.stype == "diamond")
  469.  
  470.             -- marker nibbles
  471.             if not (element.slider.markerF == nil) and (slider_lo.gap > 0) then
  472.                 local markers = element.slider.markerF()
  473.                 for _, marker in pairs(markers) do
  474.                     if (marker > element.slider.min.value) and
  475.                         (marker < element.slider.max.value) then
  476.  
  477.                         local s = get_slider_ele_pos_for(element, marker)
  478.  
  479.                         if (slider_lo.gap > 1) then -- draw triangles
  480.  
  481.                             local a = slider_lo.gap / 0.5 -- 0.866
  482.  
  483.                             -- top
  484.                             if (slider_lo.nibbles_top) then
  485.                                 static_ass:move_to(s - (a / 2), slider_lo.border)
  486.                                 static_ass:line_to(s + (a / 2), slider_lo.border)
  487.                                 static_ass:line_to(s, foV)
  488.                             end
  489.  
  490.                             -- bottom
  491.                             if (slider_lo.nibbles_bottom) then
  492.                                 static_ass:move_to(s - (a / 2), elem_geo.h -
  493.                                                        slider_lo.border)
  494.                                 static_ass:line_to(s, elem_geo.h - foV)
  495.                                 static_ass:line_to(s + (a / 2), elem_geo.h -
  496.                                                        slider_lo.border)
  497.                             end
  498.  
  499.                         else -- draw 2x1px nibbles
  500.  
  501.                             -- top
  502.                             if (slider_lo.nibbles_top) then
  503.                                 static_ass:rect_cw(s - 1, slider_lo.border,
  504.                                                    s + 1, slider_lo.border +
  505.                                                        slider_lo.gap)
  506.                             end
  507.  
  508.                             -- bottom
  509.                             if (slider_lo.nibbles_bottom) then
  510.                                 static_ass:rect_cw(s - 1, elem_geo.h -
  511.                                                        slider_lo.border -
  512.                                                        slider_lo.gap, s + 1,
  513.                                                    elem_geo.h - slider_lo.border)
  514.                             end
  515.                         end
  516.                     end
  517.                 end
  518.             end
  519.         end
  520.  
  521.         element.static_ass = static_ass
  522.  
  523.         -- if the element is supposed to be disabled,
  524.         -- style it accordingly and kill the eventresponders
  525.         if not (element.enabled) then
  526.             element.layout.alpha[1] = 136
  527.             element.eventresponder = nil
  528.         end
  529.     end
  530. end
  531.  
  532. --
  533. -- Element Rendering
  534. --
  535.  
  536. function render_elements(master_ass)
  537.  
  538.     for n = 1, #elements do
  539.         local element = elements[n]
  540.  
  541.         local style_ass = assdraw.ass_new()
  542.         style_ass:merge(element.style_ass)
  543.         ass_append_alpha(style_ass, element.layout.alpha, 0)
  544.  
  545.         if element.eventresponder and (state.active_element == n) then
  546.  
  547.             -- run render event functions
  548.             if not (element.eventresponder.render == nil) then
  549.                 element.eventresponder.render(element)
  550.             end
  551.  
  552.             if mouse_hit(element) then
  553.                 -- mouse down styling
  554.                 if (element.styledown) then
  555.                     style_ass:append(osc_styles.elementDown)
  556.                 end
  557.  
  558.                 if (element.softrepeat) and
  559.                     (state.mouse_down_counter >= 15 and state.mouse_down_counter %
  560.                         5 == 0) then
  561.  
  562.                     element.eventresponder[state.active_event_source .. "_down"](
  563.                         element)
  564.                 end
  565.                 state.mouse_down_counter = state.mouse_down_counter + 1
  566.             end
  567.  
  568.         end
  569.  
  570.         local elem_ass = assdraw.ass_new()
  571.  
  572.         elem_ass:merge(style_ass)
  573.  
  574.         if not (element.type == "button") then
  575.             elem_ass:merge(element.static_ass)
  576.         end
  577.  
  578.         if (element.type == "slider") then
  579.  
  580.             local slider_lo = element.layout.slider
  581.             local elem_geo = element.layout.geometry
  582.             local s_min = element.slider.min.value
  583.             local s_max = element.slider.max.value
  584.  
  585.             -- draw pos marker
  586.             local foH, xp
  587.             local pos = element.slider.posF()
  588.             local foV = slider_lo.border + slider_lo.gap
  589.             local innerH = elem_geo.h - (2 * foV)
  590.             local seekRanges = element.slider.seekRangesF()
  591.             local seekRangeLineHeight = innerH / 5
  592.  
  593.             if slider_lo.stype ~= "bar" then
  594.                 foH = elem_geo.h / 2
  595.             else
  596.                 foH = slider_lo.border + slider_lo.gap
  597.             end
  598.  
  599.             if pos then
  600.                 xp = get_slider_ele_pos_for(element, pos)
  601.  
  602.                 if slider_lo.stype ~= "bar" then
  603.                     local r = (user_opts.seekbarhandlesize * innerH) / 2
  604.                     ass_draw_rr_h_cw(elem_ass, xp - r, foH - r, xp + r, foH + r,
  605.                                      r, slider_lo.stype == "diamond")
  606.                 else
  607.                     local h = 0
  608.                     if seekRanges and user_opts.seekrangeseparate and
  609.                         slider_lo.rtype ~= "inverted" then
  610.                         h = seekRangeLineHeight
  611.                     end
  612.                     elem_ass:rect_cw(foH, foV, xp, elem_geo.h - foV - h)
  613.  
  614.                     if seekRanges and not user_opts.seekrangeseparate and
  615.                         slider_lo.rtype ~= "inverted" then
  616.                         -- Punch holes for the seekRanges to be drawn later
  617.                         for _, range in pairs(seekRanges) do
  618.                             if range["start"] < pos then
  619.                                 local pstart =
  620.                                     get_slider_ele_pos_for(element,
  621.                                                            range["start"])
  622.                                 local pend = xp
  623.  
  624.                                 if pos > range["end"] then
  625.                                     pend =
  626.                                         get_slider_ele_pos_for(element,
  627.                                                                range["end"])
  628.                                 end
  629.                                 elem_ass:rect_ccw(pstart, elem_geo.h - foV -
  630.                                                       seekRangeLineHeight, pend,
  631.                                                   elem_geo.h - foV)
  632.                             end
  633.                         end
  634.                     end
  635.                 end
  636.  
  637.                 if slider_lo.rtype == "slider" then
  638.                     ass_draw_rr_h_cw(elem_ass, foH - innerH / 6,
  639.                                      foH - innerH / 6, xp, foH + innerH / 6,
  640.                                      innerH / 6, slider_lo.stype == "diamond", 0)
  641.                     ass_draw_rr_h_cw(elem_ass, xp, foH - innerH / 15,
  642.                                      elem_geo.w - foH + innerH / 15,
  643.                                      foH + innerH / 15, 0,
  644.                                      slider_lo.stype == "diamond", innerH / 15)
  645.                     for _, range in pairs(seekRanges or {}) do
  646.                         local pstart = get_slider_ele_pos_for(element,
  647.                                                               range["start"])
  648.                         local pend = get_slider_ele_pos_for(element,
  649.                                                             range["end"])
  650.                         ass_draw_rr_h_ccw(elem_ass, pstart, foH - innerH / 21,
  651.                                           pend, foH + innerH / 21, innerH / 21,
  652.                                           slider_lo.stype == "diamond")
  653.                     end
  654.                 end
  655.             end
  656.  
  657.             if seekRanges then
  658.                 if slider_lo.rtype ~= "inverted" then
  659.                     elem_ass:draw_stop()
  660.                     elem_ass:merge(element.style_ass)
  661.                     ass_append_alpha(elem_ass, element.layout.alpha,
  662.                                      user_opts.seekrangealpha)
  663.                     elem_ass:merge(element.static_ass)
  664.                 end
  665.  
  666.                 for _, range in pairs(seekRanges) do
  667.                     local pstart = get_slider_ele_pos_for(element,
  668.                                                           range["start"])
  669.                     local pend = get_slider_ele_pos_for(element, range["end"])
  670.  
  671.                     if slider_lo.rtype == "slider" then
  672.                         ass_draw_rr_h_cw(elem_ass, pstart, foH - innerH / 21,
  673.                                          pend, foH + innerH / 21, innerH / 21,
  674.                                          slider_lo.stype == "diamond")
  675.                     elseif slider_lo.rtype == "line" then
  676.                         if slider_lo.stype == "bar" then
  677.                             elem_ass:rect_cw(pstart, elem_geo.h - foV -
  678.                                                  seekRangeLineHeight, pend,
  679.                                              elem_geo.h - foV)
  680.                         else
  681.                             ass_draw_rr_h_cw(elem_ass, pstart - innerH / 8,
  682.                                              foH - innerH / 8,
  683.                                              pend + innerH / 8,
  684.                                              foH + innerH / 8, innerH / 8,
  685.                                              slider_lo.stype == "diamond")
  686.                         end
  687.                     elseif slider_lo.rtype == "bar" then
  688.                         if slider_lo.stype ~= "bar" then
  689.                             ass_draw_rr_h_cw(elem_ass, pstart - innerH / 2, foV,
  690.                                              pend + innerH / 2, foV + innerH,
  691.                                              innerH / 2,
  692.                                              slider_lo.stype == "diamond")
  693.                         elseif range["end"] >= (pos or 0) then
  694.                             elem_ass:rect_cw(pstart, foV, pend, elem_geo.h - foV)
  695.                         else
  696.                             elem_ass:rect_cw(pstart, elem_geo.h - foV -
  697.                                                  seekRangeLineHeight, pend,
  698.                                              elem_geo.h - foV)
  699.                         end
  700.                     elseif slider_lo.rtype == "inverted" then
  701.                         if slider_lo.stype ~= "bar" then
  702.                             ass_draw_rr_h_ccw(elem_ass, pstart,
  703.                                               (elem_geo.h / 2) - 1, pend,
  704.                                               (elem_geo.h / 2) + 1, 1,
  705.                                               slider_lo.stype == "diamond")
  706.                         else
  707.                             elem_ass:rect_ccw(pstart, (elem_geo.h / 2) - 1,
  708.                                               pend, (elem_geo.h / 2) + 1)
  709.                         end
  710.                     end
  711.                 end
  712.             end
  713.  
  714.             elem_ass:draw_stop()
  715.  
  716.             -- add tooltip
  717.             if not (element.slider.tooltipF == nil) then
  718.  
  719.                 if mouse_hit(element) then
  720.                     local sliderpos = get_slider_value(element)
  721.                     local tooltiplabel = element.slider.tooltipF(sliderpos)
  722.  
  723.                     local an = slider_lo.tooltip_an
  724.  
  725.                     local ty
  726.  
  727.                     if (an == 2) then
  728.                         ty = element.hitbox.y1 - slider_lo.border
  729.                     else
  730.                         ty = element.hitbox.y1 + elem_geo.h / 2
  731.                     end
  732.  
  733.                     local tx = get_virt_mouse_pos()
  734.                     local thumb_tx = tx
  735.                     if (slider_lo.adjust_tooltip) then
  736.                         if (an == 2) then
  737.                             if (sliderpos < (s_min + 3)) then
  738.                                 an = an - 1
  739.                             elseif (sliderpos > (s_max - 3)) then
  740.                                 an = an + 1
  741.                             end
  742.                         elseif (sliderpos > (s_max - s_min) / 2) then
  743.                             an = an + 1
  744.                             tx = tx - 5
  745.                         else
  746.                             an = an - 1
  747.                             tx = tx + 10
  748.                         end
  749.                     end
  750.  
  751.                     -- tooltip label
  752.                     elem_ass:new_event()
  753.                     elem_ass:pos(tx, ty)
  754.                     elem_ass:an(an)
  755.                     elem_ass:append(slider_lo.tooltip_style)
  756.                     ass_append_alpha(elem_ass, slider_lo.alpha, 0)
  757.                     elem_ass:append(tooltiplabel)
  758. -- thumbnail
  759.                     if not thumbfast.disabled and thumbfast.width ~= 0 and thumbfast.height ~= 0 then
  760.                         local osd_w = mp.get_property_number("osd-width")
  761.                         if osd_w then
  762.                             local r_w, r_h = get_virt_scale_factor()
  763.                            
  764.                             local tooltip_font_size = (user_opts.layout == "box" or user_opts.layout == "slimbox") and 2 or 12
  765.                             local thumb_ty = user_opts.layout ~= "topbar" and element.hitbox.y1 - 8 or element.hitbox.y2 + tooltip_font_size + 8
  766.  
  767.                             local thumb_pad = 2
  768.                             local thumb_margin_x = 20 / r_w
  769.                             local thumb_margin_y = (4 + user_opts.tooltipborder) / r_h + thumb_pad
  770.                             local thumb_x = math.min(osd_w - thumbfast.width - thumb_margin_x, math.max(thumb_margin_x, thumb_tx / r_w - thumbfast.width / 2))
  771.                             local thumb_y = user_opts.layout ~= "topbar" and thumb_ty / r_h - thumbfast.height - tooltip_font_size / r_h - thumb_margin_y or thumb_ty / r_h + thumb_margin_y
  772.                             thumb_x = math.floor(thumb_x + 0.5)
  773.                             thumb_y = math.floor(thumb_y + 0.5)
  774.  
  775.                             elem_ass:new_event()
  776.                             elem_ass:pos(thumb_x * r_w, thumb_y * r_h)
  777.                             elem_ass:append(osc_styles.timePosBar)
  778.                             elem_ass:append("{\\1a&H20&}")
  779.                             elem_ass:draw_start()
  780.                             elem_ass:rect_cw(-thumb_pad * r_w, -thumb_pad * r_h, (thumbfast.width + thumb_pad) * r_w, (thumbfast.height + thumb_pad) * r_h)
  781.                             elem_ass:draw_stop()
  782.  
  783.                             mp.commandv("script-message-to", "thumbfast", "thumb",
  784.                                 mp.get_property_number("duration", 0) * (sliderpos / 100),
  785.                                 thumb_x,
  786.                                 thumb_y
  787.                             )
  788.                         end
  789.                     end
  790.                 else
  791.                     if thumbfast.width ~= 0 and thumbfast.height ~= 0 then
  792.                         mp.commandv("script-message-to", "thumbfast", "clear")
  793.                     end
  794.                 end
  795.             end
  796.  
  797.         elseif (element.type == "button") then
  798.  
  799.             local buttontext
  800.             if type(element.content) == "function" then
  801.                 buttontext = element.content() -- function objects
  802.             elseif not (element.content == nil) then
  803.                 buttontext = element.content -- text objects
  804.             end
  805.  
  806.             local maxchars = element.layout.button.maxchars
  807.             if not (maxchars == nil) and (#buttontext > maxchars) then
  808.                 local max_ratio = 1.25 -- up to 25% more chars while shrinking
  809.                 local limit = math.max(0, math.floor(maxchars * max_ratio) - 3)
  810.                 if (#buttontext > limit) then
  811.                     while (#buttontext > limit) do
  812.                         buttontext = buttontext:gsub(".[\128-\191]*$", "")
  813.                     end
  814.                     buttontext = buttontext .. "..."
  815.                 end
  816.                 local _, nchars2 = buttontext:gsub(".[\128-\191]*", "")
  817.                 local stretch = (maxchars / #buttontext) * 100
  818.                 buttontext = string.format("{\\fscx%f}",
  819.                                            (maxchars / #buttontext) * 100) ..
  820.                                  buttontext
  821.             end
  822.  
  823.             elem_ass:append(buttontext)
  824.         end
  825.  
  826.         master_ass:merge(elem_ass)
  827.     end
  828. end
  829.  
  830. --
  831. -- Message display
  832. --
  833.  
  834. -- pos is 1 based
  835. function limited_list(prop, pos)
  836.     local proplist = mp.get_property_native(prop, {})
  837.     local count = #proplist
  838.     if count == 0 then return count, proplist end
  839.  
  840.     local fs = tonumber(mp.get_property('options/osd-font-size'))
  841.     local max = math.ceil(osc_param.unscaled_y * 0.75 / fs)
  842.     if max % 2 == 0 then max = max - 1 end
  843.     local delta = math.ceil(max / 2) - 1
  844.     local begi = math.max(math.min(pos - delta, count - max + 1), 1)
  845.     local endi = math.min(begi + max - 1, count)
  846.  
  847.     local reslist = {}
  848.     for i = begi, endi do
  849.         local item = proplist[i]
  850.         item.current = (i == pos) and true or nil
  851.         table.insert(reslist, item)
  852.     end
  853.     return count, reslist
  854. end
  855.  
  856. function get_playlist()
  857.     local pos = mp.get_property_number('playlist-pos', 0) + 1
  858.     local count, limlist = limited_list('playlist', pos)
  859.     if count == 0 then return 'Empty playlist.' end
  860.  
  861.     local message = string.format('Playlist [%d/%d]:\n', pos, count)
  862.     for i, v in ipairs(limlist) do
  863.         local title = v.title
  864.         local _, filename = utils.split_path(v.filename)
  865.         if title == nil then title = filename end
  866.         message = string.format('%s %s %s\n', message,
  867.                                 (v.current and '●' or '○'), title)
  868.     end
  869.     return message
  870. end
  871.  
  872. function get_chapterlist()
  873.     local pos = mp.get_property_number('chapter', 0) + 1
  874.     local count, limlist = limited_list('chapter-list', pos)
  875.     if count == 0 then return 'No chapters.' end
  876.  
  877.     local message = string.format('Chapters [%d/%d]:\n', pos, count)
  878.     for i, v in ipairs(limlist) do
  879.         local time = mp.format_time(v.time)
  880.         local title = v.title
  881.         if title == nil then title = string.format('Chapter %02d', i) end
  882.         message = string.format('%s[%s] %s %s\n', message, time,
  883.                                 (v.current and '●' or '○'), title)
  884.     end
  885.     return message
  886. end
  887.  
  888. function show_message(text, duration)
  889.  
  890.     -- print("text: "..text.."   duration: " .. duration)
  891.     if duration == nil then
  892.         duration = tonumber(mp.get_property("options/osd-duration")) / 1000
  893.     elseif not type(duration) == "number" then
  894.         print("duration: " .. duration)
  895.     end
  896.  
  897.     -- cut the text short, otherwise the following functions
  898.     -- may slow down massively on huge input
  899.     text = string.sub(text, 0, 4000)
  900.  
  901.     -- replace actual linebreaks with ASS linebreaks
  902.     text = string.gsub(text, "\n", "\\N")
  903.  
  904.     state.message_text = text
  905.     state.message_timeout = mp.get_time() + duration
  906. end
  907.  
  908. function render_message(ass)
  909.     if not (state.message_timeout == nil) and not (state.message_text == nil) and
  910.         state.message_timeout > mp.get_time() then
  911.         local _, lines = string.gsub(state.message_text, "\\N", "")
  912.  
  913.         local fontsize = tonumber(mp.get_property("options/osd-font-size"))
  914.         local outline = tonumber(mp.get_property("options/osd-border-size"))
  915.         local maxlines = math.ceil(osc_param.unscaled_y * 0.75 / fontsize)
  916.         local counterscale = osc_param.playresy / osc_param.unscaled_y
  917.  
  918.         fontsize = fontsize * counterscale /
  919.                        math.max(0.65 + math.min(lines / maxlines, 1), 1)
  920.         outline = outline * counterscale /
  921.                       math.max(0.75 + math.min(lines / maxlines, 1) / 2, 1)
  922.  
  923.         local style = "{\\bord" .. outline .. "\\fs" .. fontsize .. "}"
  924.  
  925.         ass:new_event()
  926.         ass:append(style .. state.message_text)
  927.     else
  928.         state.message_text = nil
  929.         state.message_timeout = nil
  930.     end
  931. end
  932.  
  933. --
  934. -- Initialisation and Layout
  935. --
  936.  
  937. function new_element(name, type)
  938.     elements[name] = {}
  939.     elements[name].type = type
  940.  
  941.     -- add default stuff
  942.     elements[name].eventresponder = {}
  943.     elements[name].visible = true
  944.     elements[name].enabled = true
  945.     elements[name].softrepeat = false
  946.     elements[name].styledown = (type == "button")
  947.     elements[name].state = {}
  948.  
  949.     if (type == "slider") then
  950.         elements[name].slider = {min = {value = 0}, max = {value = 100}}
  951.     end
  952.  
  953.     return elements[name]
  954. end
  955.  
  956. function add_layout(name)
  957.     if not (elements[name] == nil) then
  958.         -- new layout
  959.         elements[name].layout = {}
  960.  
  961.         -- set layout defaults
  962.         elements[name].layout.layer = 50
  963.         elements[name].layout.alpha = {[1] = 0, [2] = 255, [3] = 255, [4] = 255}
  964.  
  965.         if (elements[name].type == "button") then
  966.             elements[name].layout.button = {maxchars = nil}
  967.         elseif (elements[name].type == "slider") then
  968.             -- slider defaults
  969.             elements[name].layout.slider =
  970.                 {
  971.                     border = 1,
  972.                     gap = 1,
  973.                     nibbles_top = true,
  974.                     nibbles_bottom = true,
  975.                     stype = "slider",
  976.                     adjust_tooltip = true,
  977.                     tooltip_style = "",
  978.                     tooltip_an = 2,
  979.                     alpha = {[1] = 0, [2] = 255, [3] = 88, [4] = 255}
  980.                 }
  981.         elseif (elements[name].type == "box") then
  982.             elements[name].layout.box = {radius = 0, hexagon = false}
  983.         end
  984.  
  985.         return elements[name].layout
  986.     else
  987.         msg.error("Can't add_layout to element \"" .. name ..
  988.                       "\", doesn't exist.")
  989.     end
  990. end
  991.  
  992. --
  993. -- Layouts
  994. --
  995.  
  996. local layouts = {}
  997.  
  998. -- Classic box layout
  999. layouts["box"] = function()
  1000.  
  1001.     local osc_geo = {
  1002.         w = 550, -- width
  1003.         h = 138, -- height
  1004.         r = 10, -- corner-radius
  1005.         p = 15 -- padding
  1006.     }
  1007.  
  1008.     -- make sure the OSC actually fits into the video
  1009.     if (osc_param.playresx < (osc_geo.w + (2 * osc_geo.p))) then
  1010.         osc_param.playresy = (osc_geo.w + (2 * osc_geo.p)) /
  1011.                                  osc_param.display_aspect
  1012.         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1013.     end
  1014.  
  1015.     -- position of the controller according to video aspect and valignment
  1016.     local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  1017.                                       osc_geo.w, 0))
  1018.     local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  1019.                                       osc_geo.h, 0))
  1020.  
  1021.     -- position offset for contents aligned at the borders of the box
  1022.     local pos_offsetX = (osc_geo.w - (2 * osc_geo.p)) / 2
  1023.     local pos_offsetY = (osc_geo.h - (2 * osc_geo.p)) / 2
  1024.  
  1025.     osc_param.areas = {} -- delete areas
  1026.  
  1027.     -- area for active mouse input
  1028.     add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  1029.  
  1030.     -- area for show/hide
  1031.     local sh_area_y0, sh_area_y1
  1032.     if user_opts.valign > 0 then
  1033.         -- deadzone above OSC
  1034.         sh_area_y0 = get_align(-1 + (2 * user_opts.deadzonesize),
  1035.                                posY - (osc_geo.h / 2), 0, 0)
  1036.         sh_area_y1 = osc_param.playresy
  1037.     else
  1038.         -- deadzone below OSC
  1039.         sh_area_y0 = 0
  1040.         sh_area_y1 = (posY + (osc_geo.h / 2)) +
  1041.                          get_align(1 - (2 * user_opts.deadzonesize),
  1042.                                    osc_param.playresy - (posY + (osc_geo.h / 2)),
  1043.                                    0, 0)
  1044.     end
  1045.     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1046.  
  1047.     -- fetch values
  1048.     local osc_w, osc_h, osc_r, osc_p = osc_geo.w, osc_geo.h, osc_geo.r,
  1049.                                        osc_geo.p
  1050.  
  1051.     local lo
  1052.  
  1053.     --
  1054.     -- Background box
  1055.     --
  1056.  
  1057.     new_element("bgbox", "box")
  1058.     lo = add_layout("bgbox")
  1059.  
  1060.     lo.geometry = {x = posX, y = posY, an = 5, w = osc_w, h = osc_h}
  1061.     lo.layer = 10
  1062.     lo.style = osc_styles.box
  1063.     lo.alpha[1] = user_opts.boxalpha
  1064.     lo.alpha[3] = user_opts.boxalpha
  1065.     lo.box.radius = osc_r
  1066.  
  1067.     --
  1068.     -- Title row
  1069.     --
  1070.  
  1071.     local titlerowY = posY - pos_offsetY - 10
  1072.  
  1073.     lo = add_layout("title")
  1074.     lo.geometry = {x = posX, y = titlerowY, an = 8, w = 496, h = 12}
  1075.     lo.style = osc_styles.vidtitle
  1076.     lo.button.maxchars = user_opts.boxmaxchars
  1077.  
  1078.     lo = add_layout("pl_prev")
  1079.     lo.geometry = {
  1080.         x = (posX - pos_offsetX),
  1081.         y = titlerowY,
  1082.         an = 7,
  1083.         w = 12,
  1084.         h = 12
  1085.     }
  1086.     lo.style = osc_styles.topButtons
  1087.  
  1088.     lo = add_layout("pl_next")
  1089.     lo.geometry = {
  1090.         x = (posX + pos_offsetX),
  1091.         y = titlerowY,
  1092.         an = 9,
  1093.         w = 12,
  1094.         h = 12
  1095.     }
  1096.     lo.style = osc_styles.topButtons
  1097.  
  1098.     --
  1099.     -- Big buttons
  1100.     --
  1101.  
  1102.     local bigbtnrowY = posY - pos_offsetY + 35
  1103.     local bigbtndist = 60
  1104.  
  1105.     lo = add_layout("playpause")
  1106.     lo.geometry = {x = posX, y = bigbtnrowY, an = 5, w = 40, h = 40}
  1107.     lo.style = osc_styles.bigButtons
  1108.  
  1109.     lo = add_layout("skipback")
  1110.     lo.geometry = {
  1111.         x = posX - bigbtndist,
  1112.         y = bigbtnrowY,
  1113.         an = 5,
  1114.         w = 40,
  1115.         h = 40
  1116.     }
  1117.     lo.style = osc_styles.bigButtons
  1118.  
  1119.     lo = add_layout("skipfrwd")
  1120.     lo.geometry = {
  1121.         x = posX + bigbtndist,
  1122.         y = bigbtnrowY,
  1123.         an = 5,
  1124.         w = 40,
  1125.         h = 40
  1126.     }
  1127.     lo.style = osc_styles.bigButtons
  1128.  
  1129.     lo = add_layout("ch_prev")
  1130.     lo.geometry = {
  1131.         x = posX - (bigbtndist * 2),
  1132.         y = bigbtnrowY,
  1133.         an = 5,
  1134.         w = 40,
  1135.         h = 40
  1136.     }
  1137.     lo.style = osc_styles.bigButtons
  1138.  
  1139.     lo = add_layout("ch_next")
  1140.     lo.geometry = {
  1141.         x = posX + (bigbtndist * 2),
  1142.         y = bigbtnrowY,
  1143.         an = 5,
  1144.         w = 40,
  1145.         h = 40
  1146.     }
  1147.     lo.style = osc_styles.bigButtons
  1148.  
  1149.     lo = add_layout("cy_audio")
  1150.     lo.geometry = {
  1151.         x = posX - pos_offsetX,
  1152.         y = bigbtnrowY,
  1153.         an = 1,
  1154.         w = 70,
  1155.         h = 18
  1156.     }
  1157.     lo.style = osc_styles.smallButtonsL
  1158.  
  1159.     lo = add_layout("cy_sub")
  1160.     lo.geometry = {
  1161.         x = posX - pos_offsetX,
  1162.         y = bigbtnrowY,
  1163.         an = 7,
  1164.         w = 70,
  1165.         h = 18
  1166.     }
  1167.     lo.style = osc_styles.smallButtonsL
  1168.  
  1169.     lo = add_layout("tog_fs")
  1170.     lo.geometry = {
  1171.         x = posX + pos_offsetX - 25,
  1172.         y = bigbtnrowY,
  1173.         an = 4,
  1174.         w = 25,
  1175.         h = 25
  1176.     }
  1177.     lo.style = osc_styles.smallButtonsR
  1178.  
  1179.     lo = add_layout("volume")
  1180.     lo.geometry = {
  1181.         x = posX + pos_offsetX - (25 * 2) - osc_geo.p,
  1182.         y = bigbtnrowY,
  1183.         an = 4,
  1184.         w = 25,
  1185.         h = 25
  1186.     }
  1187.     lo.style = osc_styles.smallButtonsR
  1188.  
  1189.     --
  1190.     -- Seekbar
  1191.     --
  1192.  
  1193.     lo = add_layout("seekbar")
  1194.     lo.geometry = {
  1195.         x = posX,
  1196.         y = posY + pos_offsetY - 22,
  1197.         an = 2,
  1198.         w = pos_offsetX * 2,
  1199.         h = 15
  1200.     }
  1201.     lo.style = osc_styles.timecodes
  1202.     lo.slider.tooltip_style = osc_styles.vidtitle
  1203.     lo.slider.stype = user_opts["seekbarstyle"]
  1204.     lo.slider.rtype = user_opts["seekrangestyle"]
  1205.  
  1206.     --
  1207.     -- Timecodes + Cache
  1208.     --
  1209.  
  1210.     local bottomrowY = posY + pos_offsetY - 5
  1211.  
  1212.     lo = add_layout("tc_left")
  1213.     lo.geometry = {
  1214.         x = posX - pos_offsetX,
  1215.         y = bottomrowY,
  1216.         an = 4,
  1217.         w = 110,
  1218.         h = 18
  1219.     }
  1220.     lo.style = osc_styles.timecodes
  1221.  
  1222.     lo = add_layout("tc_right")
  1223.     lo.geometry = {
  1224.         x = posX + pos_offsetX,
  1225.         y = bottomrowY,
  1226.         an = 6,
  1227.         w = 110,
  1228.         h = 18
  1229.     }
  1230.     lo.style = osc_styles.timecodes
  1231.  
  1232.     lo = add_layout("cache")
  1233.     lo.geometry = {x = posX, y = bottomrowY, an = 5, w = 110, h = 18}
  1234.     lo.style = osc_styles.timecodes
  1235.  
  1236. end
  1237.  
  1238. -- slim box layout
  1239. layouts["slimbox"] = function()
  1240.  
  1241.     local osc_geo = {
  1242.         w = 660, -- width
  1243.         h = 70, -- height
  1244.         r = 10 -- corner-radius
  1245.     }
  1246.  
  1247.     -- make sure the OSC actually fits into the video
  1248.     if (osc_param.playresx < (osc_geo.w)) then
  1249.         osc_param.playresy = (osc_geo.w) / osc_param.display_aspect
  1250.         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1251.     end
  1252.  
  1253.     -- position of the controller according to video aspect and valignment
  1254.     local posX = math.floor(get_align(user_opts.halign, osc_param.playresx,
  1255.                                       osc_geo.w, 0))
  1256.     local posY = math.floor(get_align(user_opts.valign, osc_param.playresy,
  1257.                                       osc_geo.h, 0))
  1258.  
  1259.     osc_param.areas = {} -- delete areas
  1260.  
  1261.     -- area for active mouse input
  1262.     add_area("input", get_hitbox_coords(posX, posY, 5, osc_geo.w, osc_geo.h))
  1263.  
  1264.     -- area for show/hide
  1265.     local sh_area_y0, sh_area_y1
  1266.     if user_opts.valign > 0 then
  1267.         -- deadzone above OSC
  1268.         sh_area_y0 = get_align(-1 + (2 * user_opts.deadzonesize),
  1269.                                posY - (osc_geo.h / 2), 0, 0)
  1270.         sh_area_y1 = osc_param.playresy
  1271.     else
  1272.         -- deadzone below OSC
  1273.         sh_area_y0 = 0
  1274.         sh_area_y1 = (posY + (osc_geo.h / 2)) +
  1275.                          get_align(1 - (2 * user_opts.deadzonesize),
  1276.                                    osc_param.playresy - (posY + (osc_geo.h / 2)),
  1277.                                    0, 0)
  1278.     end
  1279.     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1280.  
  1281.     local lo
  1282.  
  1283.     local tc_w, ele_h, inner_w = 100, 20, osc_geo.w - 100
  1284.  
  1285.     -- styles
  1286.     local styles = {
  1287.         box = "{\\rDefault\\blur0\\bord1\\1c&H000000\\3c&HFFFFFF}",
  1288.         timecodes = "{\\1c&HFFFFFF\\3c&H000000\\fs20\\bord2\\blur1}",
  1289.         tooltip = "{\\1c&HFFFFFF\\3c&H000000\\fs12\\bord1\\blur0.5}"
  1290.     }
  1291.  
  1292.     new_element("bgbox", "box")
  1293.     lo = add_layout("bgbox")
  1294.  
  1295.     lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  1296.     lo.layer = 10
  1297.     lo.style = osc_styles.box
  1298.     lo.alpha[1] = user_opts.boxalpha
  1299.     lo.alpha[3] = 0
  1300.     if not (user_opts["seekbarstyle"] == "bar") then
  1301.         lo.box.radius = osc_geo.r
  1302.         lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
  1303.     end
  1304.  
  1305.     lo = add_layout("seekbar")
  1306.     lo.geometry = {x = posX, y = posY - 1, an = 2, w = inner_w, h = ele_h}
  1307.     lo.style = osc_styles.timecodes
  1308.     lo.slider.border = 0
  1309.     lo.slider.gap = 1.5
  1310.     lo.slider.tooltip_style = styles.tooltip
  1311.     lo.slider.stype = user_opts["seekbarstyle"]
  1312.     lo.slider.rtype = user_opts["seekrangestyle"]
  1313.     lo.slider.adjust_tooltip = false
  1314.  
  1315.     --
  1316.     -- Timecodes
  1317.     --
  1318.  
  1319.     lo = add_layout("tc_left")
  1320.     lo.geometry = {
  1321.         x = posX - (inner_w / 2) + osc_geo.r,
  1322.         y = posY + 1,
  1323.         an = 7,
  1324.         w = tc_w,
  1325.         h = ele_h
  1326.     }
  1327.     lo.style = styles.timecodes
  1328.     lo.alpha[3] = user_opts.boxalpha
  1329.  
  1330.     lo = add_layout("tc_right")
  1331.     lo.geometry = {
  1332.         x = posX + (inner_w / 2) - osc_geo.r,
  1333.         y = posY + 1,
  1334.         an = 9,
  1335.         w = tc_w,
  1336.         h = ele_h
  1337.     }
  1338.     lo.style = styles.timecodes
  1339.     lo.alpha[3] = user_opts.boxalpha
  1340.  
  1341.     -- Cache
  1342.  
  1343.     lo = add_layout("cache")
  1344.     lo.geometry = {x = posX, y = posY + 1, an = 8, w = tc_w, h = ele_h}
  1345.     lo.style = styles.timecodes
  1346.     lo.alpha[3] = user_opts.boxalpha
  1347.  
  1348. end
  1349.  
  1350. layouts["bottombar"] = function()
  1351.     local osc_geo = {
  1352.         x = -2,
  1353.         y = osc_param.playresy - 54 - user_opts.barmargin,
  1354.         an = 7,
  1355.         w = osc_param.playresx + 4,
  1356.         h = 56
  1357.     }
  1358.  
  1359.     local padX = 9
  1360.     local padY = 3
  1361.     local buttonW = 27
  1362.     local tcW = (state.tc_ms) and 118 or 80 -- 170 or 110
  1363.     local tsW = 40 -- 90
  1364.     local minW = (buttonW + padX) * 5 + (tcW + padX) * 4 + (tsW + padX) * 2
  1365.  
  1366.     if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
  1367.         osc_param.playresy = minW / osc_param.display_aspect
  1368.         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1369.         osc_geo.y = osc_param.playresy - 54 - user_opts.barmargin
  1370.         osc_geo.w = osc_param.playresx + 4
  1371.     end
  1372.  
  1373.     local line1 = osc_geo.y + 9 + padY
  1374.     local line2 = osc_geo.y + 36 + padY
  1375.  
  1376.     osc_param.areas = {}
  1377.  
  1378.     add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
  1379.                                         osc_geo.w, osc_geo.h))
  1380.  
  1381.     local sh_area_y0, sh_area_y1
  1382.     sh_area_y0 = get_align(-1 + (2 * user_opts.deadzonesize),
  1383.                            osc_geo.y - (osc_geo.h / 2), 0, 0)
  1384.     sh_area_y1 = osc_param.playresy - user_opts.barmargin
  1385.     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1386.  
  1387.     local lo, geo
  1388.  
  1389.     -- Background bar
  1390.     new_element("bgbox", "box")
  1391.     lo = add_layout("bgbox")
  1392.  
  1393.     lo.geometry = osc_geo
  1394.     lo.layer = 10
  1395.     lo.style = osc_styles.box
  1396.     lo.alpha[1] = user_opts.boxalpha
  1397.  
  1398.     -- Playlist prev/next
  1399.     geo = {x = osc_geo.x + padX, y = line1, an = 4, w = 18, h = 18 - padY}
  1400.     lo = add_layout("pl_prev")
  1401.     lo.geometry = geo
  1402.     lo.style = osc_styles.topButtonsBar
  1403.  
  1404.     geo = {
  1405.         x = geo.x + geo.w + padX,
  1406.         y = geo.y,
  1407.         an = geo.an,
  1408.         w = geo.w,
  1409.         h = geo.h
  1410.     }
  1411.     lo = add_layout("pl_next")
  1412.     lo.geometry = geo
  1413.     lo.style = osc_styles.topButtonsBar
  1414.  
  1415.     local t_l = geo.x + geo.w + padX
  1416.  
  1417.     -- Track selection buttons
  1418.     geo = {
  1419.         x = osc_geo.x + osc_geo.w - padX,
  1420.         y = geo.y,
  1421.         an = 6,
  1422.         w = tsW,
  1423.         h = geo.h
  1424.     } -- { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
  1425.     lo = add_layout("cy_sub")
  1426.     lo.geometry = geo
  1427.     lo.style = osc_styles.smallButtonsBarS
  1428.  
  1429.     geo = {
  1430.         x = geo.x - geo.w - padX,
  1431.         y = geo.y,
  1432.         an = geo.an,
  1433.         w = geo.w,
  1434.         h = geo.h
  1435.     }
  1436.     lo = add_layout("cy_audio")
  1437.     lo.geometry = geo
  1438.     lo.style = osc_styles.smallButtonsBarS
  1439.  
  1440.     -- Cache
  1441.     geo = {x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = 50, h = geo.h}
  1442.     lo = add_layout("cache")
  1443.     lo.geometry = geo
  1444.     lo.style = osc_styles.vidtitleBar
  1445.  
  1446.     local t_r = geo.x - geo.w - padX
  1447.  
  1448.     -- Title
  1449.     geo = {x = t_l, y = geo.y, an = 4, w = t_r - t_l, h = geo.h}
  1450.     lo = add_layout("title")
  1451.     lo.geometry = geo
  1452.     lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", osc_styles.vidtitleBar,
  1453.                              geo.x, geo.y - geo.h, geo.w, geo.y + geo.h)
  1454.  
  1455.     -- Playback control buttons
  1456.     geo = {
  1457.         x = osc_geo.x + padX,
  1458.         y = line2,
  1459.         an = 4,
  1460.         w = buttonW,
  1461.         h = 36 - padY * 2
  1462.     }
  1463.     lo = add_layout("ch_prev")
  1464.     lo.geometry = geo
  1465.     lo.style = osc_styles.smallButtonsBar
  1466.  
  1467.     geo = {
  1468.         x = geo.x + geo.w + padX,
  1469.         y = geo.y,
  1470.         an = geo.an,
  1471.         w = geo.w,
  1472.         h = geo.h
  1473.     }
  1474.     lo = add_layout("playpause")
  1475.     lo.geometry = geo
  1476.     lo.style = osc_styles.smallButtonsBar
  1477.  
  1478.     geo = {
  1479.         x = geo.x + geo.w + padX,
  1480.         y = geo.y,
  1481.         an = geo.an,
  1482.         w = geo.w,
  1483.         h = geo.h
  1484.     }
  1485.     lo = add_layout("ch_next")
  1486.     lo.geometry = geo
  1487.     lo.style = osc_styles.smallButtonsBar
  1488.  
  1489.     -- Left timecode
  1490.     geo = {
  1491.         x = geo.x + geo.w + padX + tcW,
  1492.         y = geo.y,
  1493.         an = 6,
  1494.         w = tcW,
  1495.         h = geo.h
  1496.     }
  1497.     lo = add_layout("tc_left")
  1498.     lo.geometry = geo
  1499.     lo.style = osc_styles.timecodesBar
  1500.  
  1501.     local sb_l = geo.x + padX
  1502.  
  1503.     -- -- Fullscreen button
  1504.     -- geo = { x = osc_geo.x + osc_geo.w - buttonW - padX, y = geo.y, an = 4,
  1505.     -- w = buttonW, h = geo.h }
  1506.     -- lo = add_layout("tog_fs")
  1507.     -- lo.geometry = geo
  1508.     -- lo.style = osc_styles.smallButtonsBarF
  1509.  
  1510.     -- -- Volume
  1511.     -- geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1512.     -- lo = add_layout("volume")
  1513.     -- lo.geometry = geo
  1514.     -- lo.style = osc_styles.smallButtonsBarF
  1515.  
  1516.     -- -- Track selection buttons
  1517.     -- geo = { x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h }
  1518.     -- lo = add_layout("cy_sub")
  1519.     -- lo.geometry = geo
  1520.     -- lo.style = osc_styles.smallButtonsBarS
  1521.  
  1522.     -- geo = { x = geo.x - geo.w - padX, y = geo.y, an = geo.an, w = geo.w, h = geo.h }
  1523.     -- lo = add_layout("cy_audio")
  1524.     -- lo.geometry = geo
  1525.     -- lo.style = osc_styles.smallButtonsBarS
  1526.  
  1527.     -- Right timecode
  1528.     geo = {
  1529.         x = osc_geo.x + osc_geo.w - padX,
  1530.         y = geo.y,
  1531.         an = 6, -- geo.an,
  1532.         w = tcW,
  1533.         h = geo.h
  1534.     }
  1535.     lo = add_layout("tc_right")
  1536.     lo.geometry = geo
  1537.     lo.style = osc_styles.timecodesBar
  1538.  
  1539.     local sb_r = geo.x - padX - tcW
  1540.  
  1541.     -- Seekbar
  1542.     geo = {
  1543.         x = sb_l,
  1544.         y = geo.y,
  1545.         an = 4,
  1546.         w = math.max(0, sb_r - sb_l),
  1547.         h = geo.h - 15
  1548.     }
  1549.     new_element("bgbar1", "box")
  1550.     lo = add_layout("bgbar1")
  1551.  
  1552.     lo.geometry = geo
  1553.     lo.layer = 15
  1554.     lo.style = osc_styles.timecodesBar
  1555.     lo.alpha[1] = math.min(255,
  1556.                            user_opts.boxalpha + (255 - user_opts.boxalpha) * 0.2)
  1557.     if not (user_opts["seekbarstyle"] == "bar") then
  1558.         lo.box.radius = geo.h / 2
  1559.         lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
  1560.     end
  1561.  
  1562.     lo = add_layout("seekbar")
  1563.     lo.geometry = geo
  1564.     lo.style = osc_styles.timecodes
  1565.     lo.slider.border = 0
  1566.     lo.slider.gap = 3
  1567.     lo.slider.tooltip_style = osc_styles.timePosBar
  1568.     lo.slider.tooltip_an = 2
  1569.     lo.slider.stype = user_opts["seekbarstyle"]
  1570.     lo.slider.rtype = user_opts["seekrangestyle"]
  1571. end
  1572.  
  1573. layouts["topbar"] = function()
  1574.     local osc_geo = {
  1575.         x = -2,
  1576.         y = 54 + user_opts.barmargin,
  1577.         an = 1,
  1578.         w = osc_param.playresx + 4,
  1579.         h = 56
  1580.     }
  1581.  
  1582.     local padX = 9
  1583.     local padY = 3
  1584.     local buttonW = 27
  1585.     local tcW = (state.tc_ms) and 170 or 110
  1586.     local tsW = 90
  1587.     local minW = (buttonW + padX) * 5 + (tcW + padX) * 4 + (tsW + padX) * 2
  1588.  
  1589.     if ((osc_param.display_aspect > 0) and (osc_param.playresx < minW)) then
  1590.         osc_param.playresy = minW / osc_param.display_aspect
  1591.         osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1592.         osc_geo.y = 54 + user_opts.barmargin
  1593.         osc_geo.w = osc_param.playresx + 4
  1594.     end
  1595.  
  1596.     local line1 = osc_geo.y - 36 - padY
  1597.     local line2 = osc_geo.y - 9 - padY
  1598.  
  1599.     osc_param.areas = {}
  1600.  
  1601.     add_area("input", get_hitbox_coords(osc_geo.x, osc_geo.y, osc_geo.an,
  1602.                                         osc_geo.w, osc_geo.h))
  1603.  
  1604.     local sh_area_y0, sh_area_y1
  1605.     sh_area_y0 = user_opts.barmargin
  1606.     sh_area_y1 = (osc_geo.y + (osc_geo.h / 2)) +
  1607.                      get_align(1 - (2 * user_opts.deadzonesize),
  1608.                                osc_param.playresy -
  1609.                                    (osc_geo.y + (osc_geo.h / 2)), 0, 0)
  1610.     add_area("showhide", 0, sh_area_y0, osc_param.playresx, sh_area_y1)
  1611.  
  1612.     local lo, geo
  1613.  
  1614.     -- Background bar
  1615.     new_element("bgbox", "box")
  1616.     lo = add_layout("bgbox")
  1617.  
  1618.     lo.geometry = osc_geo
  1619.     lo.layer = 10
  1620.     lo.style = osc_styles.box
  1621.     lo.alpha[1] = user_opts.boxalpha
  1622.  
  1623.     -- Playback control buttons
  1624.     geo = {
  1625.         x = osc_geo.x + padX,
  1626.         y = line1,
  1627.         an = 4,
  1628.         w = buttonW,
  1629.         h = 36 - padY * 2
  1630.     }
  1631.     lo = add_layout("playpause")
  1632.     lo.geometry = geo
  1633.     lo.style = osc_styles.smallButtonsBar
  1634.  
  1635.     geo = {
  1636.         x = geo.x + geo.w + padX,
  1637.         y = geo.y,
  1638.         an = geo.an,
  1639.         w = geo.w,
  1640.         h = geo.h
  1641.     }
  1642.     lo = add_layout("ch_prev")
  1643.     lo.geometry = geo
  1644.     lo.style = osc_styles.smallButtonsBar
  1645.  
  1646.     geo = {
  1647.         x = geo.x + geo.w + padX,
  1648.         y = geo.y,
  1649.         an = geo.an,
  1650.         w = geo.w,
  1651.         h = geo.h
  1652.     }
  1653.     lo = add_layout("ch_next")
  1654.     lo.geometry = geo
  1655.     lo.style = osc_styles.smallButtonsBar
  1656.  
  1657.     -- Left timecode
  1658.     geo = {
  1659.         x = geo.x + geo.w + padX + tcW,
  1660.         y = geo.y,
  1661.         an = 6,
  1662.         w = tcW,
  1663.         h = geo.h
  1664.     }
  1665.     lo = add_layout("tc_left")
  1666.     lo.geometry = geo
  1667.     lo.style = osc_styles.timecodesBar
  1668.  
  1669.     local sb_l = geo.x + padX
  1670.  
  1671.     -- Fullscreen button
  1672.     geo = {
  1673.         x = osc_geo.x + osc_geo.w - buttonW - padX,
  1674.         y = geo.y,
  1675.         an = 4,
  1676.         w = buttonW,
  1677.         h = geo.h
  1678.     }
  1679.     lo = add_layout("tog_fs")
  1680.     lo.geometry = geo
  1681.     lo.style = osc_styles.smallButtonsBar
  1682.  
  1683.     -- Volume
  1684.     geo = {
  1685.         x = geo.x - geo.w - padX,
  1686.         y = geo.y,
  1687.         an = geo.an,
  1688.         w = geo.w,
  1689.         h = geo.h
  1690.     }
  1691.     lo = add_layout("volume")
  1692.     lo.geometry = geo
  1693.     lo.style = osc_styles.smallButtonsBar
  1694.  
  1695.     -- Track selection buttons
  1696.     geo = {x = geo.x - tsW - padX, y = geo.y, an = geo.an, w = tsW, h = geo.h}
  1697.     lo = add_layout("cy_sub")
  1698.     lo.geometry = geo
  1699.     lo.style = osc_styles.smallButtonsBar
  1700.  
  1701.     geo = {
  1702.         x = geo.x - geo.w - padX,
  1703.         y = geo.y,
  1704.         an = geo.an,
  1705.         w = geo.w,
  1706.         h = geo.h
  1707.     }
  1708.     lo = add_layout("cy_audio")
  1709.     lo.geometry = geo
  1710.     lo.style = osc_styles.smallButtonsBar
  1711.  
  1712.     -- Right timecode
  1713.     geo = {
  1714.         x = geo.x - geo.w - padX - tcW - 10,
  1715.         y = geo.y,
  1716.         an = 4,
  1717.         w = tcW,
  1718.         h = geo.h
  1719.     }
  1720.     lo = add_layout("tc_right")
  1721.     lo.geometry = geo
  1722.     lo.style = osc_styles.timecodesBar
  1723.  
  1724.     local sb_r = geo.x - padX
  1725.  
  1726.     -- Seekbar
  1727.     geo = {
  1728.         x = sb_l,
  1729.         y = user_opts.barmargin,
  1730.         an = 7,
  1731.         w = math.max(0, sb_r - sb_l),
  1732.         h = geo.h
  1733.     }
  1734.     new_element("bgbar1", "box")
  1735.     lo = add_layout("bgbar1")
  1736.  
  1737.     lo.geometry = geo
  1738.     lo.layer = 15
  1739.     lo.style = osc_styles.timecodesBar
  1740.     lo.alpha[1] = math.min(255,
  1741.                            user_opts.boxalpha + (255 - user_opts.boxalpha) * 0.8)
  1742.     if not (user_opts["seekbarstyle"] == "bar") then
  1743.         lo.box.radius = geo.h / 2
  1744.         lo.box.hexagon = user_opts["seekbarstyle"] == "diamond"
  1745.     end
  1746.  
  1747.     lo = add_layout("seekbar")
  1748.     lo.geometry = geo
  1749.     lo.style = osc_styles.timecodesBar
  1750.     lo.slider.border = 0
  1751.     lo.slider.gap = 2
  1752.     lo.slider.tooltip_style = osc_styles.timePosBar
  1753.     lo.slider.stype = user_opts["seekbarstyle"]
  1754.     lo.slider.rtype = user_opts["seekrangestyle"]
  1755.     lo.slider.tooltip_an = 5
  1756.  
  1757.     -- Playlist prev/next
  1758.     geo = {x = osc_geo.x + padX, y = line2, an = 4, w = 18, h = 18 - padY}
  1759.     lo = add_layout("pl_prev")
  1760.     lo.geometry = geo
  1761.     lo.style = osc_styles.topButtonsBar
  1762.  
  1763.     geo = {
  1764.         x = geo.x + geo.w + padX,
  1765.         y = geo.y,
  1766.         an = geo.an,
  1767.         w = geo.w,
  1768.         h = geo.h
  1769.     }
  1770.     lo = add_layout("pl_next")
  1771.     lo.geometry = geo
  1772.     lo.style = osc_styles.topButtonsBar
  1773.  
  1774.     local t_l = geo.x + geo.w + padX
  1775.  
  1776.     -- Cache
  1777.     geo = {
  1778.         x = osc_geo.x + osc_geo.w - padX,
  1779.         y = geo.y,
  1780.         an = 6,
  1781.         w = 150,
  1782.         h = geo.h
  1783.     }
  1784.     lo = add_layout("cache")
  1785.     lo.geometry = geo
  1786.     lo.style = osc_styles.vidtitleBar
  1787.  
  1788.     local t_r = geo.x - geo.w - padX * 2
  1789.  
  1790.     -- Title
  1791.     geo = {x = t_l, y = geo.y, an = 4, w = t_r - t_l, h = geo.h}
  1792.     lo = add_layout("title")
  1793.     lo.geometry = geo
  1794.     lo.style = string.format("%s{\\clip(%f,%f,%f,%f)}", osc_styles.vidtitleBar,
  1795.                              geo.x, geo.y - geo.h, geo.w, geo.y + geo.h)
  1796. end
  1797.  
  1798. -- Validate string type user options
  1799. function validate_user_opts()
  1800.     if layouts[user_opts.layout] == nil then
  1801.         msg.warn("Invalid setting \"" .. user_opts.layout .. "\" for layout")
  1802.         user_opts.layout = "bottombar"
  1803.     end
  1804.  
  1805.     if user_opts.seekbarstyle ~= "bar" and user_opts.seekbarstyle ~= "diamond" and
  1806.         user_opts.seekbarstyle ~= "knob" then
  1807.         msg.warn("Invalid setting \"" .. user_opts.seekbarstyle ..
  1808.                      "\" for seekbarstyle")
  1809.         user_opts.seekbarstyle = "bar"
  1810.     end
  1811.  
  1812.     if user_opts.seekrangestyle ~= "bar" and user_opts.seekrangestyle ~= "line" and
  1813.         user_opts.seekrangestyle ~= "slider" and user_opts.seekrangestyle ~=
  1814.         "inverted" and user_opts.seekrangestyle ~= "none" then
  1815.         msg.warn("Invalid setting \"" .. user_opts.seekrangestyle ..
  1816.                      "\" for seekrangestyle")
  1817.         user_opts.seekrangestyle = "inverted"
  1818.     end
  1819.  
  1820.     if user_opts.seekrangestyle == "slider" and user_opts.seekbarstyle == "bar" then
  1821.         msg.warn(
  1822.             "Using \"slider\" seekrangestyle together with \"bar\" seekbarstyle is not supported")
  1823.         user_opts.seekrangestyle = "inverted"
  1824.     end
  1825. end
  1826.  
  1827. -- OSC INIT
  1828. function osc_init()
  1829.     msg.debug("osc_init")
  1830.  
  1831.     -- set canvas resolution according to display aspect and scaling setting
  1832.     local baseResY = 720
  1833.     local display_w, display_h, display_aspect = mp.get_osd_size()
  1834.     local scale = 1
  1835.  
  1836.     if (mp.get_property("video") == "no") then -- dummy/forced window
  1837.         scale = user_opts.scaleforcedwindow
  1838.     elseif state.fullscreen then
  1839.         scale = user_opts.scalefullscreen
  1840.     else
  1841.         scale = user_opts.scalewindowed
  1842.     end
  1843.  
  1844.     if user_opts.vidscale then
  1845.         osc_param.unscaled_y = baseResY
  1846.     else
  1847.         osc_param.unscaled_y = display_h
  1848.     end
  1849.     osc_param.playresy = osc_param.unscaled_y / scale
  1850.     if (display_aspect > 0) then osc_param.display_aspect = display_aspect end
  1851.     osc_param.playresx = osc_param.playresy * osc_param.display_aspect
  1852.  
  1853.     -- stop seeking with the slider to prevent skipping files
  1854.     state.active_element = nil
  1855.  
  1856.     elements = {}
  1857.  
  1858.     -- some often needed stuff
  1859.     local pl_count = mp.get_property_number("playlist-count", 0)
  1860.     local have_pl = (pl_count > 1)
  1861.     local pl_pos = mp.get_property_number("playlist-pos", 0) + 1
  1862.     local have_ch = (mp.get_property_number("chapters", 0) > 0)
  1863.     local loop = mp.get_property("loop-playlist", "no")
  1864.  
  1865.     local ne
  1866.  
  1867.     -- title
  1868.     ne = new_element("title", "button")
  1869.  
  1870.     ne.content = function()
  1871.         local title = mp.command_native({"expand-text", user_opts.title})
  1872.         -- escape ASS, and strip newlines and trailing slashes
  1873.         title = title:gsub("\\n", " "):gsub("\\$", ""):gsub("{", "\\{")
  1874.         return not (title == "") and title or "mpv"
  1875.     end
  1876.  
  1877.     ne.eventresponder["mbtn_left_up"] = function()
  1878.         local title = mp.get_property_osd("media-title")
  1879.         if (have_pl) then
  1880.             title = string.format("[%d/%d] %s", countone(pl_pos - 1), pl_count,
  1881.                                   title)
  1882.         end
  1883.         show_message(title)
  1884.     end
  1885.  
  1886.     ne.eventresponder["mbtn_right_up"] =
  1887.         function() show_message(mp.get_property_osd("filename")) end
  1888.  
  1889.     -- playlist buttons
  1890.  
  1891.     -- prev
  1892.     ne = new_element("pl_prev", "button")
  1893.  
  1894.     ne.content = "\238\132\144"
  1895.     ne.enabled = (pl_pos > 1) or (loop ~= "no")
  1896.     ne.eventresponder["mbtn_left_up"] = function()
  1897.         mp.commandv("playlist-prev", "weak")
  1898.         show_message(get_playlist(), 3)
  1899.     end
  1900.     ne.eventresponder["shift+mbtn_left_up"] =
  1901.         function() show_message(get_playlist(), 3) end
  1902.     ne.eventresponder["mbtn_right_up"] =
  1903.         function() show_message(get_playlist(), 3) end
  1904.  
  1905.     -- next
  1906.     ne = new_element("pl_next", "button")
  1907.  
  1908.     ne.content = "\238\132\129"
  1909.     ne.enabled = (have_pl and (pl_pos < pl_count)) or (loop ~= "no")
  1910.     ne.eventresponder["mbtn_left_up"] = function()
  1911.         mp.commandv("playlist-next", "weak")
  1912.         show_message(get_playlist(), 3)
  1913.     end
  1914.     ne.eventresponder["shift+mbtn_left_up"] =
  1915.         function() show_message(get_playlist(), 3) end
  1916.     ne.eventresponder["mbtn_right_up"] =
  1917.         function() show_message(get_playlist(), 3) end
  1918.  
  1919.     -- big buttons
  1920.  
  1921.     -- playpause
  1922.     ne = new_element("playpause", "button")
  1923.  
  1924.     ne.content = function()
  1925.         if mp.get_property("pause") == "yes" then
  1926.             return ("\238\132\129")
  1927.         else
  1928.             return ("\238\128\130")
  1929.         end
  1930.     end
  1931.     ne.eventresponder["mbtn_left_up"] = function()
  1932.         mp.commandv("cycle", "pause")
  1933.     end
  1934.  
  1935.     -- skipback
  1936.     ne = new_element("skipback", "button")
  1937.  
  1938.     ne.softrepeat = true
  1939.     ne.content = "\238\128\132"
  1940.     ne.eventresponder["mbtn_left_down"] =
  1941.         function() mp.commandv("seek", -5, "relative", "keyframes") end
  1942.     ne.eventresponder["shift+mbtn_left_down"] =
  1943.         function() mp.commandv("frame-back-step") end
  1944.     ne.eventresponder["mbtn_right_down"] =
  1945.         function() mp.commandv("seek", -30, "relative", "keyframes") end
  1946.  
  1947.     -- skipfrwd
  1948.     ne = new_element("skipfrwd", "button")
  1949.  
  1950.     ne.softrepeat = true
  1951.     ne.content = "\238\128\133"
  1952.     ne.eventresponder["mbtn_left_down"] =
  1953.         function() mp.commandv("seek", 10, "relative", "keyframes") end
  1954.     ne.eventresponder["shift+mbtn_left_down"] =
  1955.         function() mp.commandv("frame-step") end
  1956.     ne.eventresponder["mbtn_right_down"] =
  1957.         function() mp.commandv("seek", 60, "relative", "keyframes") end
  1958.  
  1959.     -- ch_prev
  1960.     ne = new_element("ch_prev", "button")
  1961.  
  1962.     ne.enabled = have_ch
  1963.     ne.content = "\238\132\132"
  1964.     ne.eventresponder["mbtn_left_up"] = function()
  1965.         mp.commandv("add", "chapter", -1)
  1966.         show_message(get_chapterlist(), 3)
  1967.     end
  1968.     ne.eventresponder["shift+mbtn_left_up"] =
  1969.         function() show_message(get_chapterlist(), 3) end
  1970.     ne.eventresponder["mbtn_right_up"] =
  1971.         function() show_message(get_chapterlist(), 3) end
  1972.  
  1973.     -- ch_next
  1974.     ne = new_element("ch_next", "button")
  1975.  
  1976.     ne.enabled = have_ch
  1977.     ne.content = "\238\132\133"
  1978.     ne.eventresponder["mbtn_left_up"] = function()
  1979.         mp.commandv("add", "chapter", 1)
  1980.         show_message(get_chapterlist(), 3)
  1981.     end
  1982.     ne.eventresponder["shift+mbtn_left_up"] =
  1983.         function() show_message(get_chapterlist(), 3) end
  1984.     ne.eventresponder["mbtn_right_up"] =
  1985.         function() show_message(get_chapterlist(), 3) end
  1986.  
  1987.     --
  1988.     update_tracklist()
  1989.  
  1990.     -- cy_audio
  1991.     ne = new_element("cy_audio", "button")
  1992.  
  1993.     ne.enabled = (#tracks_osc.audio > 0)
  1994.     ne.content = function()
  1995.         local aid = "–"
  1996.         if not (get_track("audio") == 0) then aid = get_track("audio") end
  1997.         return ("\238\132\134" .. osc_styles.smallButtonsLlabel .. " " .. aid ..
  1998.                    "/" .. #tracks_osc.audio)
  1999.     end
  2000.     ne.eventresponder["mbtn_left_up"] = function() set_track("audio", 1) end
  2001.     ne.eventresponder["mbtn_right_up"] = function() set_track("audio", -1) end
  2002.     ne.eventresponder["shift+mbtn_left_down"] =
  2003.         function() show_message(get_tracklist("audio"), 2) end
  2004.  
  2005.     -- cy_sub
  2006.     ne = new_element("cy_sub", "button")
  2007.  
  2008.     ne.enabled = (#tracks_osc.sub > 0)
  2009.     ne.content = function()
  2010.         local sid = "–"
  2011.         if not (get_track("sub") == 0) then sid = get_track("sub") end
  2012.         return ("\238\132\135" .. osc_styles.smallButtonsLlabel .. " " .. sid ..
  2013.                    "/" .. #tracks_osc.sub)
  2014.     end
  2015.     ne.eventresponder["mbtn_left_up"] = function() set_track("sub", 1) end
  2016.     ne.eventresponder["mbtn_right_up"] = function() set_track("sub", -1) end
  2017.     ne.eventresponder["shift+mbtn_left_down"] =
  2018.         function() show_message(get_tracklist("sub"), 2) end
  2019.  
  2020.     -- tog_fs
  2021.     ne = new_element("tog_fs", "button")
  2022.     ne.content = function()
  2023.         if (state.fullscreen) then
  2024.             return ("\238\132\137")
  2025.         else
  2026.             return ("\238\132\136")
  2027.         end
  2028.     end
  2029.     ne.eventresponder["mbtn_left_up"] = function()
  2030.         mp.commandv("cycle", "fullscreen")
  2031.     end
  2032.  
  2033.     -- seekbar
  2034.     ne = new_element("seekbar", "slider")
  2035.  
  2036.     ne.enabled = not (mp.get_property("percent-pos") == nil)
  2037.     ne.slider.markerF = function()
  2038.         local duration = mp.get_property_number("duration", nil)
  2039.         if not (duration == nil) then
  2040.             local chapters = mp.get_property_native("chapter-list", {})
  2041.             local markers = {}
  2042.             for n = 1, #chapters do
  2043.                 markers[n] = (chapters[n].time / duration * 100)
  2044.             end
  2045.             return markers
  2046.         else
  2047.             return {}
  2048.         end
  2049.     end
  2050.     ne.slider.posF = function()
  2051.         return mp.get_property_number("percent-pos", nil)
  2052.     end
  2053.     ne.slider.tooltipF = function(pos)
  2054.         local duration = mp.get_property_number("duration", nil)
  2055.         if not ((duration == nil) or (pos == nil)) then
  2056.             possec = duration * (pos / 100)
  2057.             return mp.format_time(possec)
  2058.         else
  2059.             return ""
  2060.         end
  2061.     end
  2062.     ne.slider.seekRangesF = function()
  2063.         if user_opts.seekrangestyle == "none" then return nil end
  2064.         local cache_state = mp.get_property_native("demuxer-cache-state", nil)
  2065.         if not cache_state then return nil end
  2066.         local duration = mp.get_property_number("duration", nil)
  2067.         if (duration == nil) or duration <= 0 then return nil end
  2068.         local ranges = cache_state["seekable-ranges"]
  2069.         for _, range in pairs(ranges) do
  2070.             range["start"] = 100 * range["start"] / duration
  2071.             range["end"] = 100 * range["end"] / duration
  2072.         end
  2073.         if #ranges == 0 then return nil end
  2074.         return ranges
  2075.     end
  2076.     ne.eventresponder["mouse_move"] = -- keyframe seeking when mouse is dragged
  2077.     function(element)
  2078.         -- mouse move events may pile up during seeking and may still get
  2079.         -- sent when the user is done seeking, so we need to throw away
  2080.         -- identical seeks
  2081.         local seekto = get_slider_value(element)
  2082.         if (element.state.lastseek == nil) or
  2083.             (not (element.state.lastseek == seekto)) then
  2084.             mp.commandv("seek", seekto, "absolute-percent",
  2085.                         user_opts.seekbarkeyframes and "keyframes" or "exact")
  2086.             element.state.lastseek = seekto
  2087.         end
  2088.  
  2089.     end
  2090.     ne.eventresponder["mbtn_left_down"] = -- exact seeks on single clicks
  2091.     function(element)
  2092.         mp.commandv("seek", get_slider_value(element), "absolute-percent",
  2093.                     "exact")
  2094.     end
  2095.     ne.eventresponder["reset"] = function(element)
  2096.         element.state.lastseek = nil
  2097.     end
  2098.  
  2099.     -- tc_left (current pos)
  2100.     ne = new_element("tc_left", "button")
  2101.  
  2102.     ne.content = function()
  2103.         if (state.tc_ms) then
  2104.             return (mp.get_property_osd("playback-time/full"))
  2105.         else
  2106.             return (mp.get_property_osd("playback-time"))
  2107.         end
  2108.     end
  2109.     ne.eventresponder["mbtn_left_up"] = function()
  2110.         state.tc_ms = not state.tc_ms
  2111.         request_init()
  2112.     end
  2113.  
  2114.     -- tc_right (total/remaining time)
  2115.     ne = new_element("tc_right", "button")
  2116.  
  2117.     ne.visible = (mp.get_property_number("duration", 0) > 0)
  2118.     ne.content = function()
  2119.         if (state.rightTC_trem) then
  2120.             if state.tc_ms then
  2121.                 return ("-" .. mp.get_property_osd("playtime-remaining/full"))
  2122.             else
  2123.                 return ("-" .. mp.get_property_osd("playtime-remaining"))
  2124.             end
  2125.         else
  2126.             if state.tc_ms then
  2127.                 return (mp.get_property_osd("duration/full"))
  2128.             else
  2129.                 return (mp.get_property_osd("duration"))
  2130.             end
  2131.         end
  2132.     end
  2133.     ne.eventresponder["mbtn_left_up"] = function()
  2134.         state.rightTC_trem = not state.rightTC_trem
  2135.     end
  2136.  
  2137.     -- cache
  2138.     ne = new_element("cache", "button")
  2139.  
  2140.     ne.content = function()
  2141.         local cache_state = mp.get_property_native("demuxer-cache-state", {})
  2142.         if not (cache_state["seekable-ranges"] and
  2143.             #cache_state["seekable-ranges"] > 0) then
  2144.             -- probably not a network stream
  2145.             return ""
  2146.         end
  2147.         local dmx_cache = mp.get_property_number("demuxer-cache-duration")
  2148.         if dmx_cache and
  2149.             (dmx_cache > state.dmx_cache * 1.1 or dmx_cache < state.dmx_cache *
  2150.                 0.9) then
  2151.             state.dmx_cache = dmx_cache
  2152.         else
  2153.             dmx_cache = state.dmx_cache
  2154.         end
  2155.         local min = math.floor(dmx_cache / 60)
  2156.         local sec = dmx_cache % 60
  2157.         return "Cache: " ..
  2158.                    (min > 0 and string.format("%sm%02.0fs", min, sec) or
  2159.                        string.format("%3.0fs", dmx_cache))
  2160.     end
  2161.  
  2162.     -- volume
  2163.     ne = new_element("volume", "button")
  2164.  
  2165.     ne.content = function()
  2166.         local volume = mp.get_property_number("volume", 0)
  2167.         local mute = mp.get_property_native("mute")
  2168.         local volicon = {
  2169.             "\238\132\139", "\238\132\140", "\238\132\141", "\238\132\142"
  2170.         }
  2171.         if volume == 0 or mute then
  2172.             return "\238\132\138"
  2173.         else
  2174.             return volicon[math.min(4, math.ceil(volume / (100 / 3)))]
  2175.         end
  2176.     end
  2177.     ne.eventresponder["mbtn_left_up"] = function()
  2178.         mp.commandv("cycle", "mute")
  2179.     end
  2180.  
  2181.     ne.eventresponder["wheel_up_press"] =
  2182.         function() mp.commandv("osd-auto", "add", "volume", 5) end
  2183.     ne.eventresponder["wheel_down_press"] =
  2184.         function() mp.commandv("osd-auto", "add", "volume", -5) end
  2185.  
  2186.     -- load layout
  2187.     layouts[user_opts.layout]()
  2188.  
  2189.     -- do something with the elements
  2190.     prepare_elements()
  2191.  
  2192. end
  2193.  
  2194. --
  2195. -- Other important stuff
  2196. --
  2197.  
  2198. function show_osc()
  2199.     -- show when disabled can happen (e.g. mouse_move) due to async/delayed unbinding
  2200.     if not state.enabled then return end
  2201.  
  2202.     msg.trace("show_osc")
  2203.     -- remember last time of invocation (mouse move)
  2204.     state.showtime = mp.get_time()
  2205.  
  2206.     osc_visible(true)
  2207.  
  2208.     if (user_opts.fadeduration > 0) then state.anitype = nil end
  2209. end
  2210.  
  2211. function hide_osc()
  2212.     msg.trace("hide_osc")
  2213.     if not state.enabled then
  2214.         -- typically hide happens at render() from tick(), but now tick() is
  2215.         -- no-op and won't render again to remove the osc, so do that manually.
  2216.         state.osc_visible = false
  2217.         timer_stop()
  2218.         render_wipe()
  2219.     elseif (user_opts.fadeduration > 0) then
  2220.         if not (state.osc_visible == false) then
  2221.             state.anitype = "out"
  2222.             control_timer()
  2223.         end
  2224.     else
  2225.         osc_visible(false)
  2226.     end
  2227. end
  2228.  
  2229. function osc_visible(visible)
  2230.     state.osc_visible = visible
  2231.     control_timer()
  2232. end
  2233.  
  2234. function pause_state(name, enabled)
  2235.     state.paused = enabled
  2236.     control_timer()
  2237. end
  2238.  
  2239. function cache_state(name, idle)
  2240.     state.cache_idle = idle
  2241.     control_timer()
  2242. end
  2243.  
  2244. function control_timer()
  2245.     if (state.paused) and (state.osc_visible) and
  2246.         (not (state.cache_idle) or not (state.anitype == nil)) then
  2247.  
  2248.         timer_start()
  2249.     else
  2250.         timer_stop()
  2251.     end
  2252. end
  2253.  
  2254. function timer_start()
  2255.     if not (state.timer_active) then
  2256.         msg.trace("timer start")
  2257.  
  2258.         if (state.timer == nil) then
  2259.             -- create new timer
  2260.             state.timer = mp.add_periodic_timer(0.03, tick)
  2261.         else
  2262.             -- resume existing one
  2263.             state.timer:resume()
  2264.         end
  2265.  
  2266.         state.timer_active = true
  2267.     end
  2268. end
  2269.  
  2270. function timer_stop()
  2271.     if (state.timer_active) then
  2272.         msg.trace("timer stop")
  2273.  
  2274.         if not (state.timer == nil) then
  2275.             -- kill timer
  2276.             state.timer:kill()
  2277.         end
  2278.  
  2279.         state.timer_active = false
  2280.     end
  2281. end
  2282.  
  2283. function mouse_leave()
  2284.     if user_opts.hidetimeout >= 0 then hide_osc() end
  2285.     -- reset mouse position
  2286.     state.last_mouseX, state.last_mouseY = nil, nil
  2287. end
  2288.  
  2289. function request_init() state.initREQ = true end
  2290.  
  2291. function render_wipe()
  2292.     msg.trace("render_wipe()")
  2293.     mp.set_osd_ass(0, 0, "{}")
  2294. end
  2295.  
  2296. function render()
  2297.     msg.trace("rendering")
  2298.     local current_screen_sizeX, current_screen_sizeY, aspect = mp.get_osd_size()
  2299.     local mouseX, mouseY = get_virt_mouse_pos()
  2300.     local now = mp.get_time()
  2301.  
  2302.     -- check if display changed, if so request reinit
  2303.     if not (state.mp_screen_sizeX == current_screen_sizeX and
  2304.         state.mp_screen_sizeY == current_screen_sizeY) then
  2305.  
  2306.         request_init()
  2307.  
  2308.         state.mp_screen_sizeX = current_screen_sizeX
  2309.         state.mp_screen_sizeY = current_screen_sizeY
  2310.     end
  2311.  
  2312.     -- init management
  2313.     if state.initREQ then
  2314.         osc_init()
  2315.         state.initREQ = false
  2316.  
  2317.         -- store initial mouse position
  2318.         if (state.last_mouseX == nil or state.last_mouseY == nil) and
  2319.             not (mouseX == nil or mouseY == nil) then
  2320.  
  2321.             state.last_mouseX, state.last_mouseY = mouseX, mouseY
  2322.         end
  2323.     end
  2324.  
  2325.     -- fade animation
  2326.     if not (state.anitype == nil) then
  2327.  
  2328.         if (state.anistart == nil) then state.anistart = now end
  2329.  
  2330.         if (now < state.anistart + (user_opts.fadeduration / 1000)) then
  2331.  
  2332.             if (state.anitype == "in") then -- fade in
  2333.                 osc_visible(true)
  2334.                 state.animation = scale_value(state.anistart, (state.anistart +
  2335.                                                   (user_opts.fadeduration / 1000)),
  2336.                                               255, 0, now)
  2337.             elseif (state.anitype == "out") then -- fade out
  2338.                 state.animation = scale_value(state.anistart, (state.anistart +
  2339.                                                   (user_opts.fadeduration / 1000)),
  2340.                                               0, 255, now)
  2341.             end
  2342.  
  2343.         else
  2344.             if (state.anitype == "out") then osc_visible(false) end
  2345.             state.anistart = nil
  2346.             state.animation = nil
  2347.             state.anitype = nil
  2348.         end
  2349.     else
  2350.         state.anistart = nil
  2351.         state.animation = nil
  2352.         state.anitype = nil
  2353.     end
  2354.  
  2355.     -- mouse show/hide area
  2356.     for k, cords in pairs(osc_param.areas["showhide"]) do
  2357.         set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "showhide")
  2358.     end
  2359.     do_enable_keybindings()
  2360.  
  2361.     -- mouse input area
  2362.     local mouse_over_osc = false
  2363.  
  2364.     for _, cords in ipairs(osc_param.areas["input"]) do
  2365.         if state.osc_visible then -- activate only when OSC is actually visible
  2366.             set_virt_mouse_area(cords.x1, cords.y1, cords.x2, cords.y2, "input")
  2367.         end
  2368.         if state.osc_visible ~= state.input_enabled then
  2369.             if state.osc_visible then
  2370.                 mp.enable_key_bindings("input")
  2371.             else
  2372.                 mp.disable_key_bindings("input")
  2373.             end
  2374.             state.input_enabled = state.osc_visible
  2375.         end
  2376.  
  2377.         if (mouse_hit_coords(cords.x1, cords.y1, cords.x2, cords.y2)) then
  2378.             mouse_over_osc = true
  2379.         end
  2380.     end
  2381.  
  2382.     -- autohide
  2383.     if not (state.showtime == nil) and (user_opts.hidetimeout >= 0) and
  2384.         (state.showtime + (user_opts.hidetimeout / 1000) < now) and
  2385.         (state.active_element == nil) and not (mouse_over_osc) then
  2386.  
  2387.         hide_osc()
  2388.     end
  2389.  
  2390.     -- actual rendering
  2391.     local ass = assdraw.ass_new()
  2392.  
  2393.     -- Messages
  2394.     render_message(ass)
  2395.  
  2396.     -- actual OSC
  2397.     if state.osc_visible then render_elements(ass) end
  2398.  
  2399.     -- submit
  2400.     mp.set_osd_ass(osc_param.playresy * osc_param.display_aspect,
  2401.                    osc_param.playresy, ass.text)
  2402.  
  2403. end
  2404.  
  2405. --
  2406. -- Eventhandling
  2407. --
  2408.  
  2409. local function element_has_action(element, action)
  2410.     return element and element.eventresponder and element.eventresponder[action]
  2411. end
  2412.  
  2413. function process_event(source, what)
  2414.     local action = string.format("%s%s", source, what and ("_" .. what) or "")
  2415.  
  2416.     if what == "down" or what == "press" then
  2417.  
  2418.         for n = 1, #elements do
  2419.  
  2420.             if mouse_hit(elements[n]) and elements[n].eventresponder and
  2421.                 (elements[n].eventresponder[source .. "_up"] or
  2422.                     elements[n].eventresponder[action]) then
  2423.  
  2424.                 if what == "down" then
  2425.                     state.active_element = n
  2426.                     state.active_event_source = source
  2427.                 end
  2428.                 -- fire the down or press event if the element has one
  2429.                 if element_has_action(elements[n], action) then
  2430.                     elements[n].eventresponder[action](elements[n])
  2431.                 end
  2432.  
  2433.             end
  2434.         end
  2435.  
  2436.     elseif what == "up" then
  2437.  
  2438.         if elements[state.active_element] then
  2439.             local n = state.active_element
  2440.  
  2441.             if n == 0 then
  2442.                 -- click on background (does not work)
  2443.             elseif element_has_action(elements[n], action) and
  2444.                 mouse_hit(elements[n]) then
  2445.  
  2446.                 elements[n].eventresponder[action](elements[n])
  2447.             end
  2448.  
  2449.             -- reset active element
  2450.             if element_has_action(elements[n], "reset") then
  2451.                 elements[n].eventresponder["reset"](elements[n])
  2452.             end
  2453.  
  2454.         end
  2455.         state.active_element = nil
  2456.         state.mouse_down_counter = 0
  2457.  
  2458.     elseif source == "mouse_move" then
  2459.  
  2460.         local mouseX, mouseY = get_virt_mouse_pos()
  2461.         if (user_opts.minmousemove == 0) or
  2462.             (not ((state.last_mouseX == nil) or (state.last_mouseY == nil)) and
  2463.                 ((math.abs(mouseX - state.last_mouseX) >= user_opts.minmousemove) or
  2464.                     (math.abs(mouseY - state.last_mouseY) >=
  2465.                         user_opts.minmousemove))) then show_osc() end
  2466.         state.last_mouseX, state.last_mouseY = mouseX, mouseY
  2467.  
  2468.         local n = state.active_element
  2469.         if element_has_action(elements[n], action) then
  2470.             elements[n].eventresponder[action](elements[n])
  2471.         end
  2472.         tick()
  2473.     end
  2474. end
  2475.  
  2476. -- called by mpv on every frame
  2477. function tick()
  2478.     if (not state.enabled) then return end
  2479.  
  2480.     if (state.idle) then
  2481.  
  2482.         -- render idle message
  2483.         msg.trace("idle message")
  2484.         local _, _, display_aspect = mp.get_osd_size()
  2485.         local display_h = 360
  2486.         local display_w = display_h * display_aspect
  2487.         -- logo is rendered at 2^(6-1) = 32 times resolution with size 1800x1800
  2488.         local icon_x, icon_y = (display_w - 1800 / 32) / 2, 140
  2489.  
  2490.         local ass = assdraw.ass_new()
  2491.         ass:new_event()
  2492.         ass:pos(icon_x, icon_y)
  2493.         ass:append(
  2494.             "{\\rDefault\\an7\\c&H430142&\\1a&H00&\\bord0\\shad0\\p6}m 1605 828 b 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200 1324 200 1605 482 1605 828{\\p0}")
  2495.         ass:new_event()
  2496.         ass:pos(icon_x, icon_y)
  2497.         ass:append(
  2498.             "{\\rDefault\\an7\\c&HDDDBDD&\\1a&H00&\\bord0\\shad0\\p6}m 1296 910 b 1296 1131 1117 1310 897 1310 676 1310 497 1131 497 910 497 689 676 511 897 511 1117 511 1296 689 1296 910{\\p0}")
  2499.         ass:new_event()
  2500.         ass:pos(icon_x, icon_y)
  2501.         ass:append(
  2502.             "{\\rDefault\\an7\\c&H691F69&\\1a&H00&\\bord0\\shad0\\p6}m 762 1113 l 762 708 b 881 776 1000 843 1119 911 1000 978 881 1046 762 1113{\\p0}")
  2503.         ass:new_event()
  2504.         ass:pos(icon_x, icon_y)
  2505.         ass:append(
  2506.             "{\\rDefault\\an7\\c&H682167&\\1a&H00&\\bord0\\shad0\\p6}m 925 42 b 463 42 87 418 87 880 87 1343 463 1718 925 1718 1388 1718 1763 1343 1763 880 1763 418 1388 42 925 42 m 925 42 m 977 200 b 1324 200 1605 482 1605 828 1605 1175 1324 1456 977 1456 631 1456 349 1175 349 828 349 482 631 200 977 200{\\p0}")
  2507.         ass:new_event()
  2508.         ass:pos(icon_x, icon_y)
  2509.         ass:append(
  2510.             "{\\rDefault\\an7\\c&H753074&\\1a&H00&\\bord0\\shad0\\p6}m 977 198 b 630 198 348 480 348 828 348 1176 630 1458 977 1458 1325 1458 1607 1176 1607 828 1607 480 1325 198 977 198 m 977 198 m 977 202 b 1323 202 1604 483 1604 828 1604 1174 1323 1454 977 1454 632 1454 351 1174 351 828 351 483 632 202 977 202{\\p0}")
  2511.         ass:new_event()
  2512.         ass:pos(icon_x, icon_y)
  2513.         ass:append(
  2514.             "{\\rDefault\\an7\\c&HE5E5E5&\\1a&H00&\\bord0\\shad0\\p6}m 895 10 b 401 10 0 410 0 905 0 1399 401 1800 895 1800 1390 1800 1790 1399 1790 905 1790 410 1390 10 895 10 m 895 10 m 925 42 b 1388 42 1763 418 1763 880 1763 1343 1388 1718 925 1718 463 1718 87 1343 87 880 87 418 463 42 925 42{\\p0}")
  2515.         ass:new_event()
  2516.         ass:pos(display_w / 2, icon_y + 65)
  2517.         ass:an(8)
  2518.         ass:append("Drop files or URLs to play here.")
  2519.         mp.set_osd_ass(display_w, display_h, ass.text)
  2520.  
  2521.         if state.showhide_enabled then
  2522.             mp.disable_key_bindings("showhide")
  2523.             state.showhide_enabled = false
  2524.         end
  2525.  
  2526.     elseif (state.fullscreen and user_opts.showfullscreen) or
  2527.         (not state.fullscreen and user_opts.showwindowed) then
  2528.  
  2529.         -- render the OSC
  2530.         render()
  2531.     else
  2532.         -- Flush OSD
  2533.         mp.set_osd_ass(osc_param.playresy, osc_param.playresy, "")
  2534.     end
  2535. end
  2536.  
  2537. function do_enable_keybindings()
  2538.     if state.enabled then
  2539.         if not state.showhide_enabled then
  2540.             mp.enable_key_bindings("showhide",
  2541.                                    "allow-vo-dragging+allow-hide-cursor")
  2542.         end
  2543.         state.showhide_enabled = true
  2544.     end
  2545. end
  2546.  
  2547. function enable_osc(enable)
  2548.     state.enabled = enable
  2549.     if enable then
  2550.         do_enable_keybindings()
  2551.     else
  2552.         hide_osc() -- acts immediately when state.enabled == false
  2553.         if state.showhide_enabled then
  2554.             mp.disable_key_bindings("showhide")
  2555.         end
  2556.         state.showhide_enabled = false
  2557.     end
  2558. end
  2559.  
  2560. validate_user_opts()
  2561.  
  2562. mp.register_event("start-file", request_init)
  2563. mp.observe_property("osc", "bool", function(name, value)
  2564.     if value == true then
  2565.         mp.set_property("osc", "no")
  2566.     end
  2567. end)
  2568. mp.register_event("tracks-changed", request_init)
  2569. mp.observe_property("playlist", nil, request_init)
  2570.  
  2571. mp.register_script_message("osc-message", show_message)
  2572. mp.register_script_message("osc-chapterlist", function(dur)
  2573.     show_message(get_chapterlist(), dur)
  2574. end)
  2575. mp.register_script_message("osc-playlist",
  2576.                            function(dur) show_message(get_playlist(), dur) end)
  2577. mp.register_script_message("osc-tracklist", function(dur)
  2578.     local msg = {}
  2579.     for k, v in pairs(nicetypes) do table.insert(msg, get_tracklist(k)) end
  2580.     show_message(table.concat(msg, '\n\n'), dur)
  2581. end)
  2582.  
  2583. mp.observe_property("fullscreen", "bool", function(name, val)
  2584.     state.fullscreen = val
  2585.     request_init()
  2586. end)
  2587. mp.observe_property("idle-active", "bool", function(name, val)
  2588.     state.idle = val
  2589.     tick()
  2590. end)
  2591. mp.observe_property("pause", "bool", pause_state)
  2592. mp.observe_property("cache-idle", "bool", cache_state)
  2593. mp.observe_property("vo-configured", "bool", function(name, val)
  2594.     if val then
  2595.         mp.register_event("tick", tick)
  2596.     else
  2597.         mp.unregister_event(tick)
  2598.     end
  2599. end)
  2600.  
  2601. -- mouse show/hide bindings
  2602. mp.set_key_bindings({
  2603.     {"mouse_move", function(e) process_event("mouse_move", nil) end},
  2604.     {"mouse_leave", mouse_leave}
  2605. }, "showhide", "force")
  2606. do_enable_keybindings()
  2607.  
  2608. -- mouse input bindings
  2609. mp.set_key_bindings({
  2610.     {
  2611.         "mbtn_left", function(e) process_event("mbtn_left", "up") end,
  2612.         function(e) process_event("mbtn_left", "down") end
  2613.     }, {
  2614.         "shift+mbtn_left",
  2615.         function(e) process_event("shift+mbtn_left", "up") end,
  2616.         function(e) process_event("shift+mbtn_left", "down") end
  2617.     }, {
  2618.         "mbtn_right", function(e) process_event("mbtn_right", "up") end,
  2619.         function(e) process_event("mbtn_right", "down") end
  2620.     }, {"wheel_up", function(e) process_event("wheel_up", "press") end},
  2621.     {"wheel_down", function(e) process_event("wheel_down", "press") end},
  2622.     {"mbtn_left_dbl", "ignore"}, {"shift+mbtn_left_dbl", "ignore"},
  2623.     {"mbtn_right_dbl", "ignore"}
  2624. }, "input", "force")
  2625. mp.enable_key_bindings("input")
  2626.  
  2627. user_opts.hidetimeout_orig = user_opts.hidetimeout
  2628.  
  2629. function always_on(val)
  2630.     if val then
  2631.         user_opts.hidetimeout = -1 -- disable autohide
  2632.         if state.enabled then show_osc() end
  2633.     else
  2634.         user_opts.hidetimeout = user_opts.hidetimeout_orig
  2635.         if state.enabled then hide_osc() end
  2636.     end
  2637. end
  2638.  
  2639. -- mode can be auto/always/never/cycle
  2640. -- the modes only affect internal variables and not stored on its own.
  2641. function visibility_mode(mode, no_osd)
  2642.     if mode == "cycle" then
  2643.         if not state.enabled then
  2644.             mode = "auto"
  2645.         elseif user_opts.hidetimeout >= 0 then
  2646.             mode = "always"
  2647.         else
  2648.             mode = "never"
  2649.         end
  2650.     end
  2651.  
  2652.     if mode == "auto" then
  2653.         always_on(false)
  2654.         enable_osc(true)
  2655.     elseif mode == "always" then
  2656.         enable_osc(true)
  2657.         always_on(true)
  2658.     elseif mode == "never" then
  2659.         enable_osc(false)
  2660.     else
  2661.         msg.warn("Ignoring unknown visibility mode '" .. mode .. "'")
  2662.         return
  2663.     end
  2664.  
  2665.     if not no_osd and tonumber(mp.get_property("osd-level")) >= 1 then
  2666.         mp.osd_message("OSC visibility: " .. mode)
  2667.     end
  2668. end
  2669.  
  2670. visibility_mode(user_opts.visibility, true)
  2671. mp.register_script_message("osc-visibility", visibility_mode)
  2672. mp.add_key_binding(nil, "visibility", function() visibility_mode("cycle") end)
  2673. mp.register_script_message("thumbfast-info", function(json)
  2674.     local data = utils.parse_json(json)
  2675.     if type(data) ~= "table" or not data.width or not data.height then
  2676.         msg.error("thumbfast-info: received json didn't produce a table with thumbnail information")
  2677.     else
  2678.         thumbfast = data
  2679.     end
  2680. end)
  2681.  
  2682. set_virt_mouse_area(0, 0, 0, 0, "input")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement