From 70831b49d2afe31f896eaf261ec7cfa8dcd8447f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 24 Sep 2023 22:27:39 -0400 Subject: [PATCH] #344 2D radio button array --- graphics/element.lua | 1 + graphics/elements/controls/checkbox.lua | 9 +- graphics/elements/controls/radio_2d.lua | 204 ++++++++++++++++++++ graphics/elements/controls/radio_button.lua | 20 +- 4 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 graphics/elements/controls/radio_2d.lua diff --git a/graphics/element.lua b/graphics/element.lua index ea32d0d..e04a0df 100644 --- a/graphics/element.lua +++ b/graphics/element.lua @@ -28,6 +28,7 @@ local element = {} ---|hazard_button_args ---|multi_button_args ---|push_button_args +---|radio_2d_args ---|radio_button_args ---|sidebar_args ---|spinbox_args diff --git a/graphics/elements/controls/checkbox.lua b/graphics/elements/controls/checkbox.lua index 1d9e1c7..663e18d 100644 --- a/graphics/elements/controls/checkbox.lua +++ b/graphics/elements/controls/checkbox.lua @@ -6,7 +6,7 @@ local element = require("graphics.element") ---@class checkbox_args ---@field label string checkbox text ---@field box_fg_bg cpair colors for checkbox ----@field callback function function to call on press +---@field callback? function function to call on press ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -20,7 +20,6 @@ local element = require("graphics.element") local function checkbox(args) assert(type(args.label) == "string", "graphics.elements.controls.checkbox: label is a required field") assert(type(args.box_fg_bg) == "table", "graphics.elements.controls.checkbox: box_fg_bg is a required field") - assert(type(args.callback) == "function", "graphics.elements.controls.checkbox: callback is a required field") args.can_focus = true args.height = 1 @@ -72,10 +71,10 @@ local function checkbox(args) -- handle mouse interaction ---@param event mouse_interaction mouse event function e.handle_mouse(event) - if e.enabled and core.events.was_clicked(event.type) then + if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then e.value = not e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end @@ -86,7 +85,7 @@ local function checkbox(args) if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then e.value = not e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end end diff --git a/graphics/elements/controls/radio_2d.lua b/graphics/elements/controls/radio_2d.lua new file mode 100644 index 0000000..a3ac712 --- /dev/null +++ b/graphics/elements/controls/radio_2d.lua @@ -0,0 +1,204 @@ +-- 2D Radio Button Graphics Element + +local util = require("scada-common.util") + +local core = require("graphics.core") +local element = require("graphics.element") + +---@class radio_2d_args +---@field rows integer +---@field columns integer +---@field options table +---@field radio_colors cpair radio button colors (inner & outer) +---@field select_color? color color for radio button when selected +---@field color_map? table colors for each radio button when selected +---@field disable_color? color color for radio button when disabled +---@field disable_fg_bg? cpair text colors when disabled +---@field default? integer default state, defaults to options[1] +---@field callback? function function to call on touch +---@field parent graphics_element +---@field id? string element id +---@field x? integer 1 if omitted +---@field y? integer auto incremented if omitted +---@field fg_bg? cpair foreground/background colors +---@field hidden? boolean true to hide on initial draw + +-- new 2D radio button list (latch selection, exclusively one color at a time) +---@param args radio_2d_args +---@return graphics_element element, element_id id +local function radio_2d_button(args) + assert(type(args.options) == "table" and #args.options > 0, "graphics.elements.controls.radio_2d: options should be a table with length >= 1") + assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_2d: radio_colors is a required field") + assert(type(args.select_color) == "number" or type(args.color_map) == "table", "graphics.elements.controls.radio_2d: select_color or color_map is required") + assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), + "graphics.elements.controls.radio_2d: default must be nil or a number > 0") + + local array = {} + local col_widths = {} + + local next_idx = 1 + local total_width = 0 + local max_rows = 1 + + local focused_opt = 1 + local focus_x, focus_y = 1, 1 + + -- build table to display + for col = 1, args.columns do + local max_width = 0 + array[col] = {} + + for row = 1, args.rows do + local len = string.len(args.options[next_idx]) + if len > max_width then max_width = len end + if row > max_rows then max_rows = row end + + table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len }) + + next_idx = next_idx + 1 + if next_idx > #args.options then break end + end + + table.insert(col_widths, max_width + 3) + total_width = total_width + max_width + 3 + if next_idx > #args.options then break end + end + + args.can_focus = true + args.width = total_width + args.height = max_rows + + -- create new graphics element base object + local e = element.new(args) + + -- selected option (convert nil to 1 if missing) + e.value = args.default or 1 + + -- show the args.options/states + local function draw() + local col_x = 1 + + local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b) + + for col = 1, #array do + for row = 1, #array[col] do + local opt = array[col][row] + local select_color = args.select_color + + if type(args.color_map) == "table" and args.color_map[opt.id] then + select_color = args.color_map[opt.id] + end + + local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a) + local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b) + + e.w_set_cur(col_x, row) + + e.w_set_fgd(inner_color) + e.w_set_bkg(outer_color) + e.w_write("\x88") + + e.w_set_fgd(outer_color) + e.w_set_bkg(e.fg_bg.bkg) + e.w_write("\x95") + + if opt.id == focused_opt then + focus_x, focus_y = row, col + end + + -- write button text + if opt.id == focused_opt and e.is_focused() and e.enabled then + e.w_set_fgd(e.fg_bg.bkg) + e.w_set_bkg(e.fg_bg.fgd) + elseif type(args.disable_fg_bg) == "table" and not e.enabled then + e.w_set_fgd(args.disable_fg_bg.fgd) + e.w_set_bkg(args.disable_fg_bg.bkg) + else + e.w_set_fgd(e.fg_bg.fgd) + e.w_set_bkg(e.fg_bg.bkg) + end + + e.w_write(opt.text) + end + + col_x = col_x + col_widths[col] + end + end + + -- handle mouse interaction + ---@param event mouse_interaction mouse event + function e.handle_mouse(event) + if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then + -- determine what was pressed + for _, row in ipairs(array) do + local elem = row[event.current.y] + if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then + e.value = elem.id + focused_opt = elem.id + draw() + if type(args.callback) == "function" then args.callback(e.value) end + break + end + end + end + end + + -- handle keyboard interaction + ---@param event key_interaction key event + function e.handle_key(event) + if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then + if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then + e.value = focused_opt + draw() + if type(args.callback) == "function" then args.callback(e.value) end + elseif event.key == keys.down then + if focused_opt < #args.options then + focused_opt = focused_opt + 1 + draw() + end + elseif event.key == keys.up then + if focused_opt > 1 then + focused_opt = focused_opt - 1 + draw() + end + elseif event.key == keys.right then + if array[focus_y + 1] and array[focus_y + 1][focus_x] then + focused_opt = array[focus_y + 1][focus_x].id + else focused_opt = array[1][focus_x].id end + draw() + elseif event.key == keys.left then + if array[focus_y - 1] and array[focus_y - 1][focus_x] then + focused_opt = array[focus_y - 1][focus_x].id + draw() + elseif array[#array][focus_x] then + focused_opt = array[#array][focus_x].id + draw() + end + end + end + end + + -- set the value + ---@param val integer new value + function e.set_value(val) + if val > 0 and val <= #args.options then + e.value = val + draw() + end + end + + -- handle focus + e.on_focused = draw + e.on_unfocused = draw + + -- handle enable + e.on_enabled = draw + e.on_disabled = draw + + -- initial draw + draw() + + return e.complete() +end + +return radio_2d_button diff --git a/graphics/elements/controls/radio_button.lua b/graphics/elements/controls/radio_button.lua index 26c699b..cff16eb 100644 --- a/graphics/elements/controls/radio_button.lua +++ b/graphics/elements/controls/radio_button.lua @@ -5,13 +5,15 @@ local util = require("scada-common.util") local core = require("graphics.core") local element = require("graphics.element") +local KEY_CLICK = core.events.KEY_CLICK + ---@class radio_button_args ---@field options table button options ----@field callback function function to call on touch ---@field radio_colors cpair radio button colors (inner & outer) ---@field select_color color color for radio button border when selected ---@field default? integer default state, defaults to options[1] ---@field min_width? integer text length + 2 if omitted +---@field callback? function function to call on touch ---@field parent graphics_element ---@field id? string element id ---@field x? integer 1 if omitted @@ -25,7 +27,6 @@ local element = require("graphics.element") local function radio_button(args) assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field") assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required") - assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field") assert(type(args.radio_colors) == "table", "graphics.elements.controls.radio_button: radio_colors is a required field") assert(type(args.select_color) == "number", "graphics.elements.controls.radio_button: select_color is a required field") assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), @@ -95,8 +96,9 @@ local function radio_button(args) -- determine what was pressed if args.options[event.current.y] ~= nil then e.value = event.current.y + focused_opt = e.value draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end end end end @@ -104,11 +106,11 @@ local function radio_button(args) -- handle keyboard interaction ---@param event key_interaction key event function e.handle_key(event) - if event.type == core.events.KEY_CLICK.DOWN then - if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then + if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then + if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then e.value = focused_opt draw() - args.callback(e.value) + if type(args.callback) == "function" then args.callback(e.value) end elseif event.key == keys.down then if focused_opt < #args.options then focused_opt = focused_opt + 1 @@ -126,8 +128,10 @@ local function radio_button(args) -- set the value ---@param val integer new value function e.set_value(val) - e.value = val - draw() + if val > 0 and val <= #args.options then + e.value = val + draw() + end end -- handle focus