-- -- 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 ---@field options? widget.animation.options ---Represents a reference to a font stored elsewhere. ---@class widget.fontreference ---@field public container table ---@field public name string ---@alias widget.font widget.fontreference | renderer.font | string ---@alias widget.clicktype ---| "left" ---| "right" ---@alias widget.styledtext table ---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 ---@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 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 ---@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 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