From d4c4421332e2b59a490d41dcf031edc63e2e14ff Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 6 Apr 2025 05:39:29 -0500 Subject: [PATCH 01/20] devel/dflayout: gui.dflayout fort toolbars demo --- changelog.txt | 1 + devel/dflayout.lua | 318 ++++++++++++++++++++++++++++++++++++++++ docs/devel/dflayout.rst | 17 +++ 3 files changed, 336 insertions(+) create mode 100644 devel/dflayout.lua create mode 100644 docs/devel/dflayout.rst diff --git a/changelog.txt b/changelog.txt index 213e521fd3..b286ce9e97 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `devel/dflayout`: demo and visually verify gui.dflayout module (fort toolbars) ## New Features diff --git a/devel/dflayout.lua b/devel/dflayout.lua new file mode 100644 index 0000000000..2e4eb3368c --- /dev/null +++ b/devel/dflayout.lua @@ -0,0 +1,318 @@ +local gui = require('gui') +local layout = require('gui.dflayout') +local Panel = require('gui.widgets.containers.panel') +local Window = require('gui.widgets.containers.window') +local Label = require('gui.widgets.labels.label') +local Toggle = require('gui.widgets.labels.toggle_hotkey_label') +local List = require('gui.widgets.list') +local utils = require('utils') + +--- Demo Control Window and Screen --- + +local screen +local DemoScreen +local visible + +do -- limit env pollution + local function demo_available(demo) + if not demo.available then return true end + return demo.available() + end + + local visible_when_not_focused = true + function visible() + if visible_when_not_focused then return true end + if not screen then return false end + return screen:isActive() and not screen.defocused + end + + local DemoWindow = defclass(nil, Window) + DemoWindow.ATTRS{ + frame_title = 'dflayout demos', + frame = { w = 39, h = 9 }, + resizable = true, + autoarrange_subviews = true, + autoarrange_gap = 1, + } + + function DemoWindow:init(args) + self.demos = args.demos + self:addviews{ + Toggle{ + label = 'Demos visible when not focused?', + initial_option = visible_when_not_focused, + on_change = function(new, old) + visible_when_not_focused = new + end + }, + List{ + view_id = 'list', + frame = { h = 10, }, + icon_pen = COLOR_GREY, + icon_width = 3, + on_submit = function(index, item) + local demo = self.demos[index] + demo.active = demo_available(demo) and not demo.active + self:refresh() + end + }, + } + end + + local CHECK = string.char(251) -- U+221A SQUARE ROOT + + function DemoWindow:refresh() + local choices = {} + for _, demo in ipairs(self.demos) do + local icon + if not demo_available(demo) then + icon = '-' + elseif demo.active then + icon = CHECK + end + table.insert(choices, { + text = demo.text, + icon = icon, + }) + end + self.subviews.list:setChoices(choices) + return self + end + + DemoScreen = defclass(nil, gui.ZScreen) + function DemoScreen:init(args) + self.demos = args.demos + local function demo_views() + local views = {} + for _, demo in ipairs(self.demos) do + if demo.views then + table.move(demo.views, 1, #demo.views, #views + 1, views) + end + end + return views + end + self:addviews{ + DemoWindow{ demos = self.demos }:refresh(), + table.unpack(demo_views()) + } + end + + local if_percentage + function DemoScreen:render(...) + if visible_when_not_focused then + local new_if_percentage = df.global.init.display.max_interface_percentage + if new_if_percentage ~= if_percentage then + self:updateLayout() + end + end + return DemoScreen.super.render(self, ...) + end + + function DemoScreen:postComputeFrame(frame_body) + for _, demo in ipairs(self.demos) do + if demo.active and demo.update then + demo.update() + end + end + end +end + +--- Fort Toolbar Demo --- + +local fort_toolbars_demo = { + text = 'fort toolbars', + available = dfhack.world.isFortressMode, +} + +do + local function fort_toolbars_visible() + return visible() and fort_toolbars_demo.active + end + + FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, Panel) + FortToolbarDemoPanel.ATTRS{ + frame_style = function(...) + local style = gui.FRAME_THIN(...) + style.signature_pen = false + return style + end, + visible_override = true, + visible = fort_toolbars_visible, + frame_background = { ch = 32, bg = COLOR_BLACK }, + } + + local left_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'left toolbar', + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + } + local center_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'center toolbar', + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + } + local right_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'right toolbar', + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + } + local secondary_visible = false + local secondary_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'secondary toolbar', + subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + visible = function() return fort_toolbars_visible() and secondary_visible end, + } + + fort_toolbars_demo.views = { + left_toolbar_demo, + center_toolbar_demo, + right_toolbar_demo, + secondary_toolbar_demo, + } + + ---@param secondary? DFLayout.Fort.SecondaryToolbar.Names + local function update_fort_toolbars(secondary) + -- by default, draw primary toolbar demonstrations right above the primary toolbars: + -- {l demo} {c demo} {r demo} + -- [l tool] [c tool] [r tool] (bottom of UI) + local toolbar_demo_dy = -layout.TOOLBAR_HEIGHT + local ir = gui.get_interface_rect() + ---@param v widgets.Panel + ---@param frame widgets.Widget.frame + ---@param buttons DFLayout.Toolbar.NamedButtons + local function update(v, frame, buttons) + v.frame = { + w = frame.w, + h = frame.h, + l = frame.l + ir.x1, + t = frame.t + ir.y1 + toolbar_demo_dy, + } + local sorted = {} + for _, button in pairs(buttons) do + utils.insert_sorted(sorted, button, 'offset') + end + local buttons = '' + for i, o in ipairs(sorted) do + if o.offset > #buttons then + buttons = buttons .. (' '):rep(o.offset - #buttons) + end + if o.width == 1 then + buttons = buttons .. '|' + elseif o.width > 1 then + buttons = buttons .. '/' .. ('-'):rep(o.width - 2) .. '\\' + end + end + v.subviews.buttons:setText( + buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 + ) + end + if secondary then + -- a secondary toolbar is active, move the primary demonstration up to + -- let the secondary be demonstrated right above the actual secondary: + -- {l demo} {c demo} {r demo} + -- {s demo} + -- [s tool] + -- [l tool] [c tool] [r tool] (bottom of UI) + update(secondary_toolbar_demo, layout.fort.secondary_toolbars[secondary].frame(ir), + layout.fort.secondary_toolbars[secondary].buttons) + secondary_visible = true + toolbar_demo_dy = toolbar_demo_dy - 2 * layout.SECONDARY_TOOLBAR_HEIGHT + else + secondary_visible = false + end + + update(left_toolbar_demo, layout.fort.toolbars.left.frame(ir), layout.fort.toolbars.left.buttons) + update(right_toolbar_demo, layout.fort.toolbars.right.frame(ir), layout.fort.toolbars.right.buttons) + update(center_toolbar_demo, layout.fort.toolbars.center.frame(ir), layout.fort.toolbars.center.buttons) + end + + local tool_from_designation = { + -- df.main_designation_type.NONE -- not a tool + [df.main_designation_type.DIG_DIG] = 'dig', + [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'dig', + [df.main_designation_type.DIG_STAIR_UP] = 'dig', + [df.main_designation_type.DIG_STAIR_UPDOWN] = 'dig', + [df.main_designation_type.DIG_STAIR_DOWN] = 'dig', + [df.main_designation_type.DIG_RAMP] = 'dig', + [df.main_designation_type.DIG_CHANNEL] = 'dig', + [df.main_designation_type.CHOP] = 'chop', + [df.main_designation_type.GATHER] = 'gather', + [df.main_designation_type.SMOOTH] = 'smooth', + [df.main_designation_type.TRACK] = 'smooth', + [df.main_designation_type.ENGRAVE] = 'smooth', + [df.main_designation_type.FORTIFY] = 'smooth', + -- df.main_designation_type.REMOVE_CONSTRUCTION -- not used? + [df.main_designation_type.CLAIM] = 'mass_designation', + [df.main_designation_type.UNCLAIM] = 'mass_designation', + [df.main_designation_type.MELT] = 'mass_designation', + [df.main_designation_type.NO_MELT] = 'mass_designation', + [df.main_designation_type.DUMP] = 'mass_designation', + [df.main_designation_type.NO_DUMP] = 'mass_designation', + [df.main_designation_type.HIDE] = 'mass_designation', + [df.main_designation_type.NO_HIDE] = 'mass_designation', + -- df.main_designation_type.TOGGLE_ENGRAVING -- not used? + [df.main_designation_type.DIG_FROM_MARKER] = 'dig', + [df.main_designation_type.DIG_TO_MARKER] = 'dig', + [df.main_designation_type.CHOP_FROM_MARKER] = 'chop', + [df.main_designation_type.CHOP_TO_MARKER] = 'chop', + [df.main_designation_type.GATHER_FROM_MARKER] = 'gather', + [df.main_designation_type.GATHER_TO_MARKER] = 'gather', + [df.main_designation_type.SMOOTH_FROM_MARKER] = 'smooth', + [df.main_designation_type.SMOOTH_TO_MARKER] = 'smooth', + [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'traffic', + [df.main_designation_type.ERASE] = 'erase', + } + local tool_from_bottom = { + -- df.main_bottom_mode_type.NONE + -- df.main_bottom_mode_type.BUILDING + -- df.main_bottom_mode_type.BUILDING_PLACEMENT + -- df.main_bottom_mode_type.BUILDING_PICK_MATERIALS + -- df.main_bottom_mode_type.ZONE + -- df.main_bottom_mode_type.ZONE_PAINT + [df.main_bottom_mode_type.STOCKPILE] = 'stockpile', + [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'stockpile_paint', + -- df.main_bottom_mode_type.BURROW + [df.main_bottom_mode_type.BURROW_PAINT] = 'burrow_paint' + -- df.main_bottom_mode_type.HAULING + -- df.main_bottom_mode_type.ARENA_UNIT + -- df.main_bottom_mode_type.ARENA_TREE + -- df.main_bottom_mode_type.ARENA_WATER_PAINT + -- df.main_bottom_mode_type.ARENA_MAGMA_PAINT + -- df.main_bottom_mode_type.ARENA_SNOW_PAINT + -- df.main_bottom_mode_type.ARENA_MUD_PAINT + -- df.main_bottom_mode_type.ARENA_REMOVE_PAINT + } + ---@return DFLayout.Fort.SecondaryToolbar.Names? + local function active_secondary() + local designation = df.global.game.main_interface.main_designation_selected + if designation ~= df.main_designation_type.NONE then + return tool_from_designation[designation] + end + local bottom = df.global.game.main_interface.bottom_mode_selected + if bottom ~= df.main_bottom_mode_type.NONE then + return tool_from_bottom[bottom] + end + end + + fort_toolbars_demo.update = function() + update_fort_toolbars(active_secondary()) + end + + local secondary + function center_toolbar_demo:render(...) + local new_secondary = active_secondary() + if new_secondary ~= secondary then + secondary = new_secondary + update_fort_toolbars(secondary) + end + return FortToolbarDemoPanel.render(self, ...) + end +end + +--- start demo control window --- + +screen = DemoScreen{ + demos = { + fort_toolbars_demo, + }, +}:show() diff --git a/docs/devel/dflayout.rst b/docs/devel/dflayout.rst new file mode 100644 index 0000000000..fde3dae773 --- /dev/null +++ b/docs/devel/dflayout.rst @@ -0,0 +1,17 @@ +devel/dflayout +============== + +.. dfhack-tool:: + :summary: Demonstrate the DF UI element position calculations available in the gui.dflayout module. + :tags: dev + +The demonstrations are GUI-based notations that show the calculated positions of +the supported DF UI elements. The main window includes a list of toggleable +demonstrations. + +Usage +----- + +:: + + devel/dflayout From 6c26925002f48b17e0fd5a35e279917e8568044f Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 6 Apr 2025 07:49:03 -0500 Subject: [PATCH 02/20] gui/mass-remove.toolbar: adopt gui.dflayout for positioning calculation --- gui/mass-remove.lua | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index ca3cb8aab6..e733c1ae87 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -9,6 +9,7 @@ local guidm = require('gui.dwarfmode') local utils = require('utils') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') +local layout = require('gui.dflayout') local function noop() end @@ -399,13 +400,42 @@ end -- MassRemoveToolbarOverlay -- +local MR_BUTTON_WIDTH = 4 +local MR_BUTTON_HEIGHT = layout.SECONDARY_TOOLBAR_HEIGHT +local MR_TOOLTIP_WIDTH = 26 +local MR_TOOLTIP_HEIGHT = 6 +local MR_WIDTH = math.max(MR_TOOLTIP_WIDTH, MR_BUTTON_WIDTH) +local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[ empty line ]] + MR_BUTTON_HEIGHT + +local erase_toolbar = layout.fort.secondary_toolbars.erase + +-- one "gap column" past the right end of the erase secondary toolbar +local function mass_remove_button_offsets(interface_size) + local erase_frame = erase_toolbar.frame(interface_size) + return { + l = erase_frame.l + erase_frame.w + 1, + w = erase_frame.w, + r = erase_frame.r - 1 - MR_BUTTON_WIDTH, + + t = erase_frame.t, + h = erase_frame.h, + b = erase_frame.b, + } +end + +-- If the overlay version is bumped, this could be changed to +-- mass_remove_button_offsets(layout.MINIMUM_INTERFACE_RECT).l +-- Adopting the calculated value would let the overlay be moved all the way to +-- the left in a minimum-size interface. +local MR_DEFAULT_L_OFFSET = 41 + MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ desc='Adds a button to the erase toolbar to open the mass removal tool.', - default_pos={x=42, y=-4}, + default_pos={x=(MR_DEFAULT_L_OFFSET+1), y=-(layout.TOOLBAR_HEIGHT+1)}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', - frame={w=26, h=10}, + frame={w=MR_WIDTH, h=MR_HEIGHT}, } function MassRemoveToolbarOverlay:init() @@ -417,7 +447,7 @@ function MassRemoveToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, r=0, w=26, h=6}, + frame={t=0, r=0, w=MR_WIDTH, h=MR_TOOLTIP_HEIGHT}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, @@ -435,7 +465,7 @@ function MassRemoveToolbarOverlay:init() }, widgets.Panel{ view_id='icon', - frame={b=0, r=22, w=4, h=3}, + frame={b=0, r=0, w=MR_WIDTH, h=MR_BUTTON_HEIGHT}, subviews={ widgets.Label{ text=widgets.makeButtonLabelText{ @@ -469,12 +499,7 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - local w = parent_rect.width - if w <= 130 then - self.frame.w = 50 - else - self.frame.w = (parent_rect.width+1)//2 - 15 - end + self.frame.w = MR_WIDTH + math.max(0, mass_remove_button_offsets(parent_rect).l - MR_DEFAULT_L_OFFSET) end function MassRemoveToolbarOverlay:onInput(keys) From 9d5cbd99ff19eaa3947ef39417a3294db7ae4be0 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 13 Apr 2025 21:15:07 -0500 Subject: [PATCH 03/20] devel/dflayout: use top-level gui.widgets module Per https://github.com/DFHack/scripts/pull/1426#pullrequestreview-2762815886 --- devel/dflayout.lua | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 2e4eb3368c..558cd012f9 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -1,10 +1,6 @@ local gui = require('gui') local layout = require('gui.dflayout') -local Panel = require('gui.widgets.containers.panel') -local Window = require('gui.widgets.containers.window') -local Label = require('gui.widgets.labels.label') -local Toggle = require('gui.widgets.labels.toggle_hotkey_label') -local List = require('gui.widgets.list') +local widgets = require('gui.widgets') local utils = require('utils') --- Demo Control Window and Screen --- @@ -26,7 +22,7 @@ do -- limit env pollution return screen:isActive() and not screen.defocused end - local DemoWindow = defclass(nil, Window) + local DemoWindow = defclass(nil, widgets.Window) DemoWindow.ATTRS{ frame_title = 'dflayout demos', frame = { w = 39, h = 9 }, @@ -38,14 +34,14 @@ do -- limit env pollution function DemoWindow:init(args) self.demos = args.demos self:addviews{ - Toggle{ + widgets.ToggleHotkeyLabel{ label = 'Demos visible when not focused?', initial_option = visible_when_not_focused, on_change = function(new, old) visible_when_not_focused = new end }, - List{ + widgets.List{ view_id = 'list', frame = { h = 10, }, icon_pen = COLOR_GREY, @@ -129,7 +125,7 @@ do return visible() and fort_toolbars_demo.active end - FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, Panel) + FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, widgets.Panel) FortToolbarDemoPanel.ATTRS{ frame_style = function(...) local style = gui.FRAME_THIN(...) @@ -143,20 +139,20 @@ do local left_toolbar_demo = FortToolbarDemoPanel{ frame_title = 'left toolbar', - subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local center_toolbar_demo = FortToolbarDemoPanel{ frame_title = 'center toolbar', - subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local right_toolbar_demo = FortToolbarDemoPanel{ frame_title = 'right toolbar', - subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, } local secondary_visible = false local secondary_toolbar_demo = FortToolbarDemoPanel{ frame_title = 'secondary toolbar', - subviews = { Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, visible = function() return fort_toolbars_visible() and secondary_visible end, } From cc9f9d4022d08d0755fe5bb1e01cc3bcdef55f2e Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 13 Apr 2025 21:30:23 -0500 Subject: [PATCH 04/20] devel/dflayout: use globals for defclass Per https://github.com/DFHack/scripts/pull/1426#discussion_r2041183432 --- devel/dflayout.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 558cd012f9..537179a77f 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -6,7 +6,6 @@ local utils = require('utils') --- Demo Control Window and Screen --- local screen -local DemoScreen local visible do -- limit env pollution @@ -22,7 +21,7 @@ do -- limit env pollution return screen:isActive() and not screen.defocused end - local DemoWindow = defclass(nil, widgets.Window) + DemoWindow = defclass(DemoWindow, widgets.Window) DemoWindow.ATTRS{ frame_title = 'dflayout demos', frame = { w = 39, h = 9 }, @@ -75,7 +74,7 @@ do -- limit env pollution return self end - DemoScreen = defclass(nil, gui.ZScreen) + DemoScreen = defclass(DemoScreen, gui.ZScreen) function DemoScreen:init(args) self.demos = args.demos local function demo_views() From 61c793ddc4ffe38cd3e86e24865074eab91121de Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 13 Apr 2025 21:40:16 -0500 Subject: [PATCH 05/20] devel/dflayout: remove do-based scopes Per https://github.com/DFHack/scripts/pull/1426#discussion_r2041183827 --- devel/dflayout.lua | 507 ++++++++++++++++++++++----------------------- 1 file changed, 251 insertions(+), 256 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 537179a77f..4aa2c9d43a 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -6,108 +6,105 @@ local utils = require('utils') --- Demo Control Window and Screen --- local screen -local visible -do -- limit env pollution - local function demo_available(demo) - if not demo.available then return true end - return demo.available() - end +local function demo_available(demo) + if not demo.available then return true end + return demo.available() +end - local visible_when_not_focused = true - function visible() - if visible_when_not_focused then return true end - if not screen then return false end - return screen:isActive() and not screen.defocused - end +local visible_when_not_focused = true +function visible() + if visible_when_not_focused then return true end + if not screen then return false end + return screen:isActive() and not screen.defocused +end - DemoWindow = defclass(DemoWindow, widgets.Window) - DemoWindow.ATTRS{ - frame_title = 'dflayout demos', - frame = { w = 39, h = 9 }, - resizable = true, - autoarrange_subviews = true, - autoarrange_gap = 1, - } +DemoWindow = defclass(DemoWindow, widgets.Window) +DemoWindow.ATTRS{ + frame_title = 'dflayout demos', + frame = { w = 39, h = 9 }, + resizable = true, + autoarrange_subviews = true, + autoarrange_gap = 1, +} - function DemoWindow:init(args) - self.demos = args.demos - self:addviews{ - widgets.ToggleHotkeyLabel{ - label = 'Demos visible when not focused?', - initial_option = visible_when_not_focused, - on_change = function(new, old) - visible_when_not_focused = new - end - }, - widgets.List{ - view_id = 'list', - frame = { h = 10, }, - icon_pen = COLOR_GREY, - icon_width = 3, - on_submit = function(index, item) - local demo = self.demos[index] - demo.active = demo_available(demo) and not demo.active - self:refresh() - end - }, - } - end +function DemoWindow:init(args) + self.demos = args.demos + self:addviews{ + widgets.ToggleHotkeyLabel{ + label = 'Demos visible when not focused?', + initial_option = visible_when_not_focused, + on_change = function(new, old) + visible_when_not_focused = new + end + }, + widgets.List{ + view_id = 'list', + frame = { h = 10, }, + icon_pen = COLOR_GREY, + icon_width = 3, + on_submit = function(index, item) + local demo = self.demos[index] + demo.active = demo_available(demo) and not demo.active + self:refresh() + end + }, + } +end - local CHECK = string.char(251) -- U+221A SQUARE ROOT +local CHECK = string.char(251) -- U+221A SQUARE ROOT - function DemoWindow:refresh() - local choices = {} - for _, demo in ipairs(self.demos) do - local icon - if not demo_available(demo) then - icon = '-' - elseif demo.active then - icon = CHECK - end - table.insert(choices, { - text = demo.text, - icon = icon, - }) +function DemoWindow:refresh() + local choices = {} + for _, demo in ipairs(self.demos) do + local icon + if not demo_available(demo) then + icon = '-' + elseif demo.active then + icon = CHECK end - self.subviews.list:setChoices(choices) - return self + table.insert(choices, { + text = demo.text, + icon = icon, + }) end + self.subviews.list:setChoices(choices) + return self +end - DemoScreen = defclass(DemoScreen, gui.ZScreen) - function DemoScreen:init(args) - self.demos = args.demos - local function demo_views() - local views = {} - for _, demo in ipairs(self.demos) do - if demo.views then - table.move(demo.views, 1, #demo.views, #views + 1, views) - end +DemoScreen = defclass(DemoScreen, gui.ZScreen) +function DemoScreen:init(args) + self.demos = args.demos + local function demo_views() + local views = {} + for _, demo in ipairs(self.demos) do + if demo.views then + table.move(demo.views, 1, #demo.views, #views + 1, views) end - return views end - self:addviews{ - DemoWindow{ demos = self.demos }:refresh(), - table.unpack(demo_views()) - } + return views end + self:addviews{ + DemoWindow{ demos = self.demos }:refresh(), + table.unpack(demo_views()) + } +end - local if_percentage - function DemoScreen:render(...) - if visible_when_not_focused then - local new_if_percentage = df.global.init.display.max_interface_percentage - if new_if_percentage ~= if_percentage then - self:updateLayout() - end +local if_percentage +function DemoScreen:render(...) + if visible_when_not_focused then + local new_if_percentage = df.global.init.display.max_interface_percentage + if new_if_percentage ~= if_percentage then + self:updateLayout() end - return DemoScreen.super.render(self, ...) end + return DemoScreen.super.render(self, ...) +end - function DemoScreen:postComputeFrame(frame_body) - for _, demo in ipairs(self.demos) do - if demo.active and demo.update then - demo.update() - end +function DemoScreen:postComputeFrame(frame_body) + for _, demo in ipairs(self.demos) do + if demo.active and demo.update then + demo.update() end end end @@ -119,189 +116,187 @@ local fort_toolbars_demo = { available = dfhack.world.isFortressMode, } -do - local function fort_toolbars_visible() - return visible() and fort_toolbars_demo.active - end +local function fort_toolbars_visible() + return visible() and fort_toolbars_demo.active +end - FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, widgets.Panel) - FortToolbarDemoPanel.ATTRS{ - frame_style = function(...) - local style = gui.FRAME_THIN(...) - style.signature_pen = false - return style - end, - visible_override = true, - visible = fort_toolbars_visible, - frame_background = { ch = 32, bg = COLOR_BLACK }, - } +FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, widgets.Panel) +FortToolbarDemoPanel.ATTRS{ + frame_style = function(...) + local style = gui.FRAME_THIN(...) + style.signature_pen = false + return style + end, + visible_override = true, + visible = fort_toolbars_visible, + frame_background = { ch = 32, bg = COLOR_BLACK }, +} - local left_toolbar_demo = FortToolbarDemoPanel{ - frame_title = 'left toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, - } - local center_toolbar_demo = FortToolbarDemoPanel{ - frame_title = 'center toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, - } - local right_toolbar_demo = FortToolbarDemoPanel{ - frame_title = 'right toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, - } - local secondary_visible = false - local secondary_toolbar_demo = FortToolbarDemoPanel{ - frame_title = 'secondary toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, - visible = function() return fort_toolbars_visible() and secondary_visible end, - } +local left_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'left toolbar', + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, +} +local center_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'center toolbar', + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, +} +local right_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'right toolbar', + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, +} +local secondary_visible = false +local secondary_toolbar_demo = FortToolbarDemoPanel{ + frame_title = 'secondary toolbar', + subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + visible = function() return fort_toolbars_visible() and secondary_visible end, +} - fort_toolbars_demo.views = { - left_toolbar_demo, - center_toolbar_demo, - right_toolbar_demo, - secondary_toolbar_demo, - } +fort_toolbars_demo.views = { + left_toolbar_demo, + center_toolbar_demo, + right_toolbar_demo, + secondary_toolbar_demo, +} - ---@param secondary? DFLayout.Fort.SecondaryToolbar.Names - local function update_fort_toolbars(secondary) - -- by default, draw primary toolbar demonstrations right above the primary toolbars: - -- {l demo} {c demo} {r demo} - -- [l tool] [c tool] [r tool] (bottom of UI) - local toolbar_demo_dy = -layout.TOOLBAR_HEIGHT - local ir = gui.get_interface_rect() - ---@param v widgets.Panel - ---@param frame widgets.Widget.frame - ---@param buttons DFLayout.Toolbar.NamedButtons - local function update(v, frame, buttons) - v.frame = { - w = frame.w, - h = frame.h, - l = frame.l + ir.x1, - t = frame.t + ir.y1 + toolbar_demo_dy, - } - local sorted = {} - for _, button in pairs(buttons) do - utils.insert_sorted(sorted, button, 'offset') +---@param secondary? DFLayout.Fort.SecondaryToolbar.Names +local function update_fort_toolbars(secondary) + -- by default, draw primary toolbar demonstrations right above the primary toolbars: + -- {l demo} {c demo} {r demo} + -- [l tool] [c tool] [r tool] (bottom of UI) + local toolbar_demo_dy = -layout.TOOLBAR_HEIGHT + local ir = gui.get_interface_rect() + ---@param v widgets.Panel + ---@param frame widgets.Widget.frame + ---@param buttons DFLayout.Toolbar.NamedButtons + local function update(v, frame, buttons) + v.frame = { + w = frame.w, + h = frame.h, + l = frame.l + ir.x1, + t = frame.t + ir.y1 + toolbar_demo_dy, + } + local sorted = {} + for _, button in pairs(buttons) do + utils.insert_sorted(sorted, button, 'offset') + end + local buttons = '' + for i, o in ipairs(sorted) do + if o.offset > #buttons then + buttons = buttons .. (' '):rep(o.offset - #buttons) end - local buttons = '' - for i, o in ipairs(sorted) do - if o.offset > #buttons then - buttons = buttons .. (' '):rep(o.offset - #buttons) - end - if o.width == 1 then - buttons = buttons .. '|' - elseif o.width > 1 then - buttons = buttons .. '/' .. ('-'):rep(o.width - 2) .. '\\' - end + if o.width == 1 then + buttons = buttons .. '|' + elseif o.width > 1 then + buttons = buttons .. '/' .. ('-'):rep(o.width - 2) .. '\\' end - v.subviews.buttons:setText( - buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 - ) - end - if secondary then - -- a secondary toolbar is active, move the primary demonstration up to - -- let the secondary be demonstrated right above the actual secondary: - -- {l demo} {c demo} {r demo} - -- {s demo} - -- [s tool] - -- [l tool] [c tool] [r tool] (bottom of UI) - update(secondary_toolbar_demo, layout.fort.secondary_toolbars[secondary].frame(ir), - layout.fort.secondary_toolbars[secondary].buttons) - secondary_visible = true - toolbar_demo_dy = toolbar_demo_dy - 2 * layout.SECONDARY_TOOLBAR_HEIGHT - else - secondary_visible = false end - - update(left_toolbar_demo, layout.fort.toolbars.left.frame(ir), layout.fort.toolbars.left.buttons) - update(right_toolbar_demo, layout.fort.toolbars.right.frame(ir), layout.fort.toolbars.right.buttons) - update(center_toolbar_demo, layout.fort.toolbars.center.frame(ir), layout.fort.toolbars.center.buttons) + v.subviews.buttons:setText( + buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 + ) end - - local tool_from_designation = { - -- df.main_designation_type.NONE -- not a tool - [df.main_designation_type.DIG_DIG] = 'dig', - [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'dig', - [df.main_designation_type.DIG_STAIR_UP] = 'dig', - [df.main_designation_type.DIG_STAIR_UPDOWN] = 'dig', - [df.main_designation_type.DIG_STAIR_DOWN] = 'dig', - [df.main_designation_type.DIG_RAMP] = 'dig', - [df.main_designation_type.DIG_CHANNEL] = 'dig', - [df.main_designation_type.CHOP] = 'chop', - [df.main_designation_type.GATHER] = 'gather', - [df.main_designation_type.SMOOTH] = 'smooth', - [df.main_designation_type.TRACK] = 'smooth', - [df.main_designation_type.ENGRAVE] = 'smooth', - [df.main_designation_type.FORTIFY] = 'smooth', - -- df.main_designation_type.REMOVE_CONSTRUCTION -- not used? - [df.main_designation_type.CLAIM] = 'mass_designation', - [df.main_designation_type.UNCLAIM] = 'mass_designation', - [df.main_designation_type.MELT] = 'mass_designation', - [df.main_designation_type.NO_MELT] = 'mass_designation', - [df.main_designation_type.DUMP] = 'mass_designation', - [df.main_designation_type.NO_DUMP] = 'mass_designation', - [df.main_designation_type.HIDE] = 'mass_designation', - [df.main_designation_type.NO_HIDE] = 'mass_designation', - -- df.main_designation_type.TOGGLE_ENGRAVING -- not used? - [df.main_designation_type.DIG_FROM_MARKER] = 'dig', - [df.main_designation_type.DIG_TO_MARKER] = 'dig', - [df.main_designation_type.CHOP_FROM_MARKER] = 'chop', - [df.main_designation_type.CHOP_TO_MARKER] = 'chop', - [df.main_designation_type.GATHER_FROM_MARKER] = 'gather', - [df.main_designation_type.GATHER_TO_MARKER] = 'gather', - [df.main_designation_type.SMOOTH_FROM_MARKER] = 'smooth', - [df.main_designation_type.SMOOTH_TO_MARKER] = 'smooth', - [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'traffic', - [df.main_designation_type.ERASE] = 'erase', - } - local tool_from_bottom = { - -- df.main_bottom_mode_type.NONE - -- df.main_bottom_mode_type.BUILDING - -- df.main_bottom_mode_type.BUILDING_PLACEMENT - -- df.main_bottom_mode_type.BUILDING_PICK_MATERIALS - -- df.main_bottom_mode_type.ZONE - -- df.main_bottom_mode_type.ZONE_PAINT - [df.main_bottom_mode_type.STOCKPILE] = 'stockpile', - [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'stockpile_paint', - -- df.main_bottom_mode_type.BURROW - [df.main_bottom_mode_type.BURROW_PAINT] = 'burrow_paint' - -- df.main_bottom_mode_type.HAULING - -- df.main_bottom_mode_type.ARENA_UNIT - -- df.main_bottom_mode_type.ARENA_TREE - -- df.main_bottom_mode_type.ARENA_WATER_PAINT - -- df.main_bottom_mode_type.ARENA_MAGMA_PAINT - -- df.main_bottom_mode_type.ARENA_SNOW_PAINT - -- df.main_bottom_mode_type.ARENA_MUD_PAINT - -- df.main_bottom_mode_type.ARENA_REMOVE_PAINT - } - ---@return DFLayout.Fort.SecondaryToolbar.Names? - local function active_secondary() - local designation = df.global.game.main_interface.main_designation_selected - if designation ~= df.main_designation_type.NONE then - return tool_from_designation[designation] - end - local bottom = df.global.game.main_interface.bottom_mode_selected - if bottom ~= df.main_bottom_mode_type.NONE then - return tool_from_bottom[bottom] - end + if secondary then + -- a secondary toolbar is active, move the primary demonstration up to + -- let the secondary be demonstrated right above the actual secondary: + -- {l demo} {c demo} {r demo} + -- {s demo} + -- [s tool] + -- [l tool] [c tool] [r tool] (bottom of UI) + update(secondary_toolbar_demo, layout.fort.secondary_toolbars[secondary].frame(ir), + layout.fort.secondary_toolbars[secondary].buttons) + secondary_visible = true + toolbar_demo_dy = toolbar_demo_dy - 2 * layout.SECONDARY_TOOLBAR_HEIGHT + else + secondary_visible = false end - fort_toolbars_demo.update = function() - update_fort_toolbars(active_secondary()) + update(left_toolbar_demo, layout.fort.toolbars.left.frame(ir), layout.fort.toolbars.left.buttons) + update(right_toolbar_demo, layout.fort.toolbars.right.frame(ir), layout.fort.toolbars.right.buttons) + update(center_toolbar_demo, layout.fort.toolbars.center.frame(ir), layout.fort.toolbars.center.buttons) +end + +local tool_from_designation = { + -- df.main_designation_type.NONE -- not a tool + [df.main_designation_type.DIG_DIG] = 'dig', + [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'dig', + [df.main_designation_type.DIG_STAIR_UP] = 'dig', + [df.main_designation_type.DIG_STAIR_UPDOWN] = 'dig', + [df.main_designation_type.DIG_STAIR_DOWN] = 'dig', + [df.main_designation_type.DIG_RAMP] = 'dig', + [df.main_designation_type.DIG_CHANNEL] = 'dig', + [df.main_designation_type.CHOP] = 'chop', + [df.main_designation_type.GATHER] = 'gather', + [df.main_designation_type.SMOOTH] = 'smooth', + [df.main_designation_type.TRACK] = 'smooth', + [df.main_designation_type.ENGRAVE] = 'smooth', + [df.main_designation_type.FORTIFY] = 'smooth', + -- df.main_designation_type.REMOVE_CONSTRUCTION -- not used? + [df.main_designation_type.CLAIM] = 'mass_designation', + [df.main_designation_type.UNCLAIM] = 'mass_designation', + [df.main_designation_type.MELT] = 'mass_designation', + [df.main_designation_type.NO_MELT] = 'mass_designation', + [df.main_designation_type.DUMP] = 'mass_designation', + [df.main_designation_type.NO_DUMP] = 'mass_designation', + [df.main_designation_type.HIDE] = 'mass_designation', + [df.main_designation_type.NO_HIDE] = 'mass_designation', + -- df.main_designation_type.TOGGLE_ENGRAVING -- not used? + [df.main_designation_type.DIG_FROM_MARKER] = 'dig', + [df.main_designation_type.DIG_TO_MARKER] = 'dig', + [df.main_designation_type.CHOP_FROM_MARKER] = 'chop', + [df.main_designation_type.CHOP_TO_MARKER] = 'chop', + [df.main_designation_type.GATHER_FROM_MARKER] = 'gather', + [df.main_designation_type.GATHER_TO_MARKER] = 'gather', + [df.main_designation_type.SMOOTH_FROM_MARKER] = 'smooth', + [df.main_designation_type.SMOOTH_TO_MARKER] = 'smooth', + [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'traffic', + [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'traffic', + [df.main_designation_type.ERASE] = 'erase', +} +local tool_from_bottom = { + -- df.main_bottom_mode_type.NONE + -- df.main_bottom_mode_type.BUILDING + -- df.main_bottom_mode_type.BUILDING_PLACEMENT + -- df.main_bottom_mode_type.BUILDING_PICK_MATERIALS + -- df.main_bottom_mode_type.ZONE + -- df.main_bottom_mode_type.ZONE_PAINT + [df.main_bottom_mode_type.STOCKPILE] = 'stockpile', + [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'stockpile_paint', + -- df.main_bottom_mode_type.BURROW + [df.main_bottom_mode_type.BURROW_PAINT] = 'burrow_paint' + -- df.main_bottom_mode_type.HAULING + -- df.main_bottom_mode_type.ARENA_UNIT + -- df.main_bottom_mode_type.ARENA_TREE + -- df.main_bottom_mode_type.ARENA_WATER_PAINT + -- df.main_bottom_mode_type.ARENA_MAGMA_PAINT + -- df.main_bottom_mode_type.ARENA_SNOW_PAINT + -- df.main_bottom_mode_type.ARENA_MUD_PAINT + -- df.main_bottom_mode_type.ARENA_REMOVE_PAINT +} +---@return DFLayout.Fort.SecondaryToolbar.Names? +local function active_secondary() + local designation = df.global.game.main_interface.main_designation_selected + if designation ~= df.main_designation_type.NONE then + return tool_from_designation[designation] end + local bottom = df.global.game.main_interface.bottom_mode_selected + if bottom ~= df.main_bottom_mode_type.NONE then + return tool_from_bottom[bottom] + end +end - local secondary - function center_toolbar_demo:render(...) - local new_secondary = active_secondary() - if new_secondary ~= secondary then - secondary = new_secondary - update_fort_toolbars(secondary) - end - return FortToolbarDemoPanel.render(self, ...) +fort_toolbars_demo.update = function() + update_fort_toolbars(active_secondary()) +end + +local secondary +function center_toolbar_demo:render(...) + local new_secondary = active_secondary() + if new_secondary ~= secondary then + secondary = new_secondary + update_fort_toolbars(secondary) end + return FortToolbarDemoPanel.render(self, ...) end --- start demo control window --- From 291d7274be608e965c777c62d41f2761b2d4da9f Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 13 Apr 2025 21:43:20 -0500 Subject: [PATCH 06/20] devel/dflayout: docs: shrink summary Per https://github.com/DFHack/scripts/pull/1426#discussion_r2041184504 --- docs/devel/dflayout.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/devel/dflayout.rst b/docs/devel/dflayout.rst index fde3dae773..a1e5ee3be5 100644 --- a/docs/devel/dflayout.rst +++ b/docs/devel/dflayout.rst @@ -2,12 +2,13 @@ devel/dflayout ============== .. dfhack-tool:: - :summary: Demonstrate the DF UI element position calculations available in the gui.dflayout module. + :summary: Demonstrate gui.dflayout position calculations. :tags: dev -The demonstrations are GUI-based notations that show the calculated positions of -the supported DF UI elements. The main window includes a list of toggleable -demonstrations. +This script is a GUI that demonstrates the DF UI element position calculations +offered by the `gui.dflayout` module. + +The main window includes a list of toggleable demonstrations. Usage ----- From dd799bd73a8fd2c732a0e191a1a460926eaf21a9 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 13 Apr 2025 22:19:39 -0500 Subject: [PATCH 07/20] devel/dflayout: use global for ZScreen, provide focus_path Partially addresses https://github.com/DFHack/scripts/pull/1426#discussion_r2041182835 --- devel/dflayout.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 4aa2c9d43a..129428fb86 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -5,8 +5,6 @@ local utils = require('utils') --- Demo Control Window and Screen --- -local screen - local function demo_available(demo) if not demo.available then return true end return demo.available() @@ -73,6 +71,10 @@ function DemoWindow:refresh() end DemoScreen = defclass(DemoScreen, gui.ZScreen) +DemoScreen.ATTRS{ + focus_path = 'gui.dflayout-demo' +} + function DemoScreen:init(args) self.demos = args.demos local function demo_views() @@ -90,6 +92,10 @@ function DemoScreen:init(args) } end +function DemoScreen:onDismiss() + screen = nil +end + local if_percentage function DemoScreen:render(...) if visible_when_not_focused then @@ -301,7 +307,7 @@ end --- start demo control window --- -screen = DemoScreen{ +screen = screen and screen:raise() or DemoScreen{ demos = { fort_toolbars_demo, }, From 88122847a0b59e01274bea8371461d02990e4b9e Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 14 Apr 2025 08:10:27 -0500 Subject: [PATCH 08/20] devel/dflayout: docs: do not link to gui.dflayout Linking can be made to work, but would require - a ref declaration (label) in the Lua API docs, and - the gui.dflayout section to be present in the scripts CI's view (it is still in a PR, so the scripts CI does not currently see it). Just use normal code formatting. --- docs/devel/dflayout.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel/dflayout.rst b/docs/devel/dflayout.rst index a1e5ee3be5..93613321ad 100644 --- a/docs/devel/dflayout.rst +++ b/docs/devel/dflayout.rst @@ -6,7 +6,7 @@ devel/dflayout :tags: dev This script is a GUI that demonstrates the DF UI element position calculations -offered by the `gui.dflayout` module. +offered by the ``gui.dflayout`` module. The main window includes a list of toggleable demonstrations. From 91a06d3e2891cc2b5e969534ecbffcdd33a80a9e Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 14 Apr 2025 23:31:33 -0500 Subject: [PATCH 09/20] devel/dflayout: provide Demo type Makes explicit the fields required from each "demo". Remove demo_available(), just use demo.available() directly. ("Always available if .available is nil" seems a bit awkward) --- devel/dflayout.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 129428fb86..a5571538a8 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -5,10 +5,12 @@ local utils = require('utils') --- Demo Control Window and Screen --- -local function demo_available(demo) - if not demo.available then return true end - return demo.available() -end +---@class Demo +---@field text string text displayed in main window demo list +---@field available fun(): boolean? return true if demo is available in current context +---@field active? boolean whether the main window has enabled this demo (managed by main window) +---@field views gui.View[] list of views to add to main ZScreen +---@field update fun() called by main window to recompute demo frames local visible_when_not_focused = true function visible() @@ -26,6 +28,7 @@ DemoWindow.ATTRS{ autoarrange_gap = 1, } +---@param args { demos: Demo[] } function DemoWindow:init(args) self.demos = args.demos self:addviews{ @@ -43,7 +46,7 @@ function DemoWindow:init(args) icon_width = 3, on_submit = function(index, item) local demo = self.demos[index] - demo.active = demo_available(demo) and not demo.active + demo.active = demo.available() and not demo.active self:refresh() end }, @@ -56,7 +59,7 @@ function DemoWindow:refresh() local choices = {} for _, demo in ipairs(self.demos) do local icon - if not demo_available(demo) then + if not demo.available() then icon = '-' elseif demo.active then icon = CHECK @@ -117,6 +120,7 @@ end --- Fort Toolbar Demo --- +---@class FortToolbarsDemo: Demo local fort_toolbars_demo = { text = 'fort toolbars', available = dfhack.world.isFortressMode, From e83410e11193032cf6987811409d7f64c1b195a9 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 14 Apr 2025 23:54:18 -0500 Subject: [PATCH 10/20] devel/dflayout: fix interface percentage change detection The interface percentage was not being saved, so it was constantly initiating updates. Once the spurious updates were fixed, it uncovered broken update processing when activating a demo and when the fort toolbar demo was told to update itself. --- devel/dflayout.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index a5571538a8..a7f652919a 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -47,6 +47,7 @@ function DemoWindow:init(args) on_submit = function(index, item) local demo = self.demos[index] demo.active = demo.available() and not demo.active + if demo.active then demo.update() end self:refresh() end }, @@ -104,6 +105,7 @@ function DemoScreen:render(...) if visible_when_not_focused then local new_if_percentage = df.global.init.display.max_interface_percentage if new_if_percentage ~= if_percentage then + if_percentage = new_if_percentage self:updateLayout() end end @@ -203,6 +205,7 @@ local function update_fort_toolbars(secondary) v.subviews.buttons:setText( buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 ) + v:updateLayout() end if secondary then -- a secondary toolbar is active, move the primary demonstration up to From c2001bb25bc8fc72076482eedc28b05ca03ac71e Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 02:30:43 -0500 Subject: [PATCH 11/20] devel/dflayout: rework fort toolbar demos Have the toolbar demos set their own frames via computeFrame. These overridden computeFrames also allow the button extent indicator strings to draw over the edges of the Panel's border. The left, right, and secondary toolbars don't have their own "borders" like the center toolbar, so the first and last buttons in those toolbars previously had their button indicators cut off by the Panel border. Certainly not a good look for general use, but for a devel inspection tool, it should be okay (and leaves the Panel borders showing the true extent of each toolbar). Only compute the button strings once for the static left, center, and right toolbars. --- devel/dflayout.lua | 191 +++++++++++++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 59 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index a7f652919a..7aa5ebf8d5 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -132,35 +132,140 @@ local function fort_toolbars_visible() return visible() and fort_toolbars_demo.active end -FortToolbarDemoPanel = defclass(FortToolbarDemoPanel, widgets.Panel) -FortToolbarDemoPanel.ATTRS{ +local secondary_visible = false + +local function primary_toolbar_dy() + if secondary_visible then + -- When a secondary toolbar is active, move the primary demos up to let + -- the secondary demo be right above the actual secondary: + -- {l demo} {c demo} {r demo} + -- {s demo} + -- [s tool] + -- [l tool] [c tool] [r tool] (bottom of UI) + return -(layout.TOOLBAR_HEIGHT + 2 * layout.SECONDARY_TOOLBAR_HEIGHT) + else + -- Otherwise, draw primary toolbar demos right above the primary + -- toolbars: + -- {l demo} {c demo} {r demo} + -- [l tool] [c tool] [r tool] (bottom of UI) + return -layout.TOOLBAR_HEIGHT + end +end + +-- Generates a `view:computeFrame()` function that tracks the placement of the +-- given `toolbar`. +-- +-- Note: The returned function does not return a separate body rect; subviews +-- will be able to overwrite the normal UI-drawn frame! +---@param toolbar DFLayout.Toolbar +---@param dy_fn fun(): integer +---@return function +local function get_computeFrame_fn(toolbar, dy_fn) + return function(self, parent_rect) + local ir = gui.get_interface_rect() + local frame = toolbar.frame(ir) + return gui.mkdims_wh( + ir.x1 + frame.l, + ir.y1 + frame.t + dy_fn(), + frame.w, + frame.h) + end +end + +---@param buttons DFLayout.Toolbar.NamedButtons +local function buttons_string(buttons) + local sorted = {} + for _, button in pairs(buttons) do + utils.insert_sorted(sorted, button, 'offset') + end + -- For a one-column button, use | to indicate the button's position. + -- For wider buttons, use shapes like /\ or /--\ to illustrate the + -- button's position and width. + local str = '' + for i, o in ipairs(sorted) do + if o.offset > #str then + str = str .. (' '):rep(o.offset - #str) + end + if o.width == 1 then + str = str .. '|' + elseif o.width > 1 then + str = str .. '/' .. ('-'):rep(o.width - 2) .. '\\' + end + end + return str +end + +---@class ToolbarDemo.attrs: widgets.Panel.attrs +---@class ToolbarDemo.attrs.partial: widgets.Panel.attrs.partial +---@class ToolbarDemo.initTable: ToolbarDemo.attrs.partial, { toolbar?: DFLayout.Toolbar, toolbar_dy?: fun(): integer } +---@class ToolbarDemo: widgets.Panel +---@field super widgets.Panel +---@field ATTRS ToolbarDemo.attrs|fun(attributes: ToolbarDemo.attrs.partial) +---@overload fun(init_table: ToolbarDemo.initTable): self +ToolbarDemo = defclass(ToolbarDemo, widgets.Panel) +ToolbarDemo.ATTRS{ frame_style = function(...) local style = gui.FRAME_THIN(...) style.signature_pen = false return style end, - visible_override = true, visible = fort_toolbars_visible, frame_background = { ch = 32, bg = COLOR_BLACK }, } -local left_toolbar_demo = FortToolbarDemoPanel{ +---@param args ToolbarDemo.initTable +function ToolbarDemo:init(args) + self.label = widgets.Label{ frame = { l = 0 } } + if args.toolbar and args.toolbar_dy then + self:update_to_toolbar(args.toolbar, args.toolbar_dy) + end + self:addviews{ self.label } +end + +---@param toolbar DFLayout.Toolbar +---@param dy fun(): integer +---@return unknown +function ToolbarDemo:update_to_toolbar(toolbar, dy) + -- set button representation string + local text = buttons_string(toolbar.buttons) + local l_inset = 0 + if text:sub(1, 1) == ' ' then + -- don't overwrite the left border edge with a plain space + l_inset = 1 + text = text:sub(2) + end + self.label.frame.l = l_inset + self.label:setText(text) + + -- track actual toolbar, but with a y offset + self.computeFrame = get_computeFrame_fn(toolbar, dy) + + return self +end + +local left_toolbar_demo = ToolbarDemo{ frame_title = 'left toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + toolbar = layout.fort.toolbars.left, + toolbar_dy = primary_toolbar_dy, } -local center_toolbar_demo = FortToolbarDemoPanel{ + +local center_toolbar_demo = ToolbarDemo{ frame_title = 'center toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + toolbar = layout.fort.toolbars.center, + toolbar_dy = primary_toolbar_dy, } -local right_toolbar_demo = FortToolbarDemoPanel{ + +local right_toolbar_demo = ToolbarDemo{ frame_title = 'right toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, + toolbar = layout.fort.toolbars.right, + toolbar_dy = primary_toolbar_dy, } -local secondary_visible = false -local secondary_toolbar_demo = FortToolbarDemoPanel{ + +local secondary_toolbar_demo = ToolbarDemo{ frame_title = 'secondary toolbar', - subviews = { widgets.Label{ view_id = 'buttons', frame = { l = 0, r = 0 } } }, - visible = function() return fort_toolbars_visible() and secondary_visible end, + visible = function() + return fort_toolbars_visible() and secondary_visible + end, } fort_toolbars_demo.views = { @@ -172,59 +277,26 @@ fort_toolbars_demo.views = { ---@param secondary? DFLayout.Fort.SecondaryToolbar.Names local function update_fort_toolbars(secondary) - -- by default, draw primary toolbar demonstrations right above the primary toolbars: - -- {l demo} {c demo} {r demo} - -- [l tool] [c tool] [r tool] (bottom of UI) - local toolbar_demo_dy = -layout.TOOLBAR_HEIGHT - local ir = gui.get_interface_rect() - ---@param v widgets.Panel - ---@param frame widgets.Widget.frame - ---@param buttons DFLayout.Toolbar.NamedButtons - local function update(v, frame, buttons) - v.frame = { - w = frame.w, - h = frame.h, - l = frame.l + ir.x1, - t = frame.t + ir.y1 + toolbar_demo_dy, - } - local sorted = {} - for _, button in pairs(buttons) do - utils.insert_sorted(sorted, button, 'offset') - end - local buttons = '' - for i, o in ipairs(sorted) do - if o.offset > #buttons then - buttons = buttons .. (' '):rep(o.offset - #buttons) - end - if o.width == 1 then - buttons = buttons .. '|' - elseif o.width > 1 then - buttons = buttons .. '/' .. ('-'):rep(o.width - 2) .. '\\' - end + local function updateLayout(view) + if view.frame_parent_rect then + view:updateLayout() end - v.subviews.buttons:setText( - buttons:sub(2) -- the demo panel border is at offset 0, so trim first character to start at offset 1 - ) - v:updateLayout() end if secondary then - -- a secondary toolbar is active, move the primary demonstration up to - -- let the secondary be demonstrated right above the actual secondary: - -- {l demo} {c demo} {r demo} - -- {s demo} - -- [s tool] - -- [l tool] [c tool] [r tool] (bottom of UI) - update(secondary_toolbar_demo, layout.fort.secondary_toolbars[secondary].frame(ir), - layout.fort.secondary_toolbars[secondary].buttons) + -- show secondary demo just above actual secondary + local function dy() + return -layout.SECONDARY_TOOLBAR_HEIGHT + end + secondary_toolbar_demo:update_to_toolbar(layout.fort.secondary_toolbars[secondary], dy) + updateLayout(secondary_toolbar_demo) secondary_visible = true - toolbar_demo_dy = toolbar_demo_dy - 2 * layout.SECONDARY_TOOLBAR_HEIGHT else secondary_visible = false end - update(left_toolbar_demo, layout.fort.toolbars.left.frame(ir), layout.fort.toolbars.left.buttons) - update(right_toolbar_demo, layout.fort.toolbars.right.frame(ir), layout.fort.toolbars.right.buttons) - update(center_toolbar_demo, layout.fort.toolbars.center.frame(ir), layout.fort.toolbars.center.buttons) + updateLayout(left_toolbar_demo) + updateLayout(right_toolbar_demo) + updateLayout(center_toolbar_demo) end local tool_from_designation = { @@ -303,13 +375,14 @@ fort_toolbars_demo.update = function() end local secondary +local center_render = center_toolbar_demo.render function center_toolbar_demo:render(...) local new_secondary = active_secondary() if new_secondary ~= secondary then secondary = new_secondary update_fort_toolbars(secondary) end - return FortToolbarDemoPanel.render(self, ...) + return center_render(self, ...) end --- start demo control window --- From 462fde58e65b8a0e045798b9a02397197a59cb86 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 02:43:41 -0500 Subject: [PATCH 12/20] devel/dflayout: cleanup focus checking and updating chaining Use hasFocus instead of "not defocused". hasFocus also checks that the ZScreen is the topmost. Check demo availability before updating from main window. --- devel/dflayout.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 7aa5ebf8d5..848c2d6abd 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -13,10 +13,10 @@ local utils = require('utils') ---@field update fun() called by main window to recompute demo frames local visible_when_not_focused = true -function visible() - if visible_when_not_focused then return true end +function demos_are_visible() if not screen then return false end - return screen:isActive() and not screen.defocused + if visible_when_not_focused then return true end + return screen:isActive() and screen:hasFocus() end DemoWindow = defclass(DemoWindow, widgets.Window) @@ -114,7 +114,7 @@ end function DemoScreen:postComputeFrame(frame_body) for _, demo in ipairs(self.demos) do - if demo.active and demo.update then + if demo.available() and demo.active then demo.update() end end @@ -129,7 +129,7 @@ local fort_toolbars_demo = { } local function fort_toolbars_visible() - return visible() and fort_toolbars_demo.active + return demos_are_visible() and fort_toolbars_demo.active end local secondary_visible = false From 062c7e7ca420498f95cb960b47782c3219ad0930 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 05:05:10 -0500 Subject: [PATCH 13/20] devel/dflayout: use a global for visible_when_not_focused Using a local was causing it to revert and get "stuck on" when re-running the script. --- devel/dflayout.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 848c2d6abd..bf2c6d8c10 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -12,8 +12,10 @@ local utils = require('utils') ---@field views gui.View[] list of views to add to main ZScreen ---@field update fun() called by main window to recompute demo frames -local visible_when_not_focused = true -function demos_are_visible() +if visible_when_not_focused == nil then + visible_when_not_focused = true +end +local function demos_are_visible() if not screen then return false end if visible_when_not_focused then return true end return screen:isActive() and screen:hasFocus() From a0c2ccb5c81f48e2279fd0c572328a9adefe7bff Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 05:15:28 -0500 Subject: [PATCH 14/20] devel/dflayout: check interface percentage whenever demos are visible Previous code missed interface percentage updates when the ZScreen was focused but not visible_when_not_focused. --- devel/dflayout.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index bf2c6d8c10..22e0657082 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -104,7 +104,7 @@ end local if_percentage function DemoScreen:render(...) - if visible_when_not_focused then + if demos_are_visible() then local new_if_percentage = df.global.init.display.max_interface_percentage if new_if_percentage ~= if_percentage then if_percentage = new_if_percentage From 7a63210d9e1d989250be9329f34eaa8b2b1dc082 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 05:44:21 -0500 Subject: [PATCH 15/20] devel/dflayout: move Window and ZScreen classes to end Now they are right above the final "raise or create" sequence. --- devel/dflayout.lua | 206 ++++++++++++++++++++++----------------------- 1 file changed, 102 insertions(+), 104 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 22e0657082..ad9d1e0a9a 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -3,8 +3,6 @@ local layout = require('gui.dflayout') local widgets = require('gui.widgets') local utils = require('utils') ---- Demo Control Window and Screen --- - ---@class Demo ---@field text string text displayed in main window demo list ---@field available fun(): boolean? return true if demo is available in current context @@ -21,107 +19,6 @@ local function demos_are_visible() return screen:isActive() and screen:hasFocus() end -DemoWindow = defclass(DemoWindow, widgets.Window) -DemoWindow.ATTRS{ - frame_title = 'dflayout demos', - frame = { w = 39, h = 9 }, - resizable = true, - autoarrange_subviews = true, - autoarrange_gap = 1, -} - ----@param args { demos: Demo[] } -function DemoWindow:init(args) - self.demos = args.demos - self:addviews{ - widgets.ToggleHotkeyLabel{ - label = 'Demos visible when not focused?', - initial_option = visible_when_not_focused, - on_change = function(new, old) - visible_when_not_focused = new - end - }, - widgets.List{ - view_id = 'list', - frame = { h = 10, }, - icon_pen = COLOR_GREY, - icon_width = 3, - on_submit = function(index, item) - local demo = self.demos[index] - demo.active = demo.available() and not demo.active - if demo.active then demo.update() end - self:refresh() - end - }, - } -end - -local CHECK = string.char(251) -- U+221A SQUARE ROOT - -function DemoWindow:refresh() - local choices = {} - for _, demo in ipairs(self.demos) do - local icon - if not demo.available() then - icon = '-' - elseif demo.active then - icon = CHECK - end - table.insert(choices, { - text = demo.text, - icon = icon, - }) - end - self.subviews.list:setChoices(choices) - return self -end - -DemoScreen = defclass(DemoScreen, gui.ZScreen) -DemoScreen.ATTRS{ - focus_path = 'gui.dflayout-demo' -} - -function DemoScreen:init(args) - self.demos = args.demos - local function demo_views() - local views = {} - for _, demo in ipairs(self.demos) do - if demo.views then - table.move(demo.views, 1, #demo.views, #views + 1, views) - end - end - return views - end - self:addviews{ - DemoWindow{ demos = self.demos }:refresh(), - table.unpack(demo_views()) - } -end - -function DemoScreen:onDismiss() - screen = nil -end - -local if_percentage -function DemoScreen:render(...) - if demos_are_visible() then - local new_if_percentage = df.global.init.display.max_interface_percentage - if new_if_percentage ~= if_percentage then - if_percentage = new_if_percentage - self:updateLayout() - end - end - return DemoScreen.super.render(self, ...) -end - -function DemoScreen:postComputeFrame(frame_body) - for _, demo in ipairs(self.demos) do - if demo.available() and demo.active then - demo.update() - end - end -end - --- Fort Toolbar Demo --- ---@class FortToolbarsDemo: Demo @@ -387,7 +284,108 @@ function center_toolbar_demo:render(...) return center_render(self, ...) end ---- start demo control window --- +--- Demo Control Window and Screen --- + +DemoWindow = defclass(DemoWindow, widgets.Window) +DemoWindow.ATTRS{ + frame_title = 'dflayout demos', + frame = { w = 39, h = 9 }, + resizable = true, + autoarrange_subviews = true, + autoarrange_gap = 1, +} + +---@param args { demos: Demo[] } +function DemoWindow:init(args) + self.demos = args.demos + self:addviews{ + widgets.ToggleHotkeyLabel{ + label = 'Demos visible when not focused?', + initial_option = visible_when_not_focused, + on_change = function(new, old) + visible_when_not_focused = new + end + }, + widgets.List{ + view_id = 'list', + frame = { h = 10, }, + icon_pen = COLOR_GREY, + icon_width = 3, + on_submit = function(index, item) + local demo = self.demos[index] + demo.active = demo.available() and not demo.active + if demo.active then demo.update() end + self:refresh() + end + }, + } +end + +local CHECK = string.char(251) -- U+221A SQUARE ROOT + +function DemoWindow:refresh() + local choices = {} + for _, demo in ipairs(self.demos) do + local icon + if not demo.available() then + icon = '-' + elseif demo.active then + icon = CHECK + end + table.insert(choices, { + text = demo.text, + icon = icon, + }) + end + self.subviews.list:setChoices(choices) + return self +end + +DemoScreen = defclass(DemoScreen, gui.ZScreen) +DemoScreen.ATTRS{ + focus_path = 'gui.dflayout-demo' +} + +function DemoScreen:init(args) + self.demos = args.demos + local function demo_views() + local views = {} + for _, demo in ipairs(self.demos) do + if demo.views then + table.move(demo.views, 1, #demo.views, #views + 1, views) + end + end + return views + end + self:addviews{ + DemoWindow{ demos = self.demos }:refresh(), + table.unpack(demo_views()) + } +end + +function DemoScreen:onDismiss() + screen = nil +end + +local if_percentage +function DemoScreen:render(...) + if demos_are_visible() then + local new_if_percentage = df.global.init.display.max_interface_percentage + if new_if_percentage ~= if_percentage then + if_percentage = new_if_percentage + self:updateLayout() + end + end + return DemoScreen.super.render(self, ...) +end + +function DemoScreen:postComputeFrame(frame_body) + for _, demo in ipairs(self.demos) do + if demo.available() and demo.active then + demo.update() + end + end +end screen = screen and screen:raise() or DemoScreen{ demos = { From eef4fc57925ea943820a88085c25d77fe3f5557c Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 15 Apr 2025 05:56:17 -0500 Subject: [PATCH 16/20] devel/dflayout: provide on_render hook to demos Keeps the secondary toolbar demo from needing to hook into the center toolbar (or some other "always on" widget) to notice changes in the secondary toolbar. --- devel/dflayout.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index ad9d1e0a9a..c2c7df3755 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -9,6 +9,7 @@ local utils = require('utils') ---@field active? boolean whether the main window has enabled this demo (managed by main window) ---@field views gui.View[] list of views to add to main ZScreen ---@field update fun() called by main window to recompute demo frames +---@field on_render? fun() called by main window every render; useful to notice changes in overall UI state if visible_when_not_focused == nil then visible_when_not_focused = true @@ -274,14 +275,12 @@ fort_toolbars_demo.update = function() end local secondary -local center_render = center_toolbar_demo.render -function center_toolbar_demo:render(...) +fort_toolbars_demo.on_render = function() local new_secondary = active_secondary() if new_secondary ~= secondary then secondary = new_secondary update_fort_toolbars(secondary) end - return center_render(self, ...) end --- Demo Control Window and Screen --- @@ -375,6 +374,11 @@ function DemoScreen:render(...) if_percentage = new_if_percentage self:updateLayout() end + for _, demo in ipairs(self.demos) do + if demo.on_render and demo.available() and demo.active then + demo.on_render() + end + end end return DemoScreen.super.render(self, ...) end From df96afc6cbfb6f4411172fa9401b105b2860cb2b Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 22 Apr 2025 23:16:55 -0500 Subject: [PATCH 17/20] adapt to gui.dflayout overhaul devel/dflayout now highlights the "demo" element when the mouse cursor is over the (computed) real UI element --- devel/dflayout.lua | 276 +++++++++++++++++++++++++++++--------------- gui/mass-remove.lua | 43 +++---- 2 files changed, 202 insertions(+), 117 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index c2c7df3755..7b78759bf2 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -57,13 +57,13 @@ end -- -- Note: The returned function does not return a separate body rect; subviews -- will be able to overwrite the normal UI-drawn frame! ----@param toolbar DFLayout.Toolbar +---@param el DFLayout.DynamicUIElement ---@param dy_fn fun(): integer ---@return function -local function get_computeFrame_fn(toolbar, dy_fn) +local function get_computeFrame_fn(el, dy_fn) return function(self, parent_rect) local ir = gui.get_interface_rect() - local frame = toolbar.frame(ir) + local frame = el.frame_fn(ir) return gui.mkdims_wh( ir.x1 + frame.l, ir.y1 + frame.t + dy_fn(), @@ -72,43 +72,65 @@ local function get_computeFrame_fn(toolbar, dy_fn) end end ----@param buttons DFLayout.Toolbar.NamedButtons -local function buttons_string(buttons) - local sorted = {} - for _, button in pairs(buttons) do - utils.insert_sorted(sorted, button, 'offset') +---@param buttons DFLayout.Toolbar.Layout +local function buttons_tokens(buttons) + local sorted_buttons = {} + for button_name, button in pairs(buttons) do + utils.insert_sorted(sorted_buttons, { + name = button_name, + offset = button.offset, + width = button.width, + }, 'offset') end - -- For a one-column button, use | to indicate the button's position. - -- For wider buttons, use shapes like /\ or /--\ to illustrate the - -- button's position and width. - local str = '' - for i, o in ipairs(sorted) do - if o.offset > #str then - str = str .. (' '):rep(o.offset - #str) - end - if o.width == 1 then - str = str .. '|' - elseif o.width > 1 then - str = str .. '/' .. ('-'):rep(o.width - 2) .. '\\' + local offset = 0 + local sorted_button_names = {} + local tokens_by_name = {} + for _, button_info in ipairs(sorted_buttons) do + table.insert(sorted_button_names, button_info.name) + local token = { gap = button_info.offset - offset, width = button_info.width } + if button_info.width == 1 then + -- For a one-column button, use | to indicate the button's position. + token.text = '|' + elseif button_info.width > 1 then + -- For wider buttons, use shapes like /\ or /--\ to illustrate the + -- button's position and width. + token.text = '/' .. ('-'):rep(button_info.width - 2) .. '\\' end + offset = button_info.offset + button_info.width + tokens_by_name[button_info.name] = token end - return str + return sorted_button_names, tokens_by_name +end + +local normal_frame_style = function(...) + local style = gui.FRAME_THIN(...) + style.signature_pen = false + return style end +local hover_frame_style = function(...) + local style = gui.FRAME_BOLD(...) + style.signature_pen = false + return style +end + +---@class ToolbarDemo.ToolbarInfo +---@field el DFLayout.DynamicUIElement +---@field buttons DFLayout.Toolbar.Layout +---@field button_els table +---@field demo_dy fun(): integer + ---@class ToolbarDemo.attrs: widgets.Panel.attrs ---@class ToolbarDemo.attrs.partial: widgets.Panel.attrs.partial ----@class ToolbarDemo.initTable: ToolbarDemo.attrs.partial, { toolbar?: DFLayout.Toolbar, toolbar_dy?: fun(): integer } +---@field toolbar_info? ToolbarDemo.ToolbarInfo +---@class ToolbarDemo.initTable: ToolbarDemo.attrs.partial ---@class ToolbarDemo: widgets.Panel ---@field super widgets.Panel ---@field ATTRS ToolbarDemo.attrs|fun(attributes: ToolbarDemo.attrs.partial) ---@overload fun(init_table: ToolbarDemo.initTable): self ToolbarDemo = defclass(ToolbarDemo, widgets.Panel) ToolbarDemo.ATTRS{ - frame_style = function(...) - local style = gui.FRAME_THIN(...) - style.signature_pen = false - return style - end, + frame_style = normal_frame_style, visible = fort_toolbars_visible, frame_background = { ch = 32, bg = COLOR_BLACK }, } @@ -116,49 +138,116 @@ ToolbarDemo.ATTRS{ ---@param args ToolbarDemo.initTable function ToolbarDemo:init(args) self.label = widgets.Label{ frame = { l = 0 } } - if args.toolbar and args.toolbar_dy then - self:update_to_toolbar(args.toolbar, args.toolbar_dy) + if args.toolbar_info then + self:update_to_toolbar(args.toolbar_info) end self:addviews{ self.label } end ----@param toolbar DFLayout.Toolbar ----@param dy fun(): integer ----@return unknown -function ToolbarDemo:update_to_toolbar(toolbar, dy) - -- set button representation string - local text = buttons_string(toolbar.buttons) - local l_inset = 0 - if text:sub(1, 1) == ' ' then - -- don't overwrite the left border edge with a plain space - l_inset = 1 - text = text:sub(2) +---@param toolbar_info ToolbarDemo.ToolbarInfo +---@return ToolbarDemo +function ToolbarDemo:update_to_toolbar(toolbar_info) + local order, named_tokens = buttons_tokens(toolbar_info.buttons) + function set_button_text(lit_button_name) + local lit = false + local tokens = {} + for _, name in ipairs(order) do + local token = copyall(named_tokens[name]) + if name == lit_button_name then + lit = true + token.pen = { fg = COLOR_BLACK, bg = COLOR_BLUE } + end + table.insert(tokens, token) + end + self.label:setText(tokens) + return lit end - self.label.frame.l = l_inset - self.label:setText(text) + + set_button_text() -- track actual toolbar, but with a y offset - self.computeFrame = get_computeFrame_fn(toolbar, dy) + self.computeFrame = get_computeFrame_fn(toolbar_info.el, toolbar_info.demo_dy) + + self.toolbar_el = toolbar_info.el + self.button_els = toolbar_info.button_els + self.set_button_text = set_button_text return self end +-- capture computed locations of toolbar and buttons +function ToolbarDemo:postUpdateLayout() + local ir = gui.get_interface_rect() + local function vr(el) + local f = el.frame_fn(ir) + return gui.ViewRect{ rect = gui.mkdims_wh(ir.x1 + f.l, ir.y1 + f.t, f.w, f.h) } + end + if self.toolbar_el then + self.toolbar_vr = vr(self.toolbar_el) + end + if self.button_els then + local vrs = {} + for name, el in pairs(self.button_els) do + vrs[name] = vr(el) + end + self.toolbar_button_vrs = vrs + end +end + +function ToolbarDemo:render(...) + if self.toolbar_vr then + if self:getMousePos(self.toolbar_vr) then + self.frame_style = hover_frame_style + if self.toolbar_button_vrs then + local lit = false + for button_name, button_vr in pairs(self.toolbar_button_vrs) do + if self:getMousePos(button_vr) then + if self.set_button_text(button_name) then + lit = true + break + end + end + end + if not lit then + self.set_button_text() + end + end + else + self.frame_style = normal_frame_style + self.set_button_text() + end + end + return ToolbarDemo.super.render(self, ...) +end + local left_toolbar_demo = ToolbarDemo{ frame_title = 'left toolbar', - toolbar = layout.fort.toolbars.left, - toolbar_dy = primary_toolbar_dy, + toolbar_info = { + el = layout.elements.fort.toolbars.left, + buttons = layout.element_layouts.fort.toolbars.left.buttons, + button_els = layout.elements.fort.toolbar_buttons.left, + demo_dy = primary_toolbar_dy, + }, } local center_toolbar_demo = ToolbarDemo{ frame_title = 'center toolbar', - toolbar = layout.fort.toolbars.center, - toolbar_dy = primary_toolbar_dy, + toolbar_info = { + el = layout.elements.fort.toolbars.center, + buttons = layout.element_layouts.fort.toolbars.center.buttons, + button_els = layout.elements.fort.toolbar_buttons.center, + demo_dy = primary_toolbar_dy, + }, } local right_toolbar_demo = ToolbarDemo{ frame_title = 'right toolbar', - toolbar = layout.fort.toolbars.right, - toolbar_dy = primary_toolbar_dy, + toolbar_info = { + el = layout.elements.fort.toolbars.right, + buttons = layout.element_layouts.fort.toolbars.right.buttons, + button_els = layout.elements.fort.toolbar_buttons.right, + demo_dy = primary_toolbar_dy, + } } local secondary_toolbar_demo = ToolbarDemo{ @@ -187,7 +276,12 @@ local function update_fort_toolbars(secondary) local function dy() return -layout.SECONDARY_TOOLBAR_HEIGHT end - secondary_toolbar_demo:update_to_toolbar(layout.fort.secondary_toolbars[secondary], dy) + secondary_toolbar_demo:update_to_toolbar{ + el = layout.elements.fort.secondary_toolbars[secondary], + buttons = layout.element_layouts.fort.secondary_toolbars[secondary].buttons, + button_els = layout.elements.fort.secondary_toolbar_buttons[secondary], + demo_dy = dy + } updateLayout(secondary_toolbar_demo) secondary_visible = true else @@ -199,56 +293,56 @@ local function update_fort_toolbars(secondary) updateLayout(center_toolbar_demo) end -local tool_from_designation = { +local secondary_toolbar_from_designation = { -- df.main_designation_type.NONE -- not a tool - [df.main_designation_type.DIG_DIG] = 'dig', - [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'dig', - [df.main_designation_type.DIG_STAIR_UP] = 'dig', - [df.main_designation_type.DIG_STAIR_UPDOWN] = 'dig', - [df.main_designation_type.DIG_STAIR_DOWN] = 'dig', - [df.main_designation_type.DIG_RAMP] = 'dig', - [df.main_designation_type.DIG_CHANNEL] = 'dig', - [df.main_designation_type.CHOP] = 'chop', - [df.main_designation_type.GATHER] = 'gather', - [df.main_designation_type.SMOOTH] = 'smooth', - [df.main_designation_type.TRACK] = 'smooth', - [df.main_designation_type.ENGRAVE] = 'smooth', - [df.main_designation_type.FORTIFY] = 'smooth', + [df.main_designation_type.DIG_DIG] = 'DIG', + [df.main_designation_type.DIG_REMOVE_STAIRS_RAMPS] = 'DIG', + [df.main_designation_type.DIG_STAIR_UP] = 'DIG', + [df.main_designation_type.DIG_STAIR_UPDOWN] = 'DIG', + [df.main_designation_type.DIG_STAIR_DOWN] = 'DIG', + [df.main_designation_type.DIG_RAMP] = 'DIG', + [df.main_designation_type.DIG_CHANNEL] = 'DIG', + [df.main_designation_type.CHOP] = 'CHOP', + [df.main_designation_type.GATHER] = 'GATHER', + [df.main_designation_type.SMOOTH] = 'SMOOTH', + [df.main_designation_type.TRACK] = 'SMOOTH', + [df.main_designation_type.ENGRAVE] = 'SMOOTH', + [df.main_designation_type.FORTIFY] = 'SMOOTH', -- df.main_designation_type.REMOVE_CONSTRUCTION -- not used? - [df.main_designation_type.CLAIM] = 'mass_designation', - [df.main_designation_type.UNCLAIM] = 'mass_designation', - [df.main_designation_type.MELT] = 'mass_designation', - [df.main_designation_type.NO_MELT] = 'mass_designation', - [df.main_designation_type.DUMP] = 'mass_designation', - [df.main_designation_type.NO_DUMP] = 'mass_designation', - [df.main_designation_type.HIDE] = 'mass_designation', - [df.main_designation_type.NO_HIDE] = 'mass_designation', + [df.main_designation_type.CLAIM] = 'ITEM_BUILDING', + [df.main_designation_type.UNCLAIM] = 'ITEM_BUILDING', + [df.main_designation_type.MELT] = 'ITEM_BUILDING', + [df.main_designation_type.NO_MELT] = 'ITEM_BUILDING', + [df.main_designation_type.DUMP] = 'ITEM_BUILDING', + [df.main_designation_type.NO_DUMP] = 'ITEM_BUILDING', + [df.main_designation_type.HIDE] = 'ITEM_BUILDING', + [df.main_designation_type.NO_HIDE] = 'ITEM_BUILDING', -- df.main_designation_type.TOGGLE_ENGRAVING -- not used? - [df.main_designation_type.DIG_FROM_MARKER] = 'dig', - [df.main_designation_type.DIG_TO_MARKER] = 'dig', - [df.main_designation_type.CHOP_FROM_MARKER] = 'chop', - [df.main_designation_type.CHOP_TO_MARKER] = 'chop', - [df.main_designation_type.GATHER_FROM_MARKER] = 'gather', - [df.main_designation_type.GATHER_TO_MARKER] = 'gather', - [df.main_designation_type.SMOOTH_FROM_MARKER] = 'smooth', - [df.main_designation_type.SMOOTH_TO_MARKER] = 'smooth', - [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'traffic', - [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'traffic', - [df.main_designation_type.ERASE] = 'erase', + [df.main_designation_type.DIG_FROM_MARKER] = 'DIG', + [df.main_designation_type.DIG_TO_MARKER] = 'DIG', + [df.main_designation_type.CHOP_FROM_MARKER] = 'CHOP', + [df.main_designation_type.CHOP_TO_MARKER] = 'CHOP', + [df.main_designation_type.GATHER_FROM_MARKER] = 'GATHER', + [df.main_designation_type.GATHER_TO_MARKER] = 'GATHER', + [df.main_designation_type.SMOOTH_FROM_MARKER] = 'SMOOTH', + [df.main_designation_type.SMOOTH_TO_MARKER] = 'SMOOTH', + [df.main_designation_type.DESIGNATE_TRAFFIC_HIGH] = 'TRAFFIC', + [df.main_designation_type.DESIGNATE_TRAFFIC_NORMAL] = 'TRAFFIC', + [df.main_designation_type.DESIGNATE_TRAFFIC_LOW] = 'TRAFFIC', + [df.main_designation_type.DESIGNATE_TRAFFIC_RESTRICTED] = 'TRAFFIC', + [df.main_designation_type.ERASE] = 'ERASE', } -local tool_from_bottom = { +local secondary_toolbar_from_bottom = { -- df.main_bottom_mode_type.NONE -- df.main_bottom_mode_type.BUILDING -- df.main_bottom_mode_type.BUILDING_PLACEMENT -- df.main_bottom_mode_type.BUILDING_PICK_MATERIALS -- df.main_bottom_mode_type.ZONE -- df.main_bottom_mode_type.ZONE_PAINT - [df.main_bottom_mode_type.STOCKPILE] = 'stockpile', - [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'stockpile_paint', + [df.main_bottom_mode_type.STOCKPILE] = 'MAIN_STOCKPILE_MODE', + [df.main_bottom_mode_type.STOCKPILE_PAINT] = 'STOCKPILE_NEW', -- df.main_bottom_mode_type.BURROW - [df.main_bottom_mode_type.BURROW_PAINT] = 'burrow_paint' + [df.main_bottom_mode_type.BURROW_PAINT] = 'Add new burrow', -- df.main_bottom_mode_type.HAULING -- df.main_bottom_mode_type.ARENA_UNIT -- df.main_bottom_mode_type.ARENA_TREE @@ -262,11 +356,11 @@ local tool_from_bottom = { local function active_secondary() local designation = df.global.game.main_interface.main_designation_selected if designation ~= df.main_designation_type.NONE then - return tool_from_designation[designation] + return secondary_toolbar_from_designation[designation] end local bottom = df.global.game.main_interface.bottom_mode_selected if bottom ~= df.main_bottom_mode_type.NONE then - return tool_from_bottom[bottom] + return secondary_toolbar_from_bottom[bottom] end end diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index e733c1ae87..6182b5b5fa 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -407,35 +407,28 @@ local MR_TOOLTIP_HEIGHT = 6 local MR_WIDTH = math.max(MR_TOOLTIP_WIDTH, MR_BUTTON_WIDTH) local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[ empty line ]] + MR_BUTTON_HEIGHT -local erase_toolbar = layout.fort.secondary_toolbars.erase - --- one "gap column" past the right end of the erase secondary toolbar -local function mass_remove_button_offsets(interface_size) - local erase_frame = erase_toolbar.frame(interface_size) - return { - l = erase_frame.l + erase_frame.w + 1, - w = erase_frame.w, - r = erase_frame.r - 1 - MR_BUTTON_WIDTH, - - t = erase_frame.t, - h = erase_frame.h, - b = erase_frame.b, - } -end - --- If the overlay version is bumped, this could be changed to --- mass_remove_button_offsets(layout.MINIMUM_INTERFACE_RECT).l --- Adopting the calculated value would let the overlay be moved all the way to --- the left in a minimum-size interface. -local MR_DEFAULT_L_OFFSET = 41 +local MR_PLACEMENT = layout.getLeftOnlyOverlayPlacementInfo{ + size = { w = MR_WIDTH, h = MR_HEIGHT }, + + -- one "gap column" past the right end of the erase secondary toolbar + ui_element = layout.elements.fort.secondary_toolbars.ERASE, + h_placement = 'on right', + v_placement = 'align bottom edges', + offset = { x = 1 }, + + -- If the overlay version is bumped, this could be removed. + -- Using the automatic value would let the overlay be moved all the way to + -- the left in a minimum-size interface. + default_pos = { x = 42 }, +} MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ desc='Adds a button to the erase toolbar to open the mass removal tool.', - default_pos={x=(MR_DEFAULT_L_OFFSET+1), y=-(layout.TOOLBAR_HEIGHT+1)}, + default_pos=MR_PLACEMENT.default_pos, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', - frame={w=MR_WIDTH, h=MR_HEIGHT}, + frame=MR_PLACEMENT.frame, } function MassRemoveToolbarOverlay:init() @@ -498,9 +491,7 @@ function MassRemoveToolbarOverlay:init() } end -function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - self.frame.w = MR_WIDTH + math.max(0, mass_remove_button_offsets(parent_rect).l - MR_DEFAULT_L_OFFSET) -end +MassRemoveToolbarOverlay.preUpdateLayout = MR_PLACEMENT.preUpdateLayout_fn function MassRemoveToolbarOverlay:onInput(keys) if keys.CUSTOM_M then From dc4e998b2fb21f7f4e222876542ad7e61fb7fbd6 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 30 Apr 2025 01:57:46 -0500 Subject: [PATCH 18/20] adapt gui/mass-remove.toolbar and devel/dflayout to latest gui.dflayout --- devel/dflayout.lua | 143 ++++++++++++++++++++++++++++++-------------- gui/mass-remove.lua | 1 + 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 7b78759bf2..5dcb04980a 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -8,7 +8,7 @@ local utils = require('utils') ---@field available fun(): boolean? return true if demo is available in current context ---@field active? boolean whether the main window has enabled this demo (managed by main window) ---@field views gui.View[] list of views to add to main ZScreen ----@field update fun() called by main window to recompute demo frames +---@field update? fun() called by main window to recompute demo frames ---@field on_render? fun() called by main window every render; useful to notice changes in overall UI state if visible_when_not_focused == nil then @@ -20,6 +20,43 @@ local function demos_are_visible() return screen:isActive() and screen:hasFocus() end +---@param demo Demo +local function demo_active(demo) + return demos_are_visible() and demo.active +end + +-- Generates a `view:computeFrame()` function that tracks the placement of the +-- given `el`. +-- +-- Note: The returned function does not return a separate body rect; subviews +-- will be able to overwrite the normal UI-drawn frame! +---@param el DFLayout.DynamicUIElement +---@param dy_fn? fun(): integer +---@return function +local function get_computeFrame_fn(el, dy_fn) + return function(self, parent_rect) + local ir = gui.get_interface_rect() + local frame = layout.getUIElementFrame(el, ir) + return gui.mkdims_wh( + ir.x1 + frame.l, + ir.y1 + frame.t + (dy_fn and dy_fn() or 0), + frame.w, + frame.h) + end +end + +local normal_frame_style = function(...) + local style = gui.FRAME_THIN(...) + style.signature_pen = false + return style +end + +local hover_frame_style = function(...) + local style = gui.FRAME_BOLD(...) + style.signature_pen = false + return style +end + --- Fort Toolbar Demo --- ---@class FortToolbarsDemo: Demo @@ -28,9 +65,7 @@ local fort_toolbars_demo = { available = dfhack.world.isFortressMode, } -local function fort_toolbars_visible() - return demos_are_visible() and fort_toolbars_demo.active -end +local fort_toolbars_visible = curry(demo_active, fort_toolbars_demo) local secondary_visible = false @@ -52,26 +87,6 @@ local function primary_toolbar_dy() end end --- Generates a `view:computeFrame()` function that tracks the placement of the --- given `toolbar`. --- --- Note: The returned function does not return a separate body rect; subviews --- will be able to overwrite the normal UI-drawn frame! ----@param el DFLayout.DynamicUIElement ----@param dy_fn fun(): integer ----@return function -local function get_computeFrame_fn(el, dy_fn) - return function(self, parent_rect) - local ir = gui.get_interface_rect() - local frame = el.frame_fn(ir) - return gui.mkdims_wh( - ir.x1 + frame.l, - ir.y1 + frame.t + dy_fn(), - frame.w, - frame.h) - end -end - ---@param buttons DFLayout.Toolbar.Layout local function buttons_tokens(buttons) local sorted_buttons = {} @@ -102,18 +117,6 @@ local function buttons_tokens(buttons) return sorted_button_names, tokens_by_name end -local normal_frame_style = function(...) - local style = gui.FRAME_THIN(...) - style.signature_pen = false - return style -end - -local hover_frame_style = function(...) - local style = gui.FRAME_BOLD(...) - style.signature_pen = false - return style -end - ---@class ToolbarDemo.ToolbarInfo ---@field el DFLayout.DynamicUIElement ---@field buttons DFLayout.Toolbar.Layout @@ -179,7 +182,7 @@ end function ToolbarDemo:postUpdateLayout() local ir = gui.get_interface_rect() local function vr(el) - local f = el.frame_fn(ir) + local f = layout.getUIElementFrame(el, ir) return gui.ViewRect{ rect = gui.mkdims_wh(ir.x1 + f.l, ir.y1 + f.t, f.w, f.h) } end if self.toolbar_el then @@ -377,6 +380,57 @@ fort_toolbars_demo.on_render = function() end end +--- experimental Info window Demos --- + +---@param text string +---@param focus_string string +---@param el DFLayout.DynamicUIElement +---@param item_count_fn fun(): integer +---@return Demo +local function info_items_demo(text, focus_string, el, item_count_fn) + local demo = { + text = text, + available = dfhack.world.isFortressMode, + } + local panel = widgets.Panel{ + frame_style = normal_frame_style, + frame_background = nil, -- do not fill panel interior, leave it "see through" + visible = function() + return demo_active(demo) + and dfhack.gui.matchFocusString(focus_string, dfhack.gui.getDFViewscreen(true)) + end, + } + panel.computeFrame = get_computeFrame_fn(el) + panel.getMouseFramePos = function() end -- hide from ZScreen:isMouseOver(), so that mouse input passes through + + demo.views = { panel } + + local item_count + function demo.on_render() + local new_count = item_count_fn() + if new_count ~= item_count then + item_count = new_count + panel:updateLayout() + end + end + + return demo +end + +local orders_demo = info_items_demo( + 'info Orders tab', + 'dwarfmode/Info/WORK_ORDERS/Default', + layout.experimental_elements.orders, + function() return #df.global.world.manager_orders.all end) + +local zones_demo = info_items_demo( + 'info Places/Zones tab', + 'dwarfmode/Info/BUILDINGS/ZONES', + layout.experimental_elements.zones, + function() + return #df.global.game.main_interface.info.buildings.list[df.buildings_mode_type.ZONES] + end) + --- Demo Control Window and Screen --- DemoWindow = defclass(DemoWindow, widgets.Window) @@ -407,7 +461,7 @@ function DemoWindow:init(args) on_submit = function(index, item) local demo = self.demos[index] demo.active = demo.available() and not demo.active - if demo.active then demo.update() end + if demo.update and demo.active then demo.update() end self:refresh() end }, @@ -436,7 +490,7 @@ end DemoScreen = defclass(DemoScreen, gui.ZScreen) DemoScreen.ATTRS{ - focus_path = 'gui.dflayout-demo' + focus_path = 'gui.dflayout-demo', } function DemoScreen:init(args) @@ -450,10 +504,9 @@ function DemoScreen:init(args) end return views end - self:addviews{ - DemoWindow{ demos = self.demos }:refresh(), - table.unpack(demo_views()) - } + self:addviews(demo_views()) + -- put main window last so it is rendered "on top" + self:addviews{ DemoWindow{ demos = self.demos }:refresh() } end function DemoScreen:onDismiss() @@ -479,7 +532,7 @@ end function DemoScreen:postComputeFrame(frame_body) for _, demo in ipairs(self.demos) do - if demo.available() and demo.active then + if demo.update and demo.available() and demo.active then demo.update() end end @@ -488,5 +541,7 @@ end screen = screen and screen:raise() or DemoScreen{ demos = { fort_toolbars_demo, + orders_demo, + zones_demo, }, }:show() diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 6182b5b5fa..e62b2ab44e 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -408,6 +408,7 @@ local MR_WIDTH = math.max(MR_TOOLTIP_WIDTH, MR_BUTTON_WIDTH) local MR_HEIGHT = MR_TOOLTIP_HEIGHT + 1 --[[ empty line ]] + MR_BUTTON_HEIGHT local MR_PLACEMENT = layout.getLeftOnlyOverlayPlacementInfo{ + name = 'MassRemoveToolbarOverlay', size = { w = MR_WIDTH, h = MR_HEIGHT }, -- one "gap column" past the right end of the erase secondary toolbar From cfb69894e07f22782a4fae1f951764722835f39f Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 30 Apr 2025 01:59:22 -0500 Subject: [PATCH 19/20] devel/dflayout: remove Demo.update It was a remnant from before the toolbar demo panels did their own position updating via the get_computeFrame_fn helper. Interface size changes are automatically propagated from the ZScreen through the updateLayout tree, and secondary toolbar changes are noticed from the render hook. --- devel/dflayout.lua | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 5dcb04980a..4309dfef86 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -8,7 +8,6 @@ local utils = require('utils') ---@field available fun(): boolean? return true if demo is available in current context ---@field active? boolean whether the main window has enabled this demo (managed by main window) ---@field views gui.View[] list of views to add to main ZScreen ----@field update? fun() called by main window to recompute demo frames ---@field on_render? fun() called by main window every render; useful to notice changes in overall UI state if visible_when_not_focused == nil then @@ -268,7 +267,7 @@ fort_toolbars_demo.views = { } ---@param secondary? DFLayout.Fort.SecondaryToolbar.Names -local function update_fort_toolbars(secondary) +local function update_secondary_toolbar(secondary) local function updateLayout(view) if view.frame_parent_rect then view:updateLayout() @@ -291,6 +290,8 @@ local function update_fort_toolbars(secondary) secondary_visible = false end + -- update primary toolbar demos since their positions depends on whether a + -- secondary is active updateLayout(left_toolbar_demo) updateLayout(right_toolbar_demo) updateLayout(center_toolbar_demo) @@ -367,16 +368,12 @@ local function active_secondary() end end -fort_toolbars_demo.update = function() - update_fort_toolbars(active_secondary()) -end - local secondary fort_toolbars_demo.on_render = function() local new_secondary = active_secondary() if new_secondary ~= secondary then secondary = new_secondary - update_fort_toolbars(secondary) + update_secondary_toolbar(secondary) end end @@ -461,7 +458,6 @@ function DemoWindow:init(args) on_submit = function(index, item) local demo = self.demos[index] demo.active = demo.available() and not demo.active - if demo.update and demo.active then demo.update() end self:refresh() end }, @@ -530,14 +526,6 @@ function DemoScreen:render(...) return DemoScreen.super.render(self, ...) end -function DemoScreen:postComputeFrame(frame_body) - for _, demo in ipairs(self.demos) do - if demo.update and demo.available() and demo.active then - demo.update() - end - end -end - screen = screen and screen:raise() or DemoScreen{ demos = { fort_toolbars_demo, From fe806621749c357c536f8e71a35e47e9f6bc1e98 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 30 Apr 2025 04:46:15 -0500 Subject: [PATCH 20/20] devel/dflayout: use getUIElementStateChecker --- devel/dflayout.lua | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/devel/dflayout.lua b/devel/dflayout.lua index 4309dfef86..115791a52d 100644 --- a/devel/dflayout.lua +++ b/devel/dflayout.lua @@ -384,7 +384,7 @@ end ---@param el DFLayout.DynamicUIElement ---@param item_count_fn fun(): integer ---@return Demo -local function info_items_demo(text, focus_string, el, item_count_fn) +local function info_items_demo(text, focus_string, el) local demo = { text = text, available = dfhack.world.isFortressMode, @@ -402,11 +402,9 @@ local function info_items_demo(text, focus_string, el, item_count_fn) demo.views = { panel } - local item_count + local state_changed = layout.getUIElementStateChecker(el) function demo.on_render() - local new_count = item_count_fn() - if new_count ~= item_count then - item_count = new_count + if state_changed() then panel:updateLayout() end end @@ -417,16 +415,12 @@ end local orders_demo = info_items_demo( 'info Orders tab', 'dwarfmode/Info/WORK_ORDERS/Default', - layout.experimental_elements.orders, - function() return #df.global.world.manager_orders.all end) + layout.experimental_elements.orders) local zones_demo = info_items_demo( 'info Places/Zones tab', 'dwarfmode/Info/BUILDINGS/ZONES', - layout.experimental_elements.zones, - function() - return #df.global.game.main_interface.info.buildings.list[df.buildings_mode_type.ZONES] - end) + layout.experimental_elements.zones) --- Demo Control Window and Screen ---