From 153a83e5693557a253acb87ebafa634297ba0d8f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 1 Jun 2023 13:00:45 -0400 Subject: [PATCH] listbox improvements --- graphics/element.lua | 22 +++-- graphics/elements/listbox.lua | 149 +++++++++++++++++++++++++++------- 2 files changed, 138 insertions(+), 33 deletions(-) diff --git a/graphics/element.lua b/graphics/element.lua index bfeb0a5..d537171 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -62,7 +62,7 @@ local element = {} ---@param args graphics_args arguments function element.new(args) local self = { - id = -1, + id = nil, ---@type element_id|nil elem_type = debug.getinfo(2).name, define_completed = false, p_window = nil, ---@type table @@ -179,13 +179,13 @@ function element.new(args) ---@diagnostic disable: unused-local, unused-vararg -- handle a child element having been added - ---@param id string|integer element identifier + ---@param id element_id element identifier ---@param child graphics_element child element function protected.on_added(id, child) end -- handle a child element having been removed - ---@param id string|integer element identifier + ---@param id element_id element identifier function protected.on_removed(id) end @@ -282,6 +282,7 @@ function element.new(args) -- prepare the template if args.parent == nil then + self.id = args.id or "__ROOT__" protected.prepare_template(1) else self.id = args.parent.__add_child(args.id, protected) @@ -321,6 +322,8 @@ function element.new(args) v.delete() protected.children[k] = nil end + + args.parent.__remove_child(self.id) end -- ELEMENT TREE -- @@ -346,8 +349,17 @@ function element.new(args) end end + -- remove a child element + ---@param key element_id id + function public.__remove_child(key) + if protected.children[key] ~= nil then + protected.on_removed(key) + protected.children[key] = nil + end + end + -- actions to take upon a child element becoming ready (initial draw/construction completed) - ---@param key string|integer id + ---@param key element_id id ---@param child graphics_element function public.__child_ready(key, child) protected.on_added(key, child) @@ -364,8 +376,8 @@ function element.new(args) function public.remove(id) if protected.children[id] ~= nil then protected.children[id].delete() - protected.children[id] = nil protected.on_removed(id) + protected.children[id] = nil end end diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua index 105c3ee..a5d5f20 100644 --- a/graphics/elements/listbox.lua +++ b/graphics/elements/listbox.lua @@ -1,6 +1,6 @@ -- Scroll-able List Box Display Graphics Element --- local log = require("scada-common.log") +local tcd = require("scada-common.tcallbackdsp") local core = require("graphics.core") local element = require("graphics.element") @@ -10,6 +10,8 @@ local CLICK_TYPE = core.events.CLICK_TYPE ---@class listbox_args ---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled) ---@field item_pad? integer spacing (lines) between items in the list (default 0) +---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area +---@field nav_active? cpair active colors for bar held down or arrow held down ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -39,37 +41,90 @@ local function listbox(args) e.content_window = scroll_frame -- item list and scroll management - local list = {} - local item_pad = args.item_pad or 0 - local scroll_offset = 0 - local content_height = 0 + local list = {} + local item_pad = args.item_pad or 0 + local scroll_offset = 0 + local content_height = 0 local max_down_scroll = 0 - -- bar control/tracking variables - local bar_height = 0 -- full height of bar - local bar_bounds = { 0, 0 } -- top and bottom of bar - local holding_bar = false -- bar is being held by mouse - local bar_grip_pos = 0 -- where the bar was gripped by mouse down - local mouse_last_y = 0 -- last reported y coordinate of drag + local max_bar_height = e.frame.h - 2 + local bar_height = 0 -- full height of bar + local bar_bounds = { 0, 0 } -- top and bottom of bar + local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines + local holding_bar = false -- bar is being held by mouse + local bar_grip_pos = 0 -- where the bar was gripped by mouse down + local mouse_last_y = 0 -- last reported y coordinate of drag - -- draw up/down arrows - e.window.setCursorPos(e.frame.w, 1) - e.window.write("\x1e") - e.window.setCursorPos(e.frame.w, e.frame.h) - e.window.write("\x1f") + -- draw scroll bar arrows, optionally showing one of them as pressed + ---@param pressed_arrow? integer arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down) + local function draw_arrows(pressed_arrow) + local nav_fg_bg = args.nav_fg_bg or e.fg_bg + local active_fg_bg = args.nav_active or nav_fg_bg + + -- draw up/down arrows + if pressed_arrow == 1 then + e.window.setTextColor(active_fg_bg.fgd) + e.window.setBackgroundColor(active_fg_bg.bkg) + e.window.setCursorPos(e.frame.w, 1) + e.window.write("\x1e") + e.window.setTextColor(nav_fg_bg.fgd) + e.window.setBackgroundColor(nav_fg_bg.bkg) + e.window.setCursorPos(e.frame.w, e.frame.h) + e.window.write("\x1f") + elseif pressed_arrow == -1 then + e.window.setTextColor(nav_fg_bg.fgd) + e.window.setBackgroundColor(nav_fg_bg.bkg) + e.window.setCursorPos(e.frame.w, 1) + e.window.write("\x1e") + e.window.setTextColor(active_fg_bg.fgd) + e.window.setBackgroundColor(active_fg_bg.bkg) + e.window.setCursorPos(e.frame.w, e.frame.h) + e.window.write("\x1f") + else + e.window.setTextColor(nav_fg_bg.fgd) + e.window.setBackgroundColor(nav_fg_bg.bkg) + e.window.setCursorPos(e.frame.w, 1) + e.window.write("\x1e") + e.window.setCursorPos(e.frame.w, e.frame.h) + e.window.write("\x1f") + end + + e.window.setTextColor(e.fg_bg.fgd) + e.window.setBackgroundColor(e.fg_bg.bkg) + end -- render the scroll bar and re-cacluate height & bounds local function draw_bar() local offset = 2 + math.abs(scroll_offset) - bar_height = math.max(math.min(e.frame.h - 2 + max_down_scroll, e.frame.h - 2), 1) + bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height) + + if bar_height < 1 then + bar_is_scaled = true + -- can't do a 1:1 ratio + -- use minimum size bar with scaled offset + local scroll_progress = scroll_offset / max_down_scroll + offset = 2 + math.floor(scroll_progress * (max_bar_height - 1)) + bar_height = 1 + else + bar_is_scaled = false + end + bar_bounds = { offset, (bar_height + offset) - 1 } for i = 2, e.frame.h - 1 do - if i >= offset and i < (bar_height + offset) then - e.window.setBackgroundColor(e.fg_bg.fgd) + if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then + if args.nav_fg_bg ~= nil then + e.window.setBackgroundColor(args.nav_fg_bg.fgd) + else + e.window.setBackgroundColor(e.fg_bg.fgd) + end else - e.window.setBackgroundColor(e.fg_bg.bkg) + if args.nav_fg_bg ~= nil then + e.window.setBackgroundColor(args.nav_fg_bg.bkg) + else + e.window.setBackgroundColor(e.fg_bg.bkg) + end end e.window.setCursorPos(e.frame.w, i) @@ -104,22 +159,42 @@ local function listbox(args) scroll_frame.setVisible(true) draw_bar() + end - -- log.info("content_height[" .. content_height .. "] max_down_scroll[" .. max_down_scroll .. "] scroll_offset[" .. scroll_offset .. "] bar_height[" .. bar_height .. "]") + -- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship + ---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one + local function scaled_bar_scroll(direction) + local scroll_progress = scroll_offset / max_down_scroll + local bar_position = math.floor(scroll_progress * (max_bar_height - 1)) + + -- check what moving the scroll bar up or down would mean for the scroll progress + scroll_progress = (bar_position + direction) / (max_bar_height - 1) + + return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll) end -- scroll down the list - local function scroll_down() + local function scroll_down(scaled) if scroll_offset > max_down_scroll then - scroll_offset = scroll_offset - 1 + if scaled then + scroll_offset = scaled_bar_scroll(1) + else + scroll_offset = scroll_offset - 1 + end + update_positions() end end -- scroll up the list - local function scroll_up() + local function scroll_up(scaled) if scroll_offset < 0 then - scroll_offset = scroll_offset + 1 + if scaled then + scroll_offset = scaled_bar_scroll(-1) + else + scroll_offset = scroll_offset + 1 + end + update_positions() end end @@ -148,11 +223,25 @@ local function listbox(args) ---@param event mouse_interaction mouse event function e.handle_mouse(event) if e.enabled then - if event.type == CLICK_TYPE.TAP or event.type == CLICK_TYPE.DOWN then + if event.type == CLICK_TYPE.TAP then if event.current.x == e.frame.w then if event.current.y == 1 or event.current.y < bar_bounds[1] then + draw_arrows(1) + scroll_up() + if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end + elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then + draw_arrows(-1) + scroll_down() + if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end + end + end + elseif event.type == CLICK_TYPE.DOWN then + if event.current.x == e.frame.w then + if event.current.y == 1 or event.current.y < bar_bounds[1] then + draw_arrows(1) scroll_up() elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then + draw_arrows(-1) scroll_down() else -- clicked on bar @@ -163,14 +252,15 @@ local function listbox(args) end elseif event.type == CLICK_TYPE.UP then holding_bar = false + draw_arrows(0) elseif event.type == CLICK_TYPE.DRAG then if holding_bar then -- if mouse is within vertical frame, including the grip point if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then if event.current.y < mouse_last_y then - scroll_up() + scroll_up(bar_is_scaled) elseif event.current.y > mouse_last_y then - scroll_down() + scroll_down(bar_is_scaled) end mouse_last_y = event.current.y @@ -184,6 +274,9 @@ local function listbox(args) end end + draw_arrows(0) + draw_bar() + return e.complete() end