2023-05-31 15:44:41 +00:00
|
|
|
-- Scroll-able List Box Display Graphics Element
|
|
|
|
|
2023-06-03 21:40:57 +00:00
|
|
|
local tcd = require("scada-common.tcd")
|
2023-05-31 15:44:41 +00:00
|
|
|
|
|
|
|
local core = require("graphics.core")
|
|
|
|
local element = require("graphics.element")
|
|
|
|
|
2023-10-20 03:20:04 +00:00
|
|
|
local KEY_CLICK = core.events.KEY_CLICK
|
2023-09-17 01:06:16 +00:00
|
|
|
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
2023-05-31 15:44:41 +00:00
|
|
|
|
|
|
|
---@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)
|
2023-06-01 17:00:45 +00:00
|
|
|
---@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
|
2023-05-31 15:44:41 +00:00
|
|
|
---@field parent graphics_element
|
|
|
|
---@field id? string element id
|
|
|
|
---@field x? integer 1 if omitted
|
2023-07-10 03:42:44 +00:00
|
|
|
---@field y? integer auto incremented if omitted
|
2023-05-31 15:44:41 +00:00
|
|
|
---@field width? integer parent width if omitted
|
|
|
|
---@field height? integer parent height if omitted
|
|
|
|
---@field gframe? graphics_frame frame instead of x/y/width/height
|
|
|
|
---@field fg_bg? cpair foreground/background colors
|
|
|
|
---@field hidden? boolean true to hide on initial draw
|
|
|
|
|
|
|
|
---@class listbox_item
|
|
|
|
---@field id string|integer element ID
|
|
|
|
---@field e graphics_element element
|
|
|
|
---@field y integer y position
|
|
|
|
---@field h integer element height
|
|
|
|
|
|
|
|
-- new listbox element
|
|
|
|
---@nodiscard
|
|
|
|
---@param args listbox_args
|
|
|
|
---@return graphics_element element, element_id id
|
|
|
|
local function listbox(args)
|
2023-10-20 03:20:04 +00:00
|
|
|
args.can_focus = true
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
-- create new graphics element base object
|
|
|
|
local e = element.new(args)
|
|
|
|
|
|
|
|
-- create content window for child elements
|
|
|
|
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
|
|
|
|
e.content_window = scroll_frame
|
|
|
|
|
|
|
|
-- item list and scroll management
|
2023-06-01 17:00:45 +00:00
|
|
|
local list = {}
|
|
|
|
local item_pad = args.item_pad or 0
|
|
|
|
local scroll_offset = 0
|
|
|
|
local content_height = 0
|
2023-05-31 15:44:41 +00:00
|
|
|
local max_down_scroll = 0
|
|
|
|
-- bar control/tracking variables
|
2023-06-01 17:00:45 +00:00
|
|
|
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 scroll bar arrows, optionally showing one of them as pressed
|
2023-06-03 21:31:06 +00:00
|
|
|
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
|
2023-06-01 17:00:45 +00:00
|
|
|
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
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_fgd(active_fg_bg.fgd)
|
|
|
|
e.w_set_bkg(active_fg_bg.bkg)
|
|
|
|
e.w_set_cur(e.frame.w, 1)
|
|
|
|
e.w_write("\x1e")
|
|
|
|
e.w_set_fgd(nav_fg_bg.fgd)
|
|
|
|
e.w_set_bkg(nav_fg_bg.bkg)
|
|
|
|
e.w_set_cur(e.frame.w, e.frame.h)
|
|
|
|
e.w_write("\x1f")
|
2023-06-01 17:00:45 +00:00
|
|
|
elseif pressed_arrow == -1 then
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_fgd(nav_fg_bg.fgd)
|
|
|
|
e.w_set_bkg(nav_fg_bg.bkg)
|
|
|
|
e.w_set_cur(e.frame.w, 1)
|
|
|
|
e.w_write("\x1e")
|
|
|
|
e.w_set_fgd(active_fg_bg.fgd)
|
|
|
|
e.w_set_bkg(active_fg_bg.bkg)
|
|
|
|
e.w_set_cur(e.frame.w, e.frame.h)
|
|
|
|
e.w_write("\x1f")
|
2023-06-01 17:00:45 +00:00
|
|
|
else
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_fgd(nav_fg_bg.fgd)
|
|
|
|
e.w_set_bkg(nav_fg_bg.bkg)
|
|
|
|
e.w_set_cur(e.frame.w, 1)
|
|
|
|
e.w_write("\x1e")
|
|
|
|
e.w_set_cur(e.frame.w, e.frame.h)
|
|
|
|
e.w_write("\x1f")
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
|
|
|
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_fgd(e.fg_bg.fgd)
|
|
|
|
e.w_set_bkg(e.fg_bg.bkg)
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
2023-05-31 15:44:41 +00:00
|
|
|
|
|
|
|
-- render the scroll bar and re-cacluate height & bounds
|
|
|
|
local function draw_bar()
|
|
|
|
local offset = 2 + math.abs(scroll_offset)
|
|
|
|
|
2023-06-01 17:00:45 +00:00
|
|
|
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
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
bar_bounds = { offset, (bar_height + offset) - 1 }
|
|
|
|
|
|
|
|
for i = 2, e.frame.h - 1 do
|
2023-06-01 17:00:45 +00:00
|
|
|
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
|
|
|
if args.nav_fg_bg ~= nil then
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_bkg(args.nav_fg_bg.fgd)
|
2023-06-01 17:00:45 +00:00
|
|
|
else
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_bkg(e.fg_bg.fgd)
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
2023-05-31 15:44:41 +00:00
|
|
|
else
|
2023-06-01 17:00:45 +00:00
|
|
|
if args.nav_fg_bg ~= nil then
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_bkg(args.nav_fg_bg.bkg)
|
2023-06-01 17:00:45 +00:00
|
|
|
else
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_bkg(e.fg_bg.bkg)
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
2023-05-31 15:44:41 +00:00
|
|
|
end
|
|
|
|
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_cur(e.frame.w, i)
|
2023-10-20 03:20:04 +00:00
|
|
|
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
2023-05-31 15:44:41 +00:00
|
|
|
end
|
|
|
|
|
2023-08-31 01:11:57 +00:00
|
|
|
e.w_set_bkg(e.fg_bg.bkg)
|
2023-05-31 15:44:41 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- update item y positions and move elements
|
|
|
|
local function update_positions()
|
|
|
|
local next_y = 1
|
|
|
|
|
|
|
|
scroll_frame.setVisible(false)
|
|
|
|
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
|
|
|
|
scroll_frame.setTextColor(e.fg_bg.fgd)
|
|
|
|
scroll_frame.clear()
|
|
|
|
|
|
|
|
for i = 1, #list do
|
|
|
|
local item = list[i] ---@type listbox_item
|
|
|
|
item.y = next_y
|
|
|
|
next_y = next_y + item.h + item_pad
|
|
|
|
item.e.reposition(1, item.y)
|
|
|
|
item.e.show()
|
|
|
|
end
|
|
|
|
|
|
|
|
content_height = next_y
|
|
|
|
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
|
|
|
|
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
|
|
|
|
|
|
|
|
scroll_frame.reposition(1, 1 + scroll_offset)
|
|
|
|
scroll_frame.setVisible(true)
|
|
|
|
|
2023-10-15 21:02:48 +00:00
|
|
|
-- shift mouse events
|
|
|
|
e.mouse_window_shift.y = scroll_offset
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
draw_bar()
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- 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)
|
2023-05-31 15:44:41 +00:00
|
|
|
|
2023-06-01 17:00:45 +00:00
|
|
|
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
|
2023-05-31 15:44:41 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- scroll down the list
|
2023-06-01 17:00:45 +00:00
|
|
|
local function scroll_down(scaled)
|
2023-05-31 15:44:41 +00:00
|
|
|
if scroll_offset > max_down_scroll then
|
2023-06-01 17:00:45 +00:00
|
|
|
if scaled then
|
|
|
|
scroll_offset = scaled_bar_scroll(1)
|
|
|
|
else
|
|
|
|
scroll_offset = scroll_offset - 1
|
|
|
|
end
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
update_positions()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- scroll up the list
|
2023-06-01 17:00:45 +00:00
|
|
|
local function scroll_up(scaled)
|
2023-05-31 15:44:41 +00:00
|
|
|
if scroll_offset < 0 then
|
2023-06-01 17:00:45 +00:00
|
|
|
if scaled then
|
|
|
|
scroll_offset = scaled_bar_scroll(-1)
|
|
|
|
else
|
|
|
|
scroll_offset = scroll_offset + 1
|
|
|
|
end
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
update_positions()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- handle a child element having been added to the list
|
2023-06-03 21:31:06 +00:00
|
|
|
---@param id element_id element identifier
|
2023-05-31 15:44:41 +00:00
|
|
|
---@param child graphics_element child element
|
|
|
|
function e.on_added(id, child)
|
|
|
|
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
|
|
|
update_positions()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- handle a child element having been removed from the list
|
2023-06-03 21:31:06 +00:00
|
|
|
---@param id element_id element identifier
|
2023-05-31 15:44:41 +00:00
|
|
|
function e.on_removed(id)
|
|
|
|
for idx, elem in ipairs(list) do
|
|
|
|
if elem.id == id then
|
|
|
|
table.remove(list, idx)
|
|
|
|
update_positions()
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-10-20 03:20:04 +00:00
|
|
|
-- handle focus
|
|
|
|
e.on_focused = draw_bar
|
|
|
|
e.on_unfocused = draw_bar
|
|
|
|
|
2023-10-15 21:02:48 +00:00
|
|
|
-- handle a child in the list being focused, make sure it is visible
|
|
|
|
function e.on_child_focused(child)
|
|
|
|
for i = 1, #list do
|
|
|
|
local item = list[i] ---@type listbox_item
|
|
|
|
if item.e == child then
|
|
|
|
if (item.y + scroll_offset) <= 0 then
|
|
|
|
scroll_offset = 1 - item.y
|
|
|
|
update_positions()
|
|
|
|
draw_bar()
|
|
|
|
elseif (item.y + scroll_offset) == 1 then
|
|
|
|
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
|
|
|
|
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
|
|
|
|
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
|
|
|
|
update_positions()
|
|
|
|
draw_bar()
|
|
|
|
end
|
|
|
|
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
-- handle mouse interaction
|
|
|
|
---@param event mouse_interaction mouse event
|
|
|
|
function e.handle_mouse(event)
|
|
|
|
if e.enabled then
|
2023-09-17 01:06:16 +00:00
|
|
|
if event.type == MOUSE_CLICK.TAP then
|
2023-06-01 17:00:45 +00:00
|
|
|
if event.current.x == e.frame.w then
|
|
|
|
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
|
|
|
scroll_up()
|
2023-10-15 21:02:48 +00:00
|
|
|
if event.current.y == 1 then
|
|
|
|
draw_arrows(1)
|
|
|
|
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
|
|
|
end
|
2023-06-01 17:00:45 +00:00
|
|
|
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
|
|
|
scroll_down()
|
2023-10-15 21:02:48 +00:00
|
|
|
if event.current.y == e.frame.h then
|
|
|
|
draw_arrows(-1)
|
|
|
|
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
|
|
|
end
|
2023-06-01 17:00:45 +00:00
|
|
|
end
|
|
|
|
end
|
2023-09-17 01:06:16 +00:00
|
|
|
elseif event.type == MOUSE_CLICK.DOWN then
|
2023-05-31 15:44:41 +00:00
|
|
|
if event.current.x == e.frame.w then
|
|
|
|
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
|
|
|
scroll_up()
|
2023-10-15 21:02:48 +00:00
|
|
|
if event.current.y == 1 then draw_arrows(1) end
|
2023-05-31 15:44:41 +00:00
|
|
|
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
|
|
|
scroll_down()
|
2023-10-15 21:02:48 +00:00
|
|
|
if event.current.y == e.frame.h then draw_arrows(-1) end
|
2023-05-31 15:44:41 +00:00
|
|
|
else
|
|
|
|
-- clicked on bar
|
|
|
|
holding_bar = true
|
|
|
|
bar_grip_pos = event.current.y - bar_bounds[1]
|
|
|
|
mouse_last_y = event.current.y
|
|
|
|
end
|
|
|
|
end
|
2023-09-17 01:06:16 +00:00
|
|
|
elseif event.type == MOUSE_CLICK.UP then
|
2023-05-31 15:44:41 +00:00
|
|
|
holding_bar = false
|
2023-06-01 17:00:45 +00:00
|
|
|
draw_arrows(0)
|
2023-09-17 01:06:16 +00:00
|
|
|
elseif event.type == MOUSE_CLICK.DRAG then
|
2023-05-31 15:44:41 +00:00
|
|
|
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
|
2023-06-01 17:00:45 +00:00
|
|
|
scroll_up(bar_is_scaled)
|
2023-05-31 15:44:41 +00:00
|
|
|
elseif event.current.y > mouse_last_y then
|
2023-06-01 17:00:45 +00:00
|
|
|
scroll_down(bar_is_scaled)
|
2023-05-31 15:44:41 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
mouse_last_y = event.current.y
|
|
|
|
end
|
|
|
|
end
|
2023-09-17 01:06:16 +00:00
|
|
|
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
|
2023-05-31 15:44:41 +00:00
|
|
|
scroll_down()
|
2023-09-17 01:06:16 +00:00
|
|
|
elseif event.type == MOUSE_CLICK.SCROLL_UP then
|
2023-05-31 15:44:41 +00:00
|
|
|
scroll_up()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-10-20 03:20:04 +00:00
|
|
|
-- handle keyboard interaction
|
|
|
|
---@param event key_interaction key event
|
|
|
|
function e.handle_key(event)
|
|
|
|
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
|
|
|
if event.key == keys.up then
|
|
|
|
scroll_up()
|
|
|
|
elseif event.key == keys.down then
|
|
|
|
scroll_down()
|
|
|
|
elseif event.key == keys.home then
|
|
|
|
scroll_offset = 0
|
|
|
|
update_positions()
|
|
|
|
elseif event.key == keys["end"] then
|
|
|
|
scroll_offset = max_down_scroll
|
|
|
|
update_positions()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-09-29 23:34:10 +00:00
|
|
|
-- element redraw
|
|
|
|
function e.redraw()
|
|
|
|
draw_arrows(0)
|
|
|
|
draw_bar()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- initial draw
|
|
|
|
e.redraw()
|
2023-06-01 17:00:45 +00:00
|
|
|
|
2023-05-31 15:44:41 +00:00
|
|
|
return e.complete()
|
|
|
|
end
|
|
|
|
|
|
|
|
return listbox
|