From be589f62b83de26d346cbf97489175257da15c0c Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 12 Sep 2024 13:43:00 -0700 Subject: [PATCH 01/34] Update Dependencies.rst Fixes typo --- docs/dev/compile/Dependencies.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/compile/Dependencies.rst b/docs/dev/compile/Dependencies.rst index a5354ef5ec..0b930867b1 100644 --- a/docs/dev/compile/Dependencies.rst +++ b/docs/dev/compile/Dependencies.rst @@ -69,7 +69,7 @@ Here are some package install commands for various distributions: * On Arch Linux:: - pacman -Sy gcc cmake ccmake ninja git dwarffortress zlib perl-xml-libxml perl-xml-libxslt + pacman -Sy gcc cmake ccache ninja git dwarffortress zlib perl-xml-libxml perl-xml-libxslt * The ``dwarffortress`` package provides the necessary SDL packages. * For the required Perl modules: ``perl-xml-libxml`` and ``perl-xml-libxslt`` (or through ``cpan``) From c45a4f6e4ef166af6899e84fab76d60a9eea9e6d Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 16 Sep 2024 10:23:41 -0700 Subject: [PATCH 02/34] Update Dependencies.rst --- docs/dev/compile/Dependencies.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/compile/Dependencies.rst b/docs/dev/compile/Dependencies.rst index 0b930867b1..b42a833785 100644 --- a/docs/dev/compile/Dependencies.rst +++ b/docs/dev/compile/Dependencies.rst @@ -76,13 +76,13 @@ Here are some package install commands for various distributions: * On Ubuntu:: - apt-get install gcc cmake ninja-build git zlib1g-dev libsdl2-dev libxml-libxml-perl libxml-libxslt-perl + apt-get install gcc cmake ccache ninja-build git zlib1g-dev libsdl2-dev libxml-libxml-perl libxml-libxslt-perl * Other Debian-based distributions should have similar requirements. * On Fedora:: - yum install gcc-c++ cmake ninja-build git zlib-devel SDL2-devel perl-core perl-XML-LibXML perl-XML-LibXSLT ruby + yum install gcc-c++ cmake ccache ninja-build git zlib-devel SDL2-devel perl-core perl-XML-LibXML perl-XML-LibXSLT ruby To build DFHack, you need GCC 10 or newer. Note that extremely new GCC versions may not have been used to build DFHack yet, so if you run into issues with From 1c7b19ba7cd2722d7e7d3923b4f7fd94cfe0a599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 21 Sep 2024 11:44:51 +0200 Subject: [PATCH 03/34] Refactor Widget, Divider, Panel and Window widgets to own modules --- library/lua/gui/widgets.lua | 692 +--------------------------- library/lua/gui/widgets/divider.lua | 91 ++++ library/lua/gui/widgets/panel.lua | 530 +++++++++++++++++++++ library/lua/gui/widgets/widget.lua | 57 +++ library/lua/gui/widgets/window.lua | 28 ++ 5 files changed, 711 insertions(+), 687 deletions(-) create mode 100644 library/lua/gui/widgets/divider.lua create mode 100644 library/lua/gui/widgets/panel.lua create mode 100644 library/lua/gui/widgets/widget.lua create mode 100644 library/lua/gui/widgets/window.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 1be57faa8b..06c5693161 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -7,6 +7,11 @@ local guidm = require('gui.dwarfmode') local textures = require('gui.textures') local utils = require('utils') +Widget = require('gui.widgets.widget') +Divider = require('gui.widgets.divider') +Panel = require('gui.widgets.panel') +Window = require('gui.widgets.window') + local getval = utils.getval local to_pen = dfhack.pen.parse @@ -41,693 +46,6 @@ STANDARDSCROLL = { KEYBOARD_CURSOR_DOWN_FAST = '+page', } ------------- --- Widget -- ------------- - ----@class widgets.Widget.frame ----@field l? integer Gap between the left edge of the frame and the parent. ----@field t? integer Gap between the top edge of the frame and the parent. ----@field r? integer Gap between the right edge of the frame and the parent. ----@field b? integer Gap between the bottom edge of the frame and the parent. ----@field w? integer Desired width ----@field h? integer Desired height - ----@class widgets.Widget.inset ----@field l? integer Left margin ----@field t? integer Top margin ----@field r? integer Right margin ----@field b? integer Bottom margin ----@field x? integer Left/right margin (if `l` and/or `r` are ommited) ----@field y? integer Top/bottom margin (if `t` and/or `b` are ommited) - ----@class widgets.Widget.attrs: gui.View.attrs ----@field frame? widgets.Widget.frame ----@field frame_inset? widgets.Widget.inset|integer ----@field frame_background? dfhack.pen - ----@class widgets.Widget.attrs.partial: widgets.Widget.attrs - ----@class widgets.Widget: gui.View ----@field super gui.View ----@field ATTRS widgets.Widget.attrs|fun(attributes: widgets.Widget.attrs.partial) ----@overload fun(init_table: widgets.Widget.attrs.partial): self -Widget = defclass(Widget, gui.View) - -Widget.ATTRS { - frame = DEFAULT_NIL, - frame_inset = DEFAULT_NIL, - frame_background = DEFAULT_NIL, -} - ----@param parent_rect { width: integer, height: integer } ----@return any -function Widget:computeFrame(parent_rect) - local sw, sh = parent_rect.width, parent_rect.height - return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset) -end - ----@param dc gui.Painter ----@param rect { x1: integer, y1: integer, x2: integer, y2: integer } -function Widget:onRenderFrame(dc, rect) - if self.frame_background then - dc:fill(rect, self.frame_background) - end -end - -------------- --- Divider -- -------------- - ----@class widgets.Divider.attrs: widgets.Widget.attrs ----@field frame_style gui.Frame|fun(): gui.Frame ----@field interior boolean ----@field frame_style_t? false|gui.Frame|fun(): gui.Frame ----@field frame_style_b? false|gui.Frame|fun(): gui.Frame ----@field frame_style_l? false|gui.Frame|fun(): gui.Frame ----@field frame_style_r? false|gui.Frame|fun(): gui.Frame ----@field interior_t? boolean ----@field interior_b? boolean ----@field interior_l? boolean ----@field interior_r? boolean - ----@class widgets.Divider.attrs.partial: widgets.Divider.attrs - ----@class widgets.Divider: widgets.Widget, widgets.Divider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Divider.attrs|fun(attributes: widgets.Divider.attrs.partial) ----@overload fun(init_table: widgets.Divider.attrs.partial): self -Divider = defclass(Divider, Widget) - -Divider.ATTRS{ - frame_style=gui.FRAME_THIN, - interior=false, - frame_style_t=DEFAULT_NIL, - interior_t=DEFAULT_NIL, - frame_style_b=DEFAULT_NIL, - interior_b=DEFAULT_NIL, - frame_style_l=DEFAULT_NIL, - interior_l=DEFAULT_NIL, - frame_style_r=DEFAULT_NIL, - interior_r=DEFAULT_NIL, -} - -local function divider_get_val(self, base_name, edge_name) - local val = self[base_name..'_'..edge_name] - if val ~= nil then return val end - return self[base_name] -end - -local function divider_get_junction_pen(self, edge_name) - local interior = divider_get_val(self, 'interior', edge_name) - local pen_name = ('%sT%s_frame_pen'):format(edge_name, interior and 'i' or 'e') - local frame_style = divider_get_val(self, 'frame_style', edge_name) - if type(frame_style) == 'function' then - frame_style = frame_style() - end - return frame_style[pen_name] -end - ----@param dc gui.Painter -function Divider:onRenderBody(dc) - local rect, style = self.frame_rect, self.frame_style - if type(style) == 'function' then - style = style() - end - - if rect.height == 1 and rect.width == 1 then - dc:seek(0, 0):char(nil, style.x_frame_pen) - elseif rect.width == 1 then - local fill_start, fill_end = 0, rect.height-1 - if self.frame_style_t ~= false then - fill_start = 1 - dc:seek(0, 0):char(nil, divider_get_junction_pen(self, 't')) - end - if self.frame_style_b ~= false then - fill_end = rect.height-2 - dc:seek(0, rect.height-1):char(nil, divider_get_junction_pen(self, 'b')) - end - dc:fill(0, fill_start, 0, fill_end, style.v_frame_pen) - else - local fill_start, fill_end = 0, rect.width-1 - if self.frame_style_l ~= false then - fill_start = 1 - dc:seek(0, 0):char(nil, divider_get_junction_pen(self, 'l')) - end - if self.frame_style_r ~= false then - fill_end = rect.width-2 - dc:seek(rect.width-1, 0):char(nil, divider_get_junction_pen(self, 'r')) - end - dc:fill(fill_start, 0, fill_end, 0, style.h_frame_pen) - end -end - ------------ --- Panel -- ------------ - -DOUBLE_CLICK_MS = 500 - ----@class widgets.Panel.attrs: widgets.Widget.attrs ----@field frame_style? gui.Frame|fun(): gui.Frame ----@field frame_title? string ----@field on_render? fun(painter: gui.Painter) Called from `onRenderBody`. ----@field on_layout? fun(frame_body: any) Called from `postComputeFrame`. ----@field draggable boolean ----@field drag_anchors? { title: boolean, frame: boolean, body: boolean } ----@field drag_bound 'frame'|'body' ----@field on_drag_begin? fun() ----@field on_drag_end? fun(success: boolean, new_frame: gui.Frame) ----@field resizable boolean ----@field resize_anchors? { t: boolean, l: boolean, r: boolean, b: boolean } ----@field resize_min? { w: integer, h: integer } ----@field on_resize_begin? fun() ----@field on_resize_end? fun(success: boolean, new_frame: gui.Frame) ----@field autoarrange_subviews boolean ----@field autoarrange_gap integer ----@field kbd_get_pos? fun(): df.coord2d ----@field saved_frame? table ----@field saved_frame_rect? table ----@field drag_offset? table ----@field resize_edge? string ----@field last_title_click_ms number - ----@class widgets.Panel.attrs.partial: widgets.Panel.attrs - ----@class widgets.Panel.initTable: widgets.Panel.attrs.partial ----@field subviews? gui.View[] - ----@class widgets.Panel: widgets.Widget, widgets.Panel.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Panel.attrs|fun(attributes: widgets.Panel.attrs.partial) ----@overload fun(init_table: widgets.Panel.initTable): self -Panel = defclass(Panel, Widget) - -Panel.ATTRS { - frame_style = DEFAULT_NIL, -- as in gui.FramedScreen - frame_title = DEFAULT_NIL, -- as in gui.FramedScreen - on_render = DEFAULT_NIL, - on_layout = DEFAULT_NIL, - draggable = false, - drag_anchors = DEFAULT_NIL, - drag_bound = 'frame', -- or 'body' - on_drag_begin = DEFAULT_NIL, - on_drag_end = DEFAULT_NIL, - resizable = false, - resize_anchors = DEFAULT_NIL, - resize_min = DEFAULT_NIL, - on_resize_begin = DEFAULT_NIL, - on_resize_end = DEFAULT_NIL, - no_force_pause_badge = false, - autoarrange_subviews = false, -- whether to automatically lay out subviews - autoarrange_gap = 0, -- how many blank lines to insert between widgets -} - ----@param self widgets.Panel ----@param args widgets.Panel.initTable -function Panel:init(args) - if not self.drag_anchors then - self.drag_anchors = {title=true, frame=not self.resizable, body=true} - end - if not self.resize_anchors then - self.resize_anchors = {t=false, l=true, r=true, b=true} - end - self.resize_min = self.resize_min or {} - self.resize_min.w = self.resize_min.w or (self.frame or {}).w or 5 - self.resize_min.h = self.resize_min.h or (self.frame or {}).h or 5 - - self.kbd_get_pos = nil -- fn when we are in keyboard dragging mode - self.saved_frame = nil -- copy of frame when dragging started - self.saved_frame_rect = nil -- copy of frame_rect when dragging started - self.drag_offset = nil -- relative pos of held panel tile - self.resize_edge = nil -- which dimension is being resized? - - self.last_title_click_ms = 0 -- used to track double-clicking on the title - self:addviews(args.subviews) -end - -local function Panel_update_frame(self, frame, clear_state) - if clear_state then - self.kbd_get_pos = nil - self.saved_frame = nil - self.saved_frame_rect = nil - self.drag_offset = nil - self.resize_edge = nil - end - if not frame then return end - if self.frame.l == frame.l and self.frame.r == frame.r - and self.frame.t == frame.t and self.frame.b == frame.b - and self.frame.w == frame.w and self.frame.h == frame.h then - return - end - self.frame = frame - self:updateLayout() -end - --- dim: the name of the dimension var (i.e. 'h' or 'w') --- anchor: the name of the anchor var (i.e. 't', 'b', 'l', or 'r') --- opposite_anchor: the name of the anchor var for the opposite edge --- max_dim: how big this panel can get from its current pos and fit in parent --- wanted_dim: how big the player is trying to make the panel --- max_anchor: max value of the frame anchor for the edge that is being resized --- wanted_anchor: how small the player is trying to make the anchor value -local function Panel_resize_edge_base(frame, resize_min, dim, anchor, - opposite_anchor, max_dim, wanted_dim, - max_anchor, wanted_anchor) - frame[dim] = math.max(resize_min[dim], math.min(max_dim, wanted_dim)) - if frame[anchor] or not frame[opposite_anchor] then - frame[anchor] = math.max(0, math.min(max_anchor, wanted_anchor)) - end -end - -local function Panel_resize_edge(frame, resize_min, dim, anchor, - opposite_anchor, dim_base, dim_ref, anchor_ref, - dim_far, mouse_ref) - local dim_sign = (anchor == 't' or anchor == 'l') and 1 or -1 - local max_dim = dim_base - dim_ref + 1 - local wanted_dim = dim_sign * (dim_far - mouse_ref) + 1 - local max_anchor = dim_base - resize_min[dim] - dim_ref + 1 - local wanted_anchor = dim_sign * (mouse_ref - anchor_ref) - Panel_resize_edge_base(frame, resize_min, dim, anchor, opposite_anchor, - max_dim, wanted_dim, max_anchor, wanted_anchor) -end - -local function Panel_resize_frame(self, mouse_pos) - local frame, resize_min = copyall(self.frame), self.resize_min - local parent_rect = self.frame_parent_rect - local ref_rect = self.saved_frame_rect - if self.resize_edge:find('t') then - Panel_resize_edge(frame, resize_min, 'h', 't', 'b', ref_rect.y2, - parent_rect.y1, parent_rect.y1, ref_rect.y2, mouse_pos.y) - end - if self.resize_edge:find('b') then - Panel_resize_edge(frame, resize_min, 'h', 'b', 't', parent_rect.y2, - ref_rect.y1, parent_rect.y2, ref_rect.y1, mouse_pos.y) - end - if self.resize_edge:find('l') then - Panel_resize_edge(frame, resize_min, 'w', 'l', 'r', ref_rect.x2, - parent_rect.x1, parent_rect.x1, ref_rect.x2, mouse_pos.x) - end - if self.resize_edge:find('r') then - Panel_resize_edge(frame, resize_min, 'w', 'r', 'l', parent_rect.x2, - ref_rect.x1, parent_rect.x2, ref_rect.x1, mouse_pos.x) - end - return frame -end - -local function Panel_drag_frame(self, mouse_pos) - local frame = copyall(self.frame) - local parent_rect = self.frame_parent_rect - local frame_rect = gui.mkdims_wh( - self.frame_rect.x1+parent_rect.x1, - self.frame_rect.y1+parent_rect.y1, - self.frame_rect.width, - self.frame_rect.height - ) - local bound_rect = self.drag_bound == 'body' and self.frame_body - or frame_rect - local offset = self.drag_offset - local max_width = parent_rect.width - (bound_rect.x2-frame_rect.x1+1) - local max_height = parent_rect.height - (bound_rect.y2-frame_rect.y1+1) - if frame.t or not frame.b then - local min_pos = frame_rect.y1 - bound_rect.y1 - local requested_pos = mouse_pos.y - parent_rect.y1 - offset.y - frame.t = math.max(min_pos, math.min(max_height, requested_pos)) - end - if frame.b or not frame.t then - local min_pos = bound_rect.y2 - frame_rect.y2 - local requested_pos = parent_rect.y2 - mouse_pos.y + offset.y - - (frame_rect.y2 - frame_rect.y1) - frame.b = math.max(min_pos, math.min(max_height, requested_pos)) - end - if frame.l or not frame.r then - local min_pos = frame_rect.x1 - bound_rect.x1 - local requested_pos = mouse_pos.x - parent_rect.x1 - offset.x - frame.l = math.max(min_pos, math.min(max_width, requested_pos)) - end - if frame.r or not frame.l then - local min_pos = bound_rect.x2 - frame_rect.x2 - local requested_pos = parent_rect.x2 - mouse_pos.x + offset.x - - (frame_rect.x2 - frame_rect.x1) - frame.r = math.max(min_pos, math.min(max_width, requested_pos)) - end - return frame -end - -local function Panel_make_frame(self, mouse_pos) - mouse_pos = mouse_pos or xy2pos(dfhack.screen.getMousePos()) - return self.resize_edge and Panel_resize_frame(self, mouse_pos) - or Panel_drag_frame(self, mouse_pos) -end - -local function Panel_begin_drag(self, drag_offset, resize_edge) - Panel_update_frame(self, nil, true) - self.drag_offset = drag_offset or {x=0, y=0} - self.resize_edge = resize_edge - self.saved_frame = copyall(self.frame) - self.saved_frame_rect = gui.mkdims_wh( - self.frame_rect.x1+self.frame_parent_rect.x1, - self.frame_rect.y1+self.frame_parent_rect.y1, - self.frame_rect.width, - self.frame_rect.height) - self.prev_focus_owner = self.focus_group.cur - self:setFocus(true) - if self.resize_edge then - self:onResizeBegin() - else - self:onDragBegin() - end -end - -local function Panel_end_drag(self, frame, success) - success = not not success - if self.prev_focus_owner then - self.prev_focus_owner:setFocus(true) - else - self:setFocus(false) - end - local resize_edge = self.resize_edge - Panel_update_frame(self, frame, true) - if resize_edge then - self:onResizeEnd(success, self.frame) - else - self:onDragEnd(success, self.frame) - end -end - -local function Panel_on_double_click(self) - local a = self.resize_anchors - local can_vert, can_horiz = a.t or a.b, a.l or a.r - if not can_vert and not can_horiz then return false end - local f, rmin = self.frame, self.resize_min - local maximized = f.t == 0 and f.b == 0 and f.l == 0 and f.r == 0 - local frame - if maximized then - frame = { - t=not can_vert and f.t or nil, - l=not can_horiz and f.l or nil, - b=not can_vert and f.b or nil, - r=not can_horiz and f.r or nil, - w=can_vert and rmin.w or f.w, - h=can_horiz and rmin.h or f.h, - } - else - frame = { - t=can_vert and 0 or f.t, - l=can_horiz and 0 or f.l, - b=can_vert and 0 or f.b, - r=can_horiz and 0 or f.r - } - end - Panel_update_frame(self, frame, true) -end - ----@alias widgets.Keys ----| '_STRING' ----| '_MOUSE_L' ----| '_MOUSE_L_DOWN' ----| '_MOUSE_R' ----| '_MOUSE_R_DOWN' ----| '_MOUSE_M' ----| '_MOUSE_M_DOWN' - ----@param keys table ----@return boolean|nil -function Panel:onInput(keys) - if self.kbd_get_pos then - if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then - Panel_end_drag(self, not keys.SELECT and self.saved_frame or nil, - not not keys.SELECT) - return true - end - for code in pairs(keys) do - local dx, dy = guidm.get_movement_delta(code, 1, 10) - if dx then - local kbd_pos = self.kbd_get_pos() - kbd_pos.x = kbd_pos.x + dx - kbd_pos.y = kbd_pos.y + dy - Panel_update_frame(self, Panel_make_frame(self, kbd_pos)) - return true - end - end - return - end - if self.drag_offset then - if keys._MOUSE_R then - Panel_end_drag(self, self.saved_frame) - elseif keys._MOUSE_L_DOWN then - Panel_update_frame(self, Panel_make_frame(self)) - end - return true - end - if Panel.super.onInput(self, keys) then - return true - end - if not keys._MOUSE_L then return end - local x,y = self:getMouseFramePos() - if not x then return end - - if self.resizable and y == 0 then - local now_ms = dfhack.getTickCount() - if now_ms - self.last_title_click_ms <= DOUBLE_CLICK_MS then - self.last_title_click_ms = 0 - if Panel_on_double_click(self) then return true end - else - self.last_title_click_ms = now_ms - end - end - - local resize_edge = nil - if self.resizable then - local rect = gui.mkdims_wh( - self.frame_rect.x1+self.frame_parent_rect.x1, - self.frame_rect.y1+self.frame_parent_rect.y1, - self.frame_rect.width, - self.frame_rect.height) - if self.resize_anchors.r and self.resize_anchors.b - and x == rect.x2 - rect.x1 and y == rect.y2 - rect.y1 then - resize_edge = 'rb' - elseif self.resize_anchors.l and self.resize_anchors.b - and x == 0 and y == rect.y2 - rect.y1 then - resize_edge = 'lb' - elseif self.resize_anchors.r and self.resize_anchors.t - and x == rect.x2 - rect.x1 and y == 0 then - resize_edge = 'rt' - elseif self.resize_anchors.r and self.resize_anchors.t - and x == 0 and y == 0 then - resize_edge = 'lt' - elseif self.resize_anchors.r and x == rect.x2 - rect.x1 then - resize_edge = 'r' - elseif self.resize_anchors.l and x == 0 then - resize_edge = 'l' - elseif self.resize_anchors.b and y == rect.y2 - rect.y1 then - resize_edge = 'b' - elseif self.resize_anchors.t and y == 0 then - resize_edge = 't' - end - end - - local is_dragging = false - if not resize_edge and self.draggable then - local on_body = self:getMousePos() - is_dragging = (self.drag_anchors.title and self.frame_style and y == 0) - or (self.drag_anchors.frame and not on_body) -- includes inset - or (self.drag_anchors.body and on_body) - end - - if resize_edge or is_dragging then - Panel_begin_drag(self, {x=x, y=y}, resize_edge) - return true - end -end - ----@param enabled boolean -function Panel:setKeyboardDragEnabled(enabled) - if (enabled and self.kbd_get_pos) - or (not enabled and not self.kbd_get_pos) then - return - end - if enabled then - local kbd_get_pos = function() - return { - x=self.frame_rect.x1+self.frame_parent_rect.x1, - y=self.frame_rect.y1+self.frame_parent_rect.y1 - } - end - Panel_begin_drag(self) - self.kbd_get_pos = kbd_get_pos - else - Panel_end_drag(self) - end -end - -local function Panel_get_resize_data(self) - local resize_anchors = self.resize_anchors - local frame_rect = gui.mkdims_wh( - self.frame_rect.x1+self.frame_parent_rect.x1, - self.frame_rect.y1+self.frame_parent_rect.y1, - self.frame_rect.width, - self.frame_rect.height) - if resize_anchors.r and resize_anchors.b then - return 'rb', function() - return {x=frame_rect.x2, y=frame_rect.y2} end - elseif resize_anchors.l and resize_anchors.b then - return 'lb', function() - return {x=frame_rect.x1, y=frame_rect.y2} end - elseif resize_anchors.r and resize_anchors.t then - return 'rt', function() - return {x=frame_rect.x2, y=frame_rect.y1} end - elseif resize_anchors.l and resize_anchors.t then - return 'lt', function() - return {x=frame_rect.x1, y=frame_rect.y1} end - elseif resize_anchors.b then - return 'b', function() - return {x=(frame_rect.x1+frame_rect.x2)//2, - y=frame_rect.y2} end - elseif resize_anchors.r then - return 'r', function() - return {x=frame_rect.x2, - y=(frame_rect.y1+frame_rect.y2)//2} end - elseif resize_anchors.l then - return 'l', function() - return {x=frame_rect.x1, - y=(frame_rect.y1+frame_rect.y2)//2} end - elseif resize_anchors.t then - return 't', function() - return {x=(frame_rect.x1+frame_rect.x2)//2, - y=frame_rect.y1} end - end -end - ----@param enabled boolean -function Panel:setKeyboardResizeEnabled(enabled) - if (enabled and self.kbd_get_pos) - or (not enabled and not self.kbd_get_pos) then - return - end - if enabled then - local resize_edge, kbd_get_pos = Panel_get_resize_data(self) - if not resize_edge then - dfhack.printerr('cannot resize window: no anchors are enabled') - else - Panel_begin_drag(self, kbd_get_pos(), resize_edge) - self.kbd_get_pos = kbd_get_pos - end - else - Panel_end_drag(self) - end -end - -function Panel:onRenderBody(dc) - if self.on_render then self.on_render(dc) end -end - -function Panel:computeFrame(parent_rect) - local sw, sh = parent_rect.width, parent_rect.height - if self.frame then - if self.frame.t and self.frame.h and self.frame.t + self.frame.h > sh then - self.frame.t = math.max(0, sh - self.frame.h) - end - if self.frame.b and self.frame.h and self.frame.b + self.frame.h > sh then - self.frame.b = math.max(0, sh - self.frame.h) - end - if self.frame.l and self.frame.w and self.frame.l + self.frame.w > sw then - self.frame.l = math.max(0, sw - self.frame.w) - end - if self.frame.r and self.frame.w and self.frame.r + self.frame.w > sw then - self.frame.r = math.max(0, sw - self.frame.w) - end - end - return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset, - self.frame_style and 1 or 0) -end - -function Panel:postComputeFrame(body) - if self.on_layout then self.on_layout(body) end -end - --- if self.autoarrange_subviews is true, lay out visible subviews vertically, --- adding gaps between widgets according to self.autoarrange_gap. -function Panel:postUpdateLayout() - -- don't leave artifacts behind on the parent screen when we move - gui.Screen.request_full_screen_refresh = true - - if not self.autoarrange_subviews then return end - - local gap = self.autoarrange_gap - local y = 0 - for _,subview in ipairs(self.subviews) do - if not subview.frame then goto continue end - subview.frame.t = y - if getval(subview.visible) then - y = y + (subview.frame.h or 0) + gap - end - ::continue:: - end - - -- let widgets adjust to their new positions - self:updateSubviewLayout() -end - -function Panel:onRenderFrame(dc, rect) - Panel.super.onRenderFrame(self, dc, rect) - if not self.frame_style then return end - local inactive = self.parent_view and self.parent_view.hasFocus - and not self.parent_view:hasFocus() - local pause_forced = not self.no_force_pause_badge and safe_index(self.parent_view, 'force_pause') - gui.paint_frame(dc, rect, self.frame_style, self.frame_title, inactive, - pause_forced, self.resizable) - if self.kbd_get_pos then - local pos = self.kbd_get_pos() - local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK} - dc:seek(pos.x, pos.y):pen(pen):char(string.char(0xDB)) - end - if self.drag_offset and not self.kbd_get_pos - and df.global.enabler.mouse_lbut_down == 0 then - Panel_end_drag(self, nil, true) - end -end - -function Panel:onDragBegin() - if self.on_drag_begin then self.on_drag_begin() end -end - -function Panel:onDragEnd(success, new_frame) - if self.on_drag_end then self.on_drag_end(success, new_frame) end -end - -function Panel:onResizeBegin() - if self.on_resize_begin then self.on_resize_begin() end -end - -function Panel:onResizeEnd(success, new_frame) - if self.on_resize_end then self.on_resize_end(success, new_frame) end -end - ------------- --- Window -- ------------- - ----@class widgets.Window.attrs: widgets.Panel.attrs ----@field frame_style gui.Frame|fun(): gui.Frame ----@field frame_background dfhack.color|dfhack.pen ----@field frame_inset integer - ----@class widgets.Window.attrs.partial: widgets.Window.attrs - ----@class widgets.Window: widgets.Panel, widgets.Window.attrs ----@field super widgets.Panel ----@field ATTRS widgets.Window.attrs|fun(attributes: widgets.Window.attrs.partial) ----@overload fun(init_table: widgets.Window.attrs.partial): self -Window = defclass(Window, Panel) - -Window.ATTRS { - frame_style = gui.WINDOW_FRAME, - frame_background = gui.CLEAR_PEN, - frame_inset = 1, - draggable = true, -} - ------------------- -- ResizingPanel -- ------------------- diff --git a/library/lua/gui/widgets/divider.lua b/library/lua/gui/widgets/divider.lua new file mode 100644 index 0000000000..a10a7a5d6b --- /dev/null +++ b/library/lua/gui/widgets/divider.lua @@ -0,0 +1,91 @@ +local gui = require('gui') +local Widget = require('gui.widgets.widget') + +------------- +-- Divider -- +------------- + +---@class widgets.Divider.attrs: widgets.Widget.attrs +---@field frame_style gui.Frame|fun(): gui.Frame +---@field interior boolean +---@field frame_style_t? false|gui.Frame|fun(): gui.Frame +---@field frame_style_b? false|gui.Frame|fun(): gui.Frame +---@field frame_style_l? false|gui.Frame|fun(): gui.Frame +---@field frame_style_r? false|gui.Frame|fun(): gui.Frame +---@field interior_t? boolean +---@field interior_b? boolean +---@field interior_l? boolean +---@field interior_r? boolean + +---@class widgets.Divider.attrs.partial: widgets.Divider.attrs + +---@class widgets.Divider: widgets.Widget, widgets.Divider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Divider.attrs|fun(attributes: widgets.Divider.attrs.partial) +---@overload fun(init_table: widgets.Divider.attrs.partial): self +Divider = defclass(Divider, Widget) + +Divider.ATTRS{ + frame_style=gui.FRAME_THIN, + interior=false, + frame_style_t=DEFAULT_NIL, + interior_t=DEFAULT_NIL, + frame_style_b=DEFAULT_NIL, + interior_b=DEFAULT_NIL, + frame_style_l=DEFAULT_NIL, + interior_l=DEFAULT_NIL, + frame_style_r=DEFAULT_NIL, + interior_r=DEFAULT_NIL, +} + +local function divider_get_val(self, base_name, edge_name) + local val = self[base_name..'_'..edge_name] + if val ~= nil then return val end + return self[base_name] +end + +local function divider_get_junction_pen(self, edge_name) + local interior = divider_get_val(self, 'interior', edge_name) + local pen_name = ('%sT%s_frame_pen'):format(edge_name, interior and 'i' or 'e') + local frame_style = divider_get_val(self, 'frame_style', edge_name) + if type(frame_style) == 'function' then + frame_style = frame_style() + end + return frame_style[pen_name] +end + +---@param dc gui.Painter +function Divider:onRenderBody(dc) + local rect, style = self.frame_rect, self.frame_style + if type(style) == 'function' then + style = style() + end + + if rect.height == 1 and rect.width == 1 then + dc:seek(0, 0):char(nil, style.x_frame_pen) + elseif rect.width == 1 then + local fill_start, fill_end = 0, rect.height-1 + if self.frame_style_t ~= false then + fill_start = 1 + dc:seek(0, 0):char(nil, divider_get_junction_pen(self, 't')) + end + if self.frame_style_b ~= false then + fill_end = rect.height-2 + dc:seek(0, rect.height-1):char(nil, divider_get_junction_pen(self, 'b')) + end + dc:fill(0, fill_start, 0, fill_end, style.v_frame_pen) + else + local fill_start, fill_end = 0, rect.width-1 + if self.frame_style_l ~= false then + fill_start = 1 + dc:seek(0, 0):char(nil, divider_get_junction_pen(self, 'l')) + end + if self.frame_style_r ~= false then + fill_end = rect.width-2 + dc:seek(rect.width-1, 0):char(nil, divider_get_junction_pen(self, 'r')) + end + dc:fill(fill_start, 0, fill_end, 0, style.h_frame_pen) + end +end + +return Divider diff --git a/library/lua/gui/widgets/panel.lua b/library/lua/gui/widgets/panel.lua new file mode 100644 index 0000000000..3e84265210 --- /dev/null +++ b/library/lua/gui/widgets/panel.lua @@ -0,0 +1,530 @@ +local gui = require('gui') +local utils = require('utils') +local Widget = require('gui.widgets.widget') + +local getval = utils.getval +local to_pen = dfhack.pen.parse + +----------- +-- Panel -- +----------- + +DOUBLE_CLICK_MS = 500 + +---@class widgets.Panel.attrs: widgets.Widget.attrs +---@field frame_style? gui.Frame|fun(): gui.Frame +---@field frame_title? string +---@field on_render? fun(painter: gui.Painter) Called from `onRenderBody`. +---@field on_layout? fun(frame_body: any) Called from `postComputeFrame`. +---@field draggable boolean +---@field drag_anchors? { title: boolean, frame: boolean, body: boolean } +---@field drag_bound 'frame'|'body' +---@field on_drag_begin? fun() +---@field on_drag_end? fun(success: boolean, new_frame: gui.Frame) +---@field resizable boolean +---@field resize_anchors? { t: boolean, l: boolean, r: boolean, b: boolean } +---@field resize_min? { w: integer, h: integer } +---@field on_resize_begin? fun() +---@field on_resize_end? fun(success: boolean, new_frame: gui.Frame) +---@field autoarrange_subviews boolean +---@field autoarrange_gap integer +---@field kbd_get_pos? fun(): df.coord2d +---@field saved_frame? table +---@field saved_frame_rect? table +---@field drag_offset? table +---@field resize_edge? string +---@field last_title_click_ms number + +---@class widgets.Panel.attrs.partial: widgets.Panel.attrs + +---@class widgets.Panel.initTable: widgets.Panel.attrs.partial +---@field subviews? gui.View[] + +---@class widgets.Panel: widgets.Widget, widgets.Panel.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Panel.attrs|fun(attributes: widgets.Panel.attrs.partial) +---@overload fun(init_table: widgets.Panel.initTable): self +Panel = defclass(Panel, Widget) + +Panel.ATTRS { + frame_style = DEFAULT_NIL, -- as in gui.FramedScreen + frame_title = DEFAULT_NIL, -- as in gui.FramedScreen + on_render = DEFAULT_NIL, + on_layout = DEFAULT_NIL, + draggable = false, + drag_anchors = DEFAULT_NIL, + drag_bound = 'frame', -- or 'body' + on_drag_begin = DEFAULT_NIL, + on_drag_end = DEFAULT_NIL, + resizable = false, + resize_anchors = DEFAULT_NIL, + resize_min = DEFAULT_NIL, + on_resize_begin = DEFAULT_NIL, + on_resize_end = DEFAULT_NIL, + no_force_pause_badge = false, + autoarrange_subviews = false, -- whether to automatically lay out subviews + autoarrange_gap = 0, -- how many blank lines to insert between widgets +} + +---@param self widgets.Panel +---@param args widgets.Panel.initTable +function Panel:init(args) + if not self.drag_anchors then + self.drag_anchors = {title=true, frame=not self.resizable, body=true} + end + if not self.resize_anchors then + self.resize_anchors = {t=false, l=true, r=true, b=true} + end + self.resize_min = self.resize_min or {} + self.resize_min.w = self.resize_min.w or (self.frame or {}).w or 5 + self.resize_min.h = self.resize_min.h or (self.frame or {}).h or 5 + + self.kbd_get_pos = nil -- fn when we are in keyboard dragging mode + self.saved_frame = nil -- copy of frame when dragging started + self.saved_frame_rect = nil -- copy of frame_rect when dragging started + self.drag_offset = nil -- relative pos of held panel tile + self.resize_edge = nil -- which dimension is being resized? + + self.last_title_click_ms = 0 -- used to track double-clicking on the title + self:addviews(args.subviews) +end + +local function Panel_update_frame(self, frame, clear_state) + if clear_state then + self.kbd_get_pos = nil + self.saved_frame = nil + self.saved_frame_rect = nil + self.drag_offset = nil + self.resize_edge = nil + end + if not frame then return end + if self.frame.l == frame.l and self.frame.r == frame.r + and self.frame.t == frame.t and self.frame.b == frame.b + and self.frame.w == frame.w and self.frame.h == frame.h then + return + end + self.frame = frame + self:updateLayout() +end + +-- dim: the name of the dimension var (i.e. 'h' or 'w') +-- anchor: the name of the anchor var (i.e. 't', 'b', 'l', or 'r') +-- opposite_anchor: the name of the anchor var for the opposite edge +-- max_dim: how big this panel can get from its current pos and fit in parent +-- wanted_dim: how big the player is trying to make the panel +-- max_anchor: max value of the frame anchor for the edge that is being resized +-- wanted_anchor: how small the player is trying to make the anchor value +local function Panel_resize_edge_base(frame, resize_min, dim, anchor, + opposite_anchor, max_dim, wanted_dim, + max_anchor, wanted_anchor) + frame[dim] = math.max(resize_min[dim], math.min(max_dim, wanted_dim)) + if frame[anchor] or not frame[opposite_anchor] then + frame[anchor] = math.max(0, math.min(max_anchor, wanted_anchor)) + end +end + +local function Panel_resize_edge(frame, resize_min, dim, anchor, + opposite_anchor, dim_base, dim_ref, anchor_ref, + dim_far, mouse_ref) + local dim_sign = (anchor == 't' or anchor == 'l') and 1 or -1 + local max_dim = dim_base - dim_ref + 1 + local wanted_dim = dim_sign * (dim_far - mouse_ref) + 1 + local max_anchor = dim_base - resize_min[dim] - dim_ref + 1 + local wanted_anchor = dim_sign * (mouse_ref - anchor_ref) + Panel_resize_edge_base(frame, resize_min, dim, anchor, opposite_anchor, + max_dim, wanted_dim, max_anchor, wanted_anchor) +end + +local function Panel_resize_frame(self, mouse_pos) + local frame, resize_min = copyall(self.frame), self.resize_min + local parent_rect = self.frame_parent_rect + local ref_rect = self.saved_frame_rect + if self.resize_edge:find('t') then + Panel_resize_edge(frame, resize_min, 'h', 't', 'b', ref_rect.y2, + parent_rect.y1, parent_rect.y1, ref_rect.y2, mouse_pos.y) + end + if self.resize_edge:find('b') then + Panel_resize_edge(frame, resize_min, 'h', 'b', 't', parent_rect.y2, + ref_rect.y1, parent_rect.y2, ref_rect.y1, mouse_pos.y) + end + if self.resize_edge:find('l') then + Panel_resize_edge(frame, resize_min, 'w', 'l', 'r', ref_rect.x2, + parent_rect.x1, parent_rect.x1, ref_rect.x2, mouse_pos.x) + end + if self.resize_edge:find('r') then + Panel_resize_edge(frame, resize_min, 'w', 'r', 'l', parent_rect.x2, + ref_rect.x1, parent_rect.x2, ref_rect.x1, mouse_pos.x) + end + return frame +end + +local function Panel_drag_frame(self, mouse_pos) + local frame = copyall(self.frame) + local parent_rect = self.frame_parent_rect + local frame_rect = gui.mkdims_wh( + self.frame_rect.x1+parent_rect.x1, + self.frame_rect.y1+parent_rect.y1, + self.frame_rect.width, + self.frame_rect.height + ) + local bound_rect = self.drag_bound == 'body' and self.frame_body + or frame_rect + local offset = self.drag_offset + local max_width = parent_rect.width - (bound_rect.x2-frame_rect.x1+1) + local max_height = parent_rect.height - (bound_rect.y2-frame_rect.y1+1) + if frame.t or not frame.b then + local min_pos = frame_rect.y1 - bound_rect.y1 + local requested_pos = mouse_pos.y - parent_rect.y1 - offset.y + frame.t = math.max(min_pos, math.min(max_height, requested_pos)) + end + if frame.b or not frame.t then + local min_pos = bound_rect.y2 - frame_rect.y2 + local requested_pos = parent_rect.y2 - mouse_pos.y + offset.y - + (frame_rect.y2 - frame_rect.y1) + frame.b = math.max(min_pos, math.min(max_height, requested_pos)) + end + if frame.l or not frame.r then + local min_pos = frame_rect.x1 - bound_rect.x1 + local requested_pos = mouse_pos.x - parent_rect.x1 - offset.x + frame.l = math.max(min_pos, math.min(max_width, requested_pos)) + end + if frame.r or not frame.l then + local min_pos = bound_rect.x2 - frame_rect.x2 + local requested_pos = parent_rect.x2 - mouse_pos.x + offset.x - + (frame_rect.x2 - frame_rect.x1) + frame.r = math.max(min_pos, math.min(max_width, requested_pos)) + end + return frame +end + +local function Panel_make_frame(self, mouse_pos) + mouse_pos = mouse_pos or xy2pos(dfhack.screen.getMousePos()) + return self.resize_edge and Panel_resize_frame(self, mouse_pos) + or Panel_drag_frame(self, mouse_pos) +end + +local function Panel_begin_drag(self, drag_offset, resize_edge) + Panel_update_frame(self, nil, true) + self.drag_offset = drag_offset or {x=0, y=0} + self.resize_edge = resize_edge + self.saved_frame = copyall(self.frame) + self.saved_frame_rect = gui.mkdims_wh( + self.frame_rect.x1+self.frame_parent_rect.x1, + self.frame_rect.y1+self.frame_parent_rect.y1, + self.frame_rect.width, + self.frame_rect.height) + self.prev_focus_owner = self.focus_group.cur + self:setFocus(true) + if self.resize_edge then + self:onResizeBegin() + else + self:onDragBegin() + end +end + +local function Panel_end_drag(self, frame, success) + success = not not success + if self.prev_focus_owner then + self.prev_focus_owner:setFocus(true) + else + self:setFocus(false) + end + local resize_edge = self.resize_edge + Panel_update_frame(self, frame, true) + if resize_edge then + self:onResizeEnd(success, self.frame) + else + self:onDragEnd(success, self.frame) + end +end + +local function Panel_on_double_click(self) + local a = self.resize_anchors + local can_vert, can_horiz = a.t or a.b, a.l or a.r + if not can_vert and not can_horiz then return false end + local f, rmin = self.frame, self.resize_min + local maximized = f.t == 0 and f.b == 0 and f.l == 0 and f.r == 0 + local frame + if maximized then + frame = { + t=not can_vert and f.t or nil, + l=not can_horiz and f.l or nil, + b=not can_vert and f.b or nil, + r=not can_horiz and f.r or nil, + w=can_vert and rmin.w or f.w, + h=can_horiz and rmin.h or f.h, + } + else + frame = { + t=can_vert and 0 or f.t, + l=can_horiz and 0 or f.l, + b=can_vert and 0 or f.b, + r=can_horiz and 0 or f.r + } + end + Panel_update_frame(self, frame, true) +end + +---@alias widgets.Keys +---| '_STRING' +---| '_MOUSE_L' +---| '_MOUSE_L_DOWN' +---| '_MOUSE_R' +---| '_MOUSE_R_DOWN' +---| '_MOUSE_M' +---| '_MOUSE_M_DOWN' + +---@param keys table +---@return boolean|nil +function Panel:onInput(keys) + if self.kbd_get_pos then + if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then + Panel_end_drag(self, not keys.SELECT and self.saved_frame or nil, + not not keys.SELECT) + return true + end + for code in pairs(keys) do + local dx, dy = guidm.get_movement_delta(code, 1, 10) + if dx then + local kbd_pos = self.kbd_get_pos() + kbd_pos.x = kbd_pos.x + dx + kbd_pos.y = kbd_pos.y + dy + Panel_update_frame(self, Panel_make_frame(self, kbd_pos)) + return true + end + end + return + end + if self.drag_offset then + if keys._MOUSE_R then + Panel_end_drag(self, self.saved_frame) + elseif keys._MOUSE_L_DOWN then + Panel_update_frame(self, Panel_make_frame(self)) + end + return true + end + if Panel.super.onInput(self, keys) then + return true + end + if not keys._MOUSE_L then return end + local x,y = self:getMouseFramePos() + if not x then return end + + if self.resizable and y == 0 then + local now_ms = dfhack.getTickCount() + if now_ms - self.last_title_click_ms <= DOUBLE_CLICK_MS then + self.last_title_click_ms = 0 + if Panel_on_double_click(self) then return true end + else + self.last_title_click_ms = now_ms + end + end + + local resize_edge = nil + if self.resizable then + local rect = gui.mkdims_wh( + self.frame_rect.x1+self.frame_parent_rect.x1, + self.frame_rect.y1+self.frame_parent_rect.y1, + self.frame_rect.width, + self.frame_rect.height) + if self.resize_anchors.r and self.resize_anchors.b + and x == rect.x2 - rect.x1 and y == rect.y2 - rect.y1 then + resize_edge = 'rb' + elseif self.resize_anchors.l and self.resize_anchors.b + and x == 0 and y == rect.y2 - rect.y1 then + resize_edge = 'lb' + elseif self.resize_anchors.r and self.resize_anchors.t + and x == rect.x2 - rect.x1 and y == 0 then + resize_edge = 'rt' + elseif self.resize_anchors.r and self.resize_anchors.t + and x == 0 and y == 0 then + resize_edge = 'lt' + elseif self.resize_anchors.r and x == rect.x2 - rect.x1 then + resize_edge = 'r' + elseif self.resize_anchors.l and x == 0 then + resize_edge = 'l' + elseif self.resize_anchors.b and y == rect.y2 - rect.y1 then + resize_edge = 'b' + elseif self.resize_anchors.t and y == 0 then + resize_edge = 't' + end + end + + local is_dragging = false + if not resize_edge and self.draggable then + local on_body = self:getMousePos() + is_dragging = (self.drag_anchors.title and self.frame_style and y == 0) + or (self.drag_anchors.frame and not on_body) -- includes inset + or (self.drag_anchors.body and on_body) + end + + if resize_edge or is_dragging then + Panel_begin_drag(self, {x=x, y=y}, resize_edge) + return true + end +end + +---@param enabled boolean +function Panel:setKeyboardDragEnabled(enabled) + if (enabled and self.kbd_get_pos) + or (not enabled and not self.kbd_get_pos) then + return + end + if enabled then + local kbd_get_pos = function() + return { + x=self.frame_rect.x1+self.frame_parent_rect.x1, + y=self.frame_rect.y1+self.frame_parent_rect.y1 + } + end + Panel_begin_drag(self) + self.kbd_get_pos = kbd_get_pos + else + Panel_end_drag(self) + end +end + +local function Panel_get_resize_data(self) + local resize_anchors = self.resize_anchors + local frame_rect = gui.mkdims_wh( + self.frame_rect.x1+self.frame_parent_rect.x1, + self.frame_rect.y1+self.frame_parent_rect.y1, + self.frame_rect.width, + self.frame_rect.height) + if resize_anchors.r and resize_anchors.b then + return 'rb', function() + return {x=frame_rect.x2, y=frame_rect.y2} end + elseif resize_anchors.l and resize_anchors.b then + return 'lb', function() + return {x=frame_rect.x1, y=frame_rect.y2} end + elseif resize_anchors.r and resize_anchors.t then + return 'rt', function() + return {x=frame_rect.x2, y=frame_rect.y1} end + elseif resize_anchors.l and resize_anchors.t then + return 'lt', function() + return {x=frame_rect.x1, y=frame_rect.y1} end + elseif resize_anchors.b then + return 'b', function() + return {x=(frame_rect.x1+frame_rect.x2)//2, + y=frame_rect.y2} end + elseif resize_anchors.r then + return 'r', function() + return {x=frame_rect.x2, + y=(frame_rect.y1+frame_rect.y2)//2} end + elseif resize_anchors.l then + return 'l', function() + return {x=frame_rect.x1, + y=(frame_rect.y1+frame_rect.y2)//2} end + elseif resize_anchors.t then + return 't', function() + return {x=(frame_rect.x1+frame_rect.x2)//2, + y=frame_rect.y1} end + end +end + +---@param enabled boolean +function Panel:setKeyboardResizeEnabled(enabled) + if (enabled and self.kbd_get_pos) + or (not enabled and not self.kbd_get_pos) then + return + end + if enabled then + local resize_edge, kbd_get_pos = Panel_get_resize_data(self) + if not resize_edge then + dfhack.printerr('cannot resize window: no anchors are enabled') + else + Panel_begin_drag(self, kbd_get_pos(), resize_edge) + self.kbd_get_pos = kbd_get_pos + end + else + Panel_end_drag(self) + end +end + +function Panel:onRenderBody(dc) + if self.on_render then self.on_render(dc) end +end + +function Panel:computeFrame(parent_rect) + local sw, sh = parent_rect.width, parent_rect.height + if self.frame then + if self.frame.t and self.frame.h and self.frame.t + self.frame.h > sh then + self.frame.t = math.max(0, sh - self.frame.h) + end + if self.frame.b and self.frame.h and self.frame.b + self.frame.h > sh then + self.frame.b = math.max(0, sh - self.frame.h) + end + if self.frame.l and self.frame.w and self.frame.l + self.frame.w > sw then + self.frame.l = math.max(0, sw - self.frame.w) + end + if self.frame.r and self.frame.w and self.frame.r + self.frame.w > sw then + self.frame.r = math.max(0, sw - self.frame.w) + end + end + return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset, + self.frame_style and 1 or 0) +end + +function Panel:postComputeFrame(body) + if self.on_layout then self.on_layout(body) end +end + +-- if self.autoarrange_subviews is true, lay out visible subviews vertically, +-- adding gaps between widgets according to self.autoarrange_gap. +function Panel:postUpdateLayout() + -- don't leave artifacts behind on the parent screen when we move + gui.Screen.request_full_screen_refresh = true + + if not self.autoarrange_subviews then return end + + local gap = self.autoarrange_gap + local y = 0 + for _,subview in ipairs(self.subviews) do + if not subview.frame then goto continue end + subview.frame.t = y + if getval(subview.visible) then + y = y + (subview.frame.h or 0) + gap + end + ::continue:: + end + + -- let widgets adjust to their new positions + self:updateSubviewLayout() +end + +function Panel:onRenderFrame(dc, rect) + Panel.super.onRenderFrame(self, dc, rect) + if not self.frame_style then return end + local inactive = self.parent_view and self.parent_view.hasFocus + and not self.parent_view:hasFocus() + local pause_forced = not self.no_force_pause_badge and safe_index(self.parent_view, 'force_pause') + gui.paint_frame(dc, rect, self.frame_style, self.frame_title, inactive, + pause_forced, self.resizable) + if self.kbd_get_pos then + local pos = self.kbd_get_pos() + local pen = to_pen{fg=COLOR_GREEN, bg=COLOR_BLACK} + dc:seek(pos.x, pos.y):pen(pen):char(string.char(0xDB)) + end + if self.drag_offset and not self.kbd_get_pos + and df.global.enabler.mouse_lbut_down == 0 then + Panel_end_drag(self, nil, true) + end +end + +function Panel:onDragBegin() + if self.on_drag_begin then self.on_drag_begin() end +end + +function Panel:onDragEnd(success, new_frame) + if self.on_drag_end then self.on_drag_end(success, new_frame) end +end + +function Panel:onResizeBegin() + if self.on_resize_begin then self.on_resize_begin() end +end + +function Panel:onResizeEnd(success, new_frame) + if self.on_resize_end then self.on_resize_end(success, new_frame) end +end + +return Panel diff --git a/library/lua/gui/widgets/widget.lua b/library/lua/gui/widgets/widget.lua new file mode 100644 index 0000000000..97e3247f2a --- /dev/null +++ b/library/lua/gui/widgets/widget.lua @@ -0,0 +1,57 @@ +local gui = require('gui') + +------------ +-- Widget -- +------------ + +---@class widgets.Widget.frame +---@field l? integer Gap between the left edge of the frame and the parent. +---@field t? integer Gap between the top edge of the frame and the parent. +---@field r? integer Gap between the right edge of the frame and the parent. +---@field b? integer Gap between the bottom edge of the frame and the parent. +---@field w? integer Desired width +---@field h? integer Desired height + +---@class widgets.Widget.inset +---@field l? integer Left margin +---@field t? integer Top margin +---@field r? integer Right margin +---@field b? integer Bottom margin +---@field x? integer Left/right margin (if `l` and/or `r` are ommited) +---@field y? integer Top/bottom margin (if `t` and/or `b` are ommited) + +---@class widgets.Widget.attrs: gui.View.attrs +---@field frame? widgets.Widget.frame +---@field frame_inset? widgets.Widget.inset|integer +---@field frame_background? dfhack.pen + +---@class widgets.Widget.attrs.partial: widgets.Widget.attrs + +---@class widgets.Widget: gui.View +---@field super gui.View +---@field ATTRS widgets.Widget.attrs|fun(attributes: widgets.Widget.attrs.partial) +---@overload fun(init_table: widgets.Widget.attrs.partial): self +Widget = defclass(Widget, gui.View) + +Widget.ATTRS { + frame = DEFAULT_NIL, + frame_inset = DEFAULT_NIL, + frame_background = DEFAULT_NIL, +} + +---@param parent_rect { width: integer, height: integer } +---@return any +function Widget:computeFrame(parent_rect) + local sw, sh = parent_rect.width, parent_rect.height + return gui.compute_frame_body(sw, sh, self.frame, self.frame_inset) +end + +---@param dc gui.Painter +---@param rect { x1: integer, y1: integer, x2: integer, y2: integer } +function Widget:onRenderFrame(dc, rect) + if self.frame_background then + dc:fill(rect, self.frame_background) + end +end + +return Widget diff --git a/library/lua/gui/widgets/window.lua b/library/lua/gui/widgets/window.lua new file mode 100644 index 0000000000..d446900797 --- /dev/null +++ b/library/lua/gui/widgets/window.lua @@ -0,0 +1,28 @@ +local gui = require('gui') +local Panel = require('gui.widgets.panel') + +------------ +-- Window -- +------------ + +---@class widgets.Window.attrs: widgets.Panel.attrs +---@field frame_style gui.Frame|fun(): gui.Frame +---@field frame_background dfhack.color|dfhack.pen +---@field frame_inset integer + +---@class widgets.Window.attrs.partial: widgets.Window.attrs + +---@class widgets.Window: widgets.Panel, widgets.Window.attrs +---@field super widgets.Panel +---@field ATTRS widgets.Window.attrs|fun(attributes: widgets.Window.attrs.partial) +---@overload fun(init_table: widgets.Window.attrs.partial): self +Window = defclass(Window, Panel) + +Window.ATTRS { + frame_style = gui.WINDOW_FRAME, + frame_background = gui.CLEAR_PEN, + frame_inset = 1, + draggable = true, +} + +return Window From 68ef045d551720acb06d03c7e1957441c1dcf126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 21 Sep 2024 12:09:49 +0200 Subject: [PATCH 04/34] Refactor few widgets to dedicated modules ResizingPanel, Pages, EditField, HotkeyLabel, Label and Scrollbar --- library/lua/gui/widgets.lua | 1202 +------------------- library/lua/gui/widgets/edit_field.lua | 229 ++++ library/lua/gui/widgets/hotkey_label.lua | 62 + library/lua/gui/widgets/label.lua | 553 +++++++++ library/lua/gui/widgets/pages.lua | 63 + library/lua/gui/widgets/resizing_panel.lua | 58 + library/lua/gui/widgets/scrollbar.lua | 270 +++++ 7 files changed, 1244 insertions(+), 1193 deletions(-) create mode 100644 library/lua/gui/widgets/edit_field.lua create mode 100644 library/lua/gui/widgets/hotkey_label.lua create mode 100644 library/lua/gui/widgets/label.lua create mode 100644 library/lua/gui/widgets/pages.lua create mode 100644 library/lua/gui/widgets/resizing_panel.lua create mode 100644 library/lua/gui/widgets/scrollbar.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 06c5693161..82ca1014a2 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -11,18 +11,19 @@ Widget = require('gui.widgets.widget') Divider = require('gui.widgets.divider') Panel = require('gui.widgets.panel') Window = require('gui.widgets.window') +ResizingPanel = require('gui.widgets.resizing_panel') +Pages = require('gui.widgets.pages') +EditField = require('gui.widgets.edit_field') +HotkeyLabel = require('gui.widgets.hotkey_label') +Label = require('gui.widgets.label') +Scrollbar = require('gui.widgets.scrollbar') + +makeButtonLabelText = Label.makeButtonLabelText +parse_label_text = Label.parse_label_text local getval = utils.getval local to_pen = dfhack.pen.parse ----@param view gui.View ----@param vis boolean -local function show_view(view,vis) - if view then - view.visible = vis - end -end - ---@param tab? table ---@param idx integer ---@return any|integer @@ -34,1132 +35,6 @@ local function map_opttab(tab,idx) end end ----@enum STANDARDSCROLL -STANDARDSCROLL = { - STANDARDSCROLL_UP = -1, - KEYBOARD_CURSOR_UP = -1, - STANDARDSCROLL_DOWN = 1, - KEYBOARD_CURSOR_DOWN = 1, - STANDARDSCROLL_PAGEUP = '-page', - KEYBOARD_CURSOR_UP_FAST = '-page', - STANDARDSCROLL_PAGEDOWN = '+page', - KEYBOARD_CURSOR_DOWN_FAST = '+page', -} - -------------------- --- ResizingPanel -- -------------------- - ----@class widgets.ResizingPanel.attrs: widgets.Panel.attrs ----@field auto_height boolean ----@field auto_width boolean - ----@class widgets.ResizingPanel.attrs.partial: widgets.ResizingPanel.attrs - ----@class widgets.ResizingPanel: widgets.Panel, widgets.ResizingPanel.attrs ----@field super widgets.Panel ----@field ATTRS widgets.ResizingPanel.attrs|fun(attributes: widgets.ResizingPanel.attrs.partial) ----@overload fun(init_table: widgets.ResizingPanel.attrs.partial): self -ResizingPanel = defclass(ResizingPanel, Panel) - -ResizingPanel.ATTRS{ - auto_height = true, - auto_width = false, -} - --- adjust our frame dimensions according to positions and sizes of our subviews -function ResizingPanel:postUpdateLayout(frame_body) - local w, h = 0, 0 - for _,s in ipairs(self.subviews) do - if getval(s.visible) then - w = math.max(w, (s.frame and s.frame.l or 0) + - (s.frame and s.frame.w or frame_body.width)) - h = math.max(h, (s.frame and s.frame.t or 0) + - (s.frame and s.frame.h or frame_body.height)) - end - end - local l,t,r,b = gui.parse_inset(self.frame_inset) - w = w + l + r - h = h + t + b - if self.frame_style then - w = w + 2 - h = h + 2 - end - if not self.frame then self.frame = {} end - local oldw, oldh = self.frame.w, self.frame.h - if not self.auto_height then h = oldh end - if not self.auto_width then w = oldw end - self.frame.w, self.frame.h = w, h - if not self._updateLayoutGuard and (oldw ~= w or oldh ~= h) then - self._updateLayoutGuard = true -- protect against infinite loops - self:updateLayout() -- our frame has changed, we need to fully refresh - end - self._updateLayoutGuard = nil -end - ------------ --- Pages -- ------------ - ----@class widgets.Pages.initTable: widgets.Panel.attrs ----@field selected? integer|string - ----@class widgets.Pages: widgets.Panel ----@field super widgets.Panel ----@overload fun(attributes: widgets.Pages.initTable): self -Pages = defclass(Pages, Panel) - ----@param self widgets.Pages ----@param args widgets.Pages.initTable -function Pages:init(args) - for _,v in ipairs(self.subviews) do - v.visible = false - end - self:setSelected(args.selected or 1) -end - ----@param idx integer|string -function Pages:setSelected(idx) - if type(idx) ~= 'number' then - local key = idx - if type(idx) == 'string' then - key = self.subviews[key] - end - idx = utils.linear_index(self.subviews, key) - if not idx then - error('Unknown page: '..tostring(key)) - end - end - - show_view(self.subviews[self.selected], false) - self.selected = math.min(math.max(1, idx), #self.subviews) - show_view(self.subviews[self.selected], true) -end - ----@return integer index ----@return gui.View child -function Pages:getSelected() - return self.selected, self.subviews[self.selected] -end - ----@return gui.View child -function Pages:getSelectedPage() - return self.subviews[self.selected] -end - ----------------- --- Edit field -- ----------------- - ----@class widgets.EditField.attrs: widgets.Widget.attrs ----@field label_text? string ----@field text string ----@field text_pen? dfhack.color|dfhack.pen ----@field on_char? function ----@field on_change? function ----@field on_submit? function ----@field on_submit2? function ----@field key? string ----@field key_sep? string ----@field modal boolean ----@field ignore_keys? string[] - ----@class widgets.EditField.attrs.partial: widgets.EditField.attrs - ----@class widgets.EditField: widgets.Widget, widgets.EditField.attrs ----@field super widgets.Widget ----@field ATTRS widgets.EditField.attrs|fun(attributes: widgets.EditField.attrs.partial) ----@overload fun(init_table: widgets.EditField.attrs.partial): self -EditField = defclass(EditField, Widget) - -EditField.ATTRS{ - label_text = DEFAULT_NIL, - text = '', - text_pen = DEFAULT_NIL, - on_char = DEFAULT_NIL, - on_change = DEFAULT_NIL, - on_submit = DEFAULT_NIL, - on_submit2 = DEFAULT_NIL, - key = DEFAULT_NIL, - key_sep = DEFAULT_NIL, - modal = false, - ignore_keys = DEFAULT_NIL, -} - ----@param init_table widgets.EditField.attrs -function EditField:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function EditField:init() - local function on_activate() - self.saved_text = self.text - self:setFocus(true) - end - - self.start_pos = 1 - self.cursor = #self.text + 1 - - self:addviews{HotkeyLabel{frame={t=0,l=0}, - key=self.key, - key_sep=self.key_sep, - label=self.label_text, - on_activate=self.key and on_activate or nil}} -end - -function EditField:getPreferredFocusState() - return not self.key -end - -function EditField:setCursor(cursor) - if not cursor or cursor > #self.text then - self.cursor = #self.text + 1 - return - end - self.cursor = math.max(1, cursor) -end - -function EditField:setText(text, cursor) - local old = self.text - self.text = text - self:setCursor(cursor) - if self.on_change and text ~= old then - self.on_change(self.text, old) - end -end - -function EditField:postUpdateLayout() - self.text_offset = self.subviews[1]:getTextWidth() -end - ----@param dc gui.Painter -function EditField:onRenderBody(dc) - dc:pen(self.text_pen or COLOR_LIGHTCYAN) - - local cursor_char = '_' - if not getval(self.active) or not self.focus or gui.blink_visible(300) then - cursor_char = (self.cursor > #self.text) and ' ' or - self.text:sub(self.cursor, self.cursor) - end - local txt = self.text:sub(1, self.cursor - 1) .. cursor_char .. - self.text:sub(self.cursor + 1) - local max_width = dc.width - self.text_offset - self.start_pos = 1 - if #txt > max_width then - -- get the substring in the vicinity of the cursor - max_width = max_width - 2 - local half_width = math.floor(max_width/2) - local start_pos = math.max(1, self.cursor-half_width) - local end_pos = math.min(#txt, self.cursor+half_width-1) - if self.cursor + half_width > #txt then - start_pos = #txt - (max_width - 1) - end - if self.cursor - half_width <= 1 then - end_pos = max_width + 1 - end - self.start_pos = start_pos > 1 and start_pos - 1 or start_pos - txt = ('%s%s%s'):format(start_pos == 1 and '' or string.char(27), - txt:sub(start_pos, end_pos), - end_pos == #txt and '' or string.char(26)) - end - dc:advance(self.text_offset):string(txt) -end - -function EditField:insert(text) - local old = self.text - self:setText(old:sub(1,self.cursor-1)..text..old:sub(self.cursor), - self.cursor + #text) -end - -function EditField:onInput(keys) - if not self.focus then - -- only react to our hotkey - return self:inputToSubviews(keys) - end - - if self.ignore_keys then - for _,ignore_key in ipairs(self.ignore_keys) do - if keys[ignore_key] then return false end - end - end - - if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then - self:setText(self.saved_text) - self:setFocus(false) - return true - end - - if keys.SELECT or keys.SELECT_ALL then - if self.key then - self:setFocus(false) - end - if keys.SELECT_ALL then - if self.on_submit2 then - self.on_submit2(self.text) - return true - end - else - if self.on_submit then - self.on_submit(self.text) - return true - end - end - return not not self.key - elseif keys._STRING then - local old = self.text - if keys._STRING == 0 then - -- handle backspace - local del_pos = self.cursor - 1 - if del_pos > 0 then - self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), - del_pos) - end - else - local cv = string.char(keys._STRING) - if not self.on_char or self.on_char(cv, old) then - self:insert(cv) - elseif self.on_char then - return self.modal - end - end - return true - elseif keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) - return true - elseif keys.CUSTOM_CTRL_B then -- back one word - local _, prev_word_end = self.text:sub(1, self.cursor-1): - find('.*[%w_%-][^%w_%-]') - self:setCursor(prev_word_end or 1) - return true - -- commented out until we get HOME key support from DF - -- elseif keys.CUSTOM_CTRL_A then -- home - -- self:setCursor(1) - -- return true - elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) - return true - elseif keys.CUSTOM_CTRL_F then -- forward one word - local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor) - self:setCursor(next_word_start) - return true - elseif keys.CUSTOM_CTRL_E then -- end - self:setCursor() - return true - elseif keys.CUSTOM_CTRL_C then - dfhack.internal.setClipboardTextCp437(self.text) - return true - elseif keys.CUSTOM_CTRL_X then - dfhack.internal.setClipboardTextCp437(self.text) - self:setText('') - return true - elseif keys.CUSTOM_CTRL_V then - self:insert(dfhack.internal.getClipboardTextCp437()) - return true - elseif keys._MOUSE_L_DOWN then - local mouse_x = self:getMousePos() - if mouse_x then - self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) - return true - end - end - - -- if we're modal, then unconditionally eat all the input - return self.modal -end - ---------------- --- Scrollbar -- ---------------- - -SCROLL_INITIAL_DELAY_MS = 300 -SCROLL_DELAY_MS = 20 - ----@class widgets.Scrollbar.attrs: widgets.Widget.attrs ----@field on_scroll? fun(new_top_elem?: integer) - ----@class widgets.Scrollbar.attrs.partial: widgets.Scrollbar.attrs - ----@class widgets.Scrollbar: widgets.Widget, widgets.Scrollbar.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Scrollbar.attrs|fun(attributes: widgets.Scrollbar.attrs.partial) ----@overload fun(init_table: widgets.Scrollbar.attrs.partial): self -Scrollbar = defclass(Scrollbar, Widget) - -Scrollbar.ATTRS{ - on_scroll = DEFAULT_NIL, -} - ----@param init_table widgets.Scrollbar.attrs.partial -function Scrollbar:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.w = init_table.frame.w or 2 -end - -function Scrollbar:init() - self.last_scroll_ms = 0 - self.is_first_click = false - self.scroll_spec = nil - self.is_dragging = false -- index of the scrollbar tile that we're dragging - self:update(1, 1, 1) -end - -local function scrollbar_get_max_pos_and_height(scrollbar) - local frame_body = scrollbar.frame_body - local scrollbar_body_height = (frame_body and frame_body.height or 3) - 2 - - local height = math.max(2, math.floor( - (math.min(scrollbar.elems_per_page, scrollbar.num_elems) * scrollbar_body_height) / - scrollbar.num_elems)) - - return scrollbar_body_height - height, height -end - --- calculate and cache the number of tiles of empty space above the top of the --- scrollbar and the number of tiles the scrollbar should occupy to represent --- the percentage of text that is on the screen. --- if elems_per_page or num_elems are not specified, the last values passed to --- Scrollbar:update() are used. -function Scrollbar:update(top_elem, elems_per_page, num_elems) - if not top_elem then error('must specify index of new top element') end - elems_per_page = elems_per_page or self.elems_per_page - num_elems = num_elems or self.num_elems - self.top_elem = top_elem - self.elems_per_page, self.num_elems = elems_per_page, num_elems - - local max_pos, height = scrollbar_get_max_pos_and_height(self) - local pos = (num_elems == elems_per_page) and 0 or - math.ceil(((top_elem-1) * max_pos) / - (num_elems - elems_per_page)) - - self.bar_offset, self.bar_height = pos, height -end - -local function scrollbar_do_drag(scrollbar) - local x,y = dfhack.screen.getMousePos() - if not y then return end - x,y = scrollbar.frame_body:localXY(x, y) - local cur_pos = y - scrollbar.is_dragging - local max_top = scrollbar.num_elems - scrollbar.elems_per_page + 1 - local max_pos = scrollbar_get_max_pos_and_height(scrollbar) - local new_top_elem = math.floor(cur_pos * max_top / max_pos) + 1 - new_top_elem = math.max(1, math.min(new_top_elem, max_top)) - if new_top_elem ~= scrollbar.top_elem then - scrollbar.on_scroll(new_top_elem) - end -end - -local function scrollbar_is_visible(scrollbar) - return scrollbar.elems_per_page < scrollbar.num_elems -end - -local SBSO = df.global.init.scrollbar_texpos[0] --Scroll Bar Spritesheet Offset / change this to point to a different spritesheet (ui themes, anyone? :p) -local SCROLLBAR_UP_LEFT_PEN = to_pen{tile=SBSO+0, ch=47, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_UP_RIGHT_PEN = to_pen{tile=SBSO+1, ch=92, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_DOWN_LEFT_PEN = to_pen{tile=SBSO+24, ch=92, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_DOWN_RIGHT_PEN = to_pen{tile=SBSO+25, ch=47, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_UP_LEFT_PEN = to_pen{tile=SBSO+6, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_UP_RIGHT_PEN = to_pen{tile=SBSO+7, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_LEFT_PEN = to_pen{tile=SBSO+30, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_RIGHT_PEN = to_pen{tile=SBSO+31, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_UP_LEFT_PEN = to_pen{tile=SBSO+10, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_UP_RIGHT_PEN = to_pen{tile=SBSO+11, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_DOWN_LEFT_PEN = to_pen{tile=SBSO+22, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_DOWN_RIGHT_PEN = to_pen{tile=SBSO+23, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_LEFT_PEN = to_pen{tile=SBSO+18, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_RIGHT_PEN = to_pen{tile=SBSO+19, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_DOWN_LEFT_PEN = to_pen{tile=SBSO+42, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_DOWN_RIGHT_PEN = to_pen{tile=SBSO+43, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_UP_LEFT_PEN = to_pen{tile=SBSO+26, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_UP_RIGHT_PEN = to_pen{tile=SBSO+27, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_DOWN_LEFT_PEN = to_pen{tile=SBSO+38, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_DOWN_RIGHT_PEN = to_pen{tile=SBSO+39, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} -local SCROLLBAR_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+2, ch=47, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+3, ch=92, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+14, ch=92, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+15, ch=47, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+8, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+9, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_LEFT_HOVER_PEN = to_pen{tile=SBSO+32, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_RIGHT_HOVER_PEN = to_pen{tile=SBSO+33, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+34, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+35, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+46, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+47, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_LEFT_HOVER_PEN = to_pen{tile=SBSO+20, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_CENTER_RIGHT_HOVER_PEN = to_pen{tile=SBSO+21, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+44, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+45, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+28, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+29, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+40, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_2TALL_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+41, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} -local SCROLLBAR_BAR_BG_LEFT_PEN = to_pen{tile=SBSO+12, ch=176, fg=COLOR_DARKGREY, bg=COLOR_BLACK} -local SCROLLBAR_BAR_BG_RIGHT_PEN = to_pen{tile=SBSO+13, ch=176, fg=COLOR_DARKGREY, bg=COLOR_BLACK} - -function Scrollbar:onRenderBody(dc) - -- don't draw if all elements are visible - if not scrollbar_is_visible(self) then - return - end - -- determine which elements should be highlighted - local _,hover_y = self:getMousePos() - local hover_up, hover_down, hover_bar = false, false, false - if hover_y == 0 then - hover_up = true - elseif hover_y == dc.height-1 then - hover_down = true - elseif hover_y then - hover_bar = true - end - -- render up arrow - dc:seek(0, 0) - dc:char(nil, hover_up and SCROLLBAR_UP_LEFT_HOVER_PEN or SCROLLBAR_UP_LEFT_PEN) - dc:char(nil, hover_up and SCROLLBAR_UP_RIGHT_HOVER_PEN or SCROLLBAR_UP_RIGHT_PEN) - -- render scrollbar body - local starty = self.bar_offset + 1 - local endy = self.bar_offset + self.bar_height - local midy = (starty + endy)/2 - for y=1,dc.height-2 do - dc:seek(0, y) - if y >= starty and y <= endy then - if y == starty and y <= midy - 1 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_UP_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_UP_RIGHT_PEN) - elseif y == midy - 0.5 and self.bar_height == 2 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_2TALL_UP_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_2TALL_UP_RIGHT_PEN) - elseif y == midy + 0.5 and self.bar_height == 2 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_2TALL_DOWN_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_2TALL_DOWN_RIGHT_PEN) - elseif y == midy - 0.5 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_UP_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_UP_RIGHT_PEN) - elseif y == midy + 0.5 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_DOWN_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_DOWN_RIGHT_PEN) - elseif y == midy then - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_RIGHT_PEN) - elseif y == endy and y >= midy + 1 then - dc:char(nil, hover_bar and SCROLLBAR_BAR_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_DOWN_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_DOWN_RIGHT_PEN) - else - dc:char(nil, hover_bar and SCROLLBAR_BAR_LEFT_HOVER_PEN or SCROLLBAR_BAR_LEFT_PEN) - dc:char(nil, hover_bar and SCROLLBAR_BAR_RIGHT_HOVER_PEN or SCROLLBAR_BAR_RIGHT_PEN) - end - else - dc:char(nil, SCROLLBAR_BAR_BG_LEFT_PEN) - dc:char(nil, SCROLLBAR_BAR_BG_RIGHT_PEN) - end - end - -- render down arrow - dc:seek(0, dc.height-1) - dc:char(nil, hover_down and SCROLLBAR_DOWN_LEFT_HOVER_PEN or SCROLLBAR_DOWN_LEFT_PEN) - dc:char(nil, hover_down and SCROLLBAR_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_DOWN_RIGHT_PEN) - if not self.on_scroll then return end - -- manage state for dragging and continuous scrolling - if self.is_dragging then - scrollbar_do_drag(self) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.last_scroll_ms = 0 - self.is_dragging = false - self.scroll_spec = nil - return - end - if self.last_scroll_ms == 0 then return end - local now = dfhack.getTickCount() - local delay = self.is_first_click and - SCROLL_INITIAL_DELAY_MS or SCROLL_DELAY_MS - if now - self.last_scroll_ms >= delay then - self.is_first_click = false - self.on_scroll(self.scroll_spec) - self.last_scroll_ms = now - end -end - -function Scrollbar:onInput(keys) - if not self.on_scroll or not scrollbar_is_visible(self) then - return false - end - - if self.parent_view and self.parent_view:getMousePos() then - if keys.CONTEXT_SCROLL_UP then - self.on_scroll('up_small') - return true - elseif keys.CONTEXT_SCROLL_DOWN then - self.on_scroll('down_small') - return true - elseif keys.CONTEXT_SCROLL_PAGEUP then - self.on_scroll('up_large') - return true - elseif keys.CONTEXT_SCROLL_PAGEDOWN then - self.on_scroll('down_large') - return true - end - end - if not keys._MOUSE_L then return false end - local _,y = self:getMousePos() - if not y then return false end - local scroll_spec = nil - if y == 0 then scroll_spec = 'up_small' - elseif y == self.frame_body.height - 1 then scroll_spec = 'down_small' - elseif y <= self.bar_offset then scroll_spec = 'up_large' - elseif y > self.bar_offset + self.bar_height then scroll_spec = 'down_large' - else - self.is_dragging = y - self.bar_offset - return true - end - self.scroll_spec = scroll_spec - self.on_scroll(scroll_spec) - -- reset continuous scroll state - self.is_first_click = true - self.last_scroll_ms = dfhack.getTickCount() - return true -end - ------------ --- Label -- ------------ - -function parse_label_text(obj) - local text = obj.text or {} - if type(text) ~= 'table' then - text = { text } - end - local curline = nil - local out = { } - local active = nil - local idtab = nil - for _,v in ipairs(text) do - local vv - if type(v) == 'string' then - vv = v:split(NEWLINE) - else - vv = { v } - end - - for i = 1,#vv do - local cv = vv[i] - if i > 1 then - if not curline then - table.insert(out, {}) - end - curline = nil - end - if cv ~= '' then - if not curline then - curline = {} - table.insert(out, curline) - end - - if type(cv) == 'string' then - table.insert(curline, { text = cv }) - else - table.insert(curline, cv) - - if cv.on_activate then - active = active or {} - table.insert(active, cv) - end - - if cv.id then - idtab = idtab or {} - idtab[cv.id] = cv - end - end - end - end - end - obj.text_lines = out - obj.text_active = active - obj.text_ids = idtab -end - -local function is_disabled(token) - return (token.disabled ~= nil and getval(token.disabled)) or - (token.enabled ~= nil and not getval(token.enabled)) -end - --- Make the hover pen -- that is a pen that should render elements that has the --- mouse hovering over it. if hpen is specified, it just checks the fields and --- returns it (in parsed pen form) -local function make_hpen(pen, hpen) - if not hpen then - pen = to_pen(pen) - - -- Swap the foreground and background - hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) - end - - -- text_hpen needs a character in order to paint the background using - -- Painter:fill(), so let's make it paint a space to show the background - -- color - local hpen_parsed = to_pen(hpen) - hpen_parsed.ch = string.byte(' ') - return hpen_parsed -end - ----@param obj any ----@param dc gui.Painter ----@param x0 integer ----@param y0 integer ----@param pen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color ----@param dpen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color ----@param disabled boolean ----@param hpen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color ----@param hovered boolean -function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) - pen, dpen, hpen = getval(pen), getval(dpen), getval(hpen) - local width = 0 - for iline = dc and obj.start_line_num or 1, #obj.text_lines do - local x, line = 0, obj.text_lines[iline] - if dc then - local offset = (obj.start_line_num or 1) - 1 - local y = y0 + iline - offset - 1 - -- skip text outside of the containing frame - if y > dc.height - 1 then break end - dc:seek(x+x0, y) - end - for _,token in ipairs(line) do - token.line = iline - token.x1 = x - - if token.gap then - x = x + token.gap - if dc then - dc:advance(token.gap) - end - end - - if token.tile or (hovered and token.htile) then - x = x + 1 - if dc then - local tile = hovered and getval(token.htile or token.tile) or getval(token.tile) - local tile_pen = tonumber(tile) and to_pen{tile=tile} or tile - dc:char(nil, tile_pen) - if token.width then - dc:advance(token.width-1) - end - end - end - - if token.text or token.key then - local text = ''..(getval(token.text) or '') - local keypen = to_pen(token.key_pen or COLOR_LIGHTGREEN) - - if dc then - local tpen = getval(token.pen) - local dcpen = to_pen(tpen or pen) - - -- If disabled, figure out which dpen to use - if disabled or is_disabled(token) then - dcpen = to_pen(getval(token.dpen) or tpen or dpen) - if keypen.fg ~= COLOR_BLACK then - keypen.bold = false - end - - -- if hovered *and* disabled, combine both effects - if hovered then - dcpen = make_hpen(dcpen) - end - elseif hovered then - dcpen = make_hpen(dcpen, getval(token.hpen) or hpen) - end - - dc:pen(dcpen) - end - local width = getval(token.width) - local padstr - if width then - x = x + width - if #text > width then - text = string.sub(text,1,width) - else - if token.pad_char then - padstr = string.rep(token.pad_char,width-#text) - end - if dc and token.rjustify then - if padstr then dc:string(padstr) else dc:advance(width-#text) end - end - end - else - x = x + #text - end - - if token.key then - if type(token.key) == 'string' and not df.interface_key[token.key] then - error('Invalid interface_key: ' .. token.key) - end - local keystr = gui.getKeyDisplay(token.key) - local sep = token.key_sep or '' - - x = x + #keystr - - if sep:startswith('()') then - if dc then - dc:string(text) - dc:string(' ('):string(keystr,keypen) - dc:string(sep:sub(2)) - end - x = x + 1 + #sep - else - if dc then - dc:string(keystr,keypen):string(sep):string(text) - end - x = x + #sep - end - else - if dc then - dc:string(text) - end - end - - if width and dc and not token.rjustify then - if padstr then dc:string(padstr) else dc:advance(width-#text) end - end - end - - token.x2 = x - end - width = math.max(width, x) - end - obj.text_width = width -end - -function check_text_keys(self, keys) - if self.text_active then - for _,item in ipairs(self.text_active) do - if item.key and keys[item.key] and not is_disabled(item) then - item.on_activate() - return true - end - end - end -end - ----@class widgets.LabelToken ----@field text string|fun(): string ----@field gap? integer ----@field tile? integer|dfhack.pen ----@field htile? integer|dfhack.pen ----@field width? integer|fun(): integer ----@field pad_char? string ----@field key? string ----@field key_sep? string ----@field disabled? boolean|fun(): boolean ----@field enabled? boolean|fun(): boolean ----@field pen? dfhack.color|dfhack.pen ----@field dpen? dfhack.color|dfhack.pen ----@field hpen? dfhack.color|dfhack.pen ----@field on_activiate? fun() ----@field id? string ----@field line? any Internal use only ----@field x1? any Internal use only ----@field x2? any Internal use only - ----@class widgets.Label.attrs: widgets.Widget.attrs ----@field text? string|widgets.LabelToken[] ----@field text_pen dfhack.color|dfhack.pen ----@field text_dpen dfhack.color|dfhack.pen ----@field text_hpen? dfhack.color|dfhack.pen ----@field disabled? boolean|fun(): boolean ----@field enabled? boolean|fun(): boolean ----@field auto_height boolean ----@field auto_width boolean ----@field on_click? function ----@field on_rclick? function ----@field scroll_keys table - ----@class widgets.Label.attrs.partial: widgets.Label.attrs - ----@class widgets.Label: widgets.Widget, widgets.Label.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Label.attrs|fun(attributes: widgets.Label.attrs.partial) ----@overload fun(init_table: widgets.Label.attrs.partial): self -Label = defclass(Label, Widget) - -Label.ATTRS{ - text_pen = COLOR_WHITE, - text_dpen = COLOR_DARKGREY, -- disabled - text_hpen = DEFAULT_NIL, -- hover - default is to invert the fg/bg colors - disabled = DEFAULT_NIL, - enabled = DEFAULT_NIL, - auto_height = true, - auto_width = false, - on_click = DEFAULT_NIL, - on_rclick = DEFAULT_NIL, - scroll_keys = STANDARDSCROLL, -} - ----@param args widgets.Label.attrs.partial -function Label:init(args) - self.scrollbar = Scrollbar{ - frame={r=0}, - on_scroll=self:callback('on_scrollbar')} - - self:addviews{self.scrollbar} - - self:setText(args.text or self.text) -end - -local function update_label_scrollbar(label) - local body_height = label.frame_body and label.frame_body.height or 1 - label.scrollbar:update(label.start_line_num, body_height, - label:getTextHeight()) -end - -function Label:setText(text) - self.start_line_num = 1 - self.text = text - parse_label_text(self) - - if self.auto_height then - self.frame = self.frame or {} - self.frame.h = self:getTextHeight() - end - - update_label_scrollbar(self) -end - -function Label:preUpdateLayout() - if self.auto_width then - self.frame = self.frame or {} - self.frame.w = self:getTextWidth() - end -end - -function Label:postUpdateLayout() - update_label_scrollbar(self) -end - -function Label:itemById(id) - if self.text_ids then - return self.text_ids[id] - end -end - -function Label:getTextHeight() - return #self.text_lines -end - -function Label:getTextWidth() - render_text(self) - return self.text_width -end - --- Overridden by subclasses that also want to add new mouse handlers, see --- HotkeyLabel. -function Label:shouldHover() - return self.on_click or self.on_rclick -end - -function Label:onRenderBody(dc) - local text_pen = self.text_pen - local hovered = self:getMousePos() and self:shouldHover() - render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered) -end - -function Label:on_scrollbar(scroll_spec) - local v = 0 - if tonumber(scroll_spec) then - v = scroll_spec - self.start_line_num - elseif scroll_spec == 'down_large' then - v = '+halfpage' - elseif scroll_spec == 'up_large' then - v = '-halfpage' - elseif scroll_spec == 'down_small' then - v = 1 - elseif scroll_spec == 'up_small' then - v = -1 - end - - self:scroll(v) -end - -function Label:scroll(nlines) - if not nlines then return end - local text_height = math.max(1, self:getTextHeight()) - if type(nlines) == 'string' then - if nlines == '+page' then - nlines = self.frame_body.height - elseif nlines == '-page' then - nlines = -self.frame_body.height - elseif nlines == '+halfpage' then - nlines = math.ceil(self.frame_body.height/2) - elseif nlines == '-halfpage' then - nlines = -math.ceil(self.frame_body.height/2) - elseif nlines == 'home' then - nlines = 1 - self.start_line_num - elseif nlines == 'end' then - nlines = text_height - else - error(('unhandled scroll keyword: "%s"'):format(nlines)) - end - end - local n = self.start_line_num + nlines - n = math.min(n, text_height - self.frame_body.height + 1) - n = math.max(n, 1) - nlines = n - self.start_line_num - self.start_line_num = n - update_label_scrollbar(self) - return nlines -end - -function Label:onInput(keys) - if is_disabled(self) then return false end - if self:inputToSubviews(keys) then - return true - end - if keys._MOUSE_L and self:getMousePos() and self.on_click then - self.on_click() - return true - end - if keys._MOUSE_R and self:getMousePos() and self.on_rclick then - self.on_rclick() - return true - end - for k,v in pairs(self.scroll_keys) do - if keys[k] and 0 ~= self:scroll(v) then - return true - end - end - return check_text_keys(self, keys) -end - --------------------------------- --- makeButtonLabelText --- - -local function get_button_token_hover_ch(spec, x, y, ch) - local ch_hover = ch - if spec.chars_hover then - local row = spec.chars_hover[y] - if type(row) == 'string' then - ch_hover = row:sub(x, x) - else - ch_hover = row[x] - end - end - return ch_hover -end - -local function get_button_token_base_pens(spec, x, y) - local pen, pen_hover = COLOR_GRAY, COLOR_WHITE - if spec.pens then - pen = type(spec.pens) == 'table' and (safe_index(spec.pens, y, x) or spec.pens[y]) or spec.pens - if spec.pens_hover then - pen_hover = type(spec.pens_hover) == 'table' and (safe_index(spec.pens_hover, y, x) or spec.pens_hover[y]) or spec.pens_hover - else - pen_hover = pen - end - end - return pen, pen_hover -end - -local function get_button_tileset_idx(spec, x, y, tileset_offset, tileset_stride) - local idx = (tileset_offset or 1) - idx = idx + (x - 1) - idx = idx + (y - 1) * (tileset_stride or #spec.chars[1]) - return idx -end - -local function get_asset_tile(asset, x, y) - return dfhack.screen.findGraphicsTile(asset.page, asset.x+x-1, asset.y+y-1) -end - -local function get_button_token_tiles(spec, x, y) - local tile = safe_index(spec.tiles_override, y, x) - local tile_hover = safe_index(spec.tiles_hover_override, y, x) or tile - if not tile and spec.tileset then - local tileset = spec.tileset - local idx = get_button_tileset_idx(spec, x, y, spec.tileset_offset, spec.tileset_stride) - tile = dfhack.textures.getTexposByHandle(tileset[idx]) - if spec.tileset_hover then - local tileset_hover = spec.tileset_hover - local idx_hover = get_button_tileset_idx(spec, x, y, - spec.tileset_hover_offset or spec.tileset_offset, - spec.tileset_hover_stride or spec.tileset_stride) - tile_hover = dfhack.textures.getTexposByHandle(tileset_hover[idx_hover]) - else - tile_hover = tile - end - end - if not tile and spec.asset then - tile = get_asset_tile(spec.asset, x, y) - if spec.asset_hover then - tile_hover = get_asset_tile(spec.asset_hover, x, y) - else - tile_hover = tile - end - end - return tile, tile_hover -end - -local function get_button_token_pen(base_pen, tile, ch) - local pen = dfhack.pen.make(base_pen) - pen.tile = tile - pen.ch = ch - return pen -end - -local function get_button_token_pens(spec, x, y, ch, ch_hover) - local base_pen, base_pen_hover = get_button_token_base_pens(spec, x, y) - local tile, tile_hover = get_button_token_tiles(spec, x, y) - return get_button_token_pen(base_pen, tile, ch), get_button_token_pen(base_pen_hover, tile_hover, ch_hover) -end - -local function make_button_token(spec, x, y, ch) - local ch_hover = get_button_token_hover_ch(spec, x, y, ch) - local pen, pen_hover = get_button_token_pens(spec, x, y, ch, ch_hover) - return { - tile=pen, - htile=pen_hover, - width=1, - } -end - ----@class widgets.ButtonLabelSpec ----@field chars (string|string[])[] ----@field chars_hover? (string|string[])[] ----@field pens? dfhack.color|dfhack.color[][] ----@field pens_hover? dfhack.color|dfhack.color[][] ----@field tiles_override? integer[][] ----@field tiles_hover_override? integer[][] ----@field tileset? TexposHandle[] ----@field tileset_hover? TexposHandle[] ----@field tileset_offset? integer ----@field tileset_hover_offset? integer ----@field tileset_stride? integer ----@field tileset_hover_stride? integer ----@field asset? {page: string, x: integer, y: integer} ----@field asset_hover? {page: string, x: integer, y: integer} - ----@nodiscard ----@param spec widgets.ButtonLabelSpec ----@return widgets.LabelToken[] -function makeButtonLabelText(spec) - local tokens = {} - for y, row in ipairs(spec.chars) do - if type(row) == 'string' then - local x = 1 - for ch in row:gmatch('.') do - table.insert(tokens, make_button_token(spec, x, y, ch)) - x = x + 1 - end - else - for x, ch in ipairs(row) do - table.insert(tokens, make_button_token(spec, x, y, ch)) - end - end - if y < #spec.chars then - table.insert(tokens, NEWLINE) - end - end - return tokens -end - ------------------ -- WrappedLabel -- ------------------ @@ -1235,65 +110,6 @@ function TooltipLabel:init() self.visible = self.show_tooltip end ------------------ --- HotkeyLabel -- ------------------ - ----@class widgets.HotkeyLabel.attrs: widgets.Label.attrs ----@field key? string ----@field key_sep string ----@field label? string|fun(): string ----@field on_activate? function - ----@class widgets.HotkeyLabel.attrs.partial: widgets.HotkeyLabel.attrs - ----@class widgets.HotkeyLabel: widgets.Label, widgets.HotkeyLabel.attrs ----@field super widgets.Label ----@field ATTRS widgets.HotkeyLabel.attrs|fun(attributes: widgets.HotkeyLabel.attrs.partial) ----@overload fun(init_table: widgets.HotkeyLabel.attrs.partial): self -HotkeyLabel = defclass(HotkeyLabel, Label) - -HotkeyLabel.ATTRS{ - key=DEFAULT_NIL, - key_sep=': ', - label=DEFAULT_NIL, - on_activate=DEFAULT_NIL, -} - -function HotkeyLabel:init() - self:initializeLabel() -end - -function HotkeyLabel:setOnActivate(on_activate) - self.on_activate = on_activate - self:initializeLabel() -end - -function HotkeyLabel:setLabel(label) - self.label = label - self:initializeLabel() -end - -function HotkeyLabel:shouldHover() - -- When on_activate is set, text should also hover on mouseover - return self.on_activate or HotkeyLabel.super.shouldHover(self) -end - -function HotkeyLabel:initializeLabel() - self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, - on_activate=self.on_activate}} -end - -function HotkeyLabel:onInput(keys) - if HotkeyLabel.super.onInput(self, keys) then - return true - elseif keys._MOUSE_L and self:getMousePos() and self.on_activate - and not is_disabled(self) then - self.on_activate() - return true - end -end - ---------------- -- HelpButton -- ---------------- diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua new file mode 100644 index 0000000000..1d291a8be1 --- /dev/null +++ b/library/lua/gui/widgets/edit_field.lua @@ -0,0 +1,229 @@ +local gui = require('gui') +local utils = require('utils') +local Widget = require('gui.widgets.widget') +local HotkeyLabel = require('gui.widgets.hotkey_label') + +local getval = utils.getval + +---------------- +-- Edit field -- +---------------- + +---@class widgets.EditField.attrs: widgets.Widget.attrs +---@field label_text? string +---@field text string +---@field text_pen? dfhack.color|dfhack.pen +---@field on_char? function +---@field on_change? function +---@field on_submit? function +---@field on_submit2? function +---@field key? string +---@field key_sep? string +---@field modal boolean +---@field ignore_keys? string[] + +---@class widgets.EditField.attrs.partial: widgets.EditField.attrs + +---@class widgets.EditField: widgets.Widget, widgets.EditField.attrs +---@field super widgets.Widget +---@field ATTRS widgets.EditField.attrs|fun(attributes: widgets.EditField.attrs.partial) +---@overload fun(init_table: widgets.EditField.attrs.partial): self +EditField = defclass(EditField, Widget) + +EditField.ATTRS{ + label_text = DEFAULT_NIL, + text = '', + text_pen = DEFAULT_NIL, + on_char = DEFAULT_NIL, + on_change = DEFAULT_NIL, + on_submit = DEFAULT_NIL, + on_submit2 = DEFAULT_NIL, + key = DEFAULT_NIL, + key_sep = DEFAULT_NIL, + modal = false, + ignore_keys = DEFAULT_NIL, +} + +---@param init_table widgets.EditField.attrs +function EditField:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function EditField:init() + local function on_activate() + self.saved_text = self.text + self:setFocus(true) + end + + self.start_pos = 1 + self.cursor = #self.text + 1 + + self:addviews{HotkeyLabel{frame={t=0,l=0}, + key=self.key, + key_sep=self.key_sep, + label=self.label_text, + on_activate=self.key and on_activate or nil}} +end + +function EditField:getPreferredFocusState() + return not self.key +end + +function EditField:setCursor(cursor) + if not cursor or cursor > #self.text then + self.cursor = #self.text + 1 + return + end + self.cursor = math.max(1, cursor) +end + +function EditField:setText(text, cursor) + local old = self.text + self.text = text + self:setCursor(cursor) + if self.on_change and text ~= old then + self.on_change(self.text, old) + end +end + +function EditField:postUpdateLayout() + self.text_offset = self.subviews[1]:getTextWidth() +end + +---@param dc gui.Painter +function EditField:onRenderBody(dc) + dc:pen(self.text_pen or COLOR_LIGHTCYAN) + + local cursor_char = '_' + if not getval(self.active) or not self.focus or gui.blink_visible(300) then + cursor_char = (self.cursor > #self.text) and ' ' or + self.text:sub(self.cursor, self.cursor) + end + local txt = self.text:sub(1, self.cursor - 1) .. cursor_char .. + self.text:sub(self.cursor + 1) + local max_width = dc.width - self.text_offset + self.start_pos = 1 + if #txt > max_width then + -- get the substring in the vicinity of the cursor + max_width = max_width - 2 + local half_width = math.floor(max_width/2) + local start_pos = math.max(1, self.cursor-half_width) + local end_pos = math.min(#txt, self.cursor+half_width-1) + if self.cursor + half_width > #txt then + start_pos = #txt - (max_width - 1) + end + if self.cursor - half_width <= 1 then + end_pos = max_width + 1 + end + self.start_pos = start_pos > 1 and start_pos - 1 or start_pos + txt = ('%s%s%s'):format(start_pos == 1 and '' or string.char(27), + txt:sub(start_pos, end_pos), + end_pos == #txt and '' or string.char(26)) + end + dc:advance(self.text_offset):string(txt) +end + +function EditField:insert(text) + local old = self.text + self:setText(old:sub(1,self.cursor-1)..text..old:sub(self.cursor), + self.cursor + #text) +end + +function EditField:onInput(keys) + if not self.focus then + -- only react to our hotkey + return self:inputToSubviews(keys) + end + + if self.ignore_keys then + for _,ignore_key in ipairs(self.ignore_keys) do + if keys[ignore_key] then return false end + end + end + + if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then + self:setText(self.saved_text) + self:setFocus(false) + return true + end + + if keys.SELECT or keys.SELECT_ALL then + if self.key then + self:setFocus(false) + end + if keys.SELECT_ALL then + if self.on_submit2 then + self.on_submit2(self.text) + return true + end + else + if self.on_submit then + self.on_submit(self.text) + return true + end + end + return not not self.key + elseif keys._STRING then + local old = self.text + if keys._STRING == 0 then + -- handle backspace + local del_pos = self.cursor - 1 + if del_pos > 0 then + self:setText(old:sub(1, del_pos-1) .. old:sub(del_pos+1), + del_pos) + end + else + local cv = string.char(keys._STRING) + if not self.on_char or self.on_char(cv, old) then + self:insert(cv) + elseif self.on_char then + return self.modal + end + end + return true + elseif keys.KEYBOARD_CURSOR_LEFT then + self:setCursor(self.cursor - 1) + return true + elseif keys.CUSTOM_CTRL_B then -- back one word + local _, prev_word_end = self.text:sub(1, self.cursor-1): + find('.*[%w_%-][^%w_%-]') + self:setCursor(prev_word_end or 1) + return true + -- commented out until we get HOME key support from DF + -- elseif keys.CUSTOM_CTRL_A then -- home + -- self:setCursor(1) + -- return true + elseif keys.KEYBOARD_CURSOR_RIGHT then + self:setCursor(self.cursor + 1) + return true + elseif keys.CUSTOM_CTRL_F then -- forward one word + local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor) + self:setCursor(next_word_start) + return true + elseif keys.CUSTOM_CTRL_E then -- end + self:setCursor() + return true + elseif keys.CUSTOM_CTRL_C then + dfhack.internal.setClipboardTextCp437(self.text) + return true + elseif keys.CUSTOM_CTRL_X then + dfhack.internal.setClipboardTextCp437(self.text) + self:setText('') + return true + elseif keys.CUSTOM_CTRL_V then + self:insert(dfhack.internal.getClipboardTextCp437()) + return true + elseif keys._MOUSE_L_DOWN then + local mouse_x = self:getMousePos() + if mouse_x then + self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0)) + return true + end + end + + -- if we're modal, then unconditionally eat all the input + return self.modal +end + +return EditField diff --git a/library/lua/gui/widgets/hotkey_label.lua b/library/lua/gui/widgets/hotkey_label.lua new file mode 100644 index 0000000000..494b661035 --- /dev/null +++ b/library/lua/gui/widgets/hotkey_label.lua @@ -0,0 +1,62 @@ +local Label = require('gui.widgets.label') + +----------------- +-- HotkeyLabel -- +----------------- + +---@class widgets.HotkeyLabel.attrs: widgets.Label.attrs +---@field key? string +---@field key_sep string +---@field label? string|fun(): string +---@field on_activate? function + +---@class widgets.HotkeyLabel.attrs.partial: widgets.HotkeyLabel.attrs + +---@class widgets.HotkeyLabel: widgets.Label, widgets.HotkeyLabel.attrs +---@field super widgets.Label +---@field ATTRS widgets.HotkeyLabel.attrs|fun(attributes: widgets.HotkeyLabel.attrs.partial) +---@overload fun(init_table: widgets.HotkeyLabel.attrs.partial): self +HotkeyLabel = defclass(HotkeyLabel, Label) + +HotkeyLabel.ATTRS{ + key=DEFAULT_NIL, + key_sep=': ', + label=DEFAULT_NIL, + on_activate=DEFAULT_NIL, +} + +function HotkeyLabel:init() + self:initializeLabel() +end + +function HotkeyLabel:setOnActivate(on_activate) + self.on_activate = on_activate + self:initializeLabel() +end + +function HotkeyLabel:setLabel(label) + self.label = label + self:initializeLabel() +end + +function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return self.on_activate or HotkeyLabel.super.shouldHover(self) +end + +function HotkeyLabel:initializeLabel() + self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, + on_activate=self.on_activate}} +end + +function HotkeyLabel:onInput(keys) + if HotkeyLabel.super.onInput(self, keys) then + return true + elseif keys._MOUSE_L and self:getMousePos() and self.on_activate + and not is_disabled(self) then + self.on_activate() + return true + end +end + +return HotkeyLabel diff --git a/library/lua/gui/widgets/label.lua b/library/lua/gui/widgets/label.lua new file mode 100644 index 0000000000..07f178dcef --- /dev/null +++ b/library/lua/gui/widgets/label.lua @@ -0,0 +1,553 @@ +local gui = require('gui') +local utils = require('utils') +local Widget = require('gui.widgets.widget') +local Scrollbar = require('gui.widgets.scrollbar') + +local getval = utils.getval +local to_pen = dfhack.pen.parse + +----------- +-- Label -- +----------- + +function parse_label_text(obj) + local text = obj.text or {} + if type(text) ~= 'table' then + text = { text } + end + local curline = nil + local out = { } + local active = nil + local idtab = nil + for _,v in ipairs(text) do + local vv + if type(v) == 'string' then + vv = v:split(NEWLINE) + else + vv = { v } + end + + for i = 1,#vv do + local cv = vv[i] + if i > 1 then + if not curline then + table.insert(out, {}) + end + curline = nil + end + if cv ~= '' then + if not curline then + curline = {} + table.insert(out, curline) + end + + if type(cv) == 'string' then + table.insert(curline, { text = cv }) + else + table.insert(curline, cv) + + if cv.on_activate then + active = active or {} + table.insert(active, cv) + end + + if cv.id then + idtab = idtab or {} + idtab[cv.id] = cv + end + end + end + end + end + obj.text_lines = out + obj.text_active = active + obj.text_ids = idtab +end + +local function is_disabled(token) + return (token.disabled ~= nil and getval(token.disabled)) or + (token.enabled ~= nil and not getval(token.enabled)) +end + +-- Make the hover pen -- that is a pen that should render elements that has the +-- mouse hovering over it. if hpen is specified, it just checks the fields and +-- returns it (in parsed pen form) +local function make_hpen(pen, hpen) + if not hpen then + pen = to_pen(pen) + + -- Swap the foreground and background + hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = to_pen(hpen) + hpen_parsed.ch = string.byte(' ') + return hpen_parsed +end + +---@param obj any +---@param dc gui.Painter +---@param x0 integer +---@param y0 integer +---@param pen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color +---@param dpen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color +---@param disabled boolean +---@param hpen dfhack.pen|dfhack.color|fun(): dfhack.pen|dfhack.color +---@param hovered boolean +function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) + pen, dpen, hpen = getval(pen), getval(dpen), getval(hpen) + local width = 0 + for iline = dc and obj.start_line_num or 1, #obj.text_lines do + local x, line = 0, obj.text_lines[iline] + if dc then + local offset = (obj.start_line_num or 1) - 1 + local y = y0 + iline - offset - 1 + -- skip text outside of the containing frame + if y > dc.height - 1 then break end + dc:seek(x+x0, y) + end + for _,token in ipairs(line) do + token.line = iline + token.x1 = x + + if token.gap then + x = x + token.gap + if dc then + dc:advance(token.gap) + end + end + + if token.tile or (hovered and token.htile) then + x = x + 1 + if dc then + local tile = hovered and getval(token.htile or token.tile) or getval(token.tile) + local tile_pen = tonumber(tile) and to_pen{tile=tile} or tile + dc:char(nil, tile_pen) + if token.width then + dc:advance(token.width-1) + end + end + end + + if token.text or token.key then + local text = ''..(getval(token.text) or '') + local keypen = to_pen(token.key_pen or COLOR_LIGHTGREEN) + + if dc then + local tpen = getval(token.pen) + local dcpen = to_pen(tpen or pen) + + -- If disabled, figure out which dpen to use + if disabled or is_disabled(token) then + dcpen = to_pen(getval(token.dpen) or tpen or dpen) + if keypen.fg ~= COLOR_BLACK then + keypen.bold = false + end + + -- if hovered *and* disabled, combine both effects + if hovered then + dcpen = make_hpen(dcpen) + end + elseif hovered then + dcpen = make_hpen(dcpen, getval(token.hpen) or hpen) + end + + dc:pen(dcpen) + end + local width = getval(token.width) + local padstr + if width then + x = x + width + if #text > width then + text = string.sub(text,1,width) + else + if token.pad_char then + padstr = string.rep(token.pad_char,width-#text) + end + if dc and token.rjustify then + if padstr then dc:string(padstr) else dc:advance(width-#text) end + end + end + else + x = x + #text + end + + if token.key then + if type(token.key) == 'string' and not df.interface_key[token.key] then + error('Invalid interface_key: ' .. token.key) + end + local keystr = gui.getKeyDisplay(token.key) + local sep = token.key_sep or '' + + x = x + #keystr + + if sep:startswith('()') then + if dc then + dc:string(text) + dc:string(' ('):string(keystr,keypen) + dc:string(sep:sub(2)) + end + x = x + 1 + #sep + else + if dc then + dc:string(keystr,keypen):string(sep):string(text) + end + x = x + #sep + end + else + if dc then + dc:string(text) + end + end + + if width and dc and not token.rjustify then + if padstr then dc:string(padstr) else dc:advance(width-#text) end + end + end + + token.x2 = x + end + width = math.max(width, x) + end + obj.text_width = width +end + +function check_text_keys(self, keys) + if self.text_active then + for _,item in ipairs(self.text_active) do + if item.key and keys[item.key] and not is_disabled(item) then + item.on_activate() + return true + end + end + end +end + +---@class widgets.LabelToken +---@field text string|fun(): string +---@field gap? integer +---@field tile? integer|dfhack.pen +---@field htile? integer|dfhack.pen +---@field width? integer|fun(): integer +---@field pad_char? string +---@field key? string +---@field key_sep? string +---@field disabled? boolean|fun(): boolean +---@field enabled? boolean|fun(): boolean +---@field pen? dfhack.color|dfhack.pen +---@field dpen? dfhack.color|dfhack.pen +---@field hpen? dfhack.color|dfhack.pen +---@field on_activiate? fun() +---@field id? string +---@field line? any Internal use only +---@field x1? any Internal use only +---@field x2? any Internal use only + +---@class widgets.Label.attrs: widgets.Widget.attrs +---@field text? string|widgets.LabelToken[] +---@field text_pen dfhack.color|dfhack.pen +---@field text_dpen dfhack.color|dfhack.pen +---@field text_hpen? dfhack.color|dfhack.pen +---@field disabled? boolean|fun(): boolean +---@field enabled? boolean|fun(): boolean +---@field auto_height boolean +---@field auto_width boolean +---@field on_click? function +---@field on_rclick? function +---@field scroll_keys table + +---@class widgets.Label.attrs.partial: widgets.Label.attrs + +---@class widgets.Label: widgets.Widget, widgets.Label.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Label.attrs|fun(attributes: widgets.Label.attrs.partial) +---@overload fun(init_table: widgets.Label.attrs.partial): self +Label = defclass(Label, Widget) + +Label.ATTRS{ + text_pen = COLOR_WHITE, + text_dpen = COLOR_DARKGREY, -- disabled + text_hpen = DEFAULT_NIL, -- hover - default is to invert the fg/bg colors + disabled = DEFAULT_NIL, + enabled = DEFAULT_NIL, + auto_height = true, + auto_width = false, + on_click = DEFAULT_NIL, + on_rclick = DEFAULT_NIL, + scroll_keys = STANDARDSCROLL, +} + +---@param args widgets.Label.attrs.partial +function Label:init(args) + self.scrollbar = Scrollbar{ + frame={r=0}, + on_scroll=self:callback('on_scrollbar')} + + self:addviews{self.scrollbar} + + self:setText(args.text or self.text) +end + +local function update_label_scrollbar(label) + local body_height = label.frame_body and label.frame_body.height or 1 + label.scrollbar:update(label.start_line_num, body_height, + label:getTextHeight()) +end + +function Label:setText(text) + self.start_line_num = 1 + self.text = text + parse_label_text(self) + + if self.auto_height then + self.frame = self.frame or {} + self.frame.h = self:getTextHeight() + end + + update_label_scrollbar(self) +end + +function Label:preUpdateLayout() + if self.auto_width then + self.frame = self.frame or {} + self.frame.w = self:getTextWidth() + end +end + +function Label:postUpdateLayout() + update_label_scrollbar(self) +end + +function Label:itemById(id) + if self.text_ids then + return self.text_ids[id] + end +end + +function Label:getTextHeight() + return #self.text_lines +end + +function Label:getTextWidth() + render_text(self) + return self.text_width +end + +-- Overridden by subclasses that also want to add new mouse handlers, see +-- HotkeyLabel. +function Label:shouldHover() + return self.on_click or self.on_rclick +end + +function Label:onRenderBody(dc) + local text_pen = self.text_pen + local hovered = self:getMousePos() and self:shouldHover() + render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered) +end + +function Label:on_scrollbar(scroll_spec) + local v = 0 + if tonumber(scroll_spec) then + v = scroll_spec - self.start_line_num + elseif scroll_spec == 'down_large' then + v = '+halfpage' + elseif scroll_spec == 'up_large' then + v = '-halfpage' + elseif scroll_spec == 'down_small' then + v = 1 + elseif scroll_spec == 'up_small' then + v = -1 + end + + self:scroll(v) +end + +function Label:scroll(nlines) + if not nlines then return end + local text_height = math.max(1, self:getTextHeight()) + if type(nlines) == 'string' then + if nlines == '+page' then + nlines = self.frame_body.height + elseif nlines == '-page' then + nlines = -self.frame_body.height + elseif nlines == '+halfpage' then + nlines = math.ceil(self.frame_body.height/2) + elseif nlines == '-halfpage' then + nlines = -math.ceil(self.frame_body.height/2) + elseif nlines == 'home' then + nlines = 1 - self.start_line_num + elseif nlines == 'end' then + nlines = text_height + else + error(('unhandled scroll keyword: "%s"'):format(nlines)) + end + end + local n = self.start_line_num + nlines + n = math.min(n, text_height - self.frame_body.height + 1) + n = math.max(n, 1) + nlines = n - self.start_line_num + self.start_line_num = n + update_label_scrollbar(self) + return nlines +end + +function Label:onInput(keys) + if is_disabled(self) then return false end + if self:inputToSubviews(keys) then + return true + end + if keys._MOUSE_L and self:getMousePos() and self.on_click then + self.on_click() + return true + end + if keys._MOUSE_R and self:getMousePos() and self.on_rclick then + self.on_rclick() + return true + end + for k,v in pairs(self.scroll_keys) do + if keys[k] and 0 ~= self:scroll(v) then + return true + end + end + return check_text_keys(self, keys) +end + +-------------------------------- +-- makeButtonLabelText +-- + +local function get_button_token_hover_ch(spec, x, y, ch) + local ch_hover = ch + if spec.chars_hover then + local row = spec.chars_hover[y] + if type(row) == 'string' then + ch_hover = row:sub(x, x) + else + ch_hover = row[x] + end + end + return ch_hover +end + +local function get_button_token_base_pens(spec, x, y) + local pen, pen_hover = COLOR_GRAY, COLOR_WHITE + if spec.pens then + pen = type(spec.pens) == 'table' and (safe_index(spec.pens, y, x) or spec.pens[y]) or spec.pens + if spec.pens_hover then + pen_hover = type(spec.pens_hover) == 'table' and (safe_index(spec.pens_hover, y, x) or spec.pens_hover[y]) or spec.pens_hover + else + pen_hover = pen + end + end + return pen, pen_hover +end + +local function get_button_tileset_idx(spec, x, y, tileset_offset, tileset_stride) + local idx = (tileset_offset or 1) + idx = idx + (x - 1) + idx = idx + (y - 1) * (tileset_stride or #spec.chars[1]) + return idx +end + +local function get_asset_tile(asset, x, y) + return dfhack.screen.findGraphicsTile(asset.page, asset.x+x-1, asset.y+y-1) +end + +local function get_button_token_tiles(spec, x, y) + local tile = safe_index(spec.tiles_override, y, x) + local tile_hover = safe_index(spec.tiles_hover_override, y, x) or tile + if not tile and spec.tileset then + local tileset = spec.tileset + local idx = get_button_tileset_idx(spec, x, y, spec.tileset_offset, spec.tileset_stride) + tile = dfhack.textures.getTexposByHandle(tileset[idx]) + if spec.tileset_hover then + local tileset_hover = spec.tileset_hover + local idx_hover = get_button_tileset_idx(spec, x, y, + spec.tileset_hover_offset or spec.tileset_offset, + spec.tileset_hover_stride or spec.tileset_stride) + tile_hover = dfhack.textures.getTexposByHandle(tileset_hover[idx_hover]) + else + tile_hover = tile + end + end + if not tile and spec.asset then + tile = get_asset_tile(spec.asset, x, y) + if spec.asset_hover then + tile_hover = get_asset_tile(spec.asset_hover, x, y) + else + tile_hover = tile + end + end + return tile, tile_hover +end + +local function get_button_token_pen(base_pen, tile, ch) + local pen = dfhack.pen.make(base_pen) + pen.tile = tile + pen.ch = ch + return pen +end + +local function get_button_token_pens(spec, x, y, ch, ch_hover) + local base_pen, base_pen_hover = get_button_token_base_pens(spec, x, y) + local tile, tile_hover = get_button_token_tiles(spec, x, y) + return get_button_token_pen(base_pen, tile, ch), get_button_token_pen(base_pen_hover, tile_hover, ch_hover) +end + +local function make_button_token(spec, x, y, ch) + local ch_hover = get_button_token_hover_ch(spec, x, y, ch) + local pen, pen_hover = get_button_token_pens(spec, x, y, ch, ch_hover) + return { + tile=pen, + htile=pen_hover, + width=1, + } +end + +---@class widgets.ButtonLabelSpec +---@field chars (string|string[])[] +---@field chars_hover? (string|string[])[] +---@field pens? dfhack.color|dfhack.color[][] +---@field pens_hover? dfhack.color|dfhack.color[][] +---@field tiles_override? integer[][] +---@field tiles_hover_override? integer[][] +---@field tileset? TexposHandle[] +---@field tileset_hover? TexposHandle[] +---@field tileset_offset? integer +---@field tileset_hover_offset? integer +---@field tileset_stride? integer +---@field tileset_hover_stride? integer +---@field asset? {page: string, x: integer, y: integer} +---@field asset_hover? {page: string, x: integer, y: integer} + +---@nodiscard +---@param spec widgets.ButtonLabelSpec +---@return widgets.LabelToken[] +function makeButtonLabelText(spec) + local tokens = {} + for y, row in ipairs(spec.chars) do + if type(row) == 'string' then + local x = 1 + for ch in row:gmatch('.') do + table.insert(tokens, make_button_token(spec, x, y, ch)) + x = x + 1 + end + else + for x, ch in ipairs(row) do + table.insert(tokens, make_button_token(spec, x, y, ch)) + end + end + if y < #spec.chars then + table.insert(tokens, NEWLINE) + end + end + return tokens +end + +Label.makeButtonLabelText = makeButtonLabelText +Label.parse_label_text = parse_label_text + +return Label diff --git a/library/lua/gui/widgets/pages.lua b/library/lua/gui/widgets/pages.lua new file mode 100644 index 0000000000..70f0d29e7b --- /dev/null +++ b/library/lua/gui/widgets/pages.lua @@ -0,0 +1,63 @@ +local gui = require('gui') +local utils = require('utils') +local Panel = require('gui.widgets.panel') + +---@param view gui.View +---@param vis boolean +local function show_view(view,vis) + if view then + view.visible = vis + end +end + +----------- +-- Pages -- +----------- + +---@class widgets.Pages.initTable: widgets.Panel.attrs +---@field selected? integer|string + +---@class widgets.Pages: widgets.Panel +---@field super widgets.Panel +---@overload fun(attributes: widgets.Pages.initTable): self +Pages = defclass(Pages, Panel) + +---@param self widgets.Pages +---@param args widgets.Pages.initTable +function Pages:init(args) + for _,v in ipairs(self.subviews) do + v.visible = false + end + self:setSelected(args.selected or 1) +end + +---@param idx integer|string +function Pages:setSelected(idx) + if type(idx) ~= 'number' then + local key = idx + if type(idx) == 'string' then + key = self.subviews[key] + end + idx = utils.linear_index(self.subviews, key) + if not idx then + error('Unknown page: '..tostring(key)) + end + end + + show_view(self.subviews[self.selected], false) + self.selected = math.min(math.max(1, idx), #self.subviews) + show_view(self.subviews[self.selected], true) +end + +---@return integer index +---@return gui.View child +function Pages:getSelected() + return self.selected, self.subviews[self.selected] +end + +---@return gui.View child +function Pages:getSelectedPage() + return self.subviews[self.selected] +end + +return Pages diff --git a/library/lua/gui/widgets/resizing_panel.lua b/library/lua/gui/widgets/resizing_panel.lua new file mode 100644 index 0000000000..5791a8518f --- /dev/null +++ b/library/lua/gui/widgets/resizing_panel.lua @@ -0,0 +1,58 @@ +local gui = require('gui') +local utils = require('utils') +local Panel = require('gui.widgets.panel') + +local getval = utils.getval + +------------------- +-- ResizingPanel -- +------------------- + +---@class widgets.ResizingPanel.attrs: widgets.Panel.attrs +---@field auto_height boolean +---@field auto_width boolean + +---@class widgets.ResizingPanel.attrs.partial: widgets.ResizingPanel.attrs + +---@class widgets.ResizingPanel: widgets.Panel, widgets.ResizingPanel.attrs +---@field super widgets.Panel +---@field ATTRS widgets.ResizingPanel.attrs|fun(attributes: widgets.ResizingPanel.attrs.partial) +---@overload fun(init_table: widgets.ResizingPanel.attrs.partial): self +ResizingPanel = defclass(ResizingPanel, Panel) + +ResizingPanel.ATTRS{ + auto_height = true, + auto_width = false, +} + +-- adjust our frame dimensions according to positions and sizes of our subviews +function ResizingPanel:postUpdateLayout(frame_body) + local w, h = 0, 0 + for _,s in ipairs(self.subviews) do + if getval(s.visible) then + w = math.max(w, (s.frame and s.frame.l or 0) + + (s.frame and s.frame.w or frame_body.width)) + h = math.max(h, (s.frame and s.frame.t or 0) + + (s.frame and s.frame.h or frame_body.height)) + end + end + local l,t,r,b = gui.parse_inset(self.frame_inset) + w = w + l + r + h = h + t + b + if self.frame_style then + w = w + 2 + h = h + 2 + end + if not self.frame then self.frame = {} end + local oldw, oldh = self.frame.w, self.frame.h + if not self.auto_height then h = oldh end + if not self.auto_width then w = oldw end + self.frame.w, self.frame.h = w, h + if not self._updateLayoutGuard and (oldw ~= w or oldh ~= h) then + self._updateLayoutGuard = true -- protect against infinite loops + self:updateLayout() -- our frame has changed, we need to fully refresh + end + self._updateLayoutGuard = nil +end + +return ResizingPanel diff --git a/library/lua/gui/widgets/scrollbar.lua b/library/lua/gui/widgets/scrollbar.lua new file mode 100644 index 0000000000..3307cfd36e --- /dev/null +++ b/library/lua/gui/widgets/scrollbar.lua @@ -0,0 +1,270 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +---@enum STANDARDSCROLL +STANDARDSCROLL = { + STANDARDSCROLL_UP = -1, + KEYBOARD_CURSOR_UP = -1, + STANDARDSCROLL_DOWN = 1, + KEYBOARD_CURSOR_DOWN = 1, + STANDARDSCROLL_PAGEUP = '-page', + KEYBOARD_CURSOR_UP_FAST = '-page', + STANDARDSCROLL_PAGEDOWN = '+page', + KEYBOARD_CURSOR_DOWN_FAST = '+page', +} + +--------------- +-- Scrollbar -- +--------------- + +SCROLL_INITIAL_DELAY_MS = 300 +SCROLL_DELAY_MS = 20 + +---@class widgets.Scrollbar.attrs: widgets.Widget.attrs +---@field on_scroll? fun(new_top_elem?: integer) + +---@class widgets.Scrollbar.attrs.partial: widgets.Scrollbar.attrs + +---@class widgets.Scrollbar: widgets.Widget, widgets.Scrollbar.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Scrollbar.attrs|fun(attributes: widgets.Scrollbar.attrs.partial) +---@overload fun(init_table: widgets.Scrollbar.attrs.partial): self +Scrollbar = defclass(Scrollbar, Widget) + +Scrollbar.STANDARDSCROLL = STANDARDSCROLL + +Scrollbar.ATTRS{ + on_scroll = DEFAULT_NIL, +} + +---@param init_table widgets.Scrollbar.attrs.partial +function Scrollbar:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.w = init_table.frame.w or 2 +end + +function Scrollbar:init() + self.last_scroll_ms = 0 + self.is_first_click = false + self.scroll_spec = nil + self.is_dragging = false -- index of the scrollbar tile that we're dragging + self:update(1, 1, 1) +end + +local function scrollbar_get_max_pos_and_height(scrollbar) + local frame_body = scrollbar.frame_body + local scrollbar_body_height = (frame_body and frame_body.height or 3) - 2 + + local height = math.max(2, math.floor( + (math.min(scrollbar.elems_per_page, scrollbar.num_elems) * scrollbar_body_height) / + scrollbar.num_elems)) + + return scrollbar_body_height - height, height +end + +-- calculate and cache the number of tiles of empty space above the top of the +-- scrollbar and the number of tiles the scrollbar should occupy to represent +-- the percentage of text that is on the screen. +-- if elems_per_page or num_elems are not specified, the last values passed to +-- Scrollbar:update() are used. +function Scrollbar:update(top_elem, elems_per_page, num_elems) + if not top_elem then error('must specify index of new top element') end + elems_per_page = elems_per_page or self.elems_per_page + num_elems = num_elems or self.num_elems + self.top_elem = top_elem + self.elems_per_page, self.num_elems = elems_per_page, num_elems + + local max_pos, height = scrollbar_get_max_pos_and_height(self) + local pos = (num_elems == elems_per_page) and 0 or + math.ceil(((top_elem-1) * max_pos) / + (num_elems - elems_per_page)) + + self.bar_offset, self.bar_height = pos, height +end + +local function scrollbar_do_drag(scrollbar) + local x,y = dfhack.screen.getMousePos() + if not y then return end + x,y = scrollbar.frame_body:localXY(x, y) + local cur_pos = y - scrollbar.is_dragging + local max_top = scrollbar.num_elems - scrollbar.elems_per_page + 1 + local max_pos = scrollbar_get_max_pos_and_height(scrollbar) + local new_top_elem = math.floor(cur_pos * max_top / max_pos) + 1 + new_top_elem = math.max(1, math.min(new_top_elem, max_top)) + if new_top_elem ~= scrollbar.top_elem then + scrollbar.on_scroll(new_top_elem) + end +end + +local function scrollbar_is_visible(scrollbar) + return scrollbar.elems_per_page < scrollbar.num_elems +end + +local SBSO = df.global.init.scrollbar_texpos[0] --Scroll Bar Spritesheet Offset / change this to point to a different spritesheet (ui themes, anyone? :p) +local SCROLLBAR_UP_LEFT_PEN = to_pen{tile=SBSO+0, ch=47, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_UP_RIGHT_PEN = to_pen{tile=SBSO+1, ch=92, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_DOWN_LEFT_PEN = to_pen{tile=SBSO+24, ch=92, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_DOWN_RIGHT_PEN = to_pen{tile=SBSO+25, ch=47, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_UP_LEFT_PEN = to_pen{tile=SBSO+6, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_UP_RIGHT_PEN = to_pen{tile=SBSO+7, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_LEFT_PEN = to_pen{tile=SBSO+30, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_RIGHT_PEN = to_pen{tile=SBSO+31, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_UP_LEFT_PEN = to_pen{tile=SBSO+10, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_UP_RIGHT_PEN = to_pen{tile=SBSO+11, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_DOWN_LEFT_PEN = to_pen{tile=SBSO+22, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_DOWN_RIGHT_PEN = to_pen{tile=SBSO+23, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_LEFT_PEN = to_pen{tile=SBSO+18, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_RIGHT_PEN = to_pen{tile=SBSO+19, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_DOWN_LEFT_PEN = to_pen{tile=SBSO+42, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_DOWN_RIGHT_PEN = to_pen{tile=SBSO+43, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_UP_LEFT_PEN = to_pen{tile=SBSO+26, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_UP_RIGHT_PEN = to_pen{tile=SBSO+27, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_DOWN_LEFT_PEN = to_pen{tile=SBSO+38, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_DOWN_RIGHT_PEN = to_pen{tile=SBSO+39, ch=219, fg=COLOR_CYAN, bg=COLOR_BLACK} +local SCROLLBAR_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+2, ch=47, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+3, ch=92, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+14, ch=92, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+15, ch=47, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+8, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+9, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_LEFT_HOVER_PEN = to_pen{tile=SBSO+32, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_RIGHT_HOVER_PEN = to_pen{tile=SBSO+33, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+34, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+35, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+46, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+47, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_LEFT_HOVER_PEN = to_pen{tile=SBSO+20, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_CENTER_RIGHT_HOVER_PEN = to_pen{tile=SBSO+21, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+44, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+45, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_UP_LEFT_HOVER_PEN = to_pen{tile=SBSO+28, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_UP_RIGHT_HOVER_PEN = to_pen{tile=SBSO+29, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_DOWN_LEFT_HOVER_PEN = to_pen{tile=SBSO+40, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_2TALL_DOWN_RIGHT_HOVER_PEN = to_pen{tile=SBSO+41, ch=219, fg=COLOR_LIGHTCYAN, bg=COLOR_BLACK} +local SCROLLBAR_BAR_BG_LEFT_PEN = to_pen{tile=SBSO+12, ch=176, fg=COLOR_DARKGREY, bg=COLOR_BLACK} +local SCROLLBAR_BAR_BG_RIGHT_PEN = to_pen{tile=SBSO+13, ch=176, fg=COLOR_DARKGREY, bg=COLOR_BLACK} + +function Scrollbar:onRenderBody(dc) + -- don't draw if all elements are visible + if not scrollbar_is_visible(self) then + return + end + -- determine which elements should be highlighted + local _,hover_y = self:getMousePos() + local hover_up, hover_down, hover_bar = false, false, false + if hover_y == 0 then + hover_up = true + elseif hover_y == dc.height-1 then + hover_down = true + elseif hover_y then + hover_bar = true + end + -- render up arrow + dc:seek(0, 0) + dc:char(nil, hover_up and SCROLLBAR_UP_LEFT_HOVER_PEN or SCROLLBAR_UP_LEFT_PEN) + dc:char(nil, hover_up and SCROLLBAR_UP_RIGHT_HOVER_PEN or SCROLLBAR_UP_RIGHT_PEN) + -- render scrollbar body + local starty = self.bar_offset + 1 + local endy = self.bar_offset + self.bar_height + local midy = (starty + endy)/2 + for y=1,dc.height-2 do + dc:seek(0, y) + if y >= starty and y <= endy then + if y == starty and y <= midy - 1 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_UP_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_UP_RIGHT_PEN) + elseif y == midy - 0.5 and self.bar_height == 2 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_2TALL_UP_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_2TALL_UP_RIGHT_PEN) + elseif y == midy + 0.5 and self.bar_height == 2 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_2TALL_DOWN_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_2TALL_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_2TALL_DOWN_RIGHT_PEN) + elseif y == midy - 0.5 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_UP_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_UP_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_UP_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_UP_RIGHT_PEN) + elseif y == midy + 0.5 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_DOWN_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_DOWN_RIGHT_PEN) + elseif y == midy then + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_LEFT_HOVER_PEN or SCROLLBAR_BAR_CENTER_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_CENTER_RIGHT_HOVER_PEN or SCROLLBAR_BAR_CENTER_RIGHT_PEN) + elseif y == endy and y >= midy + 1 then + dc:char(nil, hover_bar and SCROLLBAR_BAR_DOWN_LEFT_HOVER_PEN or SCROLLBAR_BAR_DOWN_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_BAR_DOWN_RIGHT_PEN) + else + dc:char(nil, hover_bar and SCROLLBAR_BAR_LEFT_HOVER_PEN or SCROLLBAR_BAR_LEFT_PEN) + dc:char(nil, hover_bar and SCROLLBAR_BAR_RIGHT_HOVER_PEN or SCROLLBAR_BAR_RIGHT_PEN) + end + else + dc:char(nil, SCROLLBAR_BAR_BG_LEFT_PEN) + dc:char(nil, SCROLLBAR_BAR_BG_RIGHT_PEN) + end + end + -- render down arrow + dc:seek(0, dc.height-1) + dc:char(nil, hover_down and SCROLLBAR_DOWN_LEFT_HOVER_PEN or SCROLLBAR_DOWN_LEFT_PEN) + dc:char(nil, hover_down and SCROLLBAR_DOWN_RIGHT_HOVER_PEN or SCROLLBAR_DOWN_RIGHT_PEN) + if not self.on_scroll then return end + -- manage state for dragging and continuous scrolling + if self.is_dragging then + scrollbar_do_drag(self) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.last_scroll_ms = 0 + self.is_dragging = false + self.scroll_spec = nil + return + end + if self.last_scroll_ms == 0 then return end + local now = dfhack.getTickCount() + local delay = self.is_first_click and + SCROLL_INITIAL_DELAY_MS or SCROLL_DELAY_MS + if now - self.last_scroll_ms >= delay then + self.is_first_click = false + self.on_scroll(self.scroll_spec) + self.last_scroll_ms = now + end +end + +function Scrollbar:onInput(keys) + if not self.on_scroll or not scrollbar_is_visible(self) then + return false + end + + if self.parent_view and self.parent_view:getMousePos() then + if keys.CONTEXT_SCROLL_UP then + self.on_scroll('up_small') + return true + elseif keys.CONTEXT_SCROLL_DOWN then + self.on_scroll('down_small') + return true + elseif keys.CONTEXT_SCROLL_PAGEUP then + self.on_scroll('up_large') + return true + elseif keys.CONTEXT_SCROLL_PAGEDOWN then + self.on_scroll('down_large') + return true + end + end + if not keys._MOUSE_L then return false end + local _,y = self:getMousePos() + if not y then return false end + local scroll_spec = nil + if y == 0 then scroll_spec = 'up_small' + elseif y == self.frame_body.height - 1 then scroll_spec = 'down_small' + elseif y <= self.bar_offset then scroll_spec = 'up_large' + elseif y > self.bar_offset + self.bar_height then scroll_spec = 'down_large' + else + self.is_dragging = y - self.bar_offset + return true + end + self.scroll_spec = scroll_spec + self.on_scroll(scroll_spec) + -- reset continuous scroll state + self.is_first_click = true + self.last_scroll_ms = dfhack.getTickCount() + return true +end + +return Scrollbar From 72df6fd410c11ec723f057b9b6e5b414508365a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 21 Sep 2024 12:19:35 +0200 Subject: [PATCH 05/34] Refactor few widgets to dedicated modules WrappedLabel, TooltipLabel, HelpButton, ConfigureButton, BannerPanel --- library/lua/gui/widgets.lua | 184 +------------------ library/lua/gui/widgets/banner_panel.lua | 20 ++ library/lua/gui/widgets/configure_button.lua | 53 ++++++ library/lua/gui/widgets/help_button.lua | 53 ++++++ library/lua/gui/widgets/hotkey_label.lua | 1 + library/lua/gui/widgets/tooltip_label.lua | 28 +++ library/lua/gui/widgets/wrapped_label.lua | 56 ++++++ 7 files changed, 216 insertions(+), 179 deletions(-) create mode 100644 library/lua/gui/widgets/banner_panel.lua create mode 100644 library/lua/gui/widgets/configure_button.lua create mode 100644 library/lua/gui/widgets/help_button.lua create mode 100644 library/lua/gui/widgets/tooltip_label.lua create mode 100644 library/lua/gui/widgets/wrapped_label.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 82ca1014a2..d5086b5489 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -17,6 +17,11 @@ EditField = require('gui.widgets.edit_field') HotkeyLabel = require('gui.widgets.hotkey_label') Label = require('gui.widgets.label') Scrollbar = require('gui.widgets.scrollbar') +WrappedLabel = require('gui.widgets.wrapped_label') +TooltipLabel = require('gui.widgets.tooltip_label') +HelpButton = require('gui.widgets.help_button') +ConfigureButton = require('gui.widgets.configure_button') +BannerPanel = require('gui.widgets.banner_panel') makeButtonLabelText = Label.makeButtonLabelText parse_label_text = Label.parse_label_text @@ -35,185 +40,6 @@ local function map_opttab(tab,idx) end end ------------------- --- WrappedLabel -- ------------------- - ----@class widgets.WrappedLabel.attrs: widgets.Label.attrs ----@field text_to_wrap? string|string[]|fun(): string|string[] ----@field indent integer - ----@class widgets.WrappedLabel.attrs.partial: widgets.WrappedLabel.attrs - ----@class widgets.WrappedLabel: widgets.Label, widgets.WrappedLabel.attrs ----@field super widgets.Label ----@field ATTRS widgets.WrappedLabel.attrs|fun(attributes: widgets.WrappedLabel.attrs.partial) ----@overload fun(init_table: widgets.WrappedLabel.attrs.partial): self -WrappedLabel = defclass(WrappedLabel, Label) - -WrappedLabel.ATTRS{ - text_to_wrap=DEFAULT_NIL, - indent=0, -} - -function WrappedLabel:getWrappedText(width) - -- 0 width can happen if the parent has 0 width - if not self.text_to_wrap or width <= 0 then return nil end - local text_to_wrap = getval(self.text_to_wrap) - if type(text_to_wrap) == 'table' then - text_to_wrap = table.concat(text_to_wrap, NEWLINE) - end - return text_to_wrap:wrap(width - self.indent, {return_as_table=true}) -end - -function WrappedLabel:preUpdateLayout() - self.saved_start_line_num = self.start_line_num -end - --- we can't set the text in init() since we may not yet have a frame that we --- can get wrapping bounds from. -function WrappedLabel:postComputeFrame() - local wrapped_text = self:getWrappedText(self.frame_body.width-3) - if not wrapped_text then return end - local text = {} - for _,line in ipairs(wrapped_text) do - table.insert(text, {gap=self.indent, text=line}) - -- a trailing newline will get ignored so we don't have to manually trim - table.insert(text, NEWLINE) - end - self:setText(text) - self:scroll(self.saved_start_line_num - 1) -end - ------------------- --- TooltipLabel -- ------------------- - ----@class widgets.TooltipLabel.attrs: widgets.WrappedLabel.attrs ----@field show_tooltip? boolean|fun(): boolean - ----@class widgets.TooltipLabel.attrs.partial: widgets.TooltipLabel.attrs - ----@class widgets.TooltipLabel: widgets.WrappedLabel, widgets.TooltipLabel.attrs ----@field super widgets.WrappedLabel ----@field ATTRS widgets.TooltipLabel.attrs|fun(attributes: widgets.TooltipLabel.attrs.partial) ----@overload fun(init_table: widgets.TooltipLabel.attrs.partial): self -TooltipLabel = defclass(TooltipLabel, WrappedLabel) - -TooltipLabel.ATTRS{ - show_tooltip=DEFAULT_NIL, - indent=2, - text_pen=COLOR_GREY, -} - -function TooltipLabel:init() - self.visible = self.show_tooltip -end - ----------------- --- HelpButton -- ----------------- - ----@class widgets.HelpButton.attrs: widgets.Panel.attrs ----@field command? string - ----@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs - ----@class widgets.HelpButton: widgets.Panel, widgets.HelpButton.attrs ----@field super widgets.Panel ----@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial) ----@overload fun(init_table: widgets.HelpButton.attrs.partial): self -HelpButton = defclass(HelpButton, Panel) - -HelpButton.ATTRS{ - frame={t=0, r=1, w=3, h=1}, - command=DEFAULT_NIL, -} - -local button_pen_left = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} -local button_pen_right = to_pen{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} -local help_pen_center = to_pen{ - tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} -local configure_pen_center = dfhack.pen.parse{ - tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol - -function HelpButton:init() - self.frame.w = self.frame.w or 3 - self.frame.h = self.frame.h or 1 - - local command = self.command .. ' ' - - self:addviews{ - Label{ - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=button_pen_left}, - {tile=help_pen_center}, - {tile=button_pen_right}, - }, - on_click=function() dfhack.run_command('gui/launcher', command) end, - }, - } -end - ---------------------- --- ConfigureButton -- ---------------------- - ----@class widgets.ConfigureButton.attrs: widgets.Panel.attrs ----@field on_click? function - ----@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs - ----@class widgets.ConfigureButton: widgets.Panel, widgets.ConfigureButton.attrs ----@field super widgets.Panel ----@field ATTRS widgets.ConfigureButton.attrs|fun(attributes: widgets.ConfigureButton.attrs.partial) ----@overload fun(init_table: widgets.ConfigureButton.attrs.partial): self -ConfigureButton = defclass(ConfigureButton, Panel) - -ConfigureButton.ATTRS{ - on_click=DEFAULT_NIL, -} - -function ConfigureButton:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 - init_table.frame.w = init_table.frame.w or 3 -end - -function ConfigureButton:init() - self:addviews{ - Label{ - frame={t=0, l=0, w=3, h=1}, - text={ - {tile=button_pen_left}, - {tile=configure_pen_center}, - {tile=button_pen_right}, - }, - on_click=self.on_click, - }, - } -end - ------------------ --- BannerPanel -- ------------------ - ----@class widgets.BannerPanel: widgets.Panel ----@field super widgets.Panel -BannerPanel = defclass(BannerPanel, Panel) - ----@param dc gui.Painter -function BannerPanel:onRenderBody(dc) - dc:pen(COLOR_RED) - for y=0,self.frame_rect.height-1 do - dc:seek(0, y):char('[') - dc:seek(self.frame_rect.width-1):char(']') - end -end - ---------------- -- TextButton -- ---------------- diff --git a/library/lua/gui/widgets/banner_panel.lua b/library/lua/gui/widgets/banner_panel.lua new file mode 100644 index 0000000000..059f70c74f --- /dev/null +++ b/library/lua/gui/widgets/banner_panel.lua @@ -0,0 +1,20 @@ +local Panel = require('gui.widgets.panel') + +----------------- +-- BannerPanel -- +----------------- + +---@class widgets.BannerPanel: widgets.Panel +---@field super widgets.Panel +BannerPanel = defclass(BannerPanel, Panel) + +---@param dc gui.Painter +function BannerPanel:onRenderBody(dc) + dc:pen(COLOR_RED) + for y=0,self.frame_rect.height-1 do + dc:seek(0, y):char('[') + dc:seek(self.frame_rect.width-1):char(']') + end +end + +return BannerPanel diff --git a/library/lua/gui/widgets/configure_button.lua b/library/lua/gui/widgets/configure_button.lua new file mode 100644 index 0000000000..2a86bc0ac4 --- /dev/null +++ b/library/lua/gui/widgets/configure_button.lua @@ -0,0 +1,53 @@ +local textures = require('gui.textures') +local Panel = require('gui.widgets.panel') +local Label = require('gui.widgets.label') + +local to_pen = dfhack.pen.parse + +local button_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} +local configure_pen_center = to_pen{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + +--------------------- +-- ConfigureButton -- +--------------------- + +---@class widgets.ConfigureButton.attrs: widgets.Panel.attrs +---@field on_click? function + +---@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs + +---@class widgets.ConfigureButton: widgets.Panel, widgets.ConfigureButton.attrs +---@field super widgets.Panel +---@field ATTRS widgets.ConfigureButton.attrs|fun(attributes: widgets.ConfigureButton.attrs.partial) +---@overload fun(init_table: widgets.ConfigureButton.attrs.partial): self +ConfigureButton = defclass(ConfigureButton, Panel) + +ConfigureButton.ATTRS{ + on_click=DEFAULT_NIL, +} + +function ConfigureButton:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 + init_table.frame.w = init_table.frame.w or 3 +end + +function ConfigureButton:init() + self:addviews{ + Label{ + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=button_pen_left}, + {tile=configure_pen_center}, + {tile=button_pen_right}, + }, + on_click=self.on_click, + }, + } +end + +return ConfigureButton diff --git a/library/lua/gui/widgets/help_button.lua b/library/lua/gui/widgets/help_button.lua new file mode 100644 index 0000000000..57aa195294 --- /dev/null +++ b/library/lua/gui/widgets/help_button.lua @@ -0,0 +1,53 @@ +local textures = require('gui.textures') +local Panel = require('gui.widgets.panel') +local Label = require('gui.widgets.label') + +local to_pen = dfhack.pen.parse + +---------------- +-- HelpButton -- +---------------- + +---@class widgets.HelpButton.attrs: widgets.Panel.attrs +---@field command? string + +---@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs + +---@class widgets.HelpButton: widgets.Panel, widgets.HelpButton.attrs +---@field super widgets.Panel +---@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial) +---@overload fun(init_table: widgets.HelpButton.attrs.partial): self +HelpButton = defclass(HelpButton, Panel) + +HelpButton.ATTRS{ + frame={t=0, r=1, w=3, h=1}, + command=DEFAULT_NIL, +} + +local button_pen_left = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} +local button_pen_right = to_pen{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} +local help_pen_center = to_pen{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} + +function HelpButton:init() + self.frame.w = self.frame.w or 3 + self.frame.h = self.frame.h or 1 + + local command = self.command .. ' ' + + self:addviews{ + Label{ + frame={t=0, l=0, w=3, h=1}, + text={ + {tile=button_pen_left}, + {tile=help_pen_center}, + {tile=button_pen_right}, + }, + on_click=function() dfhack.run_command('gui/launcher', command) end, + }, + } +end + +return HelpButton diff --git a/library/lua/gui/widgets/hotkey_label.lua b/library/lua/gui/widgets/hotkey_label.lua index 494b661035..d2addce119 100644 --- a/library/lua/gui/widgets/hotkey_label.lua +++ b/library/lua/gui/widgets/hotkey_label.lua @@ -1,4 +1,5 @@ local Label = require('gui.widgets.label') +local Label = require('gui.widgets.label') ----------------- -- HotkeyLabel -- diff --git a/library/lua/gui/widgets/tooltip_label.lua b/library/lua/gui/widgets/tooltip_label.lua new file mode 100644 index 0000000000..417c145b19 --- /dev/null +++ b/library/lua/gui/widgets/tooltip_label.lua @@ -0,0 +1,28 @@ +local WrappedLabel = require('gui.widgets.wrapped_label') + +------------------ +-- TooltipLabel -- +------------------ + +---@class widgets.TooltipLabel.attrs: widgets.WrappedLabel.attrs +---@field show_tooltip? boolean|fun(): boolean + +---@class widgets.TooltipLabel.attrs.partial: widgets.TooltipLabel.attrs + +---@class widgets.TooltipLabel: widgets.WrappedLabel, widgets.TooltipLabel.attrs +---@field super widgets.WrappedLabel +---@field ATTRS widgets.TooltipLabel.attrs|fun(attributes: widgets.TooltipLabel.attrs.partial) +---@overload fun(init_table: widgets.TooltipLabel.attrs.partial): self +TooltipLabel = defclass(TooltipLabel, WrappedLabel) + +TooltipLabel.ATTRS{ + show_tooltip=DEFAULT_NIL, + indent=2, + text_pen=COLOR_GREY, +} + +function TooltipLabel:init() + self.visible = self.show_tooltip +end + +return TooltipLabel diff --git a/library/lua/gui/widgets/wrapped_label.lua b/library/lua/gui/widgets/wrapped_label.lua new file mode 100644 index 0000000000..f74a1e5c9a --- /dev/null +++ b/library/lua/gui/widgets/wrapped_label.lua @@ -0,0 +1,56 @@ +local utils = require('utils') +local Label = require('gui.widgets.label') + +local getval = utils.getval + +------------------ +-- WrappedLabel -- +------------------ + +---@class widgets.WrappedLabel.attrs: widgets.Label.attrs +---@field text_to_wrap? string|string[]|fun(): string|string[] +---@field indent integer + +---@class widgets.WrappedLabel.attrs.partial: widgets.WrappedLabel.attrs + +---@class widgets.WrappedLabel: widgets.Label, widgets.WrappedLabel.attrs +---@field super widgets.Label +---@field ATTRS widgets.WrappedLabel.attrs|fun(attributes: widgets.WrappedLabel.attrs.partial) +---@overload fun(init_table: widgets.WrappedLabel.attrs.partial): self +WrappedLabel = defclass(WrappedLabel, Label) + +WrappedLabel.ATTRS{ + text_to_wrap=DEFAULT_NIL, + indent=0, +} + +function WrappedLabel:getWrappedText(width) + -- 0 width can happen if the parent has 0 width + if not self.text_to_wrap or width <= 0 then return nil end + local text_to_wrap = getval(self.text_to_wrap) + if type(text_to_wrap) == 'table' then + text_to_wrap = table.concat(text_to_wrap, NEWLINE) + end + return text_to_wrap:wrap(width - self.indent, {return_as_table=true}) +end + +function WrappedLabel:preUpdateLayout() + self.saved_start_line_num = self.start_line_num +end + +-- we can't set the text in init() since we may not yet have a frame that we +-- can get wrapping bounds from. +function WrappedLabel:postComputeFrame() + local wrapped_text = self:getWrappedText(self.frame_body.width-3) + if not wrapped_text then return end + local text = {} + for _,line in ipairs(wrapped_text) do + table.insert(text, {gap=self.indent, text=line}) + -- a trailing newline will get ignored so we don't have to manually trim + table.insert(text, NEWLINE) + end + self:setText(text) + self:scroll(self.saved_start_line_num - 1) +end + +return WrappedLabel From 3fedab79019aeee12d0c668478ea980fd6700e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 22 Sep 2024 09:50:46 +0200 Subject: [PATCH 06/34] Refactor few widgets to dedicated modules TextButton, CycleHotkeyLabel, ButtonGroup, ToggleHotkeyLabel, List --- library/lua/gui/widgets.lua | 612 +----------------- library/lua/gui/widgets/button_group.lua | 48 ++ .../lua/gui/widgets/cycle_hotkey_label.lua | 145 +++++ library/lua/gui/widgets/hotkey_label.lua | 6 +- library/lua/gui/widgets/label.lua | 2 + library/lua/gui/widgets/list.lua | 385 +++++++++++ library/lua/gui/widgets/text_button.lua | 42 ++ .../lua/gui/widgets/toggle_hotkey_label.lua | 15 + 8 files changed, 649 insertions(+), 606 deletions(-) create mode 100644 library/lua/gui/widgets/button_group.lua create mode 100644 library/lua/gui/widgets/cycle_hotkey_label.lua create mode 100644 library/lua/gui/widgets/list.lua create mode 100644 library/lua/gui/widgets/text_button.lua create mode 100644 library/lua/gui/widgets/toggle_hotkey_label.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index d5086b5489..aaafae9da7 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -22,9 +22,16 @@ TooltipLabel = require('gui.widgets.tooltip_label') HelpButton = require('gui.widgets.help_button') ConfigureButton = require('gui.widgets.configure_button') BannerPanel = require('gui.widgets.banner_panel') +TextButton = require('gui.widgets.text_button') +CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') +ButtonGroup = require('gui.widgets.button_group') +ToggleHotkeyLabel = require('gui.widgets.toggle_hotkey_label') +List = require('gui.widgets.list') makeButtonLabelText = Label.makeButtonLabelText parse_label_text = Label.parse_label_text +render_text = Label.render_text +check_text_keys = Label.check_text_keys local getval = utils.getval local to_pen = dfhack.pen.parse @@ -40,611 +47,6 @@ local function map_opttab(tab,idx) end end ----------------- --- TextButton -- ----------------- - ----@class widgets.TextButton.initTable: widgets.Panel.attrs.partial, widgets.HotkeyLabel.attrs.partial - ----@class widgets.TextButton: widgets.BannerPanel ----@field super widgets.BannerPanel ----@overload fun(init_table: widgets.TextButton.initTable): self -TextButton = defclass(TextButton, BannerPanel) - ----@param info widgets.TextButton.initTable -function TextButton:init(info) - self.label = HotkeyLabel{ - frame={t=0, l=1, r=1}, - key=info.key, - key_sep=info.key_sep, - label=info.label, - on_activate=info.on_activate, - text_pen=info.text_pen, - text_dpen=info.text_dpen, - text_hpen=info.text_hpen, - disabled=info.disabled, - enabled=info.enabled, - auto_height=info.auto_height, - auto_width=info.auto_width, - on_click=info.on_click, - on_rclick=info.on_rclick, - scroll_keys=info.scroll_keys, - } - - self:addviews{self.label} -end - -function TextButton:setLabel(label) - self.label:setLabel(label) -end - ----------------------- --- CycleHotkeyLabel -- ----------------------- - ----@class widgets.CycleHotkeyLabel.attrs: widgets.Label.attrs ----@field key? string ----@field key_back? string ----@field key_sep string ----@field label? string|fun(): string ----@field label_width? integer ----@field label_below boolean ----@field option_gap integer ----@field options? { label: string, value: string, pen: dfhack.pen|nil }[] ----@field initial_option integer|string ----@field on_change? fun(new_option_value: integer|string, old_option_value: integer|string) - ----@class widgets.CycleHotkeyLabel.attrs.partial: widgets.CycleHotkeyLabel.attrs - ----@class widgets.CycleHotkeyLabel: widgets.Label, widgets.CycleHotkeyLabel.attrs ----@field super widgets.Label ----@field ATTRS widgets.CycleHotkeyLabel.attrs|fun(attributes: widgets.CycleHotkeyLabel.attrs.partial) ----@overload fun(init_table: widgets.CycleHotkeyLabel.attrs.partial) -CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label) - -CycleHotkeyLabel.ATTRS{ - key=DEFAULT_NIL, - key_back=DEFAULT_NIL, - key_sep=': ', - option_gap=1, - label=DEFAULT_NIL, - label_width=DEFAULT_NIL, - label_below=false, - options=DEFAULT_NIL, - initial_option=1, - on_change=DEFAULT_NIL, -} - -function CycleHotkeyLabel:init() - self:setOption(self.initial_option) - - if self.label_below then - self.option_gap = self.option_gap + (self.key_back and 1 or 0) + (self.key and 2 or 0) - end - - self:setText{ - self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, - {key=self.key, key_sep=self.key_sep, text=self.label, width=self.label_width, - on_activate=self:callback('cycle')}, - self.label_below and NEWLINE or '', - {gap=self.option_gap, text=self:callback('getOptionLabel'), - pen=self:callback('getOptionPen')}, - } -end - --- CycleHotkeyLabels are always clickable and therefore should always change --- color when hovered. -function CycleHotkeyLabel:shouldHover() - return true -end - -function CycleHotkeyLabel:cycle(backwards) - local old_option_idx = self.option_idx - if self.option_idx == #self.options and not backwards then - self.option_idx = 1 - elseif self.option_idx == 1 and backwards then - self.option_idx = #self.options - else - self.option_idx = self.option_idx + (not backwards and 1 or -1) - end - if self.on_change then - self.on_change(self:getOptionValue(), - self:getOptionValue(old_option_idx)) - end -end - -function CycleHotkeyLabel:setOption(value_or_index, call_on_change) - local option_idx = nil - for i in ipairs(self.options) do - if value_or_index == self:getOptionValue(i) then - option_idx = i - break - end - end - if not option_idx then - if self.options[value_or_index] then - option_idx = value_or_index - end - end - if not option_idx then - option_idx = 1 - end - local old_option_idx = self.option_idx - self.option_idx = option_idx - if call_on_change and self.on_change then - self.on_change(self:getOptionValue(), - self:getOptionValue(old_option_idx)) - end -end - -local function cyclehotkeylabel_getOptionElem(self, option_idx, key, require_key) - option_idx = option_idx or self.option_idx - local option = self.options[option_idx] - if type(option) == 'table' then - return option[key] - end - return not require_key and option or nil -end - -function CycleHotkeyLabel:getOptionLabel(option_idx) - return getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'label')) -end - -function CycleHotkeyLabel:getOptionValue(option_idx) - return cyclehotkeylabel_getOptionElem(self, option_idx, 'value') -end - -function CycleHotkeyLabel:getOptionPen(option_idx) - local pen = getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'pen', true)) - if type(pen) == 'string' then return nil end - return pen -end - -function CycleHotkeyLabel:onInput(keys) - if CycleHotkeyLabel.super.onInput(self, keys) then - return true - elseif keys._MOUSE_L and not is_disabled(self) then - local x = self:getMousePos() - if x then - self:cycle(self.key_back and x == 0) - return true - end - end -end - ------------------ --- ButtonGroup -- ------------------ - ----@class widgets.ButtonGroup.attrs: widgets.CycleHotkeyLabel.attrs - ----@class widgets.ButtonGroup.initTable: widgets.ButtonGroup.attrs ----@field button_specs widgets.ButtonLabelSpec[] ----@field button_specs_selected widgets.ButtonLabelSpec[] - ----@class widgets.ButtonGroup: widgets.ButtonGroup.attrs, widgets.CycleHotkeyLabel ----@field super widgets.CycleHotkeyLabel ----@overload fun(init_table: widgets.ButtonGroup.initTable): self -ButtonGroup = defclass(ButtonGroup, CycleHotkeyLabel) - -ButtonGroup.ATTRS{ - auto_height=false, -} - -function ButtonGroup:init(info) - ensure_key(self, 'frame').h = #info.button_specs[1].chars + 2 - - local start = 0 - for idx in ipairs(self.options) do - local opt_val = self:getOptionValue(idx) - local width = #info.button_specs[idx].chars[1] - self:addviews{ - Label{ - frame={t=2, l=start, w=width}, - text=makeButtonLabelText(info.button_specs[idx]), - on_click=function() self:setOption(opt_val, true) end, - visible=function() return self:getOptionValue() ~= opt_val end, - }, - Label{ - frame={t=2, l=start, w=width}, - text=makeButtonLabelText(info.button_specs_selected[idx]), - on_click=function() self:setOption(opt_val, false) end, - visible=function() return self:getOptionValue() == opt_val end, - }, - } - start = start + width - end -end - ------------------------ --- ToggleHotkeyLabel -- ------------------------ - ----@class widgets.ToggleHotkeyLabel: widgets.CycleHotkeyLabel ----@field super widgets.CycleHotkeyLabel -ToggleHotkeyLabel = defclass(ToggleHotkeyLabel, CycleHotkeyLabel) -ToggleHotkeyLabel.ATTRS{ - options={{label='On', value=true, pen=COLOR_GREEN}, - {label='Off', value=false}}, -} - ----------- --- List -- ----------- - ----@class widgets.ListChoice ----@field text string|widgets.LabelToken[] ----@field key string ----@field search_key? string ----@field icon? string|dfhack.pen|fun(): string|dfhack.pen ----@field icon_pen? dfhack.pen - ----@class widgets.List.attrs: widgets.Widget.attrs ----@field choices widgets.ListChoice[] ----@field selected integer ----@field text_pen dfhack.color|dfhack.pen ----@field text_hpen? dfhack.color|dfhack.pen ----@field cursor_pen dfhack.color|dfhack.pen ----@field inactive_pen? dfhack.color|dfhack.pen ----@field icon_pen? dfhack.color|dfhack.pen ----@field on_select? function ----@field on_submit? function ----@field on_submit2? function ----@field on_double_click? function ----@field on_double_click2? function ----@field row_height integer ----@field scroll_keys table ----@field icon_width? integer ----@field page_top integer ----@field page_size integer ----@field scrollbar widgets.Scrollbar ----@field last_select_click_ms integer - ----@class widgets.List.attrs.partial: widgets.List.attrs - ----@class widgets.List: widgets.Widget, widgets.List.attrs ----@field super widgets.Widget ----@field ATTRS widgets.List.attrs|fun(attributes: widgets.List.attrs.partial) ----@overload fun(init_table: widgets.List.attrs.partial): self -List = defclass(List, Widget) - -List.ATTRS{ - text_pen = COLOR_CYAN, - text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object - cursor_pen = COLOR_LIGHTCYAN, - inactive_pen = DEFAULT_NIL, - icon_pen = DEFAULT_NIL, - on_select = DEFAULT_NIL, - on_submit = DEFAULT_NIL, - on_submit2 = DEFAULT_NIL, - on_double_click = DEFAULT_NIL, - on_double_click2 = DEFAULT_NIL, - row_height = 1, - scroll_keys = STANDARDSCROLL, - icon_width = DEFAULT_NIL, -} - ----@param self widgets.List ----@param info widgets.List.attrs.partial -function List:init(info) - self.page_top = 1 - self.page_size = 1 - self.scrollbar = Scrollbar{ - frame={r=0}, - on_scroll=self:callback('on_scrollbar')} - - self:addviews{self.scrollbar} - - if info.choices then - self:setChoices(info.choices, info.selected) - else - self.choices = {} - self.selected = 1 - end - - self.last_select_click_ms = 0 -- used to track double-clicking on an item -end - -function List:setChoices(choices, selected) - self.choices = {} - - for i,v in ipairs(choices or {}) do - local l = utils.clone(v); - if type(v) ~= 'table' then - l = { text = v } - else - l.text = v.text or v.caption or v[1] - end - parse_label_text(l) - self.choices[i] = l - end - - self:setSelected(selected) - - -- Check if page_top needs to be adjusted - if #self.choices - self.page_size < 0 then - self.page_top = 1 - elseif self.page_top > #self.choices - self.page_size + 1 then - self.page_top = #self.choices - self.page_size + 1 - end -end - -function List:setSelected(selected) - self.selected = selected or self.selected or 1 - self:moveCursor(0, true) - return self.selected -end - -function List:getChoices() - return self.choices -end - -function List:getSelected() - if #self.choices > 0 then - return self.selected, self.choices[self.selected] - end -end - -function List:getContentWidth() - local width = 0 - for i,v in ipairs(self.choices) do - render_text(v) - local roww = v.text_width - if v.key then - roww = roww + 3 + #gui.getKeyDisplay(v.key) - end - width = math.max(width, roww) - end - return width + (self.icon_width or 0) -end - -function List:getContentHeight() - return #self.choices * self.row_height -end - -local function update_list_scrollbar(list) - list.scrollbar:update(list.page_top, list.page_size, #list.choices) -end - -function List:postComputeFrame(body) - local row_count = body.height // self.row_height - self.page_size = math.max(1, row_count) - - local num_choices = #self.choices - if num_choices == 0 then - self.page_top = 1 - update_list_scrollbar(self) - return - end - - if self.page_top > num_choices - self.page_size + 1 then - self.page_top = math.max(1, num_choices - self.page_size + 1) - end - - update_list_scrollbar(self) -end - -function List:postUpdateLayout() - update_list_scrollbar(self) -end - -function List:moveCursor(delta, force_cb) - local cnt = #self.choices - - if cnt < 1 then - self.page_top = 1 - self.selected = 1 - update_list_scrollbar(self) - if force_cb and self.on_select then - self.on_select(nil,nil) - end - return - end - - local off = self.selected+delta-1 - local ds = math.abs(delta) - - if ds > 1 then - if off >= cnt+ds-1 then - off = 0 - else - off = math.min(cnt-1, off) - end - if off <= -ds then - off = cnt-1 - else - off = math.max(0, off) - end - end - - local buffer = 1 + math.min(4, math.floor(self.page_size/10)) - - self.selected = 1 + off % cnt - if (self.selected - buffer) < self.page_top then - self.page_top = math.max(1, self.selected - buffer) - elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then - local max_page_top = cnt - self.page_size + 1 - self.page_top = math.max(1, - math.min(max_page_top, self.selected - self.page_size + buffer + 1)) - end - update_list_scrollbar(self) - - if (force_cb or delta ~= 0) and self.on_select then - self.on_select(self:getSelected()) - end -end - -function List:on_scrollbar(scroll_spec) - local v = 0 - if tonumber(scroll_spec) then - v = scroll_spec - self.page_top - elseif scroll_spec == 'down_large' then - v = math.ceil(self.page_size / 2) - elseif scroll_spec == 'up_large' then - v = -math.ceil(self.page_size / 2) - elseif scroll_spec == 'down_small' then - v = 1 - elseif scroll_spec == 'up_small' then - v = -1 - end - - local max_page_top = math.max(1, #self.choices - self.page_size + 1) - self.page_top = math.max(1, math.min(max_page_top, self.page_top + v)) - update_list_scrollbar(self) -end - -function List:onRenderBody(dc) - local choices = self.choices - local top = self.page_top - local iend = math.min(#choices, top+self.page_size-1) - local iw = self.icon_width - - local function paint_icon(icon, obj) - if type(icon) ~= 'string' then - dc:char(nil,icon) - else - if current then - dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) - else - dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen) - end - end - end - - local hoveridx = self:getIdxUnderMouse() - for i = top,iend do - local obj = choices[i] - local current = (i == self.selected) - local hovered = (i == hoveridx) - -- cur_pen and cur_dpen can't be integers or background colors get - -- messed up in render_text for subsequent renders - local cur_pen = to_pen(self.cursor_pen) - local cur_dpen = to_pen(self.text_pen) - local active_pen = (current and cur_pen or cur_dpen) - - if not getval(self.active) then - cur_pen = self.inactive_pen or self.cursor_pen - end - - local y = (i - top)*self.row_height - local icon = getval(obj.icon) - - if iw and icon then - dc:seek(0, y):pen(active_pen) - paint_icon(icon, obj) - end - - render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) - - local ip = dc.width - - if obj.key then - local keystr = gui.getKeyDisplay(obj.key) - ip = ip-3-#keystr - dc:seek(ip,y):pen(self.text_pen) - dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')') - end - - if icon and not iw then - dc:seek(ip-1,y):pen(active_pen) - paint_icon(icon, obj) - end - end -end - -function List:getIdxUnderMouse() - if self.scrollbar:getMousePos() then return end - local _,mouse_y = self:getMousePos() - if mouse_y and #self.choices > 0 and - mouse_y < (#self.choices-self.page_top+1) * self.row_height then - return self.page_top + math.floor(mouse_y/self.row_height) - end -end - -function List:submit() - if self.on_submit and #self.choices > 0 then - self.on_submit(self:getSelected()) - return true - end -end - -function List:submit2() - if self.on_submit2 and #self.choices > 0 then - self.on_submit2(self:getSelected()) - return true - end -end - -function List:double_click() - if #self.choices == 0 then return end - local cb = dfhack.internal.getModifiers().shift and - self.on_double_click2 or self.on_double_click - if cb then - cb(self:getSelected()) - return true - end -end - -function List:onInput(keys) - if self:inputToSubviews(keys) then - return true - end - if keys.SELECT then - return self:submit() - elseif keys.SELECT_ALL then - return self:submit2() - elseif keys._MOUSE_L then - local idx = self:getIdxUnderMouse() - if idx then - local now_ms = dfhack.getTickCount() - if idx ~= self:getSelected() then - self.last_select_click_ms = now_ms - else - if now_ms - self.last_select_click_ms <= DOUBLE_CLICK_MS then - self.last_select_click_ms = 0 - if self:double_click() then return true end - else - self.last_select_click_ms = now_ms - end - end - - self:setSelected(idx) - if dfhack.internal.getModifiers().shift then - self:submit2() - else - self:submit() - end - return true - end - else - for k,v in pairs(self.scroll_keys) do - if keys[k] then - if v == '+page' then - v = self.page_size - elseif v == '-page' then - v = -self.page_size - end - - self:moveCursor(v) - return true - end - end - - for i,v in ipairs(self.choices) do - if v.key and keys[v.key] then - self:setSelected(i) - self:submit() - return true - end - end - - local current = self.choices[self.selected] - if current then - return check_text_keys(current, keys) - end - end -end - ------------------- -- Filtered List -- ------------------- diff --git a/library/lua/gui/widgets/button_group.lua b/library/lua/gui/widgets/button_group.lua new file mode 100644 index 0000000000..34931ac613 --- /dev/null +++ b/library/lua/gui/widgets/button_group.lua @@ -0,0 +1,48 @@ +local CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') +local Label = require('gui.widgets.label') + +----------------- +-- ButtonGroup -- +----------------- + +---@class widgets.ButtonGroup.attrs: widgets.CycleHotkeyLabel.attrs + +---@class widgets.ButtonGroup.initTable: widgets.ButtonGroup.attrs +---@field button_specs widgets.ButtonLabelSpec[] +---@field button_specs_selected widgets.ButtonLabelSpec[] + +---@class widgets.ButtonGroup: widgets.ButtonGroup.attrs, widgets.CycleHotkeyLabel +---@field super widgets.CycleHotkeyLabel +---@overload fun(init_table: widgets.ButtonGroup.initTable): self +ButtonGroup = defclass(ButtonGroup, CycleHotkeyLabel) + +ButtonGroup.ATTRS{ + auto_height=false, +} + +function ButtonGroup:init(info) + ensure_key(self, 'frame').h = #info.button_specs[1].chars + 2 + + local start = 0 + for idx in ipairs(self.options) do + local opt_val = self:getOptionValue(idx) + local width = #info.button_specs[idx].chars[1] + self:addviews{ + Label{ + frame={t=2, l=start, w=width}, + text=Label.makeButtonLabelText(info.button_specs[idx]), + on_click=function() self:setOption(opt_val, true) end, + visible=function() return self:getOptionValue() ~= opt_val end, + }, + Label{ + frame={t=2, l=start, w=width}, + text=Label.makeButtonLabelText(info.button_specs_selected[idx]), + on_click=function() self:setOption(opt_val, false) end, + visible=function() return self:getOptionValue() == opt_val end, + }, + } + start = start + width + end +end + +return ButtonGroup diff --git a/library/lua/gui/widgets/cycle_hotkey_label.lua b/library/lua/gui/widgets/cycle_hotkey_label.lua new file mode 100644 index 0000000000..846a36f7cd --- /dev/null +++ b/library/lua/gui/widgets/cycle_hotkey_label.lua @@ -0,0 +1,145 @@ +local utils = require('utils') +local Label = require('gui.widgets.label') + +local getval = utils.getval + +local function is_disabled(token) + return (token.disabled ~= nil and getval(token.disabled)) or + (token.enabled ~= nil and not getval(token.enabled)) +end + +---------------------- +-- CycleHotkeyLabel -- +---------------------- + +---@class widgets.CycleHotkeyLabel.attrs: widgets.Label.attrs +---@field key? string +---@field key_back? string +---@field key_sep string +---@field label? string|fun(): string +---@field label_width? integer +---@field label_below boolean +---@field option_gap integer +---@field options? { label: string, value: string, pen: dfhack.pen|nil }[] +---@field initial_option integer|string +---@field on_change? fun(new_option_value: integer|string, old_option_value: integer|string) + +---@class widgets.CycleHotkeyLabel.attrs.partial: widgets.CycleHotkeyLabel.attrs + +---@class widgets.CycleHotkeyLabel: widgets.Label, widgets.CycleHotkeyLabel.attrs +---@field super widgets.Label +---@field ATTRS widgets.CycleHotkeyLabel.attrs|fun(attributes: widgets.CycleHotkeyLabel.attrs.partial) +---@overload fun(init_table: widgets.CycleHotkeyLabel.attrs.partial) +CycleHotkeyLabel = defclass(CycleHotkeyLabel, Label) + +CycleHotkeyLabel.ATTRS{ + key=DEFAULT_NIL, + key_back=DEFAULT_NIL, + key_sep=': ', + option_gap=1, + label=DEFAULT_NIL, + label_width=DEFAULT_NIL, + label_below=false, + options=DEFAULT_NIL, + initial_option=1, + on_change=DEFAULT_NIL, +} + +function CycleHotkeyLabel:init() + self:setOption(self.initial_option) + + if self.label_below then + self.option_gap = self.option_gap + (self.key_back and 1 or 0) + (self.key and 2 or 0) + end + + self:setText{ + self.key_back ~= nil and {key=self.key_back, key_sep='', width=0, on_activate=self:callback('cycle', true)} or {}, + {key=self.key, key_sep=self.key_sep, text=self.label, width=self.label_width, + on_activate=self:callback('cycle')}, + self.label_below and NEWLINE or '', + {gap=self.option_gap, text=self:callback('getOptionLabel'), + pen=self:callback('getOptionPen')}, + } +end + +-- CycleHotkeyLabels are always clickable and therefore should always change +-- color when hovered. +function CycleHotkeyLabel:shouldHover() + return true +end + +function CycleHotkeyLabel:cycle(backwards) + local old_option_idx = self.option_idx + if self.option_idx == #self.options and not backwards then + self.option_idx = 1 + elseif self.option_idx == 1 and backwards then + self.option_idx = #self.options + else + self.option_idx = self.option_idx + (not backwards and 1 or -1) + end + if self.on_change then + self.on_change(self:getOptionValue(), + self:getOptionValue(old_option_idx)) + end +end + +function CycleHotkeyLabel:setOption(value_or_index, call_on_change) + local option_idx = nil + for i in ipairs(self.options) do + if value_or_index == self:getOptionValue(i) then + option_idx = i + break + end + end + if not option_idx then + if self.options[value_or_index] then + option_idx = value_or_index + end + end + if not option_idx then + option_idx = 1 + end + local old_option_idx = self.option_idx + self.option_idx = option_idx + if call_on_change and self.on_change then + self.on_change(self:getOptionValue(), + self:getOptionValue(old_option_idx)) + end +end + +local function cyclehotkeylabel_getOptionElem(self, option_idx, key, require_key) + option_idx = option_idx or self.option_idx + local option = self.options[option_idx] + if type(option) == 'table' then + return option[key] + end + return not require_key and option or nil +end + +function CycleHotkeyLabel:getOptionLabel(option_idx) + return getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'label')) +end + +function CycleHotkeyLabel:getOptionValue(option_idx) + return cyclehotkeylabel_getOptionElem(self, option_idx, 'value') +end + +function CycleHotkeyLabel:getOptionPen(option_idx) + local pen = getval(cyclehotkeylabel_getOptionElem(self, option_idx, 'pen', true)) + if type(pen) == 'string' then return nil end + return pen +end + +function CycleHotkeyLabel:onInput(keys) + if CycleHotkeyLabel.super.onInput(self, keys) then + return true + elseif keys._MOUSE_L and not is_disabled(self) then + local x = self:getMousePos() + if x then + self:cycle(self.key_back and x == 0) + return true + end + end +end + +return CycleHotkeyLabel diff --git a/library/lua/gui/widgets/hotkey_label.lua b/library/lua/gui/widgets/hotkey_label.lua index d2addce119..29c0233a2f 100644 --- a/library/lua/gui/widgets/hotkey_label.lua +++ b/library/lua/gui/widgets/hotkey_label.lua @@ -1,5 +1,9 @@ local Label = require('gui.widgets.label') -local Label = require('gui.widgets.label') + +local function is_disabled(token) + return (token.disabled ~= nil and getval(token.disabled)) or + (token.enabled ~= nil and not getval(token.enabled)) +end ----------------- -- HotkeyLabel -- diff --git a/library/lua/gui/widgets/label.lua b/library/lua/gui/widgets/label.lua index 07f178dcef..cb49b87b74 100644 --- a/library/lua/gui/widgets/label.lua +++ b/library/lua/gui/widgets/label.lua @@ -549,5 +549,7 @@ end Label.makeButtonLabelText = makeButtonLabelText Label.parse_label_text = parse_label_text +Label.render_text = render_text +Label.check_text_keys = check_text_keys return Label diff --git a/library/lua/gui/widgets/list.lua b/library/lua/gui/widgets/list.lua new file mode 100644 index 0000000000..f56f89ea96 --- /dev/null +++ b/library/lua/gui/widgets/list.lua @@ -0,0 +1,385 @@ +local gui = require('gui') +local utils = require('utils') +local Widget = require('gui.widgets.widget') +local Scrollbar = require('gui.widgets.scrollbar') + +local getval = utils.getval + +---------- +-- List -- +---------- + +---@class widgets.ListChoice +---@field text string|widgets.LabelToken[] +---@field key string +---@field search_key? string +---@field icon? string|dfhack.pen|fun(): string|dfhack.pen +---@field icon_pen? dfhack.pen + +---@class widgets.List.attrs: widgets.Widget.attrs +---@field choices widgets.ListChoice[] +---@field selected integer +---@field text_pen dfhack.color|dfhack.pen +---@field text_hpen? dfhack.color|dfhack.pen +---@field cursor_pen dfhack.color|dfhack.pen +---@field inactive_pen? dfhack.color|dfhack.pen +---@field icon_pen? dfhack.color|dfhack.pen +---@field on_select? function +---@field on_submit? function +---@field on_submit2? function +---@field on_double_click? function +---@field on_double_click2? function +---@field row_height integer +---@field scroll_keys table +---@field icon_width? integer +---@field page_top integer +---@field page_size integer +---@field scrollbar widgets.Scrollbar +---@field last_select_click_ms integer + +---@class widgets.List.attrs.partial: widgets.List.attrs + +---@class widgets.List: widgets.Widget, widgets.List.attrs +---@field super widgets.Widget +---@field ATTRS widgets.List.attrs|fun(attributes: widgets.List.attrs.partial) +---@overload fun(init_table: widgets.List.attrs.partial): self +List = defclass(List, Widget) + +List.ATTRS{ + text_pen = COLOR_CYAN, + text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object + cursor_pen = COLOR_LIGHTCYAN, + inactive_pen = DEFAULT_NIL, + icon_pen = DEFAULT_NIL, + on_select = DEFAULT_NIL, + on_submit = DEFAULT_NIL, + on_submit2 = DEFAULT_NIL, + on_double_click = DEFAULT_NIL, + on_double_click2 = DEFAULT_NIL, + row_height = 1, + scroll_keys = STANDARDSCROLL, + icon_width = DEFAULT_NIL, +} + +---@param self widgets.List +---@param info widgets.List.attrs.partial +function List:init(info) + self.page_top = 1 + self.page_size = 1 + self.scrollbar = Scrollbar{ + frame={r=0}, + on_scroll=self:callback('on_scrollbar')} + + self:addviews{self.scrollbar} + + if info.choices then + self:setChoices(info.choices, info.selected) + else + self.choices = {} + self.selected = 1 + end + + self.last_select_click_ms = 0 -- used to track double-clicking on an item +end + +function List:setChoices(choices, selected) + self.choices = {} + + for i,v in ipairs(choices or {}) do + local l = utils.clone(v); + if type(v) ~= 'table' then + l = { text = v } + else + l.text = v.text or v.caption or v[1] + end + parse_label_text(l) + self.choices[i] = l + end + + self:setSelected(selected) + + -- Check if page_top needs to be adjusted + if #self.choices - self.page_size < 0 then + self.page_top = 1 + elseif self.page_top > #self.choices - self.page_size + 1 then + self.page_top = #self.choices - self.page_size + 1 + end +end + +function List:setSelected(selected) + self.selected = selected or self.selected or 1 + self:moveCursor(0, true) + return self.selected +end + +function List:getChoices() + return self.choices +end + +function List:getSelected() + if #self.choices > 0 then + return self.selected, self.choices[self.selected] + end +end + +function List:getContentWidth() + local width = 0 + for i,v in ipairs(self.choices) do + render_text(v) + local roww = v.text_width + if v.key then + roww = roww + 3 + #gui.getKeyDisplay(v.key) + end + width = math.max(width, roww) + end + return width + (self.icon_width or 0) +end + +function List:getContentHeight() + return #self.choices * self.row_height +end + +local function update_list_scrollbar(list) + list.scrollbar:update(list.page_top, list.page_size, #list.choices) +end + +function List:postComputeFrame(body) + local row_count = body.height // self.row_height + self.page_size = math.max(1, row_count) + + local num_choices = #self.choices + if num_choices == 0 then + self.page_top = 1 + update_list_scrollbar(self) + return + end + + if self.page_top > num_choices - self.page_size + 1 then + self.page_top = math.max(1, num_choices - self.page_size + 1) + end + + update_list_scrollbar(self) +end + +function List:postUpdateLayout() + update_list_scrollbar(self) +end + +function List:moveCursor(delta, force_cb) + local cnt = #self.choices + + if cnt < 1 then + self.page_top = 1 + self.selected = 1 + update_list_scrollbar(self) + if force_cb and self.on_select then + self.on_select(nil,nil) + end + return + end + + local off = self.selected+delta-1 + local ds = math.abs(delta) + + if ds > 1 then + if off >= cnt+ds-1 then + off = 0 + else + off = math.min(cnt-1, off) + end + if off <= -ds then + off = cnt-1 + else + off = math.max(0, off) + end + end + + local buffer = 1 + math.min(4, math.floor(self.page_size/10)) + + self.selected = 1 + off % cnt + if (self.selected - buffer) < self.page_top then + self.page_top = math.max(1, self.selected - buffer) + elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then + local max_page_top = cnt - self.page_size + 1 + self.page_top = math.max(1, + math.min(max_page_top, self.selected - self.page_size + buffer + 1)) + end + update_list_scrollbar(self) + + if (force_cb or delta ~= 0) and self.on_select then + self.on_select(self:getSelected()) + end +end + +function List:on_scrollbar(scroll_spec) + local v = 0 + if tonumber(scroll_spec) then + v = scroll_spec - self.page_top + elseif scroll_spec == 'down_large' then + v = math.ceil(self.page_size / 2) + elseif scroll_spec == 'up_large' then + v = -math.ceil(self.page_size / 2) + elseif scroll_spec == 'down_small' then + v = 1 + elseif scroll_spec == 'up_small' then + v = -1 + end + + local max_page_top = math.max(1, #self.choices - self.page_size + 1) + self.page_top = math.max(1, math.min(max_page_top, self.page_top + v)) + update_list_scrollbar(self) +end + +function List:onRenderBody(dc) + local choices = self.choices + local top = self.page_top + local iend = math.min(#choices, top+self.page_size-1) + local iw = self.icon_width + + local function paint_icon(icon, obj) + if type(icon) ~= 'string' then + dc:char(nil,icon) + else + if current then + dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) + else + dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen) + end + end + end + + local hoveridx = self:getIdxUnderMouse() + for i = top,iend do + local obj = choices[i] + local current = (i == self.selected) + local hovered = (i == hoveridx) + -- cur_pen and cur_dpen can't be integers or background colors get + -- messed up in render_text for subsequent renders + local cur_pen = to_pen(self.cursor_pen) + local cur_dpen = to_pen(self.text_pen) + local active_pen = (current and cur_pen or cur_dpen) + + if not getval(self.active) then + cur_pen = self.inactive_pen or self.cursor_pen + end + + local y = (i - top)*self.row_height + local icon = getval(obj.icon) + + if iw and icon then + dc:seek(0, y):pen(active_pen) + paint_icon(icon, obj) + end + + render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) + + local ip = dc.width + + if obj.key then + local keystr = gui.getKeyDisplay(obj.key) + ip = ip-3-#keystr + dc:seek(ip,y):pen(self.text_pen) + dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')') + end + + if icon and not iw then + dc:seek(ip-1,y):pen(active_pen) + paint_icon(icon, obj) + end + end +end + +function List:getIdxUnderMouse() + if self.scrollbar:getMousePos() then return end + local _,mouse_y = self:getMousePos() + if mouse_y and #self.choices > 0 and + mouse_y < (#self.choices-self.page_top+1) * self.row_height then + return self.page_top + math.floor(mouse_y/self.row_height) + end +end + +function List:submit() + if self.on_submit and #self.choices > 0 then + self.on_submit(self:getSelected()) + return true + end +end + +function List:submit2() + if self.on_submit2 and #self.choices > 0 then + self.on_submit2(self:getSelected()) + return true + end +end + +function List:double_click() + if #self.choices == 0 then return end + local cb = dfhack.internal.getModifiers().shift and + self.on_double_click2 or self.on_double_click + if cb then + cb(self:getSelected()) + return true + end +end + +function List:onInput(keys) + if self:inputToSubviews(keys) then + return true + end + if keys.SELECT then + return self:submit() + elseif keys.SELECT_ALL then + return self:submit2() + elseif keys._MOUSE_L then + local idx = self:getIdxUnderMouse() + if idx then + local now_ms = dfhack.getTickCount() + if idx ~= self:getSelected() then + self.last_select_click_ms = now_ms + else + if now_ms - self.last_select_click_ms <= DOUBLE_CLICK_MS then + self.last_select_click_ms = 0 + if self:double_click() then return true end + else + self.last_select_click_ms = now_ms + end + end + + self:setSelected(idx) + if dfhack.internal.getModifiers().shift then + self:submit2() + else + self:submit() + end + return true + end + else + for k,v in pairs(self.scroll_keys) do + if keys[k] then + if v == '+page' then + v = self.page_size + elseif v == '-page' then + v = -self.page_size + end + + self:moveCursor(v) + return true + end + end + + for i,v in ipairs(self.choices) do + if v.key and keys[v.key] then + self:setSelected(i) + self:submit() + return true + end + end + + local current = self.choices[self.selected] + if current then + return check_text_keys(current, keys) + end + end +end + +return List diff --git a/library/lua/gui/widgets/text_button.lua b/library/lua/gui/widgets/text_button.lua new file mode 100644 index 0000000000..51ed839c61 --- /dev/null +++ b/library/lua/gui/widgets/text_button.lua @@ -0,0 +1,42 @@ +local BannerPanel = require('gui.widgets.banner_panel') +local HotkeyLabel = require('gui.widgets.hotkey_label') + +---------------- +-- TextButton -- +---------------- + +---@class widgets.TextButton.initTable: widgets.Panel.attrs.partial, widgets.HotkeyLabel.attrs.partial + +---@class widgets.TextButton: widgets.BannerPanel +---@field super widgets.BannerPanel +---@overload fun(init_table: widgets.TextButton.initTable): self +TextButton = defclass(TextButton, BannerPanel) + +---@param info widgets.TextButton.initTable +function TextButton:init(info) + self.label = HotkeyLabel{ + frame={t=0, l=1, r=1}, + key=info.key, + key_sep=info.key_sep, + label=info.label, + on_activate=info.on_activate, + text_pen=info.text_pen, + text_dpen=info.text_dpen, + text_hpen=info.text_hpen, + disabled=info.disabled, + enabled=info.enabled, + auto_height=info.auto_height, + auto_width=info.auto_width, + on_click=info.on_click, + on_rclick=info.on_rclick, + scroll_keys=info.scroll_keys, + } + + self:addviews{self.label} +end + +function TextButton:setLabel(label) + self.label:setLabel(label) +end + +return TextButton diff --git a/library/lua/gui/widgets/toggle_hotkey_label.lua b/library/lua/gui/widgets/toggle_hotkey_label.lua new file mode 100644 index 0000000000..07ff7f898f --- /dev/null +++ b/library/lua/gui/widgets/toggle_hotkey_label.lua @@ -0,0 +1,15 @@ +local CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') + +----------------------- +-- ToggleHotkeyLabel -- +----------------------- + +---@class widgets.ToggleHotkeyLabel: widgets.CycleHotkeyLabel +---@field super widgets.CycleHotkeyLabel +ToggleHotkeyLabel = defclass(ToggleHotkeyLabel, CycleHotkeyLabel) +ToggleHotkeyLabel.ATTRS{ + options={{label='On', value=true, pen=COLOR_GREEN}, + {label='Off', value=false}}, +} + +return ToggleHotkeyLabel From b5a3068cd1e65956d679bde58b893a7a90a3baf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 22 Sep 2024 10:00:08 +0200 Subject: [PATCH 07/34] Refactor few widgets to dedicated modules FilteredList, TabBar, Tab, RangeSlider, DimensionsTooltip --- library/lua/gui/widgets.lua | 673 +----------------- .../lua/gui/widgets/dimensions_tooltip.lua | 83 +++ library/lua/gui/widgets/filtered_list.lua | 226 ++++++ library/lua/gui/widgets/hotkey_label.lua | 3 + library/lua/gui/widgets/list.lua | 13 +- library/lua/gui/widgets/panel.lua | 3 + library/lua/gui/widgets/range_slider.lua | 164 +++++ library/lua/gui/widgets/scrollbar.lua | 6 +- library/lua/gui/widgets/tab_bar.lua | 210 ++++++ 9 files changed, 710 insertions(+), 671 deletions(-) create mode 100644 library/lua/gui/widgets/dimensions_tooltip.lua create mode 100644 library/lua/gui/widgets/filtered_list.lua create mode 100644 library/lua/gui/widgets/range_slider.lua create mode 100644 library/lua/gui/widgets/tab_bar.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index aaafae9da7..37bb57003d 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -2,11 +2,6 @@ local _ENV = mkmodule('gui.widgets') -local gui = require('gui') -local guidm = require('gui.dwarfmode') -local textures = require('gui.textures') -local utils = require('utils') - Widget = require('gui.widgets.widget') Divider = require('gui.widgets.divider') Panel = require('gui.widgets.panel') @@ -27,670 +22,20 @@ CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') ButtonGroup = require('gui.widgets.button_group') ToggleHotkeyLabel = require('gui.widgets.toggle_hotkey_label') List = require('gui.widgets.list') +FilteredList = require('gui.widgets.filtered_list') +TabBar = require('gui.widgets.tab_bar') +RangeSlider = require('gui.widgets.range_slider') +DimensionsTooltip = require('gui.widgets.dimensions_tooltip') +Tab = TabBar.Tab makeButtonLabelText = Label.makeButtonLabelText parse_label_text = Label.parse_label_text render_text = Label.render_text check_text_keys = Label.check_text_keys -local getval = utils.getval -local to_pen = dfhack.pen.parse - ----@param tab? table ----@param idx integer ----@return any|integer -local function map_opttab(tab,idx) - if tab then - return tab[idx] - else - return idx - end -end - -------------------- --- Filtered List -- -------------------- - - ----@class widgets.FilteredList.attrs: widgets.Widget.attrs ----@field choices widgets.ListChoice[] ----@field selected? integer ----@field edit_pen dfhack.color|dfhack.pen ----@field edit_below boolean ----@field edit_key? string ----@field edit_ignore_keys? string[] ----@field edit_on_char? function ----@field edit_on_change? function ----@field list widgets.List ----@field edit widgets.EditField ----@field not_found widgets.Label - ----@class widgets.FilteredList.attrs.partial: widgets.FilteredList.attrs - ----@class widgets.FilteredList.initTable: widgets.FilteredList.attrs.partial, widgets.List.attrs.partial, widgets.EditField.attrs.partial ----@field not_found_label? string - ----@class widgets.FilteredList: widgets.Widget, widgets.FilteredList.attrs ----@field super widgets.Widget ----@field ATTRS widgets.FilteredList.attrs|fun(attributes: widgets.FilteredList.attrs.partial) ----@overload fun(init_table: widgets.FilteredList.initTable): self -FilteredList = defclass(FilteredList, Widget) - -FilteredList.ATTRS { - edit_below = false, - edit_key = DEFAULT_NIL, - edit_ignore_keys = DEFAULT_NIL, - edit_on_char = DEFAULT_NIL, - edit_on_change = DEFAULT_NIL, -} - ----@param self widgets.FilteredList ----@param info widgets.FilteredList.initTable -function FilteredList:init(info) - local on_change = self:callback('onFilterChange') - if self.edit_on_change then - on_change = function(text) - self.edit_on_change(text) - self:onFilterChange(text) - end - end - - self.edit = EditField{ - text_pen = info.edit_pen or info.cursor_pen, - frame = { l = info.icon_width, t = 0, h = 1 }, - on_change = on_change, - on_char = self.edit_on_char, - key = self.edit_key, - ignore_keys = self.edit_ignore_keys, - } - self.list = List{ - frame = { t = 2 }, - text_pen = info.text_pen, - cursor_pen = info.cursor_pen, - inactive_pen = info.inactive_pen, - icon_pen = info.icon_pen, - row_height = info.row_height, - scroll_keys = info.scroll_keys, - icon_width = info.icon_width, - } - if self.edit_below then - self.edit.frame = { l = info.icon_width, b = 0, h = 1 } - self.list.frame = { t = 0, b = 2 } - end - if info.on_select then - self.list.on_select = function() - return info.on_select(self:getSelected()) - end - end - if info.on_submit then - self.list.on_submit = function() - return info.on_submit(self:getSelected()) - end - end - if info.on_submit2 then - self.list.on_submit2 = function() - return info.on_submit2(self:getSelected()) - end - end - if info.on_double_click then - self.list.on_double_click = function() - return info.on_double_click(self:getSelected()) - end - end - if info.on_double_click2 then - self.list.on_double_click2 = function() - return info.on_double_click2(self:getSelected()) - end - end - self.not_found = Label{ - visible = true, - text = info.not_found_label or 'No matches', - text_pen = COLOR_LIGHTRED, - frame = { l = info.icon_width, t = self.list.frame.t }, - } - self:addviews{ self.edit, self.list, self.not_found } - if info.choices then - self:setChoices(info.choices, info.selected) - else - self.choices = {} - end -end - -function FilteredList:getChoices() - return self.choices -end - -function FilteredList:getVisibleChoices() - return self.list.choices -end - -function FilteredList:setChoices(choices, pos) - choices = choices or {} - self.edit:setText('') - self.list:setChoices(choices, pos) - self.choices = self.list.choices - self.not_found.visible = (#choices == 0) -end - -function FilteredList:submit() - return self.list:submit() -end - -function FilteredList:submit2() - return self.list:submit2() -end - -function FilteredList:canSubmit() - return not self.not_found.visible -end - -function FilteredList:getSelected() - local i,v = self.list:getSelected() - if i then - return map_opttab(self.choice_index, i), v - end -end - -function FilteredList:getContentWidth() - return self.list:getContentWidth() -end - -function FilteredList:getContentHeight() - return self.list:getContentHeight() + 2 -end - -function FilteredList:getFilter() - return self.edit.text, self.list.choices -end - -function FilteredList:setFilter(filter, pos) - local choices = self.choices - local cidx = nil - - filter = filter or '' - if filter ~= self.edit.text then - self.edit:setText(filter) - end - - if filter ~= '' then - local tokens = filter:split() - local ipos = pos - - choices = {} - cidx = {} - pos = nil - - for i,v in ipairs(self.choices) do - local search_key = v.search_key - if not search_key then - if type(v.text) ~= 'table' then - search_key = v.text - else - local texts = {} - for _,token in ipairs(v.text) do - table.insert(texts, - type(token) == 'string' and token - or getval(token.text) or '') - end - search_key = table.concat(texts, ' ') - end - end - if utils.search_text(search_key, tokens) then - table.insert(choices, v) - cidx[#choices] = i - if ipos == i then - pos = #choices - end - end - end - end - - self.choice_index = cidx - self.list:setChoices(choices, pos) - self.not_found.visible = (#choices == 0) -end - -function FilteredList:onFilterChange(text) - self:setFilter(text) -end - ----@class widgets.TabPens ----@field text_mode_tab_pen dfhack.pen ----@field text_mode_label_pen dfhack.pen ----@field lt dfhack.pen ----@field lt2 dfhack.pen ----@field t dfhack.pen ----@field rt2 dfhack.pen ----@field rt dfhack.pen ----@field lb dfhack.pen ----@field lb2 dfhack.pen ----@field b dfhack.pen ----@field rb2 dfhack.pen ----@field rb dfhack.pen - -local TSO = df.global.init.tabs_texpos[0] -- tab spritesheet offset -local DEFAULT_ACTIVE_TAB_PENS = { - text_mode_tab_pen=to_pen{fg=COLOR_YELLOW}, - text_mode_label_pen=to_pen{fg=COLOR_WHITE}, - lt=to_pen{tile=TSO+5, write_to_lower=true}, - lt2=to_pen{tile=TSO+6, write_to_lower=true}, - t=to_pen{tile=TSO+7, fg=COLOR_BLACK, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=TSO+8, write_to_lower=true}, - rt=to_pen{tile=TSO+9, write_to_lower=true}, - lb=to_pen{tile=TSO+15, write_to_lower=true}, - lb2=to_pen{tile=TSO+16, write_to_lower=true}, - b=to_pen{tile=TSO+17, fg=COLOR_BLACK, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=TSO+18, write_to_lower=true}, - rb=to_pen{tile=TSO+19, write_to_lower=true}, -} - -local DEFAULT_INACTIVE_TAB_PENS = { - text_mode_tab_pen=to_pen{fg=COLOR_BROWN}, - text_mode_label_pen=to_pen{fg=COLOR_DARKGREY}, - lt=to_pen{tile=TSO+0, write_to_lower=true}, - lt2=to_pen{tile=TSO+1, write_to_lower=true}, - t=to_pen{tile=TSO+2, fg=COLOR_WHITE, write_to_lower=true, top_of_text=true}, - rt2=to_pen{tile=TSO+3, write_to_lower=true}, - rt=to_pen{tile=TSO+4, write_to_lower=true}, - lb=to_pen{tile=TSO+10, write_to_lower=true}, - lb2=to_pen{tile=TSO+11, write_to_lower=true}, - b=to_pen{tile=TSO+12, fg=COLOR_WHITE, write_to_lower=true, bottom_of_text=true}, - rb2=to_pen{tile=TSO+13, write_to_lower=true}, - rb=to_pen{tile=TSO+14, write_to_lower=true}, -} - ---------- --- Tab -- ---------- - ----@class widgets.Tab.attrs: widgets.Widget.attrs ----@field id? string|integer ----@field label string ----@field on_select? function ----@field get_pens? fun(): widgets.TabPens - ----@class widgets.Tab.attrs.partial: widgets.Tab.attrs - ----@class widgets.Tab.initTable: widgets.Tab.attrs ----@field label string - ----@class widgets.Tab: widgets.Widget, widgets.Tab.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Tab.attrs|fun(attributes: widgets.Tab.attrs.partial) ----@overload fun(init_table: widgets.Tab.initTable): self -Tab = defclass(Tabs, Widget) -Tab.ATTRS{ - id=DEFAULT_NIL, - label=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_pens=DEFAULT_NIL, -} - -function Tab:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.w = #init_table.label + 4 - init_table.frame.h = 2 -end - -function Tab:onRenderBody(dc) - local pens = self.get_pens() - dc:seek(0, 0) - if dfhack.screen.inGraphicsMode() then - dc:char(nil, pens.lt):char(nil, pens.lt2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.t) - end - dc:char(nil, pens.rt2):char(nil, pens.rt) - dc:seek(0, 1) - dc:char(nil, pens.lb):char(nil, pens.lb2) - for i=1,#self.label do - dc:char(self.label:sub(i,i), pens.b) - end - dc:char(nil, pens.rb2):char(nil, pens.rb) - else - local tp = pens.text_mode_tab_pen - dc:char(' ', tp):char('/', tp) - for i=1,#self.label do - dc:char('-', tp) - end - dc:char('\\', tp):char(' ', tp) - dc:seek(0, 1) - dc:char('/', tp):char('-', tp) - dc:string(self.label, pens.text_mode_label_pen) - dc:char('-', tp):char('\\', tp) - end -end - -function Tab:onInput(keys) - if Tab.super.onInput(self, keys) then return true end - if keys._MOUSE_L and self:getMousePos() then - self.on_select(self.id) - return true - end -end - -------------- --- Tab Bar -- -------------- - ----@class widgets.TabBar.attrs: widgets.ResizingPanel.attrs ----@field labels string[] ----@field on_select? function ----@field get_cur_page? function ----@field active_tab_pens widgets.TabPens ----@field inactive_tab_pens widgets.TabPens ----@field get_pens? fun(index: integer, tabbar: self): widgets.TabPens ----@field key string ----@field key_back string - ----@class widgets.TabBar.attrs.partial: widgets.TabBar.attrs - ----@class widgets.TabBar.initTable: widgets.TabBar.attrs ----@field labels string[] - ----@class widgets.TabBar: widgets.ResizingPanel, widgets.TabBar.attrs ----@field super widgets.ResizingPanel ----@field ATTRS widgets.TabBar.attrs|fun(attribute: widgets.TabBar.attrs.partial) ----@overload fun(init_table: widgets.TabBar.initTable): self -TabBar = defclass(TabBar, ResizingPanel) -TabBar.ATTRS{ - labels=DEFAULT_NIL, - on_select=DEFAULT_NIL, - get_cur_page=DEFAULT_NIL, - active_tab_pens=DEFAULT_ACTIVE_TAB_PENS, - inactive_tab_pens=DEFAULT_INACTIVE_TAB_PENS, - get_pens=DEFAULT_NIL, - key='CUSTOM_CTRL_T', - key_back='CUSTOM_CTRL_Y', -} - ----@param self widgets.TabBar -function TabBar:init() - for idx,label in ipairs(self.labels) do - self:addviews{ - Tab{ - frame={t=0, l=0}, - id=idx, - label=label, - on_select=self.on_select, - get_pens=self.get_pens and function() - return self.get_pens(idx, self) - end or function() - if self.get_cur_page() == idx then - return self.active_tab_pens - end - - return self.inactive_tab_pens - end, - } - } - end -end - -function TabBar:postComputeFrame(body) - local t, l, width = 0, 0, body.width - for _,tab in ipairs(self.subviews) do - if l > 0 and l + tab.frame.w > width then - t = t + 2 - l = 0 - end - tab.frame.t = t - tab.frame.l = l - l = l + tab.frame.w - end -end - -function TabBar:onInput(keys) - if TabBar.super.onInput(self, keys) then return true end - if self.key and keys[self.key] then - local zero_idx = self.get_cur_page() - 1 - local next_zero_idx = (zero_idx + 1) % #self.labels - self.on_select(next_zero_idx + 1) - return true - end - if self.key_back and keys[self.key_back] then - local zero_idx = self.get_cur_page() - 1 - local prev_zero_idx = (zero_idx - 1) % #self.labels - self.on_select(prev_zero_idx + 1) - return true - end -end - --------------------------------- --- RangeSlider --------------------------------- - ----@class widgets.RangeSlider.attrs: widgets.Widget.attrs ----@field num_stops integer ----@field get_left_idx_fn? function ----@field get_right_idx_fn? function ----@field on_left_change? fun(index: integer) ----@field on_right_change? fun(index: integer) - ----@class widgets.RangeSlider.attrs.partial: widgets.RangeSlider.attrs - ----@class widgets.RangeSlider.initTable: widgets.RangeSlider.attrs ----@field num_stops integer - ----@class widgets.RangeSlider: widgets.Widget, widgets.RangeSlider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.RangeSlider.attrs|fun(attributes: widgets.RangeSlider.attrs.partial) ----@overload fun(init_table: widgets.RangeSlider.initTable): self -RangeSlider = defclass(RangeSlider, Widget) -RangeSlider.ATTRS{ - num_stops=DEFAULT_NIL, - get_left_idx_fn=DEFAULT_NIL, - get_right_idx_fn=DEFAULT_NIL, - on_left_change=DEFAULT_NIL, - on_right_change=DEFAULT_NIL, -} - -function RangeSlider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function RangeSlider:init() - if self.num_stops < 2 then error('too few RangeSlider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function rangeslider_get_width_per_idx(self) - return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function RangeSlider:onInput(keys) - if not keys._MOUSE_L then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(right_idx-1) + 4 - if x < left_pos then - self.on_left_change(self.get_left_idx_fn() - 1) - elseif x < left_pos+3 then - self.is_dragging_target = 'left' - self.is_dragging_idx = x - left_pos - elseif x < right_pos then - self.is_dragging_target = 'both' - self.is_dragging_idx = x - left_pos - elseif x < right_pos+3 then - self.is_dragging_target = 'right' - self.is_dragging_idx = x - right_pos - else - self.on_right_change(self.get_right_idx_fn() + 1) - end - return true -end - -local function rangeslider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = self.is_dragging_target == 'right' and -2 or 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - local new_left_idx, new_right_idx - if self.is_dragging_target == 'right' then - new_right_idx = new_idx - else - new_left_idx = new_idx - if self.is_dragging_target == 'both' then - new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() - if new_right_idx > self.num_stops then - return - end - end - end - if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then - if not new_right_idx and new_left_idx > self.get_right_idx_fn() then - self.on_right_change(new_left_idx) - end - self.on_left_change(new_left_idx) - end - if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then - if new_right_idx < self.get_left_idx_fn() then - self.on_left_change(new_right_idx) - end - self.on_right_change(new_right_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function RangeSlider:onRenderBody(dc, rect) - local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() - local width_per_idx = rangeslider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx > stop_idx or right_idx < stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif right_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if right_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tabs - dc:seek(width_per_idx*(left_idx-1)) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - dc:seek(width_per_idx*(right_idx-1)+4) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - rangeslider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - --------------------------------- --- DimensionsTooltip --------------------------------- - ----@class widgets.DimensionsTooltip.attrs: widgets.ResizingPanel.attrs ----@field display_offset? df.coord2d ----@field get_anchor_pos_fn fun(): df.coord? - ----@class widgets.DimensionsTooltip: widgets.ResizingPanel ----@field super widgets.ResizingPanel ----@overload fun(init_table: widgets.DimensionsTooltip.attrs): self -DimensionsTooltip = defclass(DimensionsTooltip, ResizingPanel) - -DimensionsTooltip.ATTRS{ - frame_style=gui.FRAME_THIN, - frame_background=gui.CLEAR_PEN, - no_force_pause_badge=true, - auto_width=true, - display_offset={x=3, y=3}, - get_anchor_pos_fn=DEFAULT_NIL, -} - -local function get_cur_area_dims(anchor_pos) - local mouse_pos = dfhack.gui.getMousePos(true) - if not mouse_pos or not anchor_pos then return 1, 1, 1 end - - -- clamp to map edges (you can start a selection from out of bounds) - mouse_pos = xyz2pos( - math.max(0, math.min(df.global.world.map.x_count-1, mouse_pos.x)), - math.max(0, math.min(df.global.world.map.y_count-1, mouse_pos.y)), - math.max(0, math.min(df.global.world.map.z_count-1, mouse_pos.z))) - anchor_pos = xyz2pos( - math.max(0, math.min(df.global.world.map.x_count-1, anchor_pos.x)), - math.max(0, math.min(df.global.world.map.y_count-1, anchor_pos.y)), - math.max(0, math.min(df.global.world.map.z_count-1, anchor_pos.z))) - - return math.abs(mouse_pos.x - anchor_pos.x) + 1, - math.abs(mouse_pos.y - anchor_pos.y) + 1, - math.abs(mouse_pos.z - anchor_pos.z) + 1 -end - -function DimensionsTooltip:init() - ensure_key(self, 'frame').w = 17 - self.frame.h = 4 - - self.label = Label{ - frame={t=0}, - auto_width=true, - text={ - { - text=function() - local anchor_pos = self.get_anchor_pos_fn() - return ('%dx%dx%d'):format(get_cur_area_dims(anchor_pos)) - end - } - }, - } - - self:addviews{ - Panel{ - -- set minimum size for tooltip frame so the DFHack frame badge fits - frame={t=0, l=0, w=7, h=2}, - }, - self.label, - } -end - -function DimensionsTooltip:render(dc) - local x, y = dfhack.screen.getMousePos() - if not x or not self.get_anchor_pos_fn() then return end - local sw, sh = dfhack.screen.getWindowSize() - local frame_width = math.max(9, self.label:getTextWidth() + 2) - self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) - self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) - self:updateLayout() - DimensionsTooltip.super.render(self, dc) -end +DOUBLE_CLICK_MS = Panel.DOUBLE_CLICK_MS +STANDARDSCROLL = Scrollbar.STANDARDSCROLL +SCROLL_INITIAL_DELAY_MS = Scrollbar.SCROLL_INITIAL_DELAY_MS +SCROLL_DELAY_MS = Scrollbar.SCROLL_DELAY_MS return _ENV diff --git a/library/lua/gui/widgets/dimensions_tooltip.lua b/library/lua/gui/widgets/dimensions_tooltip.lua new file mode 100644 index 0000000000..0b23e3a416 --- /dev/null +++ b/library/lua/gui/widgets/dimensions_tooltip.lua @@ -0,0 +1,83 @@ +local gui = require('gui') +local ResizingPanel = require('gui.widgets.resizing_panel') +local Panel = require('gui.widgets.panel') + +-------------------------------- +-- DimensionsTooltip +-------------------------------- + +---@class widgets.DimensionsTooltip.attrs: widgets.ResizingPanel.attrs +---@field display_offset? df.coord2d +---@field get_anchor_pos_fn fun(): df.coord? + +---@class widgets.DimensionsTooltip: widgets.ResizingPanel +---@field super widgets.ResizingPanel +---@overload fun(init_table: widgets.DimensionsTooltip.attrs): self +DimensionsTooltip = defclass(DimensionsTooltip, ResizingPanel) + +DimensionsTooltip.ATTRS{ + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, + no_force_pause_badge=true, + auto_width=true, + display_offset={x=3, y=3}, + get_anchor_pos_fn=DEFAULT_NIL, +} + +local function get_cur_area_dims(anchor_pos) + local mouse_pos = dfhack.gui.getMousePos(true) + if not mouse_pos or not anchor_pos then return 1, 1, 1 end + + -- clamp to map edges (you can start a selection from out of bounds) + mouse_pos = xyz2pos( + math.max(0, math.min(df.global.world.map.x_count-1, mouse_pos.x)), + math.max(0, math.min(df.global.world.map.y_count-1, mouse_pos.y)), + math.max(0, math.min(df.global.world.map.z_count-1, mouse_pos.z))) + anchor_pos = xyz2pos( + math.max(0, math.min(df.global.world.map.x_count-1, anchor_pos.x)), + math.max(0, math.min(df.global.world.map.y_count-1, anchor_pos.y)), + math.max(0, math.min(df.global.world.map.z_count-1, anchor_pos.z))) + + return math.abs(mouse_pos.x - anchor_pos.x) + 1, + math.abs(mouse_pos.y - anchor_pos.y) + 1, + math.abs(mouse_pos.z - anchor_pos.z) + 1 +end + +function DimensionsTooltip:init() + ensure_key(self, 'frame').w = 17 + self.frame.h = 4 + + self.label = Label{ + frame={t=0}, + auto_width=true, + text={ + { + text=function() + local anchor_pos = self.get_anchor_pos_fn() + return ('%dx%dx%d'):format(get_cur_area_dims(anchor_pos)) + end + } + }, + } + + self:addviews{ + Panel{ + -- set minimum size for tooltip frame so the DFHack frame badge fits + frame={t=0, l=0, w=7, h=2}, + }, + self.label, + } +end + +function DimensionsTooltip:render(dc) + local x, y = dfhack.screen.getMousePos() + if not x or not self.get_anchor_pos_fn() then return end + local sw, sh = dfhack.screen.getWindowSize() + local frame_width = math.max(9, self.label:getTextWidth() + 2) + self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) + self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) + self:updateLayout() + DimensionsTooltip.super.render(self, dc) +end + +return DimensionsTooltip diff --git a/library/lua/gui/widgets/filtered_list.lua b/library/lua/gui/widgets/filtered_list.lua new file mode 100644 index 0000000000..20d67881d3 --- /dev/null +++ b/library/lua/gui/widgets/filtered_list.lua @@ -0,0 +1,226 @@ +local utils = require('utils') +local Widget = require('gui.widgets.widget') +local EditField = require('gui.widgets.edit_field') +local List = require('gui.widgets.list') + +local getval = utils.getval + +---@param tab? table +---@param idx integer +---@return any|integer +local function map_opttab(tab,idx) + if tab then + return tab[idx] + else + return idx + end +end + +------------------- +-- Filtered List -- +------------------- + + +---@class widgets.FilteredList.attrs: widgets.Widget.attrs +---@field choices widgets.ListChoice[] +---@field selected? integer +---@field edit_pen dfhack.color|dfhack.pen +---@field edit_below boolean +---@field edit_key? string +---@field edit_ignore_keys? string[] +---@field edit_on_char? function +---@field edit_on_change? function +---@field list widgets.List +---@field edit widgets.EditField +---@field not_found widgets.Label + +---@class widgets.FilteredList.attrs.partial: widgets.FilteredList.attrs + +---@class widgets.FilteredList.initTable: widgets.FilteredList.attrs.partial, widgets.List.attrs.partial, widgets.EditField.attrs.partial +---@field not_found_label? string + +---@class widgets.FilteredList: widgets.Widget, widgets.FilteredList.attrs +---@field super widgets.Widget +---@field ATTRS widgets.FilteredList.attrs|fun(attributes: widgets.FilteredList.attrs.partial) +---@overload fun(init_table: widgets.FilteredList.initTable): self +FilteredList = defclass(FilteredList, Widget) + +FilteredList.ATTRS { + edit_below = false, + edit_key = DEFAULT_NIL, + edit_ignore_keys = DEFAULT_NIL, + edit_on_char = DEFAULT_NIL, + edit_on_change = DEFAULT_NIL, +} + +---@param self widgets.FilteredList +---@param info widgets.FilteredList.initTable +function FilteredList:init(info) + local on_change = self:callback('onFilterChange') + if self.edit_on_change then + on_change = function(text) + self.edit_on_change(text) + self:onFilterChange(text) + end + end + + self.edit = EditField{ + text_pen = info.edit_pen or info.cursor_pen, + frame = { l = info.icon_width, t = 0, h = 1 }, + on_change = on_change, + on_char = self.edit_on_char, + key = self.edit_key, + ignore_keys = self.edit_ignore_keys, + } + self.list = List{ + frame = { t = 2 }, + text_pen = info.text_pen, + cursor_pen = info.cursor_pen, + inactive_pen = info.inactive_pen, + icon_pen = info.icon_pen, + row_height = info.row_height, + scroll_keys = info.scroll_keys, + icon_width = info.icon_width, + } + if self.edit_below then + self.edit.frame = { l = info.icon_width, b = 0, h = 1 } + self.list.frame = { t = 0, b = 2 } + end + if info.on_select then + self.list.on_select = function() + return info.on_select(self:getSelected()) + end + end + if info.on_submit then + self.list.on_submit = function() + return info.on_submit(self:getSelected()) + end + end + if info.on_submit2 then + self.list.on_submit2 = function() + return info.on_submit2(self:getSelected()) + end + end + if info.on_double_click then + self.list.on_double_click = function() + return info.on_double_click(self:getSelected()) + end + end + if info.on_double_click2 then + self.list.on_double_click2 = function() + return info.on_double_click2(self:getSelected()) + end + end + self.not_found = Label{ + visible = true, + text = info.not_found_label or 'No matches', + text_pen = COLOR_LIGHTRED, + frame = { l = info.icon_width, t = self.list.frame.t }, + } + self:addviews{ self.edit, self.list, self.not_found } + if info.choices then + self:setChoices(info.choices, info.selected) + else + self.choices = {} + end +end + +function FilteredList:getChoices() + return self.choices +end + +function FilteredList:getVisibleChoices() + return self.list.choices +end + +function FilteredList:setChoices(choices, pos) + choices = choices or {} + self.edit:setText('') + self.list:setChoices(choices, pos) + self.choices = self.list.choices + self.not_found.visible = (#choices == 0) +end + +function FilteredList:submit() + return self.list:submit() +end + +function FilteredList:submit2() + return self.list:submit2() +end + +function FilteredList:canSubmit() + return not self.not_found.visible +end + +function FilteredList:getSelected() + local i,v = self.list:getSelected() + if i then + return map_opttab(self.choice_index, i), v + end +end + +function FilteredList:getContentWidth() + return self.list:getContentWidth() +end + +function FilteredList:getContentHeight() + return self.list:getContentHeight() + 2 +end + +function FilteredList:getFilter() + return self.edit.text, self.list.choices +end + +function FilteredList:setFilter(filter, pos) + local choices = self.choices + local cidx = nil + + filter = filter or '' + if filter ~= self.edit.text then + self.edit:setText(filter) + end + + if filter ~= '' then + local tokens = filter:split() + local ipos = pos + + choices = {} + cidx = {} + pos = nil + + for i,v in ipairs(self.choices) do + local search_key = v.search_key + if not search_key then + if type(v.text) ~= 'table' then + search_key = v.text + else + local texts = {} + for _,token in ipairs(v.text) do + table.insert(texts, + type(token) == 'string' and token + or getval(token.text) or '') + end + search_key = table.concat(texts, ' ') + end + end + if utils.search_text(search_key, tokens) then + table.insert(choices, v) + cidx[#choices] = i + if ipos == i then + pos = #choices + end + end + end + end + + self.choice_index = cidx + self.list:setChoices(choices, pos) + self.not_found.visible = (#choices == 0) +end + +function FilteredList:onFilterChange(text) + self:setFilter(text) +end + +return FilteredList diff --git a/library/lua/gui/widgets/hotkey_label.lua b/library/lua/gui/widgets/hotkey_label.lua index 29c0233a2f..1748ba73a2 100644 --- a/library/lua/gui/widgets/hotkey_label.lua +++ b/library/lua/gui/widgets/hotkey_label.lua @@ -1,5 +1,8 @@ +local utils = require('utils') local Label = require('gui.widgets.label') +local getval = utils.getval + local function is_disabled(token) return (token.disabled ~= nil and getval(token.disabled)) or (token.enabled ~= nil and not getval(token.enabled)) diff --git a/library/lua/gui/widgets/list.lua b/library/lua/gui/widgets/list.lua index f56f89ea96..42eef6a904 100644 --- a/library/lua/gui/widgets/list.lua +++ b/library/lua/gui/widgets/list.lua @@ -2,8 +2,11 @@ local gui = require('gui') local utils = require('utils') local Widget = require('gui.widgets.widget') local Scrollbar = require('gui.widgets.scrollbar') +local Label = require('gui.widgets.label') +local Panel = require('gui.widgets.panel') local getval = utils.getval +local to_pen = dfhack.pen.parse ---------- -- List -- @@ -92,7 +95,7 @@ function List:setChoices(choices, selected) else l.text = v.text or v.caption or v[1] end - parse_label_text(l) + Label.parse_label_text(l) self.choices[i] = l end @@ -125,7 +128,7 @@ end function List:getContentWidth() local width = 0 for i,v in ipairs(self.choices) do - render_text(v) + Label.render_text(v) local roww = v.text_width if v.key then roww = roww + 3 + #gui.getKeyDisplay(v.key) @@ -271,7 +274,7 @@ function List:onRenderBody(dc) paint_icon(icon, obj) end - render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) + Label.render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) local ip = dc.width @@ -337,7 +340,7 @@ function List:onInput(keys) if idx ~= self:getSelected() then self.last_select_click_ms = now_ms else - if now_ms - self.last_select_click_ms <= DOUBLE_CLICK_MS then + if now_ms - self.last_select_click_ms <= Panel.DOUBLE_CLICK_MS then self.last_select_click_ms = 0 if self:double_click() then return true end else @@ -377,7 +380,7 @@ function List:onInput(keys) local current = self.choices[self.selected] if current then - return check_text_keys(current, keys) + return Label.check_text_keys(current, keys) end end end diff --git a/library/lua/gui/widgets/panel.lua b/library/lua/gui/widgets/panel.lua index 3e84265210..29822b3ce6 100644 --- a/library/lua/gui/widgets/panel.lua +++ b/library/lua/gui/widgets/panel.lua @@ -1,5 +1,6 @@ local gui = require('gui') local utils = require('utils') +local guidm = require('gui.dwarfmode') local Widget = require('gui.widgets.widget') local getval = utils.getval @@ -527,4 +528,6 @@ function Panel:onResizeEnd(success, new_frame) if self.on_resize_end then self.on_resize_end(success, new_frame) end end +Panel.DOUBLE_CLICK_MS = DOUBLE_CLICK_MS + return Panel diff --git a/library/lua/gui/widgets/range_slider.lua b/library/lua/gui/widgets/range_slider.lua new file mode 100644 index 0000000000..bd1f9cb3de --- /dev/null +++ b/library/lua/gui/widgets/range_slider.lua @@ -0,0 +1,164 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- RangeSlider +-------------------------------- + +---@class widgets.RangeSlider.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field get_left_idx_fn? function +---@field get_right_idx_fn? function +---@field on_left_change? fun(index: integer) +---@field on_right_change? fun(index: integer) + +---@class widgets.RangeSlider.attrs.partial: widgets.RangeSlider.attrs + +---@class widgets.RangeSlider.initTable: widgets.RangeSlider.attrs +---@field num_stops integer + +---@class widgets.RangeSlider: widgets.Widget, widgets.RangeSlider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.RangeSlider.attrs|fun(attributes: widgets.RangeSlider.attrs.partial) +---@overload fun(init_table: widgets.RangeSlider.initTable): self +RangeSlider = defclass(RangeSlider, Widget) +RangeSlider.ATTRS{ + num_stops=DEFAULT_NIL, + get_left_idx_fn=DEFAULT_NIL, + get_right_idx_fn=DEFAULT_NIL, + on_left_change=DEFAULT_NIL, + on_right_change=DEFAULT_NIL, +} + +function RangeSlider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function RangeSlider:init() + if self.num_stops < 2 then error('too few RangeSlider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function rangeslider_get_width_per_idx(self) + return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function RangeSlider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = rangeslider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(right_idx-1) + 4 + if x < left_pos then + self.on_left_change(self.get_left_idx_fn() - 1) + elseif x < left_pos+3 then + self.is_dragging_target = 'left' + self.is_dragging_idx = x - left_pos + elseif x < right_pos then + self.is_dragging_target = 'both' + self.is_dragging_idx = x - left_pos + elseif x < right_pos+3 then + self.is_dragging_target = 'right' + self.is_dragging_idx = x - right_pos + else + self.on_right_change(self.get_right_idx_fn() + 1) + end + return true +end + +local function rangeslider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = self.is_dragging_target == 'right' and -2 or 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + local new_left_idx, new_right_idx + if self.is_dragging_target == 'right' then + new_right_idx = new_idx + else + new_left_idx = new_idx + if self.is_dragging_target == 'both' then + new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() + if new_right_idx > self.num_stops then + return + end + end + end + if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then + if not new_right_idx and new_left_idx > self.get_right_idx_fn() then + self.on_right_change(new_left_idx) + end + self.on_left_change(new_left_idx) + end + if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then + if new_right_idx < self.get_left_idx_fn() then + self.on_left_change(new_right_idx) + end + self.on_right_change(new_right_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function RangeSlider:onRenderBody(dc, rect) + local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() + local width_per_idx = rangeslider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx > stop_idx or right_idx < stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif right_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if right_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tabs + dc:seek(width_per_idx*(left_idx-1)) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + dc:seek(width_per_idx*(right_idx-1)+4) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + rangeslider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return RangeSlider diff --git a/library/lua/gui/widgets/scrollbar.lua b/library/lua/gui/widgets/scrollbar.lua index 3307cfd36e..d9d5ea6a23 100644 --- a/library/lua/gui/widgets/scrollbar.lua +++ b/library/lua/gui/widgets/scrollbar.lua @@ -32,8 +32,6 @@ SCROLL_DELAY_MS = 20 ---@overload fun(init_table: widgets.Scrollbar.attrs.partial): self Scrollbar = defclass(Scrollbar, Widget) -Scrollbar.STANDARDSCROLL = STANDARDSCROLL - Scrollbar.ATTRS{ on_scroll = DEFAULT_NIL, } @@ -267,4 +265,8 @@ function Scrollbar:onInput(keys) return true end +Scrollbar.STANDARDSCROLL = STANDARDSCROLL +Scrollbar.SCROLL_INITIAL_DELAY_MS = SCROLL_INITIAL_DELAY_MS +Scrollbar.SCROLL_DELAY_MS = SCROLL_DELAY_MS + return Scrollbar diff --git a/library/lua/gui/widgets/tab_bar.lua b/library/lua/gui/widgets/tab_bar.lua new file mode 100644 index 0000000000..6676890175 --- /dev/null +++ b/library/lua/gui/widgets/tab_bar.lua @@ -0,0 +1,210 @@ +local Widget = require('gui.widgets.widget') +local ResizingPanel = require('gui.widgets.resizing_panel') + +local to_pen = dfhack.pen.parse + +---@class widgets.TabPens +---@field text_mode_tab_pen dfhack.pen +---@field text_mode_label_pen dfhack.pen +---@field lt dfhack.pen +---@field lt2 dfhack.pen +---@field t dfhack.pen +---@field rt2 dfhack.pen +---@field rt dfhack.pen +---@field lb dfhack.pen +---@field lb2 dfhack.pen +---@field b dfhack.pen +---@field rb2 dfhack.pen +---@field rb dfhack.pen + +local TSO = df.global.init.tabs_texpos[0] -- tab spritesheet offset +local DEFAULT_ACTIVE_TAB_PENS = { + text_mode_tab_pen=to_pen{fg=COLOR_YELLOW}, + text_mode_label_pen=to_pen{fg=COLOR_WHITE}, + lt=to_pen{tile=TSO+5, write_to_lower=true}, + lt2=to_pen{tile=TSO+6, write_to_lower=true}, + t=to_pen{tile=TSO+7, fg=COLOR_BLACK, write_to_lower=true, top_of_text=true}, + rt2=to_pen{tile=TSO+8, write_to_lower=true}, + rt=to_pen{tile=TSO+9, write_to_lower=true}, + lb=to_pen{tile=TSO+15, write_to_lower=true}, + lb2=to_pen{tile=TSO+16, write_to_lower=true}, + b=to_pen{tile=TSO+17, fg=COLOR_BLACK, write_to_lower=true, bottom_of_text=true}, + rb2=to_pen{tile=TSO+18, write_to_lower=true}, + rb=to_pen{tile=TSO+19, write_to_lower=true}, +} + +local DEFAULT_INACTIVE_TAB_PENS = { + text_mode_tab_pen=to_pen{fg=COLOR_BROWN}, + text_mode_label_pen=to_pen{fg=COLOR_DARKGREY}, + lt=to_pen{tile=TSO+0, write_to_lower=true}, + lt2=to_pen{tile=TSO+1, write_to_lower=true}, + t=to_pen{tile=TSO+2, fg=COLOR_WHITE, write_to_lower=true, top_of_text=true}, + rt2=to_pen{tile=TSO+3, write_to_lower=true}, + rt=to_pen{tile=TSO+4, write_to_lower=true}, + lb=to_pen{tile=TSO+10, write_to_lower=true}, + lb2=to_pen{tile=TSO+11, write_to_lower=true}, + b=to_pen{tile=TSO+12, fg=COLOR_WHITE, write_to_lower=true, bottom_of_text=true}, + rb2=to_pen{tile=TSO+13, write_to_lower=true}, + rb=to_pen{tile=TSO+14, write_to_lower=true}, +} + +--------- +-- Tab -- +--------- + +---@class widgets.Tab.attrs: widgets.Widget.attrs +---@field id? string|integer +---@field label string +---@field on_select? function +---@field get_pens? fun(): widgets.TabPens + +---@class widgets.Tab.attrs.partial: widgets.Tab.attrs + +---@class widgets.Tab.initTable: widgets.Tab.attrs +---@field label string + +---@class widgets.Tab: widgets.Widget, widgets.Tab.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Tab.attrs|fun(attributes: widgets.Tab.attrs.partial) +---@overload fun(init_table: widgets.Tab.initTable): self +Tab = defclass(Tabs, Widget) +Tab.ATTRS{ + id=DEFAULT_NIL, + label=DEFAULT_NIL, + on_select=DEFAULT_NIL, + get_pens=DEFAULT_NIL, +} + +function Tab:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.w = #init_table.label + 4 + init_table.frame.h = 2 +end + +function Tab:onRenderBody(dc) + local pens = self.get_pens() + dc:seek(0, 0) + if dfhack.screen.inGraphicsMode() then + dc:char(nil, pens.lt):char(nil, pens.lt2) + for i=1,#self.label do + dc:char(self.label:sub(i,i), pens.t) + end + dc:char(nil, pens.rt2):char(nil, pens.rt) + dc:seek(0, 1) + dc:char(nil, pens.lb):char(nil, pens.lb2) + for i=1,#self.label do + dc:char(self.label:sub(i,i), pens.b) + end + dc:char(nil, pens.rb2):char(nil, pens.rb) + else + local tp = pens.text_mode_tab_pen + dc:char(' ', tp):char('/', tp) + for i=1,#self.label do + dc:char('-', tp) + end + dc:char('\\', tp):char(' ', tp) + dc:seek(0, 1) + dc:char('/', tp):char('-', tp) + dc:string(self.label, pens.text_mode_label_pen) + dc:char('-', tp):char('\\', tp) + end +end + +function Tab:onInput(keys) + if Tab.super.onInput(self, keys) then return true end + if keys._MOUSE_L and self:getMousePos() then + self.on_select(self.id) + return true + end +end + +------------- +-- Tab Bar -- +------------- + +---@class widgets.TabBar.attrs: widgets.ResizingPanel.attrs +---@field labels string[] +---@field on_select? function +---@field get_cur_page? function +---@field active_tab_pens widgets.TabPens +---@field inactive_tab_pens widgets.TabPens +---@field get_pens? fun(index: integer, tabbar: self): widgets.TabPens +---@field key string +---@field key_back string + +---@class widgets.TabBar.attrs.partial: widgets.TabBar.attrs + +---@class widgets.TabBar.initTable: widgets.TabBar.attrs +---@field labels string[] + +---@class widgets.TabBar: widgets.ResizingPanel, widgets.TabBar.attrs +---@field super widgets.ResizingPanel +---@field ATTRS widgets.TabBar.attrs|fun(attribute: widgets.TabBar.attrs.partial) +---@overload fun(init_table: widgets.TabBar.initTable): self +TabBar = defclass(TabBar, ResizingPanel) +TabBar.ATTRS{ + labels=DEFAULT_NIL, + on_select=DEFAULT_NIL, + get_cur_page=DEFAULT_NIL, + active_tab_pens=DEFAULT_ACTIVE_TAB_PENS, + inactive_tab_pens=DEFAULT_INACTIVE_TAB_PENS, + get_pens=DEFAULT_NIL, + key='CUSTOM_CTRL_T', + key_back='CUSTOM_CTRL_Y', +} + +---@param self widgets.TabBar +function TabBar:init() + for idx,label in ipairs(self.labels) do + self:addviews{ + Tab{ + frame={t=0, l=0}, + id=idx, + label=label, + on_select=self.on_select, + get_pens=self.get_pens and function() + return self.get_pens(idx, self) + end or function() + if self.get_cur_page() == idx then + return self.active_tab_pens + end + + return self.inactive_tab_pens + end, + } + } + end +end + +function TabBar:postComputeFrame(body) + local t, l, width = 0, 0, body.width + for _,tab in ipairs(self.subviews) do + if l > 0 and l + tab.frame.w > width then + t = t + 2 + l = 0 + end + tab.frame.t = t + tab.frame.l = l + l = l + tab.frame.w + end +end + +function TabBar:onInput(keys) + if TabBar.super.onInput(self, keys) then return true end + if self.key and keys[self.key] then + local zero_idx = self.get_cur_page() - 1 + local next_zero_idx = (zero_idx + 1) % #self.labels + self.on_select(next_zero_idx + 1) + return true + end + if self.key_back and keys[self.key_back] then + local zero_idx = self.get_cur_page() - 1 + local prev_zero_idx = (zero_idx - 1) % #self.labels + self.on_select(prev_zero_idx + 1) + return true + end +end + +TabBar.Tab = Tab + +return TabBar From 0dce152d5a153dab2c1265a3b21d13fd584888cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 22 Sep 2024 10:44:08 +0200 Subject: [PATCH 08/34] Refactor (split) widgets into containers, labels and buttons --- library/lua/gui/widgets.lua | 32 +++++++++---------- .../widgets/{ => buttons}/button_group.lua | 4 +-- .../{ => buttons}/configure_button.lua | 4 +-- .../gui/widgets/{ => buttons}/help_button.lua | 4 +-- .../gui/widgets/{ => buttons}/text_button.lua | 4 +-- .../widgets/{ => containers}/banner_panel.lua | 2 +- .../gui/widgets/{ => containers}/pages.lua | 2 +- .../gui/widgets/{ => containers}/panel.lua | 0 .../{ => containers}/resizing_panel.lua | 2 +- .../gui/widgets/{ => containers}/tab_bar.lua | 2 +- .../gui/widgets/{ => containers}/window.lua | 2 +- .../lua/gui/widgets/dimensions_tooltip.lua | 4 +-- library/lua/gui/widgets/edit_field.lua | 2 +- .../{ => labels}/cycle_hotkey_label.lua | 2 +- .../gui/widgets/{ => labels}/hotkey_label.lua | 2 +- .../lua/gui/widgets/{ => labels}/label.lua | 0 .../{ => labels}/toggle_hotkey_label.lua | 2 +- .../widgets/{ => labels}/tooltip_label.lua | 2 +- .../widgets/{ => labels}/wrapped_label.lua | 2 +- library/lua/gui/widgets/list.lua | 4 +-- 20 files changed, 39 insertions(+), 39 deletions(-) rename library/lua/gui/widgets/{ => buttons}/button_group.lua (92%) rename library/lua/gui/widgets/{ => buttons}/configure_button.lua (93%) rename library/lua/gui/widgets/{ => buttons}/help_button.lua (93%) rename library/lua/gui/widgets/{ => buttons}/text_button.lua (89%) rename library/lua/gui/widgets/{ => containers}/banner_panel.lua (88%) rename library/lua/gui/widgets/{ => containers}/pages.lua (96%) rename library/lua/gui/widgets/{ => containers}/panel.lua (100%) rename library/lua/gui/widgets/{ => containers}/resizing_panel.lua (97%) rename library/lua/gui/widgets/{ => containers}/tab_bar.lua (98%) rename library/lua/gui/widgets/{ => containers}/window.lua (93%) rename library/lua/gui/widgets/{ => labels}/cycle_hotkey_label.lua (98%) rename library/lua/gui/widgets/{ => labels}/hotkey_label.lua (97%) rename library/lua/gui/widgets/{ => labels}/label.lua (100%) rename library/lua/gui/widgets/{ => labels}/toggle_hotkey_label.lua (84%) rename library/lua/gui/widgets/{ => labels}/tooltip_label.lua (92%) rename library/lua/gui/widgets/{ => labels}/wrapped_label.lua (97%) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 37bb57003d..c35dd3b095 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -4,26 +4,26 @@ local _ENV = mkmodule('gui.widgets') Widget = require('gui.widgets.widget') Divider = require('gui.widgets.divider') -Panel = require('gui.widgets.panel') -Window = require('gui.widgets.window') -ResizingPanel = require('gui.widgets.resizing_panel') -Pages = require('gui.widgets.pages') +Panel = require('gui.widgets.containers.panel') +Window = require('gui.widgets.containers.window') +ResizingPanel = require('gui.widgets.containers.resizing_panel') +Pages = require('gui.widgets.containers.pages') EditField = require('gui.widgets.edit_field') -HotkeyLabel = require('gui.widgets.hotkey_label') -Label = require('gui.widgets.label') +HotkeyLabel = require('gui.widgets.labels.hotkey_label') +Label = require('gui.widgets.labels.label') Scrollbar = require('gui.widgets.scrollbar') -WrappedLabel = require('gui.widgets.wrapped_label') -TooltipLabel = require('gui.widgets.tooltip_label') -HelpButton = require('gui.widgets.help_button') -ConfigureButton = require('gui.widgets.configure_button') -BannerPanel = require('gui.widgets.banner_panel') -TextButton = require('gui.widgets.text_button') -CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') -ButtonGroup = require('gui.widgets.button_group') -ToggleHotkeyLabel = require('gui.widgets.toggle_hotkey_label') +WrappedLabel = require('gui.widgets.labels.wrapped_label') +TooltipLabel = require('gui.widgets.labels.tooltip_label') +HelpButton = require('gui.widgets.buttons.help_button') +ConfigureButton = require('gui.widgets.buttons.configure_button') +BannerPanel = require('gui.widgets.containers.banner_panel') +TextButton = require('gui.widgets.buttons.text_button') +CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') +ButtonGroup = require('gui.widgets.buttons.button_group') +ToggleHotkeyLabel = require('gui.widgets.labels.toggle_hotkey_label') List = require('gui.widgets.list') FilteredList = require('gui.widgets.filtered_list') -TabBar = require('gui.widgets.tab_bar') +TabBar = require('gui.widgets.containers.tab_bar') RangeSlider = require('gui.widgets.range_slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') diff --git a/library/lua/gui/widgets/button_group.lua b/library/lua/gui/widgets/buttons/button_group.lua similarity index 92% rename from library/lua/gui/widgets/button_group.lua rename to library/lua/gui/widgets/buttons/button_group.lua index 34931ac613..033f498472 100644 --- a/library/lua/gui/widgets/button_group.lua +++ b/library/lua/gui/widgets/buttons/button_group.lua @@ -1,5 +1,5 @@ -local CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') -local Label = require('gui.widgets.label') +local CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') +local Label = require('gui.widgets.labels.label') ----------------- -- ButtonGroup -- diff --git a/library/lua/gui/widgets/configure_button.lua b/library/lua/gui/widgets/buttons/configure_button.lua similarity index 93% rename from library/lua/gui/widgets/configure_button.lua rename to library/lua/gui/widgets/buttons/configure_button.lua index 2a86bc0ac4..b93b1e18b2 100644 --- a/library/lua/gui/widgets/configure_button.lua +++ b/library/lua/gui/widgets/buttons/configure_button.lua @@ -1,6 +1,6 @@ local textures = require('gui.textures') -local Panel = require('gui.widgets.panel') -local Label = require('gui.widgets.label') +local Panel = require('gui.widgets.containers.panel') +local Label = require('gui.widgets.labels.label') local to_pen = dfhack.pen.parse diff --git a/library/lua/gui/widgets/help_button.lua b/library/lua/gui/widgets/buttons/help_button.lua similarity index 93% rename from library/lua/gui/widgets/help_button.lua rename to library/lua/gui/widgets/buttons/help_button.lua index 57aa195294..9f9b7dc989 100644 --- a/library/lua/gui/widgets/help_button.lua +++ b/library/lua/gui/widgets/buttons/help_button.lua @@ -1,6 +1,6 @@ local textures = require('gui.textures') -local Panel = require('gui.widgets.panel') -local Label = require('gui.widgets.label') +local Panel = require('gui.widgets.containers.panel') +local Label = require('gui.widgets.labels.label') local to_pen = dfhack.pen.parse diff --git a/library/lua/gui/widgets/text_button.lua b/library/lua/gui/widgets/buttons/text_button.lua similarity index 89% rename from library/lua/gui/widgets/text_button.lua rename to library/lua/gui/widgets/buttons/text_button.lua index 51ed839c61..2fa7928be9 100644 --- a/library/lua/gui/widgets/text_button.lua +++ b/library/lua/gui/widgets/buttons/text_button.lua @@ -1,5 +1,5 @@ -local BannerPanel = require('gui.widgets.banner_panel') -local HotkeyLabel = require('gui.widgets.hotkey_label') +local BannerPanel = require('gui.widgets.containers.banner_panel') +local HotkeyLabel = require('gui.widgets.labels.hotkey_label') ---------------- -- TextButton -- diff --git a/library/lua/gui/widgets/banner_panel.lua b/library/lua/gui/widgets/containers/banner_panel.lua similarity index 88% rename from library/lua/gui/widgets/banner_panel.lua rename to library/lua/gui/widgets/containers/banner_panel.lua index 059f70c74f..68d78d74e8 100644 --- a/library/lua/gui/widgets/banner_panel.lua +++ b/library/lua/gui/widgets/containers/banner_panel.lua @@ -1,4 +1,4 @@ -local Panel = require('gui.widgets.panel') +local Panel = require('gui.widgets.containers.panel') ----------------- -- BannerPanel -- diff --git a/library/lua/gui/widgets/pages.lua b/library/lua/gui/widgets/containers/pages.lua similarity index 96% rename from library/lua/gui/widgets/pages.lua rename to library/lua/gui/widgets/containers/pages.lua index 70f0d29e7b..73b82a5da5 100644 --- a/library/lua/gui/widgets/pages.lua +++ b/library/lua/gui/widgets/containers/pages.lua @@ -1,6 +1,6 @@ local gui = require('gui') local utils = require('utils') -local Panel = require('gui.widgets.panel') +local Panel = require('gui.widgets.containers.panel') ---@param view gui.View ---@param vis boolean diff --git a/library/lua/gui/widgets/panel.lua b/library/lua/gui/widgets/containers/panel.lua similarity index 100% rename from library/lua/gui/widgets/panel.lua rename to library/lua/gui/widgets/containers/panel.lua diff --git a/library/lua/gui/widgets/resizing_panel.lua b/library/lua/gui/widgets/containers/resizing_panel.lua similarity index 97% rename from library/lua/gui/widgets/resizing_panel.lua rename to library/lua/gui/widgets/containers/resizing_panel.lua index 5791a8518f..b20268ba08 100644 --- a/library/lua/gui/widgets/resizing_panel.lua +++ b/library/lua/gui/widgets/containers/resizing_panel.lua @@ -1,6 +1,6 @@ local gui = require('gui') local utils = require('utils') -local Panel = require('gui.widgets.panel') +local Panel = require('gui.widgets.containers.panel') local getval = utils.getval diff --git a/library/lua/gui/widgets/tab_bar.lua b/library/lua/gui/widgets/containers/tab_bar.lua similarity index 98% rename from library/lua/gui/widgets/tab_bar.lua rename to library/lua/gui/widgets/containers/tab_bar.lua index 6676890175..aedc214433 100644 --- a/library/lua/gui/widgets/tab_bar.lua +++ b/library/lua/gui/widgets/containers/tab_bar.lua @@ -1,5 +1,5 @@ local Widget = require('gui.widgets.widget') -local ResizingPanel = require('gui.widgets.resizing_panel') +local ResizingPanel = require('gui.widgets.containers.resizing_panel') local to_pen = dfhack.pen.parse diff --git a/library/lua/gui/widgets/window.lua b/library/lua/gui/widgets/containers/window.lua similarity index 93% rename from library/lua/gui/widgets/window.lua rename to library/lua/gui/widgets/containers/window.lua index d446900797..941ad8cc48 100644 --- a/library/lua/gui/widgets/window.lua +++ b/library/lua/gui/widgets/containers/window.lua @@ -1,5 +1,5 @@ local gui = require('gui') -local Panel = require('gui.widgets.panel') +local Panel = require('gui.widgets.containers.panel') ------------ -- Window -- diff --git a/library/lua/gui/widgets/dimensions_tooltip.lua b/library/lua/gui/widgets/dimensions_tooltip.lua index 0b23e3a416..6114c26d58 100644 --- a/library/lua/gui/widgets/dimensions_tooltip.lua +++ b/library/lua/gui/widgets/dimensions_tooltip.lua @@ -1,6 +1,6 @@ local gui = require('gui') -local ResizingPanel = require('gui.widgets.resizing_panel') -local Panel = require('gui.widgets.panel') +local ResizingPanel = require('gui.widgets.containers.resizing_panel') +local Panel = require('gui.widgets.containers.panel') -------------------------------- -- DimensionsTooltip diff --git a/library/lua/gui/widgets/edit_field.lua b/library/lua/gui/widgets/edit_field.lua index 1d291a8be1..b4f861f466 100644 --- a/library/lua/gui/widgets/edit_field.lua +++ b/library/lua/gui/widgets/edit_field.lua @@ -1,7 +1,7 @@ local gui = require('gui') local utils = require('utils') local Widget = require('gui.widgets.widget') -local HotkeyLabel = require('gui.widgets.hotkey_label') +local HotkeyLabel = require('gui.widgets.labels.hotkey_label') local getval = utils.getval diff --git a/library/lua/gui/widgets/cycle_hotkey_label.lua b/library/lua/gui/widgets/labels/cycle_hotkey_label.lua similarity index 98% rename from library/lua/gui/widgets/cycle_hotkey_label.lua rename to library/lua/gui/widgets/labels/cycle_hotkey_label.lua index 846a36f7cd..df4bf38d59 100644 --- a/library/lua/gui/widgets/cycle_hotkey_label.lua +++ b/library/lua/gui/widgets/labels/cycle_hotkey_label.lua @@ -1,5 +1,5 @@ local utils = require('utils') -local Label = require('gui.widgets.label') +local Label = require('gui.widgets.labels.label') local getval = utils.getval diff --git a/library/lua/gui/widgets/hotkey_label.lua b/library/lua/gui/widgets/labels/hotkey_label.lua similarity index 97% rename from library/lua/gui/widgets/hotkey_label.lua rename to library/lua/gui/widgets/labels/hotkey_label.lua index 1748ba73a2..2070cba95f 100644 --- a/library/lua/gui/widgets/hotkey_label.lua +++ b/library/lua/gui/widgets/labels/hotkey_label.lua @@ -1,5 +1,5 @@ local utils = require('utils') -local Label = require('gui.widgets.label') +local Label = require('gui.widgets.labels.label') local getval = utils.getval diff --git a/library/lua/gui/widgets/label.lua b/library/lua/gui/widgets/labels/label.lua similarity index 100% rename from library/lua/gui/widgets/label.lua rename to library/lua/gui/widgets/labels/label.lua diff --git a/library/lua/gui/widgets/toggle_hotkey_label.lua b/library/lua/gui/widgets/labels/toggle_hotkey_label.lua similarity index 84% rename from library/lua/gui/widgets/toggle_hotkey_label.lua rename to library/lua/gui/widgets/labels/toggle_hotkey_label.lua index 07ff7f898f..d2e61cedb7 100644 --- a/library/lua/gui/widgets/toggle_hotkey_label.lua +++ b/library/lua/gui/widgets/labels/toggle_hotkey_label.lua @@ -1,4 +1,4 @@ -local CycleHotkeyLabel = require('gui.widgets.cycle_hotkey_label') +local CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label') ----------------------- -- ToggleHotkeyLabel -- diff --git a/library/lua/gui/widgets/tooltip_label.lua b/library/lua/gui/widgets/labels/tooltip_label.lua similarity index 92% rename from library/lua/gui/widgets/tooltip_label.lua rename to library/lua/gui/widgets/labels/tooltip_label.lua index 417c145b19..7d0ff7ca5d 100644 --- a/library/lua/gui/widgets/tooltip_label.lua +++ b/library/lua/gui/widgets/labels/tooltip_label.lua @@ -1,4 +1,4 @@ -local WrappedLabel = require('gui.widgets.wrapped_label') +local WrappedLabel = require('gui.widgets.labels.wrapped_label') ------------------ -- TooltipLabel -- diff --git a/library/lua/gui/widgets/wrapped_label.lua b/library/lua/gui/widgets/labels/wrapped_label.lua similarity index 97% rename from library/lua/gui/widgets/wrapped_label.lua rename to library/lua/gui/widgets/labels/wrapped_label.lua index f74a1e5c9a..f291b59a4d 100644 --- a/library/lua/gui/widgets/wrapped_label.lua +++ b/library/lua/gui/widgets/labels/wrapped_label.lua @@ -1,5 +1,5 @@ local utils = require('utils') -local Label = require('gui.widgets.label') +local Label = require('gui.widgets.labels.label') local getval = utils.getval diff --git a/library/lua/gui/widgets/list.lua b/library/lua/gui/widgets/list.lua index 42eef6a904..eaa6654b68 100644 --- a/library/lua/gui/widgets/list.lua +++ b/library/lua/gui/widgets/list.lua @@ -2,8 +2,8 @@ local gui = require('gui') local utils = require('utils') local Widget = require('gui.widgets.widget') local Scrollbar = require('gui.widgets.scrollbar') -local Label = require('gui.widgets.label') -local Panel = require('gui.widgets.panel') +local Label = require('gui.widgets.labels.label') +local Panel = require('gui.widgets.containers.panel') local getval = utils.getval local to_pen = dfhack.pen.parse From e299f4d6af2782735035a470760aabcc83af4372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Mon, 23 Sep 2024 14:16:35 +0200 Subject: [PATCH 09/34] adjust material size for melting for armor parts, weapons, trap components and tools to minimize differences between cost of forging and melting return, heavily inspired by adamantine-cloth-wear.h --- plugins/tweak/tweak.cpp | 11 ++ .../tweak/tweaks/material-size-for-melting.h | 106 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 plugins/tweak/tweaks/material-size-for-melting.h diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index e10a9535a3..0ea8069746 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -20,6 +20,7 @@ using namespace DFHack; #include "tweaks/named-codices.h" #include "tweaks/partial-items.h" #include "tweaks/reaction-gloves.h" +#include "tweaks/material-size-for-melting.h" class tweak_onupdate_hookst { public: @@ -74,6 +75,16 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector + +static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t base_material_size, float production_stack_size) { + + if (item->mat_type == 0) // INORGANIC only + { + const float melt_return_per_material_size = 0.3f; + auto inorganic = df::inorganic_raw::find(item->mat_index); + if (inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)){ + return static_cast(std::round(static_cast(base_material_size) / production_stack_size / melt_return_per_material_size)); + // get size for melting to minimize diff between forging cost and melting return, for adamantine forging cost == base_material_size, divided by amount of items created in batch + } + return static_cast(std::round(std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size / melt_return_per_material_size)); + // (std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size) - forging cost for non adamantine item + } + return base_material_size; +} + +static int32_t get_material_size_for_melting(df::item_constructed* item, int32_t base_size) { + return get_material_size_for_melting(item, base_size, 1.0f); +} + +struct material_size_for_melting_armor_hook : df::item_armorst { + typedef df::item_armorst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_armor_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_gloves_hook : df::item_glovesst { + typedef df::item_glovesst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(), 2.0f); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_gloves_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_shoes_hook : df::item_shoesst { + typedef df::item_shoesst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(), 2.0f); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_shoes_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_helm_hook : df::item_helmst { + typedef df::item_helmst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_helm_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_pants_hook : df::item_pantsst { + typedef df::item_pantsst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_pants_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_weapon_hook : df::item_weaponst { + typedef df::item_weaponst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_weapon_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_trapcomp_hook : df::item_trapcompst { + typedef df::item_trapcompst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_trapcomp_hook, getMaterialSizeForMelting); + +struct material_size_for_melting_tool_hook : df::item_toolst { + typedef df::item_toolst interpose_base; + + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); + } +}; +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_tool_hook, getMaterialSizeForMelting); From 41b67904fe2a34a40fa93b6ea909c0170907f8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Tue, 24 Sep 2024 21:47:54 +0200 Subject: [PATCH 10/34] Change how material size for melting is calculated. On average weapons, trap components, armor parts, shields and tools will return 95% of their forging cost. Each wear level will decrease melt return by additional 10% --- .../tweak/tweaks/material-size-for-melting.h | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 56da416de5..86887a0ec1 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -12,19 +12,40 @@ #include "df/item_toolst.h" #include "df/item_trapcompst.h" #include +#include + + +static Random::MersenneRNG rng; +static bool init_rng = true; + +static float get_random() { + if (init_rng) { + rng.init(); + init_rng = false; + } + return static_cast (rng.drandom1()); +} static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t base_material_size, float production_stack_size) { + const float melt_return_per_material_size = 0.3f, base_melt_recovery = 0.95f, loss_per_wear_level = 0.1f; if (item->mat_type == 0) // INORGANIC only { - const float melt_return_per_material_size = 0.3f; + float calculated_size; + auto inorganic = df::inorganic_raw::find(item->mat_index); if (inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)){ - return static_cast(std::round(static_cast(base_material_size) / production_stack_size / melt_return_per_material_size)); - // get size for melting to minimize diff between forging cost and melting return, for adamantine forging cost == base_material_size, divided by amount of items created in batch + calculated_size =static_cast(base_material_size) / production_stack_size / melt_return_per_material_size; + // get size for melting, for adamantine forging cost == base_material_size, divided by amount of items created in batch } - return static_cast(std::round(std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size / melt_return_per_material_size)); + else { + calculated_size = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size / melt_return_per_material_size; // (std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size) - forging cost for non adamantine item + } + float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; + calculated_size = calculated_size * melt_recovery; + int32_t random_part = ((modf(calculated_size, &calculated_size) > get_random()) ? 1 : 0); + return static_cast(calculated_size) + random_part; } return base_material_size; } From baa33375f99a8681b772632d90336aab47e38f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Tue, 24 Sep 2024 22:25:34 +0200 Subject: [PATCH 11/34] add tweak description to .rst --- docs/plugins/tweak.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 996b0ec103..a4c6616efd 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -53,6 +53,17 @@ Commands Names filled waterskins, flasks, and vials according to their contents, the same way other containers such as barrels, bins, and cages are named. (:bug:`4914`) +``material-size-for-melting`` + Changes how item size for melting is calculated. Affects weapons, shields, + armor parts, tools, trap components including items made from adamantine. + As result base return for melting is equal to 95% of production cost. + Each level of wear decreases melt return by further 10%. + Limitation of melting return being handled by game in 0,3 bar increments is + solved by adding random component to material size. E.g. for cap with item + size 1 melting return will be equal to 0,9 bar base return with 16,(6)% + chance for additional 0,3 bar. As result average return for melting cap + will be ~0,95 bar. + (:bug:`6027`). ``named-codices`` Displays titles for books instead of the default material description. ``partial-items`` From e140794c61d0265941b350c796f90e90ccce047e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Wed, 25 Sep 2024 12:21:16 +0200 Subject: [PATCH 12/34] move rng init to tweak plugin_init --- plugins/tweak/tweak.cpp | 2 +- plugins/tweak/tweaks/material-size-for-melting.h | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 0ea8069746..f6da66a84c 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -75,7 +75,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector #include - static Random::MersenneRNG rng; -static bool init_rng = true; - static float get_random() { - if (init_rng) { - rng.init(); - init_rng = false; - } return static_cast (rng.drandom1()); } From 1c6cb586c9a7d9e17bd1cfda30b1b822934ce53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Thu, 26 Sep 2024 18:53:52 +0200 Subject: [PATCH 13/34] update changelog, fix include order --- docs/changelog.txt | 1 + plugins/tweak/tweak.cpp | 2 +- plugins/tweak/tweaks/material-size-for-melting.h | 11 ++++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e6baf71475..07b0aaf8d7 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `tweak`: ``material-size-for-melting``: change melting return for inorganic armor parts, shields, weapons, trap components and tools to stop smelters from creating metal, bring melt return for adamantine in line with other metals to ~95% of forging cost. wear reduces melt return by 10% per level ## Fixes diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index f6da66a84c..412b18d078 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -17,10 +17,10 @@ using namespace DFHack; #include "tweaks/eggs-fertile.h" #include "tweaks/fast-heat.h" #include "tweaks/flask-contents.h" +#include "tweaks/material-size-for-melting.h" #include "tweaks/named-codices.h" #include "tweaks/partial-items.h" #include "tweaks/reaction-gloves.h" -#include "tweaks/material-size-for-melting.h" class tweak_onupdate_hookst { public: diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 1829d5ba32..66e6b4310a 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -1,18 +1,19 @@ +#include + #include "modules/Materials.h" +#include "modules/Random.h" #include "df/inorganic_raw.h" -#include "df/item_constructed.h" #include "df/item_armorst.h" +#include "df/item_constructed.h" #include "df/item_glovesst.h" #include "df/item_helmst.h" -#include "df/item_shoesst.h" #include "df/item_pantsst.h" -#include "df/item_weaponst.h" #include "df/item_shieldst.h" +#include "df/item_shoesst.h" #include "df/item_toolst.h" #include "df/item_trapcompst.h" -#include -#include +#include "df/item_weaponst.h" static Random::MersenneRNG rng; static float get_random() { From 5673722d3e6ddf7f223c3d7ac953ba065b673c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Thu, 26 Sep 2024 19:24:21 +0200 Subject: [PATCH 14/34] replace modf with modff --- plugins/tweak/tweaks/material-size-for-melting.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 66e6b4310a..85f6c47218 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -38,7 +38,7 @@ static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t } float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; calculated_size = calculated_size * melt_recovery; - int32_t random_part = ((modf(calculated_size, &calculated_size) > get_random()) ? 1 : 0); + int32_t random_part = ((modff(calculated_size, &calculated_size) > get_random()) ? 1 : 0); return static_cast(calculated_size) + random_part; } return base_material_size; From b1255b0b3a10f2de7d881874f1b67c8c442525de Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 21 Sep 2024 18:52:23 +0200 Subject: [PATCH 15/34] DFHack::Units: new function `setGoal` --- docs/changelog.txt | 1 + docs/dev/Lua API.rst | 5 +++++ library/LuaApi.cpp | 1 + library/include/modules/Units.h | 4 ++++ library/modules/Units.cpp | 23 +++++++++++++++++++++++ 5 files changed, 34 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index f3c68bb4a9..a03fe0fa2f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -113,6 +113,7 @@ Template for new versions: - ``Units``: new ``isWildlife`` and ``isAgitated`` property checks - ``Items::createItem``: removed ``growth_print`` parameter; now determined automatically - ``DFHack::cuboid``: ``cuboid::clampMap`` now returns the cuboid itself (instead of boolean) to allow method chaining; call ``cuboid::isValid`` to determine success +- ``DFHack::Units``: new function ``setGoal`` ## Lua - ``dfhack.units``: ``isWildlife`` and ``isAgitated`` property checks diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 4967c58f0e..3b77b2561a 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1703,6 +1703,11 @@ Units module ``dfhack.units.isOwnCiv`` or another appropriate predicate on the unit in question. +* ``dfhack.units.setGoal(unit, pos, goal)`` + + Set target coordinates and goal (of type ``df.unit_path_goal``) for the given + unit. In case of a change, also clears the unit's current path. + * ``dfhack.units.create(race, caste)`` Creates a new unit from scratch. The unit will be added to the diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index e193bb8cc6..196b67276c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2065,6 +2065,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getIdentity), WRAPM(Units, getNemesis), WRAPM(Units, makeown), + WRAPM(Units, setGoal), WRAPM(Units, create), WRAPM(Units, getPhysicalAttrValue), WRAPM(Units, getMentalAttrValue), diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 7eaa61ec62..7060f0406b 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -42,6 +42,7 @@ distribution. #include "df/physical_attribute_type.h" #include "df/unit_action.h" #include "df/unit_action_type_group.h" +#include "df/unit_path_goal.h" #include @@ -238,6 +239,9 @@ DFHACK_EXPORT bool unassignTrainer(df::unit *unit); /// to determine if the makeown operation was successful. DFHACK_EXPORT void makeown(df::unit *unit); +// Set the units target location and goal, clearing any existing goal or path +DFHACK_EXPORT void setGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal); + /// Create new unit and add to all units vector (but not active). No HF, nemesis, pos, /// labors, or group associations. Will have race, caste, name, soul, body, and mind. DFHACK_EXPORT df::unit *create(int16_t race, int16_t caste); diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 6f58a38b2f..93ee63d5fc 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -73,6 +73,7 @@ distribution. #include "df/unit_action_type_group.h" #include "df/unit_inventory_item.h" #include "df/unit_misc_trait.h" +#include "df/unit_path_goal.h" #include "df/unit_relationship_type.h" #include "df/unit_skill.h" #include "df/unit_soul.h" @@ -933,6 +934,28 @@ void Units::makeown(df::unit *unit) { (*f)(unit); } +// functionality reverse-engineered from DF's unitst::set_goal +void Units::setGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal) +{ + if (unit->path.dest != pos || unit->path.goal != goal) + { + unit->path.dest = pos; + unit->path.goal = goal; + unit->path.path.clear(); + } + + if (unit->flags1.bits.rider && unit->mount_type == df::rider_positions_type::STANDARD) + { + auto mount = df::unit::find(unit->relationship_ids[df::unit_relationship_type::RiderMount]); + if (mount) + { + mount->path.dest = pos; + mount->path.goal = goal; + mount->path.path.clear(); + } + } +} + df::unit *Units::create(int16_t race, int16_t caste) { auto fp = df::global::unitst_more_convenient_create; CHECK_NULL_POINTER(fp); From 2c834b995a5c8b5e18fc9af4933f5a14eedcccac Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:33:50 +0200 Subject: [PATCH 16/34] rename `setGoal` to `setPathGoal` --- docs/changelog.txt | 2 +- library/LuaApi.cpp | 2 +- library/include/modules/Units.h | 2 +- library/modules/Units.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index a03fe0fa2f..dbc1e97b64 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -113,7 +113,7 @@ Template for new versions: - ``Units``: new ``isWildlife`` and ``isAgitated`` property checks - ``Items::createItem``: removed ``growth_print`` parameter; now determined automatically - ``DFHack::cuboid``: ``cuboid::clampMap`` now returns the cuboid itself (instead of boolean) to allow method chaining; call ``cuboid::isValid`` to determine success -- ``DFHack::Units``: new function ``setGoal`` +- ``DFHack::Units``: new function ``setPathGoal`` ## Lua - ``dfhack.units``: ``isWildlife`` and ``isAgitated`` property checks diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 196b67276c..ddea7702f3 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2065,7 +2065,7 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = { WRAPM(Units, getIdentity), WRAPM(Units, getNemesis), WRAPM(Units, makeown), - WRAPM(Units, setGoal), + WRAPM(Units, setPathGoal), WRAPM(Units, create), WRAPM(Units, getPhysicalAttrValue), WRAPM(Units, getMentalAttrValue), diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 7060f0406b..9b356de105 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -240,7 +240,7 @@ DFHACK_EXPORT bool unassignTrainer(df::unit *unit); DFHACK_EXPORT void makeown(df::unit *unit); // Set the units target location and goal, clearing any existing goal or path -DFHACK_EXPORT void setGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal); +DFHACK_EXPORT void setPathGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal); /// Create new unit and add to all units vector (but not active). No HF, nemesis, pos, /// labors, or group associations. Will have race, caste, name, soul, body, and mind. diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 93ee63d5fc..c1c0a2c635 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -935,7 +935,7 @@ void Units::makeown(df::unit *unit) { } // functionality reverse-engineered from DF's unitst::set_goal -void Units::setGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal) +void Units::setPathGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal) { if (unit->path.dest != pos || unit->path.goal != goal) { From 3f213ec47e6f891d44240e922053cbef46c12ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Fri, 27 Sep 2024 17:42:34 +0200 Subject: [PATCH 17/34] use #define to reduce repetitive code --- .../tweak/tweaks/material-size-for-melting.h | 92 ++++--------------- 1 file changed, 17 insertions(+), 75 deletions(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 85f6c47218..cc20ca10e6 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -44,78 +44,20 @@ static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t return base_material_size; } -static int32_t get_material_size_for_melting(df::item_constructed* item, int32_t base_size) { - return get_material_size_for_melting(item, base_size, 1.0f); -} - -struct material_size_for_melting_armor_hook : df::item_armorst { - typedef df::item_armorst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_armor_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_gloves_hook : df::item_glovesst { - typedef df::item_glovesst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(), 2.0f); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_gloves_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_shoes_hook : df::item_shoesst { - typedef df::item_shoesst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(), 2.0f); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_shoes_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_helm_hook : df::item_helmst { - typedef df::item_helmst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_helm_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_pants_hook : df::item_pantsst { - typedef df::item_pantsst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_pants_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_weapon_hook : df::item_weaponst { - typedef df::item_weaponst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_weapon_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_trapcomp_hook : df::item_trapcompst { - typedef df::item_trapcompst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_trapcomp_hook, getMaterialSizeForMelting); - -struct material_size_for_melting_tool_hook : df::item_toolst { - typedef df::item_toolst interpose_base; - - DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) { - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)()); - } -}; -IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_tool_hook, getMaterialSizeForMelting); +#define DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(TYPE, PRODUCTION_STACK_SIZE) \ +struct material_size_for_melting_##TYPE##_hook : df::item_##TYPE##st {\ + typedef df::item_##TYPE##st interpose_base;\ + DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) {\ + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(),PRODUCTION_STACK_SIZE);\ + }\ +};\ +IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_##TYPE##_hook, getMaterialSizeForMelting); + +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(armor, 1.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(gloves, 2.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(shoes, 2.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(helm, 1.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(pants, 1.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(weapon, 1.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(trapcomp, 1.0f) +DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(tool, 1.0f) From e1e4c87c2426c8e0fd4d1ff5c2a54b096264ce39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Fri, 27 Sep 2024 17:43:13 +0200 Subject: [PATCH 18/34] shortened long lines, improved readability --- plugins/tweak/tweaks/material-size-for-melting.h | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index cc20ca10e6..570c3df2ea 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -25,19 +25,22 @@ static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t if (item->mat_type == 0) // INORGANIC only { - float calculated_size; + float calculated_size, forging_cost_per_item; auto inorganic = df::inorganic_raw::find(item->mat_index); if (inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)){ - calculated_size =static_cast(base_material_size) / production_stack_size / melt_return_per_material_size; - // get size for melting, for adamantine forging cost == base_material_size, divided by amount of items created in batch + //adamantine items + forging_cost_per_item = static_cast(base_material_size) / production_stack_size; + calculated_size = forging_cost_per_item / melt_return_per_material_size; } else { - calculated_size = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size / melt_return_per_material_size; - // (std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f) / production_stack_size) - forging cost for non adamantine item + // non adamantine items + forging_cost_per_item = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f); + forging_cost_per_item /= production_stack_size; + calculated_size = forging_cost_per_item / melt_return_per_material_size; } float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; - calculated_size = calculated_size * melt_recovery; + calculated_size *= melt_recovery; int32_t random_part = ((modff(calculated_size, &calculated_size) > get_random()) ? 1 : 0); return static_cast(calculated_size) + random_part; } From f94979adb550cdeba72d8b9344caf8d67819ff1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Fri, 27 Sep 2024 23:58:26 +0200 Subject: [PATCH 19/34] removed duplicated code --- plugins/tweak/tweaks/material-size-for-melting.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 570c3df2ea..65fa090853 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -31,14 +31,13 @@ static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t if (inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)){ //adamantine items forging_cost_per_item = static_cast(base_material_size) / production_stack_size; - calculated_size = forging_cost_per_item / melt_return_per_material_size; } else { // non adamantine items forging_cost_per_item = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f); forging_cost_per_item /= production_stack_size; - calculated_size = forging_cost_per_item / melt_return_per_material_size; } + calculated_size = forging_cost_per_item / melt_return_per_material_size; float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; calculated_size *= melt_recovery; int32_t random_part = ((modff(calculated_size, &calculated_size) > get_random()) ? 1 : 0); From 48ef685cdd00323def70139ebd7d008e5f64d529 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 27 Sep 2024 20:57:19 -0700 Subject: [PATCH 20/34] don't keep reservations for units not in armies this fixes old reservations kept from before we excluded expelled units as a side effect, reservations for prisoners are no longer kept --- docs/changelog.txt | 1 + docs/plugins/preserve-rooms.rst | 5 ++--- plugins/preserve-rooms.cpp | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index f3c68bb4a9..e81919394d 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - DFHack text edit fields now delete the character at the cursor when you hit the Delete key - DFHack text edit fields now move the cursor by one word left or right with Ctrl-Left and Ctrl-Right - DFHack text edit fields now move the cursor to the beginning or end of the line with Home and End +- `preserve-rooms`: automatically release room reservations for captured squad members. we were kidding ourselves with our optimistic kept reservations. they're unlikely to come back : (( ## Documentation diff --git a/docs/plugins/preserve-rooms.rst b/docs/plugins/preserve-rooms.rst index f084c2c3b5..26e570186f 100644 --- a/docs/plugins/preserve-rooms.rst +++ b/docs/plugins/preserve-rooms.rst @@ -19,9 +19,8 @@ This tool mitigates both issues. It records when units leave the map and reserves their assigned bedrooms, offices, etc. for them. The zones will be disabled in their absence (so other units don't steal them), and will be re-enabled and reassigned to them when they appear back on the map. If they die -away from the fort, the zone will become unreserved and available for reuse. If -they are captured and held prisoner, their room will continue to be reserved in -their name in the (optimistic) hope of their safe return. +away from the fort (or are captured), the zone will become unreserved and +available for reuse. When you click on an assignable zone, you will also now have the option to associate the room with a noble or administrative role. The room will be diff --git a/plugins/preserve-rooms.cpp b/plugins/preserve-rooms.cpp index e170bd87a1..b6bec6b322 100644 --- a/plugins/preserve-rooms.cpp +++ b/plugins/preserve-rooms.cpp @@ -389,13 +389,15 @@ static void clear_reservation(color_ostream &out, int32_t zone_id, df::building_ zone->spec_sub_flag.bits.active = true; } -// stop reserving zones for dead units +// stop reserving zones for dead units or units that are no longer in an army static void scrub_reservations(color_ostream &out) { vector hfids_to_scrub; for (auto &[hfid, zone_ids] : pending_reassignment) { - if (auto hf = df::historical_figure::find(hfid); hf && hf->died_year == -1) + auto hf = df::historical_figure::find(hfid); + if (hf && hf->died_year == -1 && hf->info && hf->info->whereabouts && hf->info->whereabouts->army_id > -1) continue; - DEBUG(cycle,out).print("removed reservation for dead or culled hfid %d\n", hfid); + DEBUG(cycle,out).print("removed reservation for dead, culled, or non-army hfid %d: %s\n", hfid, + hf ? DF2CONSOLE(Units::getReadableName(hf)).c_str() : "culled"); hfids_to_scrub.push_back(hfid); for (int32_t zone_id : zone_ids) { if (scrub_id_from_entries(hfid, zone_id, reserved_zones)) { From 9fb0d9efba3da0b885c2e42fb081f7d6e576e6cd Mon Sep 17 00:00:00 2001 From: Birkow <56039938+Birkow@users.noreply.github.com> Date: Sat, 28 Sep 2024 09:22:37 +0200 Subject: [PATCH 21/34] Update docs/plugins/tweak.rst Co-authored-by: Myk --- docs/plugins/tweak.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index a4c6616efd..7649c97315 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -54,16 +54,15 @@ Commands the same way other containers such as barrels, bins, and cages are named. (:bug:`4914`) ``material-size-for-melting`` - Changes how item size for melting is calculated. Affects weapons, shields, - armor parts, tools, trap components including items made from adamantine. - As result base return for melting is equal to 95% of production cost. - Each level of wear decreases melt return by further 10%. - Limitation of melting return being handled by game in 0,3 bar increments is - solved by adding random component to material size. E.g. for cap with item - size 1 melting return will be equal to 0,9 bar base return with 16,(6)% - chance for additional 0,3 bar. As result average return for melting cap - will be ~0,95 bar. - (:bug:`6027`). + Makes amortized metal bar returns for melting uniform across all item types. + Affects weapons, shields, armor parts, tools, and trap components. The target + amount of metal produced by melting is 95% of the metal used for production + of the item. Each level of wear decreases melt return by a further 10%. The game + has a fixed granularity of 0.3 for metal bar returns, so individual items will + randomly return an amount that may be above or below the target. For example + a metal cap with item size 1 will produce 0.9 of a bar with a 16.6% chance of + producing an additional 0.3 of a bar. Over time, the average return for melting + these types of caps will be ~0.95 of a bar. (:bug:`6027`) ``named-codices`` Displays titles for books instead of the default material description. ``partial-items`` From 7a5e0fdd7a587a85d9bd446b2acd3f6ad9d07918 Mon Sep 17 00:00:00 2001 From: Birkow <56039938+Birkow@users.noreply.github.com> Date: Sat, 28 Sep 2024 09:29:13 +0200 Subject: [PATCH 22/34] Apply suggestions from code review Co-authored-by: Myk --- .../tweak/tweaks/material-size-for-melting.h | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/plugins/tweak/tweaks/material-size-for-melting.h b/plugins/tweak/tweaks/material-size-for-melting.h index 65fa090853..ea6548b6af 100644 --- a/plugins/tweak/tweaks/material-size-for-melting.h +++ b/plugins/tweak/tweaks/material-size-for-melting.h @@ -15,42 +15,46 @@ #include "df/item_trapcompst.h" #include "df/item_weaponst.h" -static Random::MersenneRNG rng; +struct Mrng { + Random::MersenneRNG rng; + Mrng() { rng.init(); } +}; + static float get_random() { - return static_cast (rng.drandom1()); + static Mrng mrng; + return static_cast (mrng.rng.drandom1()); } static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t base_material_size, float production_stack_size) { const float melt_return_per_material_size = 0.3f, base_melt_recovery = 0.95f, loss_per_wear_level = 0.1f; - if (item->mat_type == 0) // INORGANIC only + if (item->mat_type != 0) // bail if not INORGANIC + return base_material_size; + + float forging_cost_per_item; + if (auto inorganic = df::inorganic_raw::find(item->mat_index); + inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)) { - float calculated_size, forging_cost_per_item; - - auto inorganic = df::inorganic_raw::find(item->mat_index); - if (inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)){ - //adamantine items - forging_cost_per_item = static_cast(base_material_size) / production_stack_size; - } - else { - // non adamantine items - forging_cost_per_item = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f); - forging_cost_per_item /= production_stack_size; - } - calculated_size = forging_cost_per_item / melt_return_per_material_size; - float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; - calculated_size *= melt_recovery; - int32_t random_part = ((modff(calculated_size, &calculated_size) > get_random()) ? 1 : 0); - return static_cast(calculated_size) + random_part; + // adamantine items + forging_cost_per_item = static_cast(base_material_size) / production_stack_size; + } else { + // non adamantine items + forging_cost_per_item = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f); + forging_cost_per_item /= production_stack_size; } - return base_material_size; + + float calculated_size = forging_cost_per_item / melt_return_per_material_size; + float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; + calculated_size *= melt_recovery; + int32_t random_part = ((modff(calculated_size, &calculated_size) > get_random()) ? 1 : 0); + return static_cast(calculated_size) + random_part; } #define DEFINE_MATERIAL_SIZE_FOR_MELTING_TWEAK(TYPE, PRODUCTION_STACK_SIZE) \ struct material_size_for_melting_##TYPE##_hook : df::item_##TYPE##st {\ typedef df::item_##TYPE##st interpose_base;\ DEFINE_VMETHOD_INTERPOSE(int32_t, getMaterialSizeForMelting, ()) {\ - return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(),PRODUCTION_STACK_SIZE);\ + return get_material_size_for_melting(this, INTERPOSE_NEXT(getMaterialSizeForMelting)(), PRODUCTION_STACK_SIZE);\ }\ };\ IMPLEMENT_VMETHOD_INTERPOSE(material_size_for_melting_##TYPE##_hook, getMaterialSizeForMelting); From 119e169f1cd0662e6da29d6d5ecd0d6b067d559d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 09:35:41 +0200 Subject: [PATCH 23/34] remove redundant call to rng.init() from tweak.cpp, remove white spaces --- plugins/tweak/tweak.cpp | 1 - plugins/tweak/tweaks/material-size-for-melting.h | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 412b18d078..6cf13c434f 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -83,7 +83,6 @@ DFhackCExport command_result plugin_init(color_ostream &out, vectormat_type != 0) // bail if not INORGANIC return base_material_size; - + float forging_cost_per_item; if (auto inorganic = df::inorganic_raw::find(item->mat_index); inorganic && inorganic->flags.is_set(df::inorganic_flags::DEEP_SPECIAL)) @@ -42,7 +42,7 @@ static int32_t get_material_size_for_melting(df::item_constructed *item, int32_t forging_cost_per_item = std::max(std::floor(static_cast(base_material_size) / 3.0f), 1.0f); forging_cost_per_item /= production_stack_size; } - + float calculated_size = forging_cost_per_item / melt_return_per_material_size; float melt_recovery = base_melt_recovery - static_cast(item->wear) * loss_per_wear_level; calculated_size *= melt_recovery; From b2d150128c979eef8624d6446a0c331fb8711fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 10:11:40 +0200 Subject: [PATCH 24/34] renamed tweak from "material-size-for-melting" to "realistic-melting" --- docs/changelog.txt | 2 +- docs/plugins/tweak.rst | 2 +- plugins/tweak/tweak.cpp | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index e2e52d4a6d..c7ab543db2 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,7 +54,7 @@ Template for new versions: ## New Tools ## New Features -- `tweak`: ``material-size-for-melting``: change melting return for inorganic armor parts, shields, weapons, trap components and tools to stop smelters from creating metal, bring melt return for adamantine in line with other metals to ~95% of forging cost. wear reduces melt return by 10% per level +- `tweak`: ``realistic-melting``: change melting return for inorganic armor parts, shields, weapons, trap components and tools to stop smelters from creating metal, bring melt return for adamantine in line with other metals to ~95% of forging cost. wear reduces melt return by 10% per level ## Fixes diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 7649c97315..4997bd4c51 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -53,7 +53,7 @@ Commands Names filled waterskins, flasks, and vials according to their contents, the same way other containers such as barrels, bins, and cages are named. (:bug:`4914`) -``material-size-for-melting`` +``realistic-melting`` Makes amortized metal bar returns for melting uniform across all item types. Affects weapons, shields, armor parts, tools, and trap components. The target amount of metal produced by melting is 95% of the metal used for production diff --git a/plugins/tweak/tweak.cpp b/plugins/tweak/tweak.cpp index 6cf13c434f..e621a14d5e 100644 --- a/plugins/tweak/tweak.cpp +++ b/plugins/tweak/tweak.cpp @@ -75,14 +75,14 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector Date: Sat, 28 Sep 2024 10:23:45 +0200 Subject: [PATCH 25/34] Remove private functions from widgets public API --- library/lua/gui/widgets.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index c35dd3b095..341675e1c1 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -29,9 +29,6 @@ DimensionsTooltip = require('gui.widgets.dimensions_tooltip') Tab = TabBar.Tab makeButtonLabelText = Label.makeButtonLabelText -parse_label_text = Label.parse_label_text -render_text = Label.render_text -check_text_keys = Label.check_text_keys DOUBLE_CLICK_MS = Panel.DOUBLE_CLICK_MS STANDARDSCROLL = Scrollbar.STANDARDSCROLL From 0badb26cc86d2b9daa7eb2c2459a997291d92aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 28 Sep 2024 10:25:58 +0200 Subject: [PATCH 26/34] Move tabbar container out of containers, as its only bar, no container --- library/lua/gui/widgets.lua | 2 +- library/lua/gui/widgets/{containers => }/tab_bar.lua | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename library/lua/gui/widgets/{containers => }/tab_bar.lua (100%) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 341675e1c1..cd0482284c 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -23,7 +23,7 @@ ButtonGroup = require('gui.widgets.buttons.button_group') ToggleHotkeyLabel = require('gui.widgets.labels.toggle_hotkey_label') List = require('gui.widgets.list') FilteredList = require('gui.widgets.filtered_list') -TabBar = require('gui.widgets.containers.tab_bar') +TabBar = require('gui.widgets.tab_bar') RangeSlider = require('gui.widgets.range_slider') DimensionsTooltip = require('gui.widgets.dimensions_tooltip') diff --git a/library/lua/gui/widgets/containers/tab_bar.lua b/library/lua/gui/widgets/tab_bar.lua similarity index 100% rename from library/lua/gui/widgets/containers/tab_bar.lua rename to library/lua/gui/widgets/tab_bar.lua From a0097a6ccb5a2002df769d7682b93ce5579c403a Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:12:15 +0200 Subject: [PATCH 27/34] Apply suggestions from code review Co-authored-by: Myk --- docs/dev/Lua API.rst | 2 +- library/modules/Units.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 3b77b2561a..1bf0b40192 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -1703,7 +1703,7 @@ Units module ``dfhack.units.isOwnCiv`` or another appropriate predicate on the unit in question. -* ``dfhack.units.setGoal(unit, pos, goal)`` +* ``dfhack.units.setPathGoal(unit, pos, goal)`` Set target coordinates and goal (of type ``df.unit_path_goal``) for the given unit. In case of a change, also clears the unit's current path. diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index c1c0a2c635..fc38bc2f34 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -946,8 +946,7 @@ void Units::setPathGoal(df::unit *unit, df::coord pos, df::unit_path_goal goal) if (unit->flags1.bits.rider && unit->mount_type == df::rider_positions_type::STANDARD) { - auto mount = df::unit::find(unit->relationship_ids[df::unit_relationship_type::RiderMount]); - if (mount) + if (auto mount = df::unit::find(unit->relationship_ids[df::unit_relationship_type::RiderMount])) { mount->path.dest = pos; mount->path.goal = goal; From 2995c23c75ccdc3ad13e9b568c50123bafb31aab Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:23:07 +0200 Subject: [PATCH 28/34] fix changelog --- docs/changelog.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index dbc1e97b64..bfdcf087b0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -66,8 +66,12 @@ Template for new versions: ## API +- ``DFHack::Units``: new function ``setPathGoal`` + ## Lua +- ``dfhack.units``: new function ``setPathGoal`` + ## Removed # 50.14-r1 @@ -113,7 +117,6 @@ Template for new versions: - ``Units``: new ``isWildlife`` and ``isAgitated`` property checks - ``Items::createItem``: removed ``growth_print`` parameter; now determined automatically - ``DFHack::cuboid``: ``cuboid::clampMap`` now returns the cuboid itself (instead of boolean) to allow method chaining; call ``cuboid::isValid`` to determine success -- ``DFHack::Units``: new function ``setPathGoal`` ## Lua - ``dfhack.units``: ``isWildlife`` and ``isAgitated`` property checks From 076031af52308922f9d65a665777445dc931cfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 17:54:24 +0200 Subject: [PATCH 29/34] Added information about production cost calculation to description --- docs/plugins/tweak.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 4997bd4c51..76f1b7a475 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -62,7 +62,10 @@ Commands randomly return an amount that may be above or below the target. For example a metal cap with item size 1 will produce 0.9 of a bar with a 16.6% chance of producing an additional 0.3 of a bar. Over time, the average return for melting - these types of caps will be ~0.95 of a bar. (:bug:`6027`) + these types of caps will be ~0.95 of a bar. Calculations for melting return are + done for items with base game production cost. Melting return might not be + calculated correctly for modded items or created in custom reactions not + respecting vanilla production costs. (:bug:`6027`) ``named-codices`` Displays titles for books instead of the default material description. ``partial-items`` From 6b553c608ffc955395839799c384d8cfc99d1d36 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 28 Sep 2024 11:57:47 -0700 Subject: [PATCH 30/34] reword description --- docs/plugins/tweak.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/tweak.rst b/docs/plugins/tweak.rst index 76f1b7a475..7c72f946c7 100644 --- a/docs/plugins/tweak.rst +++ b/docs/plugins/tweak.rst @@ -64,8 +64,8 @@ Commands producing an additional 0.3 of a bar. Over time, the average return for melting these types of caps will be ~0.95 of a bar. Calculations for melting return are done for items with base game production cost. Melting return might not be - calculated correctly for modded items or created in custom reactions not - respecting vanilla production costs. (:bug:`6027`) + calculated correctly for modded items or items created in custom reactions + that don't respect vanilla production costs. (:bug:`6027`) ``named-codices`` Displays titles for books instead of the default material description. ``partial-items`` From 7e6e816b8dd7e6d3ccaa558d6958b77593ad5a99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Sep 2024 12:02:28 -0700 Subject: [PATCH 31/34] document that animals need empty floor space --- docs/plugins/dwarfvet.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/plugins/dwarfvet.rst b/docs/plugins/dwarfvet.rst index 94fb7c20aa..f160a7bd82 100644 --- a/docs/plugins/dwarfvet.rst +++ b/docs/plugins/dwarfvet.rst @@ -16,6 +16,8 @@ unassigned from the pasture while in treatment. The animal will be reassigned to its original pasture shortly after recovery. Animals that are on restraints or in cages will not be designated for treatment. +Animals require an empty tile of floor to rest on while recovering, so your +hospital must have some unoccupied floor space in order for animals to use it. You can enable ``dwarfvet`` in `gui/control-panel`, and you can choose to start ``dwarfvet`` automatically in new forts in the ``Gameplay`` -> ``Autostart`` From d096b8335d489c97bd1852767b54c1d962f815dc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Sep 2024 13:45:32 -0700 Subject: [PATCH 32/34] rewrite help, increase dig priority, blueprint stairs --- data/blueprints/aquifer_tap.csv | 91 +++++++++++++++++++++------------ docs/changelog.txt | 2 + 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/data/blueprints/aquifer_tap.csv b/data/blueprints/aquifer_tap.csv index 16611266fb..939f1a916c 100644 --- a/data/blueprints/aquifer_tap.csv +++ b/data/blueprints/aquifer_tap.csv @@ -7,19 +7,20 @@ Here's the procedure: "" 2) Dig a one-tile-wide tunnel from where you want the water to end up (e.g. your well cistern) to an area on the same z-level directly below the target light aquifer. Dig a one-tile-wide diagonal segment in this tunnel near the cistern side so water that will eventually flow through the tunnel is depressurized. "" -"3) From the end of that tunnel, go down one z-level, enable damp dig mode in the dig toolbar, then dig a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). Change the top staircase tile (the down-only stairs) to a ""blueprint"" tile (default hotkey: m-L) so your miners don't dig it yet." +"3) Pause the game. From the end of that tunnel, go down one z-level, enable damp dig mode in the dig toolbar, then designate for digging a staircase straight up so that the top is in the lowest aquifer level (a tile with a two-drop icon). If you only have one layer of aquifer, you should end the staircase one level below the aquifer so when we dig the tap, it will extend up into the aquifer level. Your tunnel should connect to the staircase one z-level above the bottom of the staircase." "" -"4) From the bottom of the staircase (the z-level below where the water will flow to your cisterns), dig a straight, one-tile wide tunnel to the closest edge of the map. This is your emergency drainage tunnel. Smooth the map edge tile and carve a fortification. The water can flow through the fortification and off the map, allowing the dwarves to dig out the aquifer tap without drowning." +"4) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level at the top of the staircase. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint designates ramps for digging so two layers of aquifer can contribute to the water collector. It also changes the staircase tile below the tap to a ""blueprint"" tile so your miners don't dig the tap before your drainage tunnel is ready." "" -5) Place a lever-controlled floodgate in the drainage tunnel and open the floodgate. Place the lever somewhere else in your fort so that it will remain dry and accessible. +"5) You can now unpause the game. From the bottom of the staircase (the z-level below where the water will flow to your cisterns), dig a straight, one-tile wide tunnel to the closest edge of the map. This is your emergency drainage tunnel. Smooth the map edge tile and carve a fortification. The water can flow through the fortification and off the map, allowing the dwarves to dig out the aquifer tap without drowning." "" -"6) If you want, haul away any boulders in the tunnels and/or smooth the tiles (e.g. mark them for dumping -- hotkey i-p -- and wait for them to be dumped). You won't be able to access any of this area once it fills up with water!" +6) Place a lever-controlled floodgate in the drainage tunnel and open the floodgate. Place the lever somewhere else in your fort so that it will remain dry and accessible. "" -"7) Apply this blueprint (gui/quickfort aquifer_tap /dig) to the z-level above the top of the staircase, inside the lowest aquifer level. The tiles will be designated in ""damp dig"" mode so your miners can dig it out without the damp tiles canceling the digging designations. This blueprint designates ramps for digging so two layers of aquifer can contribute to the water collector." +"7) If you want, haul away any boulders in the tunnels and/or smooth the tiles (e.g. mark them for dumping -- hotkey i-p -- and wait for them to be dumped). Enable prioritize in gui/control-panel to focus dwarves on dumping tasks and make it go faster. You won't be able to access any of this area once it fills up with water!" "" -"8) Dig out the tap. You can haul away any boulders and remove the ramps if you like. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." +"8) Convert the ""blueprint"" stairway tile to a regular up/down stair dig designation to allow your miners to dig out the tap. You can haul away any boulders and remove the ramps if you like. There is no rush. The water will safely flow down the staircase, through the open floodgate, down the drainage tunnel, and off the map as long as the floodgate is open." +"8b) Sometimes, DF gets into a bad state with mining designations and miners will refuse to dig the stairway tile. If this happens to you, enter mining mode, enable the keyboard cursor if it's not already enabled (hotkey: Alt-k), highlight the undug stair designation, and run dig-now here in gui/launcher. You might also have to do this for the down stair designation in the center of the aquifer tap. Your miners should be able to handle the rest without assistance." -"9) Once everything is dug out and all dwarves are out of the waterways, close the floodgate. Your cisterns will fill with water. Since the waterway to your cisterns is depressurized, the cisterns will stay forever full, but will not flood." +"9) Once everything is dug out and all dwarves are out of the waterways, close the floodgate. Your cisterns will fill with water. Since the waterway to your cisterns is depressurized (due to the diagonal tunnel you dug), the cisterns will stay forever full, but will not flood." "" A diagram might be useful. Here is a vertical view through the z-levels. This blueprint goes at the top: "" @@ -27,32 +28,58 @@ A diagram might be useful. Here is a vertical view through the z-levels. This bl i "... <- up/down stairs, make this as tall as you need" i -i <- cistern outlet level +i <- cistern outlet level with diagonal tunnel to depressurize "u <- up stairs, drainage level" "" "Good luck! If done right, this method is the safest way to supply your fort with clean water." #dig label(dig) start(13 13 center of tap) light aquifer water collector - -,,,,,,,,,,,,mdr -,,,,,,,,,,,,mdr -,,,,,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr -,,,,,,,,,,,,mdr -,,,mdr,,,,,,,,,mdr,,,,,,,,,mdr -,,,mdr,,,,,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,,,,,mdr -,,,mdr,,,,,,,,,mdr,,,,,,,,,mdr -,,,mdr,,,mdr,,,,,,mdr,,,,,,mdr,,,mdr -,,,mdr,,,mdr,,,,,mdr,mdr,mdr,,,,,mdr,,,mdr -,,,mdr,,,mdr,,,,,,mdr,,,,,,mdr,,,mdr -,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr -,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdj,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr -,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr,,,mdr -,,,mdr,,,mdr,,,,,,mdr,,,,,,mdr,,,mdr -,,,mdr,,,mdr,,,,,mdr,mdr,mdr,,,,,mdr,,,mdr -,,,mdr,,,mdr,,,,,,mdr,,,,,,mdr,,,mdr -,,,mdr,,,,,,,,,mdr,,,,,,,,,mdr -,,,mdr,,,,,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,,,,,mdr -,,,mdr,,,,,,,,,mdr,,,,,,,,,mdr -,,,,,,,,,,,,mdr -,,,,,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr,mdr -,,,,,,,,,,,,mdr -,,,,,,,,,,,,mdr +,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, +,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, +,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, +,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdj3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3, +,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,mdr3,mdr3,mdr3,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,mdr3,,,,,,mdr3,,,,,,mdr3,,,mdr3,,, +,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, +,,,mdr3,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,,mdr3,,, +,,,mdr3,,,,,,,,,mdr3,,,,,,,,,mdr3,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,,,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,mdr3,,,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,,,,,,,,,,mdr3,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,, +#>,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,`,,,,,,,,,,,, +,,,,,,,,,,,,`,,,,,,,,,,,, +,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, +,,,,,,,,,,,,`,,,,,,,,,,,, +,,,`,,,,,,,,,`,,,,,,,,,`,,, +,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, +,,,`,,,,,,,,,`,,,,,,,,,`,,, +,,,`,,,`,,,,,,`,,,,,,`,,,`,,, +,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, +,,,`,,,`,,,,,,`,,,,,,`,,,`,,, +,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, +,`,`,`,`,`,`,`,`,`,`,`,mbmdi3,`,`,`,`,`,`,`,`,`,`,`, +,,,`,,,`,,,`,,,`,,,`,,,`,,,`,,, +,,,`,,,`,,,,,,`,,,,,,`,,,`,,, +,,,`,,,`,,,,,`,`,`,,,,,`,,,`,,, +,,,`,,,`,,,,,,`,,,,,,`,,,`,,, +,,,`,,,,,,,,,`,,,,,,,,,`,,, +,,,`,,,,,`,`,`,`,`,`,`,`,`,,,,,`,,, +,,,`,,,,,,,,,`,,,,,,,,,`,,, +,,,,,,,,,,,,`,,,,,,,,,,,, +,,,,,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,,,,, +,,,,,,,,,,,,`,,,,,,,,,,,, +,,,,,,,,,,,,`,,,,,,,,,,,, diff --git a/docs/changelog.txt b/docs/changelog.txt index 10ac5f7e37..ce6a297b5e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,8 @@ Template for new versions: - DFHack text edit fields now delete the character at the cursor when you hit the Delete key - DFHack text edit fields now move the cursor by one word left or right with Ctrl-Left and Ctrl-Right - DFHack text edit fields now move the cursor to the beginning or end of the line with Home and End +- Quickfort blueprint library: ``aquifer_tap`` blueprint walkthough rewritten for clarity +- Quickfort blueprint library: ``aquifer_tap`` blueprint now designated at priority 3 and marks the stairway tile below the tap in "blueprint" mode to prevent drips while the drainage pipe is being prepared - `preserve-rooms`: automatically release room reservations for captured squad members. we were kidding ourselves with our optimistic kept reservations. they're unlikely to come back : (( ## Documentation From 8dda7394569db97a4f7ec8b06b2f24e7a4fc5d5d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Sep 2024 13:49:04 -0700 Subject: [PATCH 33/34] call out blueprint tile to diagram --- data/blueprints/aquifer_tap.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/blueprints/aquifer_tap.csv b/data/blueprints/aquifer_tap.csv index 939f1a916c..b6675cee28 100644 --- a/data/blueprints/aquifer_tap.csv +++ b/data/blueprints/aquifer_tap.csv @@ -24,8 +24,8 @@ Here's the procedure: "" A diagram might be useful. Here is a vertical view through the z-levels. This blueprint goes at the top: "" -"j <- down stairs, center of this blueprint" -i +"j <- down stairs, center of this blueprint" +"i <- up/down stairs, initially in ""blueprint mode"" to prevent digging before drainage is ready" "... <- up/down stairs, make this as tall as you need" i i <- cistern outlet level with diagonal tunnel to depressurize From ca01706b8f2834263d81dfe0a6e9b719a6f3fb53 Mon Sep 17 00:00:00 2001 From: DFHack-Urist via GitHub Actions <63161697+DFHack-Urist@users.noreply.github.com> Date: Sat, 28 Sep 2024 21:00:29 +0000 Subject: [PATCH 34/34] Auto-update submodules scripts: master --- scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts b/scripts index 7b8adddf4b..edd662aef5 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 7b8adddf4b0380ef4503c2f4d810307acb1c5f59 +Subproject commit edd662aef53002bb9e47c24e338e28a5318e307b