dotfiles/.config/lite-xl/libraries/widget/init.lua

1473 lines
38 KiB
Lua

--
-- Base widget implementation for lite.
-- @copyright Jefferson Gonzalez
-- @license MIT
--
local core = require "core"
local config = require "core.config"
local style = require "core.style"
local keymap = require "core.keymap"
local View = require "core.view"
local RootView = require "core.rootview"
local ScrollBar = require "libraries.widget.scrollbar"
---Represents the border of a widget.
---@class widget.border
---@field public width number
---@field public color renderer.color | nil
---Represents the position of a widget.
---@class widget.position
---@field public x number Real X
---@field public y number Real y
---@field public rx number Relative X
---@field public ry number Relative Y
---@field public dx number Dragging initial x position
---@field public dy number Dragging initial y position
---@class widget.animation.options
---Prevents duplicated animations from getting added.
---@field name? string
---Speed of the animation, defaults to 0.5
---@field rate? number
---Called each time the value of a property changes.
---@field on_step? fun(target:table, property:string, value:number)
---Called when the animation finishes.
---@field on_complete? fun(widget:widget)
---@class widget.animation
---@field target table
---@field properties table<string,number>
---@field options? widget.animation.options
---Represents a reference to a font stored elsewhere.
---@class widget.fontreference
---@field public container table<string, renderer.font>
---@field public name string
---@alias widget.font widget.fontreference | renderer.font | string
---@alias widget.clicktype
---| "left"
---| "right"
---@alias widget.styledtext table<integer, renderer.font|widget.fontreference|renderer.color|integer|string>
---A base widget
---@class widget : core.view
---@field public super widget
---@field public parent widget | nil
---@field public name string
---@field public position widget.position
---Modifying this property directly is not advised, use set_size() instead.
---@field public size widget.position
---@field public childs table<integer,widget>
---@field public child_active widget | nil
---@field public zindex integer
---@field public border widget.border
---@field public clickable boolean
---@field public draggable boolean
---@field public scrollable boolean
---@field public font widget.font
---@field public foreground_color renderer.color
---@field public background_color renderer.color
---@field public render_background boolean
---@field public type_name string
---@field protected visible boolean
---@field protected has_focus boolean
---@field protected dragged boolean
---@field protected tooltip string
---@field protected label string | widget.styledtext
---@field protected input_text boolean
---@field protected textview widget
---@field protected next_zindex integer
---@field protected mouse widget.position
---@field protected prev_size widget.position
---@field protected mouse_is_pressed boolean
---@field protected mouse_is_hovering boolean
---@field protected mouse_pressed_outside boolean
---@field protected is_scrolling boolean
---@field protected animations widget.animation[]
local Widget = View:extend()
---Indicates on a widget.styledtext that a new line follows.
---@type integer
Widget.NEWLINE = 1
---Keep track of last hovered widget to properly trigger on_mouse_leave
---@type widget | nil
local last_hovered_child = nil
---A list of floating widgets that need to receive events.
---@type table<integer, widget>
local floating_widgets = {}
---When no parent is given to the widget constructor it will automatically
---overwrite RootView methods to intercept system events.
---@param parent? widget
---@param floating? boolean | nil
function Widget:new(parent, floating)
Widget.super.new(self)
self.v_scrollbar = ScrollBar(self, {direction = "v", alignment = "e"})
self.h_scrollbar = ScrollBar(self, {direction = "h", alignment = "e"})
self.type_name = "widget"
self.parent = parent
self.name = "---" -- defaults to the application name
if type(floating) == "boolean" then
self.defer_draw = floating
else
self.defer_draw = true
end
self.childs = {}
self.child_active = nil
self.zindex = nil
self.next_zindex = 1
self.border = {
width = 1,
color = nil
}
self.foreground_color = nil
self.background_color = nil
self.render_background = true
self.visible = parent and true or false
self.has_focus = false
self.clickable = true
self.draggable = false
self.dragged = false
self.font = "font"
self.tooltip = ""
self.label = ""
self.input_text = false
self.textview = nil
self.mouse = {x = 0, y = 0}
self.prev_size = {x = 0, y = 0}
self.is_scrolling = false
self.mouse_is_pressed = false
self.mouse_is_hovering = false
-- used to allow proper node resizing
self.mouse_pressed_outside = false
self.animations = {}
if parent then
parent:add_child(self)
elseif self.defer_draw then
table.insert(floating_widgets, self)
Widget.override_rootview()
end
self:set_position(0, 0)
end
---Useful for debugging.
function Widget:__tostring()
return self.type_name
end
---Add a child widget, automatically assign a zindex if non set and sorts
---them in reverse order for better events matching.
---@param child widget
function Widget:add_child(child)
if not child.zindex then
child.zindex = self.next_zindex
self.next_zindex = self.next_zindex + 1
end
table.insert(self.childs, child)
table.sort(self.childs, function(t1, t2) return t1.zindex > t2.zindex end)
end
---Remove a child widget.
---@param child widget
function Widget:remove_child(child)
for position, element in ipairs(self.childs) do
if child == element then
child:destroy_childs()
table.remove(self.childs, position)
break
end
end
end
---Show the widget.
function Widget:show()
if not self.parent then
if self.size.x <= 0 or self.size.y <= 0 then
self.size.x = self.prev_size.x
self.size.y = self.prev_size.y
end
self.prev_size.x = 0
self.prev_size.y = 0
end
self.visible = true
-- re-triggers update to make sure everything was properly calculated
-- and redraw the interface once, maybe something else can be changed
-- to not require this action, but for now lets do this.
core.add_thread(function()
self:update()
core.redraw = true
end)
end
---Perform an animated show.
---@param lock_x? boolean Do not resize width while animating
---@param lock_y? boolean Do not resize height while animating
---@param options? widget.animation.options
function Widget:show_animated(lock_x, lock_y, options)
if not self.parent then
if self.size.x <= 0 or self.size.y <= 0 then
self.size.x = self.prev_size.x
self.size.y = self.prev_size.y
end
self.prev_size.x = 0
self.prev_size.y = 0
end
local target_x, target_y = math.floor(self.size.x), math.floor(self.size.y)
self.size.x = lock_x and target_x or 0
self.size.y = lock_y and target_y or 0
local properties = {}
if not lock_x then properties["x"] = target_x end
if not lock_y then properties["y"] = target_y end
options = options or {}
self:animate(self.size, properties, {
name = options.name or "show_animated",
rate = options.rate,
on_step = options.on_step,
on_complete = options.on_complete
})
self.visible = true
end
---Hide the widget.
function Widget:hide()
self.visible = false
-- we need to force size to zero on parent widget to properly hide it
-- when used as a lite node, otherwise the reserved space of the node
-- will stay visible and dragging will reveal empty space.
if not self.parent then
if self.size.x > 0 or self.size.y > 0 then
-- we only do this once to prevent issues of consecutive hide calls
if self.prev_size.x == 0 and self.prev_size.y == 0 then
self.prev_size.x = self.size.x
self.prev_size.y = self.size.y
end
self.size.x = 0
self.size.y = 0
end
end
end
---Perform an animated hide.
---@param lock_x? boolean Do not resize width while animating
---@param lock_y? boolean Do not resize height while animating
---@param options? widget.animation.options
function Widget:hide_animated(lock_x, lock_y, options)
local x, y = self.size.x, self.size.y
local target_x = lock_x and self.size.x or 0
local target_y = lock_y and self.size.y or 0
local properties = {}
if not lock_x then properties["x"] = target_x end
if not lock_y then properties["y"] = target_y end
options = options or {}
self:animate(self.size, properties, {
name = options.name or "hide_animated",
rate = options.rate,
on_step = options.on_step,
on_complete = function()
self.size.x, self.size.y = x, y
self:hide()
if options.on_complete then
options.on_complete(self)
end
end
})
end
---When set to false the background rendering is disabled.
---@param enable? boolean | nil
function Widget:toggle_background(enable)
if type(enable) == "boolean" then
self.render_background = enable
else
self.render_background = not self.render_background
end
end
---Toggle visibility of widget.
---@param animated? boolean
---@param lock_x? boolean
---@param lock_y? boolean
---@param options? widget.animation.options
function Widget:toggle_visible(animated, lock_x, lock_y, options)
if not self.visible then
if not animated then
self:show()
else
self:show_animated(lock_x, lock_y, options)
end
else
if not animated then
self:hide()
else
self:hide_animated(lock_x, lock_y, options)
end
end
end
---Check if the widget is visible also recursing to the parent visibility.
---@return boolean
function Widget:is_visible()
if
not self.visible or (self.parent and not self.parent:is_visible())
then
return false
end
return true
end
---Taken from the logview and modified it a tiny bit.
---TODO: something similar should be on lite-xl core.
---@param font widget.font
---@param text string
---@param x integer
---@param y integer
---@param color renderer.color
---@param only_calc boolean
---@return integer resx
---@return integer resy
---@return integer width
---@return integer height
function Widget:draw_text_multiline(font, text, x, y, color, only_calc)
font = self:get_font(font)
local th = font:get_height()
local resx, resy = x, y
local width, height = 0, 0
for line in (text .. "\n"):gmatch("(.-)\n") do
resy = y
if only_calc then
resx = x + font:get_width(line)
else
resx = renderer.draw_text(font, line, x, y, color)
end
y = y + th
width = math.max(width, resx - x)
height = height + th
end
return resx, resy, width, height
end
---Render or calculate the size of the specified range of elements
---in a styled text elemet.
---@param text widget.styledtext
---@param start_idx integer
---@param end_idx integer
---@param x integer
---@param y integer
---@param only_calc boolean
---@return integer width
---@return integer height
function Widget:draw_styled_text(text, x, y, only_calc, start_idx, end_idx)
local font = self:get_font()
local color = self.foreground_color or style.text
local width = 0
local height = font:get_height()
local new_line = false
local nx = x
start_idx = start_idx or 1
end_idx = end_idx or #text
for pos=start_idx, end_idx, 1 do
local element = text[pos]
local ele_type = type(element)
if
ele_type == "userdata"
or
(element.container or type(element[1]) == "userdata")
then
if ele_type == "table" and element.container then
font = element.container[element.name]
else
font = element
end
elseif ele_type == "table" then
color = element
elseif element == Widget.NEWLINE then
y = y + font:get_height()
nx = x
new_line = true
elseif ele_type == "string" then
local rx, ry, w, h = self:draw_text_multiline(
font, element, nx, y, color, only_calc
)
y = ry
nx = rx
if new_line then
height = height + h
width = math.max(width, w)
new_line = false
else
height = math.max(height, h)
width = width + w
end
end
end
return width, height
end
---Draw the widget configured border or custom one.
---@param x? number
---@param y? number
---@param w? number
---@param h? number
function Widget:draw_border(x, y, w, h)
if self.border.width <= 0 then return end
x = x or self.position.x
y = y or self.position.y
w = w or self.size.x
h = h or self.size.y
x = x - self.border.width
y = y - self.border.width
w = w + (self.border.width * 2)
h = h + (self.border.width * 2)
-- Draw lines instead of full rectangle to be able to draw on top
--top
renderer.draw_rect(
x, y, w + x % 1 - self.border.width, self.border.width,
self.border.color or style.text
)
--bottom
renderer.draw_rect(
x, y+h - self.border.width, w + x % 1 - self.border.width, self.border.width,
self.border.color or style.text
)
--right
renderer.draw_rect(
x+w - self.border.width, y, self.border.width, h,
self.border.color or style.text
)
--left
renderer.draw_rect(
x, y, self.border.width, h,
self.border.color or style.text
)
end
---Called by lite node system to properly resize the widget.
---@param axis string | "'x'" | "'y'"
---@param value number
function Widget:set_target_size(axis, value)
if not self.visible then
return false
end
if axis == "x" then
self:set_size(value)
else
self:set_size(nil, value)
end
return true
end
---@param width? integer
---@param height? integer
function Widget:set_size(width, height)
-- take into consideration the border as part of size
if width then
if width > (self.border.width * 2) then
width = width - (self.border.width * 2)
else
width = 0
end
end
if height then
if height > (self.border.width * 2) then
height = height - (self.border.width * 2)
else
height = 0
end
end
if not self.parent and not self.visible then
if width then self.prev_size.x = width end
if height then self.prev_size.y = height end
else
if width then self.size.x = width end
if height then self.size.y = height end
end
end
---Set the widget border size and appropriately re-set the widget size.
---@param width integer
function Widget:set_border_width(width)
local wwidth, wheight = 0, 0;
if self.border.width > 0 then
local prev_width = self.border.width * 2
if not self.parent and not self.visible then
wwidth = self.prev_size.x + prev_width
wheight = self.prev_size.y + prev_width
else
wwidth = self.size.x + prev_width
wheight = self.size.y + prev_width
end
end
self.border.width = width
self:set_size(wwidth, wheight)
end
---Called on the update function to be able to scroll the child widgets.
function Widget:update_position()
if self.parent then
self.position.x = self.position.rx + self.border.width
self.position.y = self.position.ry + self.border.width
-- add offset to properly scroll
local ox, oy = self.parent:get_content_offset()
self.position.x = ox + self.position.x
self.position.y = oy + self.position.y
end
for _, child in pairs(self.childs) do
child:update_position()
end
end
---Set the position of the widget and updates the child absolute coordinates
---@param x? integer
---@param y? integer
function Widget:set_position(x, y)
if x then self.position.x = x + self.border.width end
if y then self.position.y = y + self.border.width end
if self.parent then
-- add offset to properly scroll
local ox, oy = self.parent:get_content_offset()
if x then
self.position.rx = x
self.position.x = ox + self.position.x
end
if y then
self.position.ry = y
self.position.y = oy + self.position.y
end
end
if x or y then
for _, child in pairs(self.childs) do
child:set_position(child.position.rx, child.position.ry)
end
end
end
---Get the real renderer.font associated with a widget.font.
---@param font? widget.font
---@return renderer.font
function Widget:get_font(font)
if not font then font = self.font end
local font_type = type(font)
if font_type == "userdata" then
return font
elseif font_type == "string" then
return style[font]
elseif font and font.container then
return font.container[font.name]
end
if not font then
return style.font
end
return font
end
---Get the relative position in relation to parent
---@return widget.position
function Widget:get_position()
local position = { x = self.position.x, y = self.position.y }
if self.parent then
position.x = self.position.rx
position.y = self.position.ry
end
return position
end
---Get width including borders.
---@return number
function Widget:get_width()
return self.size.x + (self.border.width * 2)
end
---Get height including borders.
---@return number
function Widget:get_height()
return self.size.y + (self.border.width * 2)
end
---Get the right x coordinate relative to parent
---@return number
function Widget:get_right()
return self:get_position().x + self:get_width()
end
---Get the bottom y coordinate relative to parent
---@return number
function Widget:get_bottom()
return self:get_position().y + self:get_height()
end
---Overall height of the widget accounting all visible child widgets.
---@return number
function Widget:get_real_height()
local size = 0
for _, child in pairs(self.childs) do
if child.visible then
size = math.max(size, child:get_bottom())
end
end
return size
end
---Overall width of the widget accounting all visible child widgets.
---@return number
function Widget:get_real_width()
local size = 0
for _, child in pairs(self.childs) do
if child.visible then
size = math.max(size, child:get_right())
end
end
return size
end
---Check if the given mouse coordinate is hovering the widget
---@param x number
---@param y number
---@return boolean
function Widget:mouse_on_top(x, y)
return
self.visible
and
x >= self.position.x - self.border.width
and
x <= self.position.x - self.border.width + self:get_width()
and
y >= self.position.y - self.border.width
and
y <= self.position.y - self.border.width + self:get_height()
end
---Mark a widget as having focus.
---TODO: Implement set focus system by pressing a key like tab?
function Widget:set_focus(has_focus)
self.set_focus = has_focus
end
---Text displayed when the widget is hovered.
---@param tooltip string
function Widget:set_tooltip(tooltip)
self.tooltip = tooltip
end
---A text label for the widget, not all widgets support this.
---@param text string | widget.styledtext
function Widget:set_label(text)
self.label = text
end
---Used internally when dragging is activated.
---@param x number
---@param y number
function Widget:drag(x, y)
self:set_position(x - self.position.dx, y - self.position.dy)
end
---Center the widget horizontally and vertically to the screen or parent widget.
function Widget:centered()
local w, h = system.get_window_size();
if self.parent then
w = self.parent:get_width()
h = self.parent:get_height()
end
self:set_position(
(w / 2) - (self:get_width() / 2),
(h / 2) - (self:get_height() / 2)
)
end
---Replaces current active child with a new one and calls the
---activate/deactivate events of the child. This is especially
---used to send text input events to widgets with input_text support.
---@param child? widget If nil deactivates current child
function Widget:swap_active_child(child)
if self.parent then
self.parent:swap_active_child(child)
return
end
if child and child == self.child_active then return end
local active_child = self.child_active
if self.child_active then
self.child_active:deactivate()
end
self.child_active = child
if child then
if not self.prev_view then
self.prev_view = core.active_view
end
core.set_active_view(child.input_text and child.textview or child)
self.child_active:activate()
elseif self.prev_view then
-- return focus to previous view
if self.prev_view ~= active_child then
core.set_active_view(self.prev_view)
else
core.set_active_view(self)
end
self.prev_view = nil
end
end
---Calculates the y scrollable size taking into account the bottom most
---widget or the size of the widget it self if greater.
---@return number
function Widget:get_scrollable_size()
return math.max(self.size.y, self:get_real_height())
end
---Calculates the x scrollable size taking into account the right most
---widget or the size of the widget it self if greater.
---@return number
function Widget:get_h_scrollable_size()
return math.max(self.size.x, self:get_real_width())
end
---The name that is displayed on lite-xl tabs.
function Widget:get_name()
return self.name
end
--
-- Events
--
---Send file drop event to hovered child.
---@param filename string
---@param x number
---@param y number
---@return boolean processed
function Widget:on_file_dropped(filename, x, y)
if not self.visible then return false end
for _, child in pairs(self.childs) do
if child:mouse_on_top(x, y) then
return child:on_file_dropped(filename, x, y)
end
end
return false
end
---Redirects any text input to active child with the input_text flag.
---@param text string
---@return boolean processed
function Widget:on_text_input(text)
if not self.visible then return false end
Widget.super.on_text_input(self, text)
if self.child_active then
self.child_active:on_text_input(text)
return true
end
return false
end
---Send mouse pressed events to hovered child or starts dragging if enabled.
---@param button widget.clicktype
---@param x number
---@param y number
---@param clicks integer
---@return boolean processed
function Widget:on_mouse_pressed(button, x, y, clicks)
if not self.visible then return false end
if Widget.super.on_mouse_pressed(self, button, x, y, clicks) then
local parent = self.parent
while parent do
-- propagate to parents so if mouse is not on top still
-- reach the childrens when the mouse is released
parent.is_scrolling = true
parent = parent.parent
end
self.is_scrolling = true
return true
end
for _, child in pairs(self.childs) do
if child:mouse_on_top(x, y) and child.clickable then
child:on_mouse_pressed(button, x, y, clicks)
return true
end
end
if self:mouse_on_top(x, y) then
self.mouse_is_pressed = true
if self.parent then
-- propagate to parents so if mouse is not on top still
-- reach the childrens when the mouse is released
self.parent.mouse_is_pressed = true
end
if self.draggable and not self.child_active then
self.position.dx = x - self.position.x
self.position.dy = y - self.position.y
system.set_cursor("hand")
end
else
self:swap_active_child()
return false
end
return true
end
---Send mouse released events to hovered child, ends dragging if enabled and
---emits on click events if applicable.
---@param button widget.clicktype
---@param x number
---@param y number
---@return boolean processed
function Widget:on_mouse_released(button, x, y)
if not self.visible then return false end
Widget.super.on_mouse_released(self, button, x, y)
if self.is_scrolling then
self.is_scrolling = false
local parent = self.parent
while parent do
parent.is_scrolling = false
parent = parent.parent
end
for _, child in pairs(self.childs) do
if child.is_scrolling then
child:on_mouse_released(button, x, y)
end
end
return true
end
self:swap_active_child()
if self.child_active then
self.child_active:on_mouse_released(button, x, y)
end
if not self.dragged then
for _, child in pairs(self.childs) do
local mouse_on_top = child:mouse_on_top(x, y)
if
mouse_on_top
or
child.mouse_is_pressed
then
child:on_mouse_released(button, x, y)
if child.input_text then
self:swap_active_child(child)
end
if mouse_on_top and child.mouse_is_pressed then
child:on_click(button, x, y)
end
return true
end
end
end
if
not self.dragged
and
not self.mouse_is_pressed
then
return false
end
if self.mouse_is_pressed then
if self:mouse_on_top(x, y) then
self:on_click(button, x, y)
end
self.mouse_is_pressed = false
if self.parent then
self.parent.mouse_is_pressed = false
end
if self.draggable then
system.set_cursor("arrow")
end
end
self.dragged = false
return true
end
---Event emitted when the value of the widget changes.
---If applicable, child widgets should call this method
---when its value changes.
---@param value any
function Widget:on_change(value) end
---Click event emitted on a succesful on_mouse_pressed
---and on_mouse_released events combo.
---@param button widget.clicktype
---@param x number
---@param y number
function Widget:on_click(button, x, y) end
---Emitted to input_text widgets when clicked.
function Widget:activate() end
---Emitted to input_text widgets on lost focus.
function Widget:deactivate() end
---Besides the on_mouse_moved this event emits on_mouse_enter
---and on_mouse_leave for easy hover effects. Also, if the
---widget is scrollable and pressed this will drag it unless
---there is an active input_text child active.
---@param x number
---@param y number
---@param dx number
---@param dy number
function Widget:on_mouse_moved(x, y, dx, dy)
if not self.visible then return false end
Widget.super.on_mouse_moved(self, x, y, dx, dy)
if self.is_scrolling then
if not self:scrollbar_dragging() then
for _, child in pairs(self.childs) do
if child.is_scrolling then
child:on_mouse_moved(x, y, dx, dy)
break
end
end
end
return true
end
-- store latest mouse coordinates for usage on the on_mouse_wheel event.
self.mouse.x = x
self.mouse.y = y
if self.child_active then
self.child_active:on_mouse_moved(x, y, dx, dy)
end
if not self.dragged then
local hovered = nil
for _, child in pairs(self.childs) do
if
not hovered
and
(child:mouse_on_top(x, y) or child.mouse_is_pressed)
then
hovered = child
elseif child.mouse_is_hovering then
child.mouse_is_hovering = false
if #child.tooltip > 0 then
core.status_view:remove_tooltip()
end
child:on_mouse_leave(x, y, dx, dy)
system.set_cursor("arrow")
end
end
if hovered then
hovered:on_mouse_moved(x, y, dx, dy)
if last_hovered_child and not last_hovered_child:mouse_on_top(x, y) then
last_hovered_child:on_mouse_leave(x, y, dx, dy)
last_hovered_child.mouse_is_hovering = false
last_hovered_child = nil
end
return true;
end
end
if
not self:mouse_on_top(x, y)
and
not self.mouse_is_pressed
and
not self.mouse_is_hovering
then
return false
end
local is_over = true
if self:mouse_on_top(x, y) then
if not self.mouse_is_hovering then
system.set_cursor("arrow")
self.mouse_is_hovering = true
if #self.tooltip > 0 then
core.status_view:show_tooltip(self.tooltip)
end
self:on_mouse_enter(x, y, dx, dy)
last_hovered_child = self
end
else
self:on_mouse_leave(x, y, dx, dy)
self.mouse_is_hovering = false
is_over = false
end
if not self.child_active and self.mouse_is_pressed and self.draggable then
system.set_cursor("hand")
self:drag(x, y)
self.dragged = true
return true
end
return is_over
end
---Emitted once when the mouse hovers the widget.
function Widget:on_mouse_enter(x, y, dx, dy)
for _, child in pairs(self.childs) do
if child:mouse_on_top(x, y) then
child:on_mouse_enter(x, y, dx, dy)
break
end
end
end
---Emitted once when the mouse leaves the widget.
function Widget:on_mouse_leave(x, y, dx, dy)
for _, child in pairs(self.childs) do
if child.mouse_is_hovering then
child:on_mouse_leave(x, y, dx, dy)
end
end
end
function Widget:on_mouse_wheel(y, x)
if
not self.visible
or
not self:mouse_on_top(self.mouse.x, self.mouse.y)
then
return false
end
for _, child in pairs(self.childs) do
if child:mouse_on_top(self.mouse.x, self.mouse.y) then
if child:on_mouse_wheel(y, x) then
return true
end
end
end
if self.scrollable then
if keymap.modkeys["shift"] then
x = y
y = 0
end
if y and y ~= 0 then
self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll
end
if x and x ~= 0 then
self.scroll.to.x = self.scroll.to.x + x * -config.mouse_wheel_scroll
end
return true
end
return false
end
---Can be overriden by widgets to listen for scale change events to apply
---any neccesary changes in sizes, padding, etc...
---@param new_scale number
---@param prev_scale number
function Widget:on_scale_change(new_scale, prev_scale)
local font_type = type(self.font)
if
font_type == "userdata"
or
(font_type == "table" and not self.font.container)
then
self.font:set_size(
self.font:get_size() * (new_scale / prev_scale)
)
end
end
---Registers a new animation to be ran on the update cycle.
---@param target? table If nil assumes properties belong to widget it self.
---@param properties table<string,number>
---@param options? widget.animation.options
function Widget:animate(target, properties, options)
if not target then target = self end
-- if name is set then prevent adding if another one with the same
-- animation name is already running
if options and options.name then
for _, animation in ipairs(self.animations) do
if animation.options and animation.options.name == options.name then
return
end
end
end
table.insert(self.animations, {
target = target,
properties = properties,
options = options
})
end
---Runs all registered animations removing duplicated and finished ones.
function Widget:run_animations()
if #self.animations > 0 then
---@type table<widget.animation, widget.animation>
local duplicates = {}
local targets = {}
local deleted = 0
for i=1, #self.animations do
local animation = self.animations[i - deleted]
-- do not run animations that change same target to prevent conflicts.
if not targets[animation.target] then
local finished = true
local options = animation.options or {}
for name, value in pairs(animation.properties) do
if animation.target[name] ~= value then
self:move_towards(animation.target, name, value, options.rate)
if options.on_step then
options.on_step(animation.target, name, animation.target[name])
end
if animation.target[name] ~= value then
finished = false
end
end
end
if finished then
if options.on_complete then
options.on_complete(self)
end
table.remove(self.animations, i - deleted)
deleted = deleted + 1
end
targets[animation.target] = animation
-- only registers it as duplicated if the animation does needs to
-- perform any tasks on completion.
elseif not targets[animation.target].on_complete then
duplicates[targets[animation.target]] = animation
end
end
-- remove older duplcated animations that modify same target and properties
for duplicate, newer_animation in pairs(duplicates) do
local exact_properties = true
for name, _ in pairs(duplicate.properties) do
if not newer_animation.properties[name] then
exact_properties = false
break
end
end
if exact_properties then
for name, _ in pairs(newer_animation.properties) do
if not duplicate.properties[name] then
exact_properties = false
break
end
end
end
if exact_properties then
for i, animation in ipairs(self.animations) do
if animation == duplicate then
table.remove(self.animations, i)
break
end
end
end
end
end
end
---If visible execute the widget calculations and returns true.
---@return boolean
function Widget:update()
if not self:is_visible() then return false end
Widget.super.update(self)
-- call this to be able to properly scroll
self:update_position()
-- run any pending animations
self:run_animations()
for _, child in pairs(self.childs) do
child:update()
end
return true
end
function Widget:draw_scrollbar()
if self.scrollable then
Widget.super.draw_scrollbar(self)
end
end
---If visible draw the widget and returns true.
---@return boolean
function Widget:draw()
if not self:is_visible() then return false end
Widget.super.draw(self)
self:draw_border()
if self.render_background then
if self.background_color then
self:draw_background(self.background_color)
else
self:draw_background(
self.parent and style.background or style.background2
)
end
end
if #self.childs > 0 then
core.push_clip_rect(
self.position.x,
self.position.y,
self.size.x,
self.size.y
)
end
for i=#self.childs, 1, -1 do
self.childs[i]:draw()
end
if #self.childs > 0 then
core.pop_clip_rect()
end
self:draw_scrollbar()
return true
end
---Recursively destroy all childs from the widget.
function Widget:destroy_childs()
for _=1, #self.childs do
self.childs[1]:destroy_childs()
table.remove(self.childs, 1)
end
end
---If floating, remove the widget from the floating widgets list
---to allow proper garbage collection.
function Widget:destroy()
if not self.parent or self.defer_draw then
for idx, widget in ipairs(floating_widgets) do
if widget == self then
widget:destroy_childs()
floating_widgets[idx] = nil
table.remove(floating_widgets, idx)
break
end
end
end
end
---Flag that indicates if the rootview events are already overrided.
---@type boolean
local root_overrided = false
---Called when initializing a floating widget to generate RootView overrides,
---this function will only override the events once.
function Widget.override_rootview()
if root_overrided then return end
root_overrided = true
local root_view_on_mouse_pressed = RootView.on_mouse_pressed
local root_view_on_mouse_released = RootView.on_mouse_released
local root_view_on_mouse_moved = RootView.on_mouse_moved
local root_view_on_mouse_wheel = RootView.on_mouse_wheel
local root_view_update = RootView.update
local root_view_draw = RootView.draw
local root_view_on_file_dropped = RootView.on_file_dropped
local root_view_on_text_input = RootView.on_text_input
function RootView:on_mouse_pressed(button, x, y, clicks)
local pressed = false
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if widget.visible then
widget.mouse_pressed_outside = not widget:mouse_on_top(x, y)
if
(not widget.defer_draw and not widget.child_active)
or
widget.mouse_pressed_outside
or
(pressed or not widget:on_mouse_pressed(button, x, y, clicks))
then
widget:swap_active_child()
else
pressed = true
end
end
end
if not pressed then
return root_view_on_mouse_pressed(self, button, x, y, clicks)
else
return true
end
end
function RootView:on_mouse_released(button, x, y)
local released = false
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if widget.visible then
if
(not widget.defer_draw and not widget.child_active)
or
widget.mouse_pressed_outside
or
not widget:on_mouse_released(button, x, y)
then
widget.mouse_pressed_outside = false
else
released = true
end
end
end
if not released then
root_view_on_mouse_released(self, button, x, y)
end
end
function RootView:on_mouse_moved(x, y, dx, dy)
local moved = false
if core.active_view ~= core.command_view then
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if widget.visible then
if
(not widget.defer_draw and not widget.child_active)
or
widget.mouse_pressed_outside
or
(moved or not widget:on_mouse_moved(x, y, dx, dy))
then
if
not widget.is_scrolling
and
not widget.child_active
and
widget.outside_view
then
core.set_active_view(widget.outside_view)
widget.outside_view = nil
end
elseif not moved then
if not widget.child_active and widget.defer_draw then
if not widget.outside_view then
widget.outside_view = core.active_view
end
core.set_active_view(widget)
moved = true
end
end
end
end
end
if not moved then
root_view_on_mouse_moved(self, x, y, dx, dy)
end
end
function RootView:on_mouse_wheel(y, x)
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if
widget.visible and widget.defer_draw and widget:on_mouse_wheel(y, x)
then
return true
end
end
return root_view_on_mouse_wheel(self, y, x)
end
function RootView:on_file_dropped(filename, x, y)
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if
widget.visible and widget.defer_draw
and
widget:on_file_dropped(filename, x, y)
then
return true
end
end
return root_view_on_file_dropped(self, filename, x, y)
end
function RootView:on_text_input(text)
for i=#floating_widgets, 1, -1 do
local widget = floating_widgets[i]
if
widget.visible and widget.defer_draw and widget:on_text_input(text)
then
return true
end
end
return root_view_on_text_input(self, text)
end
function RootView:update()
root_view_update(self)
local count = #floating_widgets
for i=1, count, 1 do
local widget = floating_widgets[i]
if widget.visible and widget.defer_draw then
widget:update()
end
end
end
function RootView:draw()
local count = #floating_widgets
for i=1, count, 1 do
local widget = floating_widgets[i]
if widget.visible and widget.defer_draw then
core.root_view:defer_draw(widget.draw, widget)
end
end
root_view_draw(self)
end
end
return Widget