diff --git a/graphics/element.lua b/graphics/element.lua index a775eb7..bfeb0a5 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -45,6 +45,7 @@ local element = {} ---|colormap_args ---|displaybox_args ---|div_args +---|listbox_args ---|multipane_args ---|pipenet_args ---|rectangle_args @@ -66,7 +67,6 @@ function element.new(args) define_completed = false, p_window = nil, ---@type table position = { x = 1, y = 1 }, ---@type coordinate_2d - child_offset = { x = 0, y = 0 }, ---@type coordinate_2d bounds = { x1 = 1, y1 = 1, x2 = 1, y2 = 1 }, ---@class element_bounds next_y = 1, subscriptions = {}, @@ -112,7 +112,7 @@ function element.new(args) else local w, h = self.p_window.getSize() protected.frame.x = args.x or 1 - protected.frame.y = args.y or next_y + protected.frame.y = args.y or next_y protected.frame.w = args.width or w protected.frame.h = args.height or h end diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua new file mode 100644 index 0000000..105c3ee --- /dev/null +++ b/graphics/elements/listbox.lua @@ -0,0 +1,190 @@ +-- Scroll-able List Box Display Graphics Element + +-- local log = require("scada-common.log") + +local core = require("graphics.core") +local element = require("graphics.element") + +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 parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer 1 if omitted +---@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) + -- 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 + 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 + + -- 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") + + -- 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_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) + else + e.window.setBackgroundColor(e.fg_bg.bkg) + end + + e.window.setCursorPos(e.frame.w, i) + e.window.write(" ") + end + + e.window.setBackgroundColor(e.fg_bg.bkg) + 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) + + draw_bar() + + -- log.info("content_height[" .. content_height .. "] max_down_scroll[" .. max_down_scroll .. "] scroll_offset[" .. scroll_offset .. "] bar_height[" .. bar_height .. "]") + end + + -- scroll down the list + local function scroll_down() + if scroll_offset > max_down_scroll then + scroll_offset = scroll_offset - 1 + update_positions() + end + end + + -- scroll up the list + local function scroll_up() + if scroll_offset < 0 then + scroll_offset = scroll_offset + 1 + update_positions() + end + end + + -- handle a child element having been added to the list + ---@param id string|integer element identifier + ---@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 + ---@param id string|integer element identifier + 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 + + -- handle mouse interaction + ---@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.current.x == e.frame.w then + if event.current.y == 1 or event.current.y < bar_bounds[1] then + scroll_up() + elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then + scroll_down() + else + -- clicked on bar + holding_bar = true + bar_grip_pos = event.current.y - bar_bounds[1] + mouse_last_y = event.current.y + end + end + elseif event.type == CLICK_TYPE.UP then + holding_bar = false + 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() + elseif event.current.y > mouse_last_y then + scroll_down() + end + + mouse_last_y = event.current.y + end + end + elseif event.type == CLICK_TYPE.SCROLL_DOWN then + scroll_down() + elseif event.type == CLICK_TYPE.SCROLL_UP then + scroll_up() + end + end + end + + return e.complete() +end + +return listbox diff --git a/supervisor/panel/front_panel.lua b/supervisor/panel/front_panel.lua index bf95c6b..940a116 100644 --- a/supervisor/panel/front_panel.lua +++ b/supervisor/panel/front_panel.lua @@ -74,7 +74,7 @@ local function init(panel) -- plc page - local plc_page = Div{parent=page_div,x=1,y=1} + local plc_page = Div{parent=page_div,x=1,y=1,hidden=true} local plc_list = Div{parent=plc_page,x=2,y=2,width=49} for i = 1, config.NUM_REACTORS do @@ -106,12 +106,19 @@ local function init(panel) -- rtu page - local rtu_page = Div{parent=page_div,x=1,y=1} - local rtu_list = Div{parent=rtu_page,x=2,y=2,width=49} + local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true} + local rtu_list = ListBox{parent=rtu_page,x=2,y=2,height=15,width=49,scroll_height=1000,item_pad=1,fg_bg=cpair(colors.black,colors.white)} + + local item_1 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.red),hidden=true} + local item_2 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.orange),hidden=true} + local item_3 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.yellow),hidden=true} + local item_4 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.green),hidden=true} + local item_5 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.blue),hidden=true} + local item_6 = Div{parent=rtu_list,height=3,fg_bg=cpair(colors.black,colors.purple),hidden=true} -- coordinator page - local crd_page = Div{parent=page_div,x=1,y=1} + local crd_page = Div{parent=page_div,x=1,y=1,hidden=true} local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=cpair(colors.black,colors.white)} local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green,colors.green_off)} @@ -133,7 +140,7 @@ local function init(panel) -- pocket page - local pkt_page = Div{parent=page_div,x=1,y=1} + local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true} local pkt_box = Div{parent=pkt_page,x=2,y=2,width=49} local panes = { main_page, plc_page, rtu_page, crd_page, pkt_page }