Skip to content
1 change: 1 addition & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Template for new versions:
## Documentation

## API
- ``widgets.RadioButton``: New button widget resembling those used in ``gui/control-panel``

## Lua

Expand Down
33 changes: 25 additions & 8 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6356,12 +6356,27 @@ This is a specialized subclass of CycleHotkeyLabel that has two options:
``On`` (with a value of ``true``) and ``Off`` (with a value of ``false``). The
``On`` option is rendered in green.

ConfigureButton class
---------------------

A 3x1 tile button with a gear symbol on it, intended to represent a configure
icon. Clicking on the icon will run the given callback. The graphics can also
be overridden to create custom buttons.

It has the following attributes:

:on_click: The function to run when the icon is clicked.
:pen_left: Pen or function returning a pen to overwrite the left tile of the button.
:pen_center: As above, but for the center tile (gear symbol).
:pen_right: As above, but for the right tile.

HelpButton class
----------------

A 3x1 tile button with a question mark on it, intended to represent a help
icon. Clicking on the icon will launch `gui/launcher` with a given command
string, showing the help text for that command.
Subclass of ConfigureButton; a 3x1 tile button with a question mark on it,
intended to represent a help icon. Clicking on the icon will launch
`gui/launcher` with a given command string, showing the help text for that
command.

It has the following attributes:

Expand All @@ -6371,15 +6386,17 @@ It also sets the ``frame`` attribute so the button appears in the upper right
corner of the parent, but you can override this to your liking if you want a
different position.

ConfigureButton class
---------------------
RadioButton class
-----------------

A 3x1 tile button with a gear mark on it, intended to represent a configure
icon. Clicking on the icon will run the given callback.
Subclass of ConfigureButton; a 3x1 tile button that resembles a radio button
(or check box in ASCII mode), identical to the ones found in
`gui/control-panel`. Clicking on the button will toggle its enabled state.
This state is represented by the boolean value ``toggle_state``.

It has the following attributes:

:on_click: The function on run when the icon is clicked.
:initial_state: Start in the ``true`` or ``false`` state. Defaults to ``true``.

BannerPanel class
-----------------
Expand Down
3 changes: 2 additions & 1 deletion library/lua/gui/widgets.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ Label = require('gui.widgets.labels.label')
Scrollbar = require('gui.widgets.scrollbar')
WrappedLabel = require('gui.widgets.labels.wrapped_label')
TooltipLabel = require('gui.widgets.labels.tooltip_label')
HelpButton = require('gui.widgets.buttons.help_button')
ConfigureButton = require('gui.widgets.buttons.configure_button')
HelpButton = require('gui.widgets.buttons.help_button')
RadioButton = require('gui.widgets.buttons.radio_button')
BannerPanel = require('gui.widgets.containers.banner_panel')
TextButton = require('gui.widgets.buttons.text_button')
CycleHotkeyLabel = require('gui.widgets.labels.cycle_hotkey_label')
Expand Down
40 changes: 29 additions & 11 deletions library/lua/gui/widgets/buttons/configure_button.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- A 3x1 tile button with a gear symbol on it. Clicking on it will run a callback

local textures = require('gui.textures')
local Panel = require('gui.widgets.containers.panel')
local Label = require('gui.widgets.labels.label')
Expand All @@ -6,17 +8,20 @@ local to_pen = dfhack.pen.parse

