diff --git a/graphics/core.lua b/graphics/core.lua index 2305990..3878288 100644 --- a/graphics/core.lua +++ b/graphics/core.lua @@ -111,4 +111,182 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr) } end +-- Interactive Field Manager + +---@param e graphics_base +---@param max_len any +---@param fg_bg any +---@param dis_fg_bg any +function core.new_ifield(e, max_len, fg_bg, dis_fg_bg) + local self = { + frame_start = 1, + visible_text = e.value, + cursor_pos = string.len(e.value) + 1, + selected_all = false + } + + -- update visible text + local function _update_visible() + self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1) + end + + -- try shifting frame left + local function _try_lshift() + if self.frame_start > 1 then + self.frame_start = self.frame_start - 1 + return true + end + end + + -- try shifting frame right + local function _try_rshift() + if (self.frame_start + e.frame.w - 1) < string.len(e.value) then + self.frame_start = self.frame_start + 1 + return true + end + end + + ---@class ifield + local public = {} + + -- show the field + function public.show() + _update_visible() + + if e.enabled then + e.w_set_bkg(fg_bg.bkg) + e.w_set_fgd(fg_bg.fgd) + else + e.w_set_bkg(dis_fg_bg.bkg) + e.w_set_fgd(dis_fg_bg.fgd) + end + + -- clear and print + e.w_set_cur(1, 1) + e.w_write(string.rep(" ", e.frame.w)) + e.w_set_cur(1, 1) + + if e.is_focused() and e.enabled then + -- write text with cursor + if self.selected_all then + e.w_set_bkg(fg_bg.fgd) + e.w_set_fgd(fg_bg.bkg) + e.w_write(self.visible_text) + elseif self.cursor_pos == (string.len(self.visible_text) + 1) then + -- write text with cursor at the end, no need to blit + e.w_write(self.visible_text) + e.w_set_fgd(colors.lightGray) + e.w_write("_") + else + local a, b = "", "" + + if self.cursor_pos <= string.len(self.visible_text) then + a = fg_bg.blit_bkg + b = fg_bg.blit_fgd + end + + local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos) + local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos) + + e.w_blit(self.visible_text, b_fgd, b_bkg) + end + else + self.selected_all = false + + -- write text without cursor + e.w_write(self.visible_text) + end + end + + -- move cursor to x + ---@param x integer + function public.move_cursor(x) + self.selected_all = false + self.cursor_pos = math.min(x, string.len(self.visible_text) + 1) + public.show() + end + + -- select all text + function public.select_all() + self.selected_all = true + public.show() + end + + -- set field value + ---@param val string + function public.set_value(val) + e.value = string.sub(val, 1, math.min(max_len, string.len(val))) + + self.selected_all = false + self.frame_start = 1 + math.max(0, string.len(val) - e.frame.w) + + _update_visible() + self.cursor_pos = string.len(self.visible_text) + 1 + + public.show() + end + + -- try to insert a character if there is space + ---@param char string + function public.try_insert_char(char) + -- limit length + if string.len(e.value) >= max_len then return end + + -- replace if selected all, insert otherwise + if self.selected_all then + self.selected_all = false + self.cursor_pos = 2 + self.frame_start = 1 + + e.value = char + public.show() + else + e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value)) + _update_visible() + + if self.cursor_pos <= string.len(self.visible_text) then + self.cursor_pos = self.cursor_pos + 1 + public.show() + elseif _try_rshift() then public.show() end + end + end + + -- remove charcter before cursor if there is anything to remove, or delete all if selected all + function public.backspace() + if self.selected_all then + self.selected_all = false + e.value = "" + self.cursor_pos = 1 + self.frame_start = 1 + public.show() + else + if self.frame_start + self.cursor_pos > 2 then + e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value)) + if self.cursor_pos > 1 then + self.cursor_pos = self.cursor_pos - 1 + public.show() + elseif _try_lshift() then public.show() end + end + end + end + + -- move cursor left by one + function public.nav_left() + if self.cursor_pos > 1 then + self.cursor_pos = self.cursor_pos - 1 + public.show() + elseif _try_lshift() then public.show() end + end + + -- move cursor right by one + function public.nav_right() + if self.cursor_pos <= string.len(self.visible_text) then + self.cursor_pos = self.cursor_pos + 1 + public.show() + elseif _try_rshift() then public.show() end + end + + return public +end + return core diff --git a/graphics/elements/form/number_field.lua b/graphics/elements/form/number_field.lua index e0e2426..ee75224 100644 --- a/graphics/elements/form/number_field.lua +++ b/graphics/elements/form/number_field.lua @@ -1,11 +1,10 @@ -- 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 +local MOUSE_CLICK = core.events.MOUSE_CLICK ---@class number_field_args ---@field default? number default value, defaults to 0 @@ -38,7 +37,11 @@ local function number_field(args) args.max_digits = args.max_digits or e.frame.w -- set initial value - e.value = util.strval(args.default or 0) + e.value = "" .. (args.default or 0) + + -- make an interactive field manager + local ifield = core.new_ifield(e, args.max_digits, args.fg_bg, args.dis_fg_bg) + -- draw input local function show() @@ -67,8 +70,16 @@ local function number_field(args) ---@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() + if e.enabled then + if core.events.was_clicked(event.type) then + e.req_focus() + + if event.type == MOUSE_CLICK.UP then + ifield.move_cursor(event.current.x) + end + elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then + ifield.select_all() + end end end @@ -77,44 +88,52 @@ local function number_field(args) 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) - show() + if e.value == 0 then e.value = "" end + ifield.try_insert_char(event.name) end 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) + ifield.backspace() 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() + ifield.try_insert_char(".") elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then - e.value = "-" - show() + ifield.set_value("-") + elseif event.key == keys.left then + ifield.nav_left() + elseif event.key == keys.right then + ifield.nav_right() + elseif event.key == keys.a and event.ctrl then + ifield.select_all() end end end - -- set the value + -- set the value (must be a number) ---@param val number number to show function e.set_value(val) - e.value = val - show() + if tonumber(val) then + ifield.set_value("" .. tonumber(val)) + end end -- set minimum input value ---@param min integer minimum allowed value - function e.set_min(min) - args.min = min - show() - end + 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 - show() + function e.set_max(max) args.max = max end + + -- replace text with pasted text if its a number + ---@param text string string pasted + function e.handle_paste(text) + if tonumber(text) then + ifield.set_value("" .. tonumber(text)) + else + ifield.set_value("0") + end end -- handle focused @@ -136,15 +155,15 @@ local function number_field(args) e.value = "" end - show() + ifield.show() end -- on enable/disable - e.enable = show - e.disable = show + e.enable = ifield.show + e.disable = ifield.show -- initial draw - show() + ifield.show() return e.complete() end diff --git a/graphics/elements/form/text_field.lua b/graphics/elements/form/text_field.lua index 908d3fc..37a91d3 100644 --- a/graphics/elements/form/text_field.lua +++ b/graphics/elements/form/text_field.lua @@ -1,8 +1,5 @@ -- Text Value Entry Graphics Element -local util = require("scada-common.util") -local events = require("graphics.events") - local core = require("graphics.core") local element = require("graphics.element") @@ -34,71 +31,8 @@ local function text_field(args) -- set initial value e.value = args.value or "" - local max_len = args.max_len or e.frame.w - local frame_start = 1 - local visible_text = e.value - local cursor_pos = string.len(visible_text) + 1 - - local function frame__update_visible() - visible_text = string.sub(e.value, frame_start, frame_start + math.min(string.len(e.value), e.frame.w) - 1) - end - - -- draw input - local function show() - frame__update_visible() - - if e.enabled then - e.w_set_bkg(args.fg_bg.bkg) - e.w_set_fgd(args.fg_bg.fgd) - else - e.w_set_bkg(args.dis_fg_bg.bkg) - e.w_set_fgd(args.dis_fg_bg.fgd) - end - - -- clear and print - e.w_set_cur(1, 1) - e.w_write(string.rep(" ", e.frame.w)) - e.w_set_cur(1, 1) - - if e.is_focused() and e.enabled then - -- write text with cursor - if cursor_pos == (string.len(visible_text) + 1) then - -- write text with cursor at the end, no need to blit - e.w_write(visible_text) - e.w_set_fgd(colors.lightGray) - e.w_write("_") - else - local a, b = "", "" - - if cursor_pos <= string.len(visible_text) then - a = args.fg_bg.blit_bkg - b = args.fg_bg.blit_fgd - end - - local b_fgd = string.rep(args.fg_bg.blit_fgd, cursor_pos - 1) .. a .. string.rep(args.fg_bg.blit_fgd, string.len(visible_text) - cursor_pos) - local b_bkg = string.rep(args.fg_bg.blit_bkg, cursor_pos - 1) .. b .. string.rep(args.fg_bg.blit_bkg, string.len(visible_text) - cursor_pos) - - e.w_blit(visible_text, b_fgd, b_bkg) - end - else - -- write text without cursor - e.w_write(visible_text) - end - end - - local function frame__try_lshift() - if frame_start > 1 then - frame_start = frame_start - 1 - return true - end - end - - local function frame__try_rshift() - if (frame_start + e.frame.w - 1) < string.len(e.value) then - frame_start = frame_start + 1 - return true - end - end + -- make an interactive field manager + local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg) -- handle mouse interaction ---@param event mouse_interaction mouse event @@ -109,9 +43,10 @@ local function text_field(args) e.req_focus() if event.type == MOUSE_CLICK.UP then - cursor_pos = math.min(event.current.x, string.len(visible_text) + 1) - show() + ifield.move_cursor(event.current.x) end + elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then + ifield.select_all() end end end @@ -119,61 +54,43 @@ local function text_field(args) -- 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) < max_len then - e.value = string.sub(e.value, 1, frame_start + cursor_pos - 2) .. event.name .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) - frame__update_visible() - if cursor_pos <= string.len(visible_text) then - cursor_pos = cursor_pos + 1 - show() - elseif frame__try_rshift() then show() end + if event.type == KEY_CLICK.CHAR then + ifield.try_insert_char(event.name) elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then if (event.key == keys.backspace or event.key == keys.delete) then - -- remove charcter at cursor if there is anything to remove - if frame_start + cursor_pos > 2 then - e.value = string.sub(e.value, 1, frame_start + cursor_pos - 3) .. string.sub(e.value, frame_start + cursor_pos - 1, string.len(e.value)) - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - show() - elseif frame__try_lshift() then show() end - end + ifield.backspace() elseif event.key == keys.left then - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - show() - elseif frame__try_lshift() then show() end + ifield.nav_left() elseif event.key == keys.right then - if cursor_pos <= string.len(visible_text) then - cursor_pos = cursor_pos + 1 - show() - elseif frame__try_rshift() then show() end + ifield.nav_right() + elseif event.key == keys.a and event.ctrl then + ifield.select_all() end end end -- set the value - ---@param val string string to show + ---@param val string string to set function e.set_value(val) - e.value = string.sub(val, 1, math.min(max_len, string.len(val))) - frame_start = 1 + math.max(0, string.len(val) - e.frame.w) - frame__update_visible() - cursor_pos = string.len(visible_text) + 1 - show() + ifield.set_value(val) end + -- replace text with pasted text + ---@param text string string to set function e.handle_paste(text) - e.set_value(text) + ifield.set_value(text) end -- handle focus - e.on_focused = show - e.on_unfocused = show + e.on_focused = ifield.show + e.on_unfocused = ifield.show -- on enable/disable - e.enable = show - e.disable = show + e.enable = ifield.show + e.disable = ifield.show -- initial draw - show() + ifield.show() return e.complete() end