#342 added element focusing feature to graphics library

This commit is contained in:
Mikayla Fischler 2023-09-16 21:06:16 -04:00
parent 1cb240b1b0
commit 29e910ba3c
10 changed files with 540 additions and 222 deletions

View File

@ -5,6 +5,7 @@
"colors",
"fs",
"http",
"keys",
"parallel",
"periphemu",
"peripheral",

View File

@ -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

View File

@ -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<br>
-- 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<br>
-- 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<br>
-- 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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<br>
-- 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<br>
-- 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<br>
-- 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