local button_pen_left = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')}
local button_pen_center = to_pen{
tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol
local button_pen_right = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')}
local configure_pen_center = to_pen{
tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol

---------------------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not Bumber's work here, but I think this code is more than a bit strange.

first, curry() cannot return falsey; it either throws an error or returns a function. accordingly, the or nil clauses don't ever execute.

second, that curry() is used at all. if I understand this construction, tp.control_panel() will be called to get the relevant texpos each time the pen is drawn instead of adding the texture once at module load time with e.g. tile=tp.control_panel(10). I do not know very much about textures, but I would be extremely surprised if the texpos for a control panel texture changes dynamically.

and third, that three individual pens are used instead of a three-tile table. I now see that a three-tile Label is created in the postinit stage.

myk checked this in a651f64 and he certainly knows more about textures than I do.

anyway, not directly relevant to the PR.

-- ConfigureButton --
---------------------

---@class widgets.ConfigureButton.attrs: widgets.Panel.attrs
---@field on_click? function
---@field pen_left dfhack.pen|fun(): dfhack.pen
---@field pen_center dfhack.pen|fun(): dfhack.pen
---@field pen_right dfhack.pen|fun(): dfhack.pen

---@class widgets.ConfigureButton.attrs.partial: widgets.ConfigureButton.attrs

Expand All @@ -27,27 +32,40 @@ local configure_pen_center = to_pen{
ConfigureButton = defclass(ConfigureButton, Panel)

ConfigureButton.ATTRS{
frame={t=0, l=0, w=3, h=1},
on_click=DEFAULT_NIL,
pen_left=button_pen_left,
pen_center=button_pen_center,
pen_right=button_pen_right,
Comment on lines +35 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like moving the pens into self via ATTRS. that's where it belongs. that preinit(init_table) was weird and unneccessary.

I'm not so sure about allowing instances, even subclassed instances, to override frame size.

it's fine.

}

function ConfigureButton:preinit(init_table)
init_table.frame = init_table.frame or {}
init_table.frame.h = init_table.frame.h or 1
init_table.frame.w = init_table.frame.w or 3
end

function ConfigureButton:init()
self.frame.h = self.frame.h or 1
self.frame.w = self.frame.w or 3

self:addviews{
Label{
view_id='label',
frame={t=0, l=0, w=3, h=1},
text={
{tile=button_pen_left},
{tile=configure_pen_center},
{tile=button_pen_right},
{tile=self.pen_left},
{tile=self.pen_center},
{tile=self.pen_right},
},
on_click=self.on_click,
},
}
end

function ConfigureButton:postinit()
local l = self.subviews.label

l.on_click = self.on_click
l.pen_left = self.pen_left
l.pen_center = self.pen_center
l.pen_right = self.pen_right

l:setText({{tile=self.pen_left}, {tile=self.pen_center}, {tile=self.pen_right}})
end

return ConfigureButton
40 changes: 11 additions & 29 deletions library/lua/gui/widgets/buttons/help_button.lua
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
-- A 3x1 tile button with a question mark on it. Clicking on it will show help text for a command

local textures = require('gui.textures')
local Panel = require('gui.widgets.containers.panel')
local Label = require('gui.widgets.labels.label')
local ConfigureButton = require('gui.widgets.buttons.configure_button')

local to_pen = dfhack.pen.parse
local help_pen_center = dfhack.pen.parse{
tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')}

----------------
-- HelpButton --
----------------

---@class widgets.HelpButton.attrs: widgets.Panel.attrs
---@class widgets.HelpButton.attrs: widgets.ConfigureButton.attrs
---@field command? string

---@class widgets.HelpButton.attrs.partial: widgets.HelpButton.attrs

---@class widgets.HelpButton: widgets.Panel, widgets.HelpButton.attrs
---@field super widgets.Panel
---@class widgets.HelpButton: widgets.ConfigureButton, widgets.HelpButton.attrs
---@field super widgets.ConfigureButton
---@field ATTRS widgets.HelpButton.attrs|fun(attributes: widgets.HelpButton.attrs.partial)
---@overload fun(init_table: widgets.HelpButton.attrs.partial): self
HelpButton = defclass(HelpButton, Panel)
HelpButton = defclass(HelpButton, ConfigureButton)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subclassing ConfigureButton is reasonable I suppose. better than copy-paste for sure.

there ought to be an abstract class used as the base of ConfigureButton, HelpButton, and RadioButton, but the class system doesn't really support abstract classes meant to only be subclassed.


HelpButton.ATTRS{
frame={t=0, r=1, w=3, h=1},
command=DEFAULT_NIL,
pen_center=help_pen_center,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the frame and command are redundant, aren't they? they'll be picked up from ConfigureButton, the same way that pen_left and pen_right will be.

overriding pen_center shows the benefit of moving the pens into ATTRS, good.

not worth holding up the merge over a quibble.

}

local button_pen_left = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')}
local button_pen_right = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')}
local help_pen_center = to_pen{
tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')}

function HelpButton:init()
self.frame.w = self.frame.w or 3
self.frame.h = self.frame.h or 1

local command = self.command .. ' '

self:addviews{
Label{
frame={t=0, l=0, w=3, h=1},
text={
{tile=button_pen_left},
{tile=help_pen_center},
{tile=button_pen_right},
},
on_click=function() dfhack.run_command('gui/launcher', command) end,
},
}
self.on_click = function() dfhack.run_command('gui/launcher', command) end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if line 26 is redundant, then this is as well. but harmless.

end

return HelpButton
49 changes: 49 additions & 0 deletions library/lua/gui/widgets/buttons/radio_button.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- A 3x1 tile button that toggles state when clicked

local textures = require('gui.textures')
local ConfigureButton = require('gui.widgets.buttons.configure_button')

local to_pen = dfhack.pen.parse

local enabled_pen_left = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe the or nil clauses are necessary, but I will pint out this tile doesn't have one.

local enabled_pen_center = to_pen{fg=COLOR_LIGHTGREEN,
tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check
local enabled_pen_right = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')}
local disabled_pen_left = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')}
local disabled_pen_center = to_pen{fg=COLOR_RED,
tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')}
local disabled_pen_right = to_pen{fg=COLOR_CYAN,
tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')}

-----------------
-- RadioButton --
-----------------

---@class widgets.RadioButton.attrs: widgets.ConfigureButton.attrs
---@field initial_state boolean

---@class widgets.RadioButton.attrs.partial: widgets.RadioButton.attrs

---@class widgets.RadioButton: widgets.ConfigureButton, widgets.RadioButton.attrs
---@field super widgets.ConfigureButton
---@field ATTRS widgets.RadioButton.attrs|fun(attributes: widgets.RadioButton.attrs.partial)
---@overload fun(init_table: widgets.RadioButton.attrs.partial): self
RadioButton = defclass(RadioButton, ConfigureButton)

RadioButton.ATTRS{
initial_state=true,
}

function RadioButton:init()
self.toggle_state = self.initial_state

self.on_click = function() self.toggle_state = not self.toggle_state end
self.pen_left = function() return self.toggle_state and enabled_pen_left or disabled_pen_left end
self.pen_center = function() return self.toggle_state and enabled_pen_center or disabled_pen_center end
self.pen_right = function() return self.toggle_state and enabled_pen_right or disabled_pen_right end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no getter function for the toggle state? are users supposed to probe an instance's toggle_state field ?

yeah, that's what the new documenation says. hm.

I don't like that.

are there other widgets that similarly require looking inside them for state info?

also, no callbacks? on_enable, on_disable, on_toggle ?`

I guess a user could catch the on_click that the ConfigureButton base class sets up, and then probe the toggle_state.

return RadioButton
Loading