refactor: cursor events consolidated into one interface (#483)

`mbtn_left` and `wheel` events are now only fired on a `cursor.on_{event}` object, which resets at the beginning of each frame, so elements need to bind these listeners in their render functions. These properties are overwritable which allows elements to take over cursor event handling from their parents if necessary.
This commit is contained in:
Tomas Klaen
2023-03-29 09:08:18 +02:00
committed by GitHub
parent 9839e7b726
commit f635df18f7
9 changed files with 155 additions and 123 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,
})

View File

@@ -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 +

View File

@@ -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