diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9e81f80..b5e7976 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,6 +5,7 @@
"colors",
"fs",
"http",
+ "keys",
"parallel",
"periphemu",
"peripheral",
diff --git a/graphics/core.lua b/graphics/core.lua
index 5177b8c..2305990 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
-core.version = "1.1.3"
+core.version = "2.0.0"
core.flasher = flasher
core.events = events
@@ -15,11 +15,7 @@ core.events = events
-- Core Types
---@enum TEXT_ALIGN
-core.TEXT_ALIGN = {
- LEFT = 1,
- CENTER = 2,
- RIGHT = 3
-}
+core.TEXT_ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
---@class graphics_border
---@field width integer
@@ -73,15 +69,9 @@ end
function core.cpair(a, b)
return {
-- color pairs
- color_a = a,
- color_b = b,
- blit_a = colors.toBlit(a),
- blit_b = colors.toBlit(b),
+ color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
-- aliases
- fgd = a,
- bkg = b,
- blit_fgd = colors.toBlit(a),
- blit_bkg = colors.toBlit(b)
+ fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
}
end
diff --git a/graphics/element.lua b/graphics/element.lua
index 05fbb74..4399ac1 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -19,6 +19,7 @@ local element = {}
---@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
+---@field can_focus? boolean true if this element can be focused, false by default
---@alias graphics_args graphics_args_generic
---|waiting_args
@@ -32,6 +33,7 @@ local element = {}
---|spinbox_args
---|switch_button_args
---|tabbar_args
+---|number_field_args
---|alarm_indicator_light
---|core_map_args
---|data_indicator_args
@@ -69,6 +71,7 @@ local element = {}
function element.new(args, child_offset_x, child_offset_y)
local self = {
id = nil, ---@type element_id|nil
+ is_root = args.parent == nil,
elem_type = debug.getinfo(2).name,
define_completed = false,
p_window = nil, ---@type table
@@ -78,6 +81,7 @@ function element.new(args, child_offset_x, child_offset_y)
next_id = 0, -- next child ID
subscriptions = {},
button_down = { events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1), events.new_coord_2d(-1, -1) },
+ focused = false,
mt = {}
}
@@ -89,7 +93,8 @@ function element.new(args, child_offset_x, child_offset_y)
content_window = nil, ---@type table|nil
fg_bg = core.cpair(colors.white, colors.black),
frame = core.gframe(1, 1, 1, 1),
- children = {}
+ children = {},
+ child_id_map = {}
}
local name_brief = "graphics.element{" .. self.elem_type .. "}: "
@@ -104,6 +109,69 @@ function element.new(args, child_offset_x, child_offset_y)
setmetatable(public, self.mt)
+ -----------------------
+ -- PRIVATE FUNCTIONS --
+ -----------------------
+
+ -- use tab to jump to the next focusable field
+ ---@param reverse boolean
+ local function _tab_focusable(reverse)
+ local first_f = nil ---@type graphics_element|nil
+ local prev_f = nil ---@type graphics_element|nil
+ local cur_f = nil ---@type graphics_element|nil
+ local done = false
+
+ ---@param elem graphics_element
+ local function handle_element(elem)
+ if elem.is_visible() and elem.is_focusable() and elem.is_enabled() then
+ if first_f == nil then first_f = elem end
+
+ if cur_f == nil then
+ if elem.is_focused() then
+ cur_f = elem
+ if (not done) and (reverse and prev_f ~= nil) then
+ cur_f.unfocus()
+ prev_f.focus()
+ done = true
+ end
+ end
+ else
+ if elem.is_focused() then
+ elem.unfocus()
+ elseif not (reverse or done) then
+ cur_f.unfocus()
+ elem.focus()
+ done = true
+ end
+ end
+
+ prev_f = elem
+ end
+ end
+
+ ---@param children table
+ local function traverse(children)
+ for i = 1, #children do
+ local child = children[i] ---@type graphics_base
+ handle_element(child.get())
+ if child.get().is_visible() then traverse(child.children) end
+ end
+ end
+
+ traverse(protected.children)
+
+ -- if no element was focused, wrap focus
+ if first_f ~= nil and not done then
+ if reverse then
+ if cur_f ~= nil then cur_f.unfocus() end
+ if prev_f ~= nil then prev_f.focus() end
+ else
+ if cur_f ~= nil then cur_f.unfocus() end
+ first_f.focus()
+ end
+ end
+ end
+
-------------------------
-- PROTECTED FUNCTIONS --
-------------------------
@@ -214,85 +282,6 @@ function element.new(args, child_offset_x, child_offset_y)
return in_x and in_y
end
--- luacheck: push ignore
----@diagnostic disable: unused-local, unused-vararg
-
- -- handle a child element having been added
- ---@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 element_id element identifier
- function protected.on_removed(id)
- end
-
- -- handle a mouse event
- ---@param event mouse_interaction mouse interaction event
- function protected.handle_mouse(event)
- end
-
- -- handle data value changes
- ---@vararg any value(s)
- function protected.on_update(...)
- end
-
- -- callback on control press responses
- ---@param result any
- function protected.response_callback(result)
- end
-
- -- get value
- ---@nodiscard
- function protected.get_value()
- return protected.value
- end
-
- -- set value
- ---@param value any value to set
- function protected.set_value(value)
- end
-
- -- set minimum input value
- ---@param min integer minimum allowed value
- function protected.set_min(min)
- end
-
- -- set maximum input value
- ---@param max integer maximum allowed value
- function protected.set_max(max)
- end
-
- -- enable the control
- function protected.enable()
- end
-
- -- disable the control
- function protected.disable()
- end
-
- -- custom recolor command, varies by element if implemented
- ---@vararg cpair|color color(s)
- function protected.recolor(...)
- end
-
- -- custom resize command, varies by element if implemented
- ---@vararg integer sizing
- function protected.resize(...)
- end
-
--- luacheck: pop
----@diagnostic enable: unused-local, unused-vararg
-
- -- start animations
- function protected.start_anim()
- end
-
- -- stop animations
- function protected.stop_anim()
- end
-
-- get public interface
---@nodiscard
---@return graphics_element element, element_id id
@@ -306,6 +295,98 @@ function element.new(args, child_offset_x, child_offset_y)
return public, self.id
end
+ -- protected version of public is_focused()
+ ---@nodiscard
+ ---@return boolean is_focused
+ function protected.is_focused() return self.focused end
+
+ -- defocus this element
+ function protected.defocus() public.unfocus_all() end
+
+ -- request focus management to focus this element
+ function protected.req_focus() args.parent.__focus_child(public) end
+
+ -- action handlers --
+
+-- luacheck: push ignore
+---@diagnostic disable: unused-local, unused-vararg
+
+ -- handle a child element having been added
+ ---@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 element_id element identifier
+ function protected.on_removed(id) end
+
+ -- handle this element having been focused
+ function protected.on_focused() end
+
+ -- handle this element having been unfocused
+ function protected.on_unfocused() end
+
+ -- handle this element having been shown
+ function protected.on_shown() end
+
+ -- handle this element having been hidden
+ function protected.on_hidden() end
+
+ -- handle a mouse event
+ ---@param event mouse_interaction mouse interaction event
+ function protected.handle_mouse(event) end
+
+ -- handle a keyboard event
+ ---@param event key_interaction key interaction event
+ function protected.handle_key(event) end
+
+ -- handle data value changes
+ ---@vararg any value(s)
+ function protected.on_update(...) end
+
+ -- callback on control press responses
+ ---@param result any
+ function protected.response_callback(result) end
+
+ -- get value
+ ---@nodiscard
+ function protected.get_value() return protected.value end
+
+ -- set value
+ ---@param value any value to set
+ function protected.set_value(value) end
+
+ -- set minimum input value
+ ---@param min integer minimum allowed value
+ function protected.set_min(min) end
+
+ -- set maximum input value
+ ---@param max integer maximum allowed value
+ function protected.set_max(max) end
+
+ -- enable the control
+ function protected.enable() end
+
+ -- disable the control
+ function protected.disable() end
+
+ -- custom recolor command, varies by element if implemented
+ ---@vararg cpair|color color(s)
+ function protected.recolor(...) end
+
+ -- custom resize command, varies by element if implemented
+ ---@vararg integer sizing
+ function protected.resize(...) end
+
+-- luacheck: pop
+---@diagnostic enable: unused-local, unused-vararg
+
+ -- start animations
+ function protected.start_anim() end
+
+ -- stop animations
+ function protected.stop_anim() end
+
-----------
-- SETUP --
-----------
@@ -324,7 +405,7 @@ function element.new(args, child_offset_x, child_offset_y)
self.id = args.id or "__ROOT__"
protected.prepare_template(0, 0, 1)
else
- self.id = args.parent.__add_child(args.id, protected)
+ self.id, self.ordinal = args.parent.__add_child(args.id, protected)
end
----------------------
@@ -358,7 +439,7 @@ function element.new(args, child_offset_x, child_offset_y)
-- delete all children
for k, v in pairs(protected.children) do
- v.delete()
+ v.get().delete()
protected.children[k] = nil
end
@@ -380,47 +461,59 @@ function element.new(args, child_offset_x, child_offset_y)
self.next_y = child.frame.y + child.frame.h
- local child_element = child.get()
-
local id = key ---@type string|integer|nil
if id == nil then
id = self.next_id
self.next_id = self.next_id + 1
end
- protected.children[id] = child_element
+ table.insert(protected.children, child)
+
+ protected.child_id_map[id] = #protected.children
+
return id
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
+ ---@param id element_id id
+ function public.__remove_child(id)
+ local index = protected.child_id_map[id]
+ if protected.children[index] ~= nil then
+ protected.on_removed(id)
+ protected.children[index] = nil
+ protected.child_id_map[id] = nil
end
end
-- actions to take upon a child element becoming ready (initial draw/construction completed)
---@param key element_id id
---@param child graphics_element
- function public.__child_ready(key, child)
- protected.on_added(key, child)
+ function public.__child_ready(key, child) protected.on_added(key, child) end
+
+ -- focus solely on this child
+ ---@param child graphics_element
+ function public.__focus_child(child)
+ if self.is_root then
+ public.unfocus_all()
+ child.focus()
+ else args.parent.__focus_child(child) end
end
-- get a child element
---@nodiscard
---@param id element_id
---@return graphics_element
- function public.get_child(id) return protected.children[id] end
+ function public.get_child(id) return protected.children[protected.child_id_map[id]].get() end
-- remove a child element
---@param id element_id
function public.remove(id)
- if protected.children[id] ~= nil then
- protected.children[id].delete()
+ local index = protected.child_id_map[id]
+ if protected.children[index] ~= nil then
+ protected.children[index].get().delete()
protected.on_removed(id)
- protected.children[id] = nil
+ protected.children[index] = nil
+ protected.child_id_map[id] = nil
end
end
@@ -429,16 +522,13 @@ function element.new(args, child_offset_x, child_offset_y)
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
- if protected.children[id] == nil then
+ local index = protected.child_id_map[id]
+ if protected.children[index] == nil then
for _, child in pairs(protected.children) do
- local elem = child.get_element_by_id(id)
+ local elem = child.get().get_element_by_id(id)
if elem ~= nil then return elem end
end
- else
- return protected.children[id]
- end
-
- return nil
+ else return protected.children[index].get() end
end
-- AUTO-PLACEMENT --
@@ -450,97 +540,113 @@ function element.new(args, child_offset_x, child_offset_y)
-- PROPERTIES --
- -- get the foreground/background colors
+ -- get element id
---@nodiscard
- ---@return cpair fg_bg
- function public.get_fg_bg()
- return protected.fg_bg
- end
+ ---@return element_id
+ function public.get_id() return self.id end
-- get element x
---@nodiscard
---@return integer x
- function public.get_x()
- return protected.frame.x
- end
+ function public.get_x() return protected.frame.x end
-- get element y
---@nodiscard
---@return integer y
- function public.get_y()
- return protected.frame.y
- end
+ function public.get_y() return protected.frame.y end
-- get element width
---@nodiscard
---@return integer width
- function public.get_width()
- return protected.frame.w
- end
+ function public.get_width() return protected.frame.w end
-- get element height
---@nodiscard
---@return integer height
- function public.get_height()
- return protected.frame.h
- end
+ function public.get_height() return protected.frame.h end
+
+ -- get the foreground/background colors
+ ---@nodiscard
+ ---@return cpair fg_bg
+ function public.get_fg_bg() return protected.fg_bg end
-- get the element value
---@nodiscard
---@return any value
- function public.get_value()
- return protected.get_value()
- end
+ function public.get_value() return protected.get_value() end
-- set the element value
---@param value any new value
- function public.set_value(value)
- protected.set_value(value)
- end
+ function public.set_value(value) protected.set_value(value) end
-- set minimum input value
---@param min integer minimum allowed value
- function public.set_min(min)
- protected.set_min(min)
- end
+ function public.set_min(min) protected.set_min(min) end
-- set maximum input value
---@param max integer maximum allowed value
- function public.set_max(max)
- protected.set_max(max)
- end
+ function public.set_max(max) protected.set_max(max) end
+
+ -- check if this element is enabled
+ function public.is_enabled() return protected.enabled end
-- enable the element
function public.enable()
- protected.enabled = true
- protected.enable()
+ if not protected.enabled then
+ protected.enabled = true
+ protected.enable()
+ end
end
-- disable the element
function public.disable()
- protected.enabled = false
- protected.disable()
+ if protected.enabled then
+ protected.enabled = false
+ protected.disable()
+ end
+ end
+
+ -- can this element be focused
+ function public.is_focusable() return args.can_focus end
+
+ -- is this element focused
+ function public.is_focused() return self.focused end
+
+ -- focus the element
+ function public.focus()
+ if args.can_focus and not self.focused then
+ self.focused = true
+ protected.on_focused()
+ end
+ end
+
+ -- unfocus this element
+ function public.unfocus()
+ if args.can_focus and self.focused then
+ self.focused = false
+ protected.on_unfocused()
+ end
+ end
+
+ -- unfocus this element and all its children
+ function public.unfocus_all()
+ public.unfocus()
+ for _, child in pairs(protected.children) do child.get().unfocus() end
end
-- custom recolor command, varies by element if implemented
---@vararg cpair|color color(s)
- function public.recolor(...)
- protected.recolor(...)
- end
+ function public.recolor(...) protected.recolor(...) end
-- resize attributes of the element value if supported
---@vararg number dimensions (element specific)
- function public.resize(...)
- protected.resize(...)
- end
+ function public.resize(...) protected.resize(...) end
-- reposition the element window
-- offsets relative to parent frame are where (1, 1) would be on top of the parent's top left corner
---@param x integer x position relative to parent frame
---@param y integer y position relative to parent frame
- function public.reposition(x, y)
- protected.window.reposition(x, y)
- end
+ function public.reposition(x, y) protected.window.reposition(x, y) end
-- FUNCTION CALLBACKS --
@@ -553,12 +659,12 @@ function element.new(args, child_offset_x, child_offset_y)
local ini_in = protected.in_window_bounds(x_ini, y_ini)
if ini_in then
- if event.type == events.CLICK_TYPE.UP or event.type == events.CLICK_TYPE.DRAG then
+ if event.type == events.MOUSE_CLICK.UP or event.type == events.MOUSE_CLICK.DRAG then
-- make sure we don't handle mouse events that started before this element was made visible
if (event.initial.x ~= self.button_down[event.button].x) or (event.initial.y ~= self.button_down[event.button].y) then
return
end
- elseif event.type == events.CLICK_TYPE.DOWN then
+ elseif event.type == events.MOUSE_CLICK.DOWN then
self.button_down[event.button] = event.initial
end
@@ -566,25 +672,39 @@ function element.new(args, child_offset_x, child_offset_y)
-- handle the mouse event then pass to children
protected.handle_mouse(event_T)
- for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
+ for _, child in pairs(protected.children) do child.get().handle_mouse(event_T) end
+ elseif event.type == events.MOUSE_CLICK.DOWN or event.type == events.MOUSE_CLICK.TAP then
+ -- clicked out, unfocus this element and children
+ public.unfocus_all()
end
- elseif event.type == events.CLICK_TYPE.DOWN then
- -- don't track this click
+ else
+ -- don't track clicks while hidden
self.button_down[event.button] = events.new_coord_2d(-1, -1)
end
end
+ -- handle a keyboard click if this element is visible and focused
+ ---@param event key_interaction keyboard interaction event
+ function public.handle_key(event)
+ if protected.window.isVisible() then
+ if self.is_root and (event.type == events.KEY_CLICK.DOWN) and (event.key == keys.tab) then
+ -- try to jump to the next/previous focusable field
+ _tab_focusable(event.shift)
+ else
+ -- handle the key event then pass to children
+ if self.focused then protected.handle_key(event) end
+ for _, child in pairs(protected.children) do child.get().handle_key(event) end
+ end
+ end
+ end
+
-- draw the element given new data
---@vararg any new data
- function public.update(...)
- protected.on_update(...)
- end
+ function public.update(...) protected.on_update(...) end
-- on a control request response
---@param result any
- function public.on_response(result)
- protected.response_callback(result)
- end
+ function public.on_response(result) protected.response_callback(result) end
-- register a callback with a PSIL, allowing for automatic unregister on delete
-- do not use graphics elements directly with PSIL subscribe()
@@ -598,6 +718,9 @@ function element.new(args, child_offset_x, child_offset_y)
-- VISIBILITY & ANIMATIONS --
+ -- check if this element is visible
+ function public.is_visible() return protected.window.isVisible() end
+
-- show the element and enables animations by default
---@param animate? boolean true (default) to automatically resume animations
function public.show(animate)
@@ -610,44 +733,39 @@ function element.new(args, child_offset_x, child_offset_y)
---@see graphics_element.content_redraw
function public.hide()
public.freeze_all() -- stop animations for efficiency/performance
+ public.unfocus_all()
protected.window.setVisible(false)
end
-- start/resume animation(s)
- function public.animate()
- protected.start_anim()
- end
+ function public.animate() protected.start_anim() end
-- start/resume animation(s) for this element and all its children
-- only animates if a window is visible
function public.animate_all()
if protected.window.isVisible() then
public.animate()
- for _, child in pairs(protected.children) do child.animate_all() end
+ for _, child in pairs(protected.children) do child.get().animate_all() end
end
end
-- freeze animation(s)
- function public.freeze()
- protected.stop_anim()
- end
+ function public.freeze() protected.stop_anim() end
-- freeze animation(s) for this element and all its children
function public.freeze_all()
public.freeze()
- for _, child in pairs(protected.children) do child.freeze_all() end
+ for _, child in pairs(protected.children) do child.get().freeze_all() end
end
-- re-draw the element
- function public.redraw()
- protected.window.redraw()
- end
+ function public.redraw() protected.window.redraw() end
-- if a content window is set, clears it then re-draws all children
function public.content_redraw()
if protected.content_window ~= nil then
protected.content_window.clear()
- for _, child in pairs(protected.children) do child.redraw() end
+ for _, child in pairs(protected.children) do child.get().redraw() end
end
end
diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua
index 574be66..a0d5949 100644
--- a/graphics/elements/controls/app.lua
+++ b/graphics/elements/controls/app.lua
@@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
-local CLICK_TYPE = core.events.CLICK_TYPE
+local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class app_button_args
---@field text string app icon text
@@ -98,14 +98,14 @@ local function app_button(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
- if event.type == CLICK_TYPE.TAP then
+ if event.type == MOUSE_CLICK.TAP then
show_pressed()
-- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback()
- elseif event.type == CLICK_TYPE.DOWN then
+ elseif event.type == MOUSE_CLICK.DOWN then
show_pressed()
- elseif event.type == CLICK_TYPE.UP then
+ elseif event.type == MOUSE_CLICK.UP then
show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback()
@@ -117,7 +117,7 @@ local function app_button(args)
-- set the value (true simulates pressing the app button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- initial draw
diff --git a/graphics/elements/controls/hazard_button.lua b/graphics/elements/controls/hazard_button.lua
index ac1b23d..f6ac2d3 100644
--- a/graphics/elements/controls/hazard_button.lua
+++ b/graphics/elements/controls/hazard_button.lua
@@ -174,7 +174,7 @@ local function hazard_button(args)
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- show the button as disabled
diff --git a/graphics/elements/controls/push_button.lua b/graphics/elements/controls/push_button.lua
index 36e1914..da555be 100644
--- a/graphics/elements/controls/push_button.lua
+++ b/graphics/elements/controls/push_button.lua
@@ -5,7 +5,8 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
-local CLICK_TYPE = core.events.CLICK_TYPE
+local MOUSE_CLICK = core.events.MOUSE_CLICK
+local KEY_CLICK = core.events.KEY_CLICK
---@class push_button_args
---@field text string button text
@@ -32,7 +33,8 @@ local function push_button(args)
local text_width = string.len(args.text)
- -- single line height, calculate width
+ -- set automatic settings
+ args.can_focus = true
args.height = 1
args.min_width = args.min_width or 0
args.width = math.max(text_width, args.min_width)
@@ -76,14 +78,14 @@ local function push_button(args)
---@param event mouse_interaction mouse event
function e.handle_mouse(event)
if e.enabled then
- if event.type == CLICK_TYPE.TAP then
+ if event.type == MOUSE_CLICK.TAP then
show_pressed()
-- show as unpressed in 0.25 seconds
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
args.callback()
- elseif event.type == CLICK_TYPE.DOWN then
+ elseif event.type == MOUSE_CLICK.DOWN then
show_pressed()
- elseif event.type == CLICK_TYPE.UP then
+ elseif event.type == MOUSE_CLICK.UP then
show_unpressed()
if e.in_frame_bounds(event.current.x, event.current.y) then
args.callback()
@@ -92,10 +94,21 @@ local function push_button(args)
end
end
+ -- handle keyboard interaction
+ ---@param event key_interaction key event
+ function e.handle_key(event)
+ if event.type == KEY_CLICK.DOWN then
+ if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
+ args.callback()
+ e.defocus()
+ end
+ end
+ end
+
-- set the value (true simulates pressing the button)
---@param val boolean new value
function e.set_value(val)
- if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
+ if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
end
-- show butten as enabled
@@ -118,6 +131,9 @@ local function push_button(args)
end
end
+ e.on_focused = show_pressed
+ e.on_unfocused = show_unpressed
+
-- initial draw
draw()
diff --git a/graphics/elements/controls/sidebar.lua b/graphics/elements/controls/sidebar.lua
index 9185430..646c724 100644
--- a/graphics/elements/controls/sidebar.lua
+++ b/graphics/elements/controls/sidebar.lua
@@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
-local CLICK_TYPE = core.events.CLICK_TYPE
+local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab
---@field char string character identifier
@@ -85,22 +85,22 @@ local function sidebar(args)
local ini_idx = math.ceil(event.initial.y / 3)
if args.tabs[cur_idx] ~= nil then
- if event.type == CLICK_TYPE.TAP then
+ if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value)
- elseif event.type == CLICK_TYPE.DOWN then
+ elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx)
- elseif event.type == CLICK_TYPE.UP then
+ elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
args.callback(e.value)
else draw(false) end
end
- elseif event.type == CLICK_TYPE.UP then
+ elseif event.type == MOUSE_CLICK.UP then
draw(false)
end
end
diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua
new file mode 100644
index 0000000..a32d43f
--- /dev/null
+++ b/graphics/elements/form/number_field.lua
@@ -0,0 +1,136 @@
+-- Numeric Value Entry Graphics Element
+
+local util = require("scada-common.util")
+
+local core = require("graphics.core")
+local element = require("graphics.element")
+
+local KEY_CLICK = core.events.KEY_CLICK
+
+---@class number_field_args
+---@field default? number default value, defaults to 0
+---@field min? number minimum, forced on unfocus
+---@field max? number maximum, forced on unfocus
+---@field max_digits? integer maximum number of digits, defaults to width
+---@field allow_decimal? boolean true to allow decimals
+---@field allow_negative? boolean true to allow negative numbers
+---@field parent graphics_element
+---@field id? string element id
+---@field x? integer 1 if omitted
+---@field y? integer auto incremented if omitted
+---@field width? integer parent width if omitted
+---@field fg_bg? cpair foreground/background colors
+---@field hidden? boolean true to hide on initial draw
+
+-- new numeric form field
+---@param args number_field_args
+---@return graphics_element element, element_id id
+local function number_field(args)
+ args.height = 1
+ args.can_focus = true
+
+ -- create new graphics element base object
+ local e = element.new(args)
+
+ local has_decimal = false
+
+ args.max_digits = args.max_digits or e.frame.w
+
+ -- set initial value
+ e.value = "" .. (args.default or 0)
+
+ local function show()
+ -- clear and print
+ e.w_set_cur(1, 1)
+ e.w_write(string.rep(" ", e.frame.w))
+ e.w_set_cur(1, 1)
+ e.w_set_fgd(colors.black)
+ e.w_write(e.value)
+ if e.is_focused() then
+ e.w_set_fgd(colors.lightGray)
+ e.w_write("_")
+ end
+ end
+
+ -- handle mouse interaction
+ ---@param event mouse_interaction mouse event
+ function e.handle_mouse(event)
+ -- only handle if on an increment or decrement arrow
+ if e.enabled and core.events.was_clicked(event.type) then
+ e.req_focus()
+ end
+ end
+
+ -- handle keyboard interaction
+ ---@param event key_interaction key event
+ function e.handle_key(event)
+ if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_digits then
+ if tonumber(event.name) then
+ e.value = util.trinary(e.value == "0", "", e.value) .. tonumber(event.name)
+ end
+
+ show()
+ elseif event.type == KEY_CLICK.DOWN then
+ if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
+ e.value = string.sub(e.value, 1, string.len(e.value) - 1)
+ has_decimal = string.find(e.value, "%.") ~= nil
+ show()
+ elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
+ e.value = e.value .. "."
+ has_decimal = true
+ show()
+ elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
+ e.value = "-"
+ show()
+ end
+ end
+ end
+
+ -- set the value
+ ---@param val number number to show
+ function e.set_value(val) e.value = val end
+
+ -- set minimum input value
+ ---@param min integer minimum allowed value
+ function e.set_min(min) args.min = min end
+
+ -- set maximum input value
+ ---@param max integer maximum allowed value
+ function e.set_max(max) args.max = max end
+
+ -- handle focus change
+ e.on_focused = show
+
+ function e.on_unfocused()
+ local val = tonumber(e.value)
+ local max = tonumber(args.max)
+ local min = tonumber(args.min)
+
+ if type(val) == "number" then
+ if type(args.max) == "number" and val > max then
+ e.value = "" .. max
+ elseif type(args.min) == "number" and val < min then
+ e.value = "" .. min
+ end
+ else
+ e.value = ""
+ end
+
+ show()
+ end
+
+ -- enable this input
+ function e.enable()
+ end
+
+ -- disable this input
+ function e.disable()
+ end
+
+ -- initial draw
+ show()
+
+ return e.complete()
+end
+
+return number_field
diff --git a/graphics/elements/listbox.lua b/graphics/elements/listbox.lua
index 1bc3556..0268951 100644
--- a/graphics/elements/listbox.lua
+++ b/graphics/elements/listbox.lua
@@ -5,7 +5,7 @@ local tcd = require("scada-common.tcd")
local core = require("graphics.core")
local element = require("graphics.element")
-local CLICK_TYPE = core.events.CLICK_TYPE
+local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class listbox_args
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
@@ -223,7 +223,7 @@ 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 then
+ if event.type == MOUSE_CLICK.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)
@@ -235,7 +235,7 @@ local function listbox(args)
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
+ elseif event.type == MOUSE_CLICK.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)
@@ -250,10 +250,10 @@ local function listbox(args)
mouse_last_y = event.current.y
end
end
- elseif event.type == CLICK_TYPE.UP then
+ elseif event.type == MOUSE_CLICK.UP then
holding_bar = false
draw_arrows(0)
- elseif event.type == CLICK_TYPE.DRAG then
+ elseif event.type == MOUSE_CLICK.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
@@ -266,9 +266,9 @@ local function listbox(args)
mouse_last_y = event.current.y
end
end
- elseif event.type == CLICK_TYPE.SCROLL_DOWN then
+ elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
scroll_down()
- elseif event.type == CLICK_TYPE.SCROLL_UP then
+ elseif event.type == MOUSE_CLICK.SCROLL_UP then
scroll_up()
end
end
diff --git a/graphics/events.lua b/graphics/events.lua
index ca56588..df5f7eb 100644
--- a/graphics/events.lua
+++ b/graphics/events.lua
@@ -14,8 +14,8 @@ events.CLICK_BUTTON = {
MID_BUTTON = 3
}
----@enum CLICK_TYPE
-events.CLICK_TYPE = {
+---@enum MOUSE_CLICK
+local MOUSE_CLICK = {
TAP = 1, -- screen tap (complete click)
DOWN = 2, -- button down
UP = 3, -- button up (completed a click)
@@ -24,6 +24,18 @@ events.CLICK_TYPE = {
SCROLL_UP = 6 -- scroll up
}
+events.MOUSE_CLICK = MOUSE_CLICK
+
+---@enum KEY_CLICK
+local KEY_CLICK = {
+ DOWN = 1,
+ HELD = 2,
+ UP = 3,
+ CHAR = 4
+}
+
+events.KEY_CLICK = KEY_CLICK
+
-- create a new 2D coordinate
---@param x integer
---@param y integer
@@ -35,13 +47,25 @@ events.new_coord_2d = _coord2d
---@class mouse_interaction
---@field monitor string
---@field button CLICK_BUTTON
----@field type CLICK_TYPE
+---@field type MOUSE_CLICK
---@field initial coordinate_2d
---@field current coordinate_2d
+---@class key_interaction
+---@field type KEY_CLICK
+---@field key number key code
+---@field name string key character name
+---@field shift boolean shift held
+---@field ctrl boolean ctrl held
+---@field alt boolean alt held
+
local handler = {
-- left, right, middle button down tracking
- button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) }
+ button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
+ -- keyboard modifiers
+ shift = false,
+ alt = false,
+ ctrl = false
}
-- create a new monitor touch mouse interaction event
@@ -54,7 +78,7 @@ local function _monitor_touch(monitor, x, y)
return {
monitor = monitor,
button = events.CLICK_BUTTON.GENERIC,
- type = events.CLICK_TYPE.TAP,
+ type = MOUSE_CLICK.TAP,
initial = _coord2d(x, y),
current = _coord2d(x, y)
}
@@ -63,7 +87,7 @@ end
-- create a new mouse button mouse interaction event
---@nodiscard
---@param button CLICK_BUTTON mouse button
----@param type CLICK_TYPE click type
+---@param type MOUSE_CLICK click type
---@param x1 integer initial x
---@param y1 integer initial y
---@param x2 integer current x
@@ -81,7 +105,7 @@ end
-- create a new generic mouse interaction event
---@nodiscard
----@param type CLICK_TYPE
+---@param type MOUSE_CLICK
---@param x integer
---@param y integer
---@return mouse_interaction
@@ -113,8 +137,8 @@ end
-- check if an event qualifies as a click (tap or up)
---@nodiscard
----@param t CLICK_TYPE
-function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
+---@param t MOUSE_CLICK
+function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
-- create a new mouse event to pass onto graphics renderer
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
@@ -127,32 +151,65 @@ function events.new_mouse_event(event_type, opt, x, y)
if event_type == "mouse_click" then
---@cast opt 1|2|3
handler.button_down[opt] = _coord2d(x, y)
- return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
+ return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
elseif event_type == "mouse_up" then
---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d
- return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
+ return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
elseif event_type == "monitor_touch" then
---@cast opt string
return _monitor_touch(opt, x, y)
elseif event_type == "mouse_drag" then
---@cast opt 1|2|3
local initial = handler.button_down[opt] ---@type coordinate_2d
- return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
+ return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
elseif event_type == "mouse_scroll" then
---@cast opt 1|-1
- local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
+ local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
end
end
--- create a new key event to pass onto graphics renderer
+-- create a new keyboard interaction event
+---@nodiscard
+---@param click_type KEY_CLICK key click type
+---@param key integer|string keyboard key code or character for 'char' event
+---@return key_interaction
+local function _key_event(click_type, key)
+ local name = key
+ if type(key) == "number" then name = keys.getName(key) end
+ return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
+end
+
+-- create a new keyboard event to pass onto graphics renderer
-- supports: char, key, and key_up
----@param event_type os_event
-function events.new_key_event(event_type)
+---@param event_type os_event OS event to handle
+---@param key integer keyboard key code
+---@param held boolean? if the key is being held (for 'key' event)
+---@return key_interaction|nil
+function events.new_key_event(event_type, key, held)
if event_type == "char" then
+ return _key_event(KEY_CLICK.CHAR, key)
elseif event_type == "key" then
+ if key == keys.leftShift or key == keys.rightShift then
+ handler.shift = true
+ elseif key == keys.leftCtrl or key == keys.rightCtrl then
+ handler.ctrl = true
+ elseif key == keys.leftAlt or key == keys.rightAlt then
+ handler.alt = true
+ else
+ return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
+ end
elseif event_type == "key_up" then
+ if key == keys.leftShift or key == keys.rightShift then
+ handler.shift = false
+ elseif key == keys.leftCtrl or key == keys.rightCtrl then
+ handler.ctrl = false
+ elseif key == keys.leftAlt or key == keys.rightAlt then
+ handler.alt = false
+ else
+ return _key_event(KEY_CLICK.UP, key)
+ end
end
end