summaryrefslogtreecommitdiffstats
path: root/.config/mpv/scripts/uosc_shared/elements/Element.lua
blob: 718782d444c36d939f39f5f551f3d539bb4a60c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
---@alias ElementProps {enabled?: boolean; ax?: number; ay?: number; bx?: number; by?: number; ignores_menu?: boolean; anchor_id?: string;}

-- Base class all elements inherit from.
---@class Element : Class
local Element = class()

---@param id string
---@param props? ElementProps
function Element:init(id, props)
	self.id = id
	-- `false` means element won't be rendered, or receive events
	self.enabled = true
	-- Element coordinates
	self.ax, self.ay, self.bx, self.by = 0, 0, 0, 0
	-- Relative proximity from `0` - mouse outside `proximity_max` range, to `1` - mouse within `proximity_min` range.
	self.proximity = 0
	-- Raw proximity in pixels.
	self.proximity_raw = infinity
	---@type number `0-1` factor to force min visibility. Used for toggling element's permanent visibility.
	self.min_visibility = 0
	---@type number `0-1` factor to force a visibility value. Used for flashing, fading out, and other animations
	self.forced_visibility = nil
	---@type boolean Render this element even when menu is open.
	self.ignores_menu = false
	---@type nil|string ID of an element from which this one should inherit visibility.
	self.anchor_id = nil

	if props then table_assign(self, props) end

	-- Flash timer
	self._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
		local getTo = function() return self.proximity end
		self:tween_property('forced_visibility', 1, getTo, function()
			self.forced_visibility = nil
		end)
	end)
	self._flash_out_timer:kill()

	Elements:add(self)
end

function Element:destroy()
	self.destroyed = true
	Elements:remove(self)
end

---@param ax number
---@param ay number
---@param bx number
---@param by number
function Element:set_coordinates(ax, ay, bx, by)
	self.ax, self.ay, self.bx, self.by = ax, ay, bx, by
	Elements:update_proximities()
	self:maybe('on_coordinates')
end

function Element:update_proximity()
	if cursor.hidden then
		self.proximity_raw = infinity
		self.proximity = 0
	else
		local range = options.proximity_out - options.proximity_in
		self.proximity_raw = get_point_to_rectangle_proximity(cursor, self)
		self.proximity = 1 - (clamp(0, self.proximity_raw - options.proximity_in, range) / range)
	end
end

-- Decide elements visibility based on proximity and various other factors
function Element:get_visibility()
	-- Hide when menu is open, unless this is a menu
	---@diagnostic disable-next-line: undefined-global
	if not self.ignores_menu and Menu and Menu:is_open() then return 0 end

	-- Persistency
	local persist = config[self.id .. '_persistency']
	if persist and (
		(persist.audio and state.is_audio)
			or (persist.paused and state.pause)
			or (persist.video and state.is_video)
			or (persist.image and state.is_image)
			or (persist.idle and state.is_idle)
		) then return 1 end

	-- Forced visibility
	if self.forced_visibility then return math.max(self.forced_visibility, self.min_visibility) end

	-- Anchor inheritance
	-- If anchor returns -1, it means all attached elements should force hide.
	local anchor = self.anchor_id and Elements[self.anchor_id]
	local anchor_visibility = anchor and anchor:get_visibility() or 0

	return anchor_visibility == -1 and 0 or math.max(self.proximity, anchor_visibility, self.min_visibility)
end

-- Call method if it exists
function Element:maybe(name, ...)
	if self[name] then return self[name](self, ...) end
end

-- Attach a tweening animation to this element
---@param from number
---@param to number|fun():number
---@param setter fun(value: number)
---@param factor_or_callback? number|fun()
---@param callback? fun() Called either on animation end, or when animation is killed.
function Element:tween(from, to, setter, factor_or_callback, callback)
	self:tween_stop()
	self._kill_tween = self.enabled and tween(
		from, to, setter, factor_or_callback,
		function()
			self._kill_tween = nil
			if callback then callback() end
		end
	)
end

function Element:is_tweening() return self and self._kill_tween end
function Element:tween_stop() self:maybe('_kill_tween') end

-- Animate an element property between 2 values.
---@param prop string
---@param from number
---@param to number|fun():number
---@param factor_or_callback? number|fun()
---@param callback? fun() Called either on animation end, or when animation is killed.
function Element:tween_property(prop, from, to, factor_or_callback, callback)
	self:tween(from, to, function(value) self[prop] = value end, factor_or_callback, callback)
end

---@param name string
function Element:trigger(name, ...)
	local result = self:maybe('on_' .. name, ...)
	request_render()
	return result
end

-- Briefly flashes the element for `options.flash_duration` milliseconds.
-- Useful to visualize changes of volume and timeline when changed via hotkeys.
function Element:flash()
	if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
		self:tween_stop()
		self.forced_visibility = 1
		request_render()
		self._flash_out_timer:kill()
		self._flash_out_timer:resume()
	end
end

return Element