diff --git a/scripts/uosc.lua b/scripts/uosc.lua index 894067b..8c64443 100644 --- a/scripts/uosc.lua +++ b/scripts/uosc.lua @@ -296,7 +296,31 @@ end --[[ STATE ]] display = {width = 1280, height = 720, scale_x = 1, scale_y = 1, initialized = false} -cursor = {hidden = true, hover_raw = false, x = 0, y = 0} +cursor = { + x = 0, + y = 0, + hidden = true, + hover_raw = false, + -- Event handlers that are only fired on cursor, bound during render loop. Guidelines: + -- - element activations (clicks) go to `mbtn_left_down` handler + -- - `mbtn_button_up` is only for clearing dragging/swiping + on_primary_down = nil, + on_primary_up = nil, + on_wheel_down = nil, + on_wheel_up = nil, + -- Called at the beginning of each render + reset_handlers = function() + cursor.on_primary_down, cursor.on_primary_up = nil, nil + cursor.on_wheel_down, cursor.on_wheel_up = nil, nil + end, + -- Enables pointer key group captures needed by handlers (called at the end of each render) + decide_keybinds = function() + local mbtn_left_decision = (cursor.on_primary_down or cursor.on_primary_up) and 'enable' or 'disable' + local wheel_decision = (cursor.on_wheel_down or cursor.on_wheel_up) and 'enable' or 'disable' + mp[mbtn_left_decision .. '_key_bindings']('mbtn_left') + mp[wheel_decision .. '_key_bindings']('wheel') + end +} state = { os = (function() if os.getenv('windir') ~= nil then return 'windows' end @@ -771,6 +795,23 @@ mp.observe_property('core-idle', 'native', create_state_setter('core_idle')) --[[ KEY BINDS ]] +-- Pointer related binding groups +mp.set_key_bindings({ + { + 'mbtn_left', + function(...) call_maybe(cursor.on_primary_up, ...) end, + function(...) + update_mouse_pos(nil, mp.get_property_native('mouse-pos')) + call_maybe(cursor.on_primary_down, ...) + end, + }, + {'mbtn_left_dbl', 'ignore'}, +}, 'mbtn_left', 'force') +mp.set_key_bindings({ + {'wheel_up', function(...) call_maybe(cursor.on_wheel_up, ...) end}, + {'wheel_down', function(...) call_maybe(cursor.on_wheel_down, ...) end}, +}, 'wheel', 'force') + -- Adds a key binding that respects rerouting set by `key_binding_overwrites` table. ---@param name string ---@param callback fun(event: table) diff --git a/scripts/uosc_shared/elements/Button.lua b/scripts/uosc_shared/elements/Button.lua index 8d0f725..e57d614 100644 --- a/scripts/uosc_shared/elements/Button.lua +++ b/scripts/uosc_shared/elements/Button.lua @@ -23,19 +23,20 @@ function Button:init(id, props) end function Button:on_coordinates() self.font_size = round((self.by - self.ay) * 0.7) end -function Button:on_mbtn_left_down() - -- Don't accept clicks while hidden. - if self:get_visibility() <= 0 then return end +function Button:handle_cursor_down() -- We delay the callback to next tick, otherwise we are risking race -- conditions as we are in the middle of event dispatching. -- For example, handler might add a menu to the end of the element stack, and that - -- than picks up this click even we are in right now, and instantly closes itself. + -- than picks up this click event we are in right now, and instantly closes itself. mp.add_timeout(0.01, self.on_click) end function Button:render() local visibility = self:get_visibility() if visibility <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() self:handle_cursor_down() end + end local ass = assdraw.ass_new() local is_hover = self.proximity_raw == 0 @@ -54,7 +55,6 @@ function Button:render() -- Tooltip on hover if is_hover and self.tooltip then ass:tooltip(self, self.tooltip) end - -- Badge local icon_clip if self.badge then diff --git a/scripts/uosc_shared/elements/Elements.lua b/scripts/uosc_shared/elements/Elements.lua index f6fba01..186b9e9 100644 --- a/scripts/uosc_shared/elements/Elements.lua +++ b/scripts/uosc_shared/elements/Elements.lua @@ -29,8 +29,6 @@ function Elements:remove(idOrElement) end function Elements:update_proximities() - local capture_mbtn_left = false - local capture_wheel = false local menu_only = Elements.menu ~= nil local mouse_leave_elements = {} local mouse_enter_elements = {} @@ -42,26 +40,13 @@ function Elements:update_proximities() -- If menu is open, all other elements have to be disabled if menu_only then - if element.ignores_menu then - capture_mbtn_left = true - capture_wheel = true - element:update_proximity() - else - element:reset_proximity() - end + if element.ignores_menu then element:update_proximity() + else element:reset_proximity() end else element:update_proximity() end - -- Element has global forced key listeners - if element.on_global_mbtn_left_down then capture_mbtn_left = true end - if element.on_global_wheel_up or element.on_global_wheel_down then capture_wheel = true end - if element.proximity_raw == 0 then - -- Element has local forced key listeners - if element.on_mbtn_left_down then capture_mbtn_left = true end - if element.on_wheel_up or element.on_wheel_up then capture_wheel = true end - -- Mouse entered element area if previous_proximity_raw ~= 0 then mouse_enter_elements[#mouse_enter_elements + 1] = element @@ -75,10 +60,6 @@ function Elements:update_proximities() end end - -- Enable key group captures requested by elements - mp[capture_mbtn_left and 'enable_key_bindings' or 'disable_key_bindings']('mbtn_left') - mp[capture_wheel and 'enable_key_bindings' or 'disable_key_bindings']('wheel') - -- Trigger `mouse_leave` and `mouse_enter` events for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end @@ -142,26 +123,4 @@ end function Elements:has(id) return self[id] ~= nil end function Elements:ipairs() return ipairs(self.itable) end ----@param name string Event name. -function Elements:create_proximity_dispatcher(name) - return function(...) self:proximity_trigger(name, ...) end -end - -mp.set_key_bindings({ - { - 'mbtn_left', - Elements:create_proximity_dispatcher('mbtn_left_up'), - function(...) - update_mouse_pos(nil, mp.get_property_native('mouse-pos')) - Elements:proximity_trigger('mbtn_left_down', ...) - end, - }, - {'mbtn_left_dbl', 'ignore'}, -}, 'mbtn_left', 'force') - -mp.set_key_bindings({ - {'wheel_up', Elements:create_proximity_dispatcher('wheel_up')}, - {'wheel_down', Elements:create_proximity_dispatcher('wheel_down')}, -}, 'wheel', 'force') - return Elements diff --git a/scripts/uosc_shared/elements/Menu.lua b/scripts/uosc_shared/elements/Menu.lua index 82f20dd..fee7b8d 100644 --- a/scripts/uosc_shared/elements/Menu.lua +++ b/scripts/uosc_shared/elements/Menu.lua @@ -518,7 +518,7 @@ end function Menu:on_display() self:update_dimensions() end function Menu:on_prop_fullormaxed() self:update_content_dimensions() end -function Menu:on_global_mbtn_left_down() +function Menu:handle_cursor_down() if self.proximity_raw == 0 then self.drag_data = {{y = cursor.y, time = mp.get_time()}} self.current.fling = nil @@ -538,7 +538,7 @@ function Menu:fling_distance() return #self.drag_data < 2 and 0 or ((first.y - last.y) / ((first.time - last.time) / 0.03)) * 10 end -function Menu:on_global_mbtn_left_up() +function Menu:handle_cursor_up() if self.proximity_raw == 0 and self.drag_data and not self.is_dragging then self:select_item_below_cursor() self:open_selected_item({preselect_submenu_item = false, keep_open = self.modifiers and self.modifiers.shift}) @@ -570,8 +570,8 @@ function Menu:on_global_mouse_move() request_render() end -function Menu:on_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end -function Menu:on_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end +function Menu:handle_wheel_up() self:scroll_by(self.scroll_step * -3, nil, {update_cursor = true}) end +function Menu:handle_wheel_down() self:scroll_by(self.scroll_step * 3, nil, {update_cursor = true}) end function Menu:on_pgup() local menu = self.current @@ -647,8 +647,8 @@ function Menu:create_modified_mbtn_left_handler(modifiers) return function() self.mouse_nav = true self.modifiers = modifiers - self:on_global_mbtn_left_down() - self:on_global_mbtn_left_up() + self:handle_cursor_down() + self:handle_cursor_up() self.modifiers = nil end end @@ -677,6 +677,13 @@ function Menu:render() end if update_cursor then self:select_item_below_cursor() end + cursor.on_primary_down = function() self:handle_cursor_down() end + cursor.on_primary_up = function() self:handle_cursor_up() end + if self.proximity_raw == 0 then + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + local ass = assdraw.ass_new() local opacity = options.menu_opacity * self.opacity local spacing = self.item_padding diff --git a/scripts/uosc_shared/elements/Speed.lua b/scripts/uosc_shared/elements/Speed.lua index 9681ccf..6ea5097 100644 --- a/scripts/uosc_shared/elements/Speed.lua +++ b/scripts/uosc_shared/elements/Speed.lua @@ -44,9 +44,7 @@ function Speed:speed_step(speed, up) end end -function Speed:on_mbtn_left_down() - -- Don't accept clicks while hidden. - if self:get_visibility() <= 0 then return end +function Speed:handle_cursor_down() self:tween_stop() -- Stop and cleanup possible ongoing animations self.dragging = { start_time = mp.get_time(), @@ -87,14 +85,13 @@ function Speed:on_global_mouse_move() end end -function Speed:on_mbtn_left_up() - -- Reset speed on short clicks - if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then - mp.set_property_native('speed', 1) +function Speed:handle_cursor_up() + if self.proximity_raw == 0 then + -- Reset speed on short clicks + if self.dragging and math.abs(self.dragging.distance) < 6 and mp.get_time() - self.dragging.start_time < 0.15 then + mp.set_property_native('speed', 1) + end end -end - -function Speed:on_global_mbtn_left_up() self.dragging = nil request_render() end @@ -104,8 +101,8 @@ function Speed:on_global_mouse_leave() request_render() end -function Speed:on_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end -function Speed:on_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end +function Speed:handle_wheel_up() mp.set_property_native('speed', self:speed_step(state.speed, true)) end +function Speed:handle_wheel_down() mp.set_property_native('speed', self:speed_step(state.speed, false)) end function Speed:render() local visibility = self:get_visibility() @@ -113,6 +110,18 @@ function Speed:render() if opacity <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() + self:handle_cursor_down() + cursor.on_primary_up = function() self:handle_cursor_up() end + end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + if self.dragging then + cursor.on_primary_up = function() self:handle_cursor_up() end + end + local ass = assdraw.ass_new() -- Background diff --git a/scripts/uosc_shared/elements/Timeline.lua b/scripts/uosc_shared/elements/Timeline.lua index 490e7e1..cdca43b 100644 --- a/scripts/uosc_shared/elements/Timeline.lua +++ b/scripts/uosc_shared/elements/Timeline.lua @@ -6,6 +6,7 @@ local Timeline = class(Element) function Timeline:new() return Class.new(self) --[[@as Timeline]] end function Timeline:init() Element.init(self, 'timeline') + ---@type false|{pause: boolean} self.pressed = false self.obstructed = false self.size_max = 0 @@ -13,7 +14,8 @@ function Timeline:init() self.size_min_override = options.timeline_start_hidden and 0 or nil self.font_size = 0 self.top_border = options.timeline_border - self.hovered_chapter = nil + self.is_hovered = false + self.has_thumbnail = false -- Release any dragging when file gets unloaded mp.register_event('end-file', function() self.pressed = false end) @@ -44,7 +46,7 @@ function Timeline:get_effective_line_width() return state.fullormaxed and options.timeline_line_width_fullscreen or options.timeline_line_width end -function Timeline:get_is_hovered() return self.enabled and (self.proximity_raw == 0 or self.hovered_chapter ~= nil) end +function Timeline:get_is_hovered() return self.enabled and self.is_hovered end function Timeline:update_dimensions() if state.fullormaxed then @@ -91,41 +93,20 @@ function Timeline:set_from_cursor(fast) end function Timeline:clear_thumbnail() mp.commandv('script-message-to', 'thumbfast', 'clear') end -function Timeline:determine_chapter_click_handler() - if self.hovered_chapter then - if not self.on_global_mbtn_left_down then - self.on_global_mbtn_left_down = function() - if self.hovered_chapter then mp.commandv('seek', self.hovered_chapter.time, 'absolute+exact') end - end - end - else - if self.on_global_mbtn_left_down then - self.on_global_mbtn_left_down = nil - if self.proximity_raw ~= 0 then self:clear_thumbnail() end - end - end -end - -function Timeline:on_mbtn_left_down() - -- `self.on_global_mbtn_left_down` has precedent - if self.on_global_mbtn_left_down then return end - - self.pressed = true - self.pressed_pause = state.pause +function Timeline:handle_cursor_down() + self.pressed = {pause = state.pause} mp.set_property_native('pause', true) self:set_from_cursor() + cursor.on_primary_up = function() self:handle_cursor_up() end end function Timeline:on_prop_duration() self:decide_enabled() end function Timeline:on_prop_time() self:decide_enabled() end function Timeline:on_prop_border() self:update_dimensions() end function Timeline:on_prop_fullormaxed() self:update_dimensions() end function Timeline:on_display() self:update_dimensions() end -function Timeline:on_mouse_leave() - if not self.hovered_chapter then self:clear_thumbnail() end -end -function Timeline:on_global_mbtn_left_up() +function Timeline:handle_cursor_up() if self.pressed then - mp.set_property_native('pause', self.pressed_pause) + mp.set_property_native('pause', self.pressed.pause) self.pressed = false end self:clear_thumbnail() @@ -144,11 +125,12 @@ function Timeline:on_global_mouse_move() self.seek_timer:kill() self.seek_timer:resume() else self:set_from_cursor() end + elseif self.has_thumbnail and self.proximity_raw > 0 then + self:clear_thumbnail() end - self:determine_chapter_click_handler() end -function Timeline:on_wheel_up() mp.commandv('seek', options.timeline_step) end -function Timeline:on_wheel_down() mp.commandv('seek', -options.timeline_step) end +function Timeline:handle_wheel_up() mp.commandv('seek', options.timeline_step) end +function Timeline:handle_wheel_down() mp.commandv('seek', -options.timeline_step) end function Timeline:render() if self.size_max == 0 then return end @@ -156,8 +138,18 @@ function Timeline:render() local size_min = self:get_effective_size_min() local size = self:get_effective_size() local visibility = self:get_visibility() + self.is_hovered = false if size < 1 then return end + if self.proximity_raw == 0 then + self.is_hovered = true + cursor.on_primary_down = function() self:handle_cursor_down() end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + if self.pressed then + cursor.on_primary_up = function() self:handle_cursor_up() end + end local ass = assdraw.ass_new() @@ -248,7 +240,7 @@ function Timeline:render() end -- Chapters - self.hovered_chapter = nil + local hovered_chapter = nil if (options.timeline_chapters_opacity > 0 and (#state.chapters > 0 or state.ab_loop_a or state.ab_loop_b) ) then @@ -274,7 +266,7 @@ function Timeline:render() if #state.chapters > 0 then -- Find hovered chapter indicator - local hovered_chapter, closest_delta = nil, INFINITY + local closest_delta = INFINITY if self.proximity_raw < diamond_radius_hovered then for i, chapter in ipairs(state.chapters) do @@ -282,6 +274,10 @@ function Timeline:render() local cursor_chapter_delta = math.sqrt((cursor.x - chapter_x) ^ 2 + (cursor.y - chapter_y) ^ 2) if cursor_chapter_delta <= diamond_radius_hovered and cursor_chapter_delta < closest_delta then hovered_chapter, closest_delta = chapter, cursor_chapter_delta + self.is_hovered = true + cursor.on_primary_down = function() + mp.commandv('seek', hovered_chapter.time, 'absolute+exact') + end end end end @@ -291,11 +287,7 @@ function Timeline:render() end -- Render hovered chapter above others - if hovered_chapter then - draw_chapter(hovered_chapter.time, diamond_radius_hovered) - self.hovered_chapter = hovered_chapter - self:determine_chapter_click_handler() - end + if hovered_chapter then draw_chapter(hovered_chapter.time, diamond_radius_hovered) end end -- A-B loop indicators @@ -363,10 +355,10 @@ function Timeline:render() end -- Hovered time and chapter - if (self.proximity_raw == 0 or self.pressed or self.hovered_chapter) and + if (self.proximity_raw == 0 or self.pressed or hovered_chapter) and not (Elements.speed and Elements.speed.dragging) then - local cursor_x = self.hovered_chapter and t2x(self.hovered_chapter.time) or cursor.x - local hovered_seconds = self.hovered_chapter and self.hovered_chapter.time or self:get_time_at_x(cursor.x) + local cursor_x = hovered_chapter and t2x(hovered_chapter.time) or cursor.x + local hovered_seconds = hovered_chapter and hovered_chapter.time or self:get_time_at_x(cursor.x) -- Cursor line -- 0.5 to switch when the pixel is half filled in @@ -397,7 +389,10 @@ function Timeline:render() local ax, ay = (thumb_x - border) / scale_x, (thumb_y - border) / scale_y local bx, by = (thumb_x + thumb_width + border) / scale_x, (thumb_y + thumb_height + border) / scale_y ass:rect(ax, ay, bx, by, {color = bg, border = 1, border_color = fg, border_opacity = 0.08, radius = 2}) - mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y) + if not self.pressed then + mp.commandv('script-message-to', 'thumbfast', 'thumb', hovered_seconds, thumb_x, thumb_y) + self.has_thumbnail = true + end tooltip_anchor.ax, tooltip_anchor.bx, tooltip_anchor.ay = ax, bx, ay end diff --git a/scripts/uosc_shared/elements/TopBar.lua b/scripts/uosc_shared/elements/TopBar.lua index daf208a..5feff4c 100644 --- a/scripts/uosc_shared/elements/TopBar.lua +++ b/scripts/uosc_shared/elements/TopBar.lua @@ -16,7 +16,7 @@ function TopBarButton:init(id, props) self.command = props.command end -function TopBarButton:on_mbtn_left_down() +function TopBarButton:handle_cursor_down() mp.command(type(self.command) == 'function' and self.command() or self.command) end @@ -28,6 +28,7 @@ function TopBarButton:render() -- Background on hover if self.proximity_raw == 0 then ass:rect(self.ax, self.ay, self.bx, self.by, {color = self.background, opacity = visibility}) + cursor.on_primary_down = function() self:handle_cursor_down() end end local width, height = self.bx - self.ax, self.by - self.ay @@ -151,10 +152,6 @@ function TopBar:on_prop_maximized() self:update_dimensions() end -function TopBar:on_mbtn_left_down() - if cursor.x < self.title_bx then self:toggle_title() end -end - function TopBar:on_display() self:update_dimensions() end function TopBar:render() @@ -193,6 +190,12 @@ function TopBar:render() } local bx = math.min(max_bx, title_ax + text_width(main_title, opts) + padding * 2) local by = self.by - bg_margin + local rect = {ax = title_ax, ay = self.ay, bx = bx, by = self.by} + + if get_point_to_rectangle_proximity(cursor, rect) == 0 then + cursor.on_primary_down = function() self:toggle_title() end + end + ass:rect(title_ax, title_ay, bx, by, { color = bg, opacity = visibility * options.top_bar_title_opacity, radius = 2, }) diff --git a/scripts/uosc_shared/elements/Volume.lua b/scripts/uosc_shared/elements/Volume.lua index be2d852..2f591b6 100644 --- a/scripts/uosc_shared/elements/Volume.lua +++ b/scripts/uosc_shared/elements/Volume.lua @@ -7,10 +7,12 @@ local MuteButton = class(Element) ---@param props? ElementProps function MuteButton:new(props) return Class.new(self, 'volume_mute', props) --[[@as MuteButton]] end function MuteButton:get_visibility() return Elements.volume:get_visibility(self) end -function MuteButton:on_mbtn_left_down() mp.commandv('cycle', 'mute') end function MuteButton:render() local visibility = self:get_visibility() if visibility <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() mp.commandv('cycle', 'mute') end + end local ass = assdraw.ass_new() local icon_name = state.mute and 'volume_off' or 'volume_up' local width = self.bx - self.ax @@ -58,17 +60,11 @@ function VolumeSlider:on_coordinates() self.spacing = round(width * 0.2) self.radius = math.max(2, (self.bx - self.ax) / 10) end -function VolumeSlider:on_mbtn_left_down() - self.pressed = true - self:set_from_cursor() -end -function VolumeSlider:on_global_mbtn_left_up() self.pressed = false end -function VolumeSlider:on_global_mouse_leave() self.pressed = false end function VolumeSlider:on_global_mouse_move() if self.pressed then self:set_from_cursor() end end -function VolumeSlider:on_wheel_up() self:set_volume(state.volume + options.volume_step) end -function VolumeSlider:on_wheel_down() self:set_volume(state.volume - options.volume_step) end +function VolumeSlider:handle_wheel_up() self:set_volume(state.volume + options.volume_step) end +function VolumeSlider:handle_wheel_down() self:set_volume(state.volume - options.volume_step) end function VolumeSlider:render() local visibility = self:get_visibility() @@ -77,6 +73,19 @@ function VolumeSlider:render() if width <= 0 or height <= 0 or visibility <= 0 then return end + if self.proximity_raw == 0 then + cursor.on_primary_down = function() + self.pressed = true + self:set_from_cursor() + cursor.on_primary_up = function() self.pressed = false end + end + cursor.on_wheel_down = function() self:handle_wheel_down() end + cursor.on_wheel_up = function() self:handle_wheel_up() end + end + if self.pressed then cursor.on_primary_up = function() + self.pressed = false end + end + local ass = assdraw.ass_new() local nudge_y, nudge_size = self.draw_nudge and self.nudge_y or -INFINITY, self.nudge_size local volume_y = self.ay + options.volume_border + diff --git a/scripts/uosc_shared/lib/utils.lua b/scripts/uosc_shared/lib/utils.lua index 2569fd6..be257e9 100644 --- a/scripts/uosc_shared/lib/utils.lua +++ b/scripts/uosc_shared/lib/utils.lua @@ -93,6 +93,11 @@ function get_point_to_rectangle_proximity(point, rect) return math.sqrt(dx * dx + dy * dy) end +-- Call function with args if it exists +function call_maybe(fn, ...) + if type(fn) == 'function' then fn(...) end +end + -- Extracts the properties used by property expansion of that string. ---@param str string ---@param res { [string] : boolean } | nil @@ -553,6 +558,8 @@ function render() if not display.initialized then return end state.render_last_time = mp.get_time() + cursor.reset_handlers() + -- Actual rendering local ass = assdraw.ass_new() @@ -566,6 +573,8 @@ function render() end end + cursor.decide_keybinds() + -- submit if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then return