Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ Template for new versions:

## Lua
- ``dfhack.military.addToSquad``: expose Military API function
- ``gui.dflayout``: provide DF fort mode toolbar position information and automatic overlay positioning

## Removed
- ``dfhack.buildings.getOwner``: make new Buildings API available to Lua

# 51.10-r1
Expand Down
212 changes: 212 additions & 0 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6521,6 +6521,218 @@ Example usage::
local first_border_texpos = textures.tp_border_thin(1)


gui.dflayout
============

This module provides help with positioning overlay widgets relative to DF UI
elements that may not always be straightforward to locate across multiple
interface area sizes (and thus, window sizes).

It currently supports the fortress mode toolbars at the bottom of the screen.

Unless otherwise noted, the dimensions used by this module are in UI tiles and
are always inside the DF interface area (which, depending on DF settings, may be
narrower than the DF window).

General Constants
-----------------

This module provides these convenience constants:

* ``MINIMUM_INTERFACE_SIZE``

The dimensions (``width`` and ``height``) of the minimum-size DF window:
114x46 UI tiles.

* ``TOOLBAR_HEIGHT``

The height of the primary toolbars at the bottom of the DF window (3 UI rows).

* ``SECONDARY_TOOLBAR_HEIGHT``

The height of the secondary toolbars that are sometimes placed above the
primary toolbars (also 3 UI rows).

Fortress Mode Toolbars
----------------------

Fortress mode DF draws three primary toolbars and (depending on the DF "mode")
possibly one of several secondary toolbars at the bottom of the interface area.

Layout Information
~~~~~~~~~~~~~~~~~~

The "raw" layout description for toolbars gives the width of the toolbar and the
sizes and (relative) positions of its buttons.

The layouts of the primary toolbars are available through these module fields:

* ``element_layouts.fort.toolbars.left``
* ``element_layouts.fort.toolbars.center``
* ``element_layouts.fort.toolbars.right``

The layouts of the secondary toolbars are available through the fields of
``element_layouts.fort.secondary_toolbars``:

* ``DIG``
* ``CHOP``
* ``GATHER``
* ``SMOOTH``
* ``ERASE``
* ``MAIN_STOCKPILE_MODE``
* ``STOCKPILE_NEW``
* ``Add new burrow``
* ``TRAFFIC``
* ``ITEM_BUILDING``

Except for ``Add new burrow``, these field names are taken from the
``df.main_hover_instruction`` enum names of the "button" that activates the
secondary toolbar (except for ``Add new burrow`` and ``STOCKPILE_NEW``, these
are buttons in the center toolbar).

The DF build menu (which is displayed in mostly the same place and activated in
the same way as the other secondary toolbars) is not currently supported. It has
significant differences from the other secondary toolbars.

Each toolbar layout description table provides these fields:

``width``
the width of the toolbar

``buttons``
a table indexed by "button names" that provides info about individual buttons:

* ``offset``: the left-offset from left edge of the toolbar
* ``width``: the width of the button

Please consult the module source for each toolbar's button names.

UI Elements
~~~~~~~~~~~

The ``element_layouts`` toolbar descriptions are combined with custom
positioning code to form "dynamic UI elements" that can compute where individual
UI elements will be positioned inside interface areas of various sizes.

The toolbar "UI elements" are available through these module fields:

* ``elements.fort.toolbars.left``
* ``elements.fort.toolbars.center``
* ``elements.fort.toolbars.right``
* ``elements.fort.toolbars_buttons.left[button_name]``
* ``elements.fort.toolbars_buttons.center[button_name]``
* ``elements.fort.toolbars_buttons.center_close[button_name]``
* ``elements.fort.toolbars_buttons.right[button_name]``
* ``elements.fort.secondary_toolbars.[secondary_name]``
* ``elements.fort.secondary_toolbar_buttons.[secondary_name][button_name]``

The ``secondary_name`` and ``button_names`` values are the same names as used
for the layout descriptions.

These "UI element" values should generally be treated as opaque. They can be
passed to the overlay positioning helper functions described below.

Automatic Overlay Positioning
-----------------------------

This module provides higher-level functions that use the provided "dynamic UI
elements" to help automatically position an overlay widget with respect to the
UI element:

* ``getOverlayPlacementInfo(overlay_placement_spec)``

The ``overlay_placement_spec`` parameter should be a table with the following
fields:

``size``
a table with ``width`` and ``height`` fields that specifies the static size
of the overlay widget

``ui_element``
the overlay will be positioned relative to the specified UI element; UI
element values can be retrieved from this module's ``elements`` field.

``h_placement``
a string that specifies the overlay's horizontal placement with respect to
the ``ui_element``

* ``'on left'``: the overlay's right edge will be just to the left of the
``ui_element``'s left edge
* ``'align left edges'``: the overlay's left edge will be aligned to the
``ui_element``'s left edge
* ``'align right edges'``: the overlay's right edge will be aligned to the
``ui_element``'s right edge
* ``'on right'``: the overlay's left edge will be just to the right of the
``ui_element``'s right edge

``v_placement``
a string that specifies the overlay's vertical placement with respect to
the ``ui_element``

* ``'above'``: the overlay's bottom edge will be just above the reference
frame's top edge
* ``'align top edges'``: the overlay's top edge will be aligned to the
``ui_element``'s top edge
* ``'align bottom edges'``: the overlay's bottom edge will be aligned to the
``ui_element``'s bottom edge
* ``'below'``: the overlay's top edge will be just below the
``ui_element``'s bottom edge

``offset``
an optional table with ``x`` and ``y`` fields that gives an additional
position offset that is applied after the overlay is positioned relative to
the ``ui_element``.

``default_pos``
an optional table with ``x`` and/or ``y`` fields that overrides the returned
``default_pos``. This field should be omitted for new overlays, but may be
needed for compatibility with existing "UI element relative" overlay
positioning code.

A table with the following fields is returned:

* ``default_pos``: a table that should be used for the overlay's ``default_pos``
* ``frame``: a table that may be used to initialize the overlay's ``frame``
* ``preUpdateLayout_fn``: a function that used as (or called from) the
overlay's ``preUpdateLayout`` method

This function can be used like this::

local dflayout = require('gui.dflayout')
local PLACEMENT = dflayout.getOverlayPlacementInfo({
size = { w = 26, h = 11 }, -- whatever the overlay uses
-- position the overlay one column to the right of
-- the MAIN_STOCKPILE_MODE toolbar
-- (the one with the STOCKPILE_NEW button)
ui_element = dflayout.elements.fort.secondary_toolbars.MAIN_STOCKPILE_MODE,
h_placement = 'on right',
v_placement = 'align bottom edges',
offset = { x = 1 },
})
TheOverlay = defclass(TheOverlay, overlay.OverlayWidget)
TheOverlay.ATTRS{
default_pos=PLACEMENT.default_pos,
frame=PLACEMENT.frame,
-- ...
}
function TheOverlay:init()
-- ...
end
TheOverlay.preUpdateLayout = PLACEMENT.preUpdateLayout_fn

The ``preUpdateLayout_fn`` function will adjust the overlay widget's
``frame.w``, ``frame.h``, and ``frame_inset`` fields to arrange for the
overlay to be positioned as requested. The overlay position remains
player-adjustable, but is made relative to the ``ui_element`` position instead
of being relative to the edges of the interface area.

* ``getLeftOnlyOverlayPlacementInfo(overlay_placement_spec)``

This function works like ``getOverlayPlacementInfo``, but it only "pads" the
overlay on the left. This is useful for compatibility with existing "UI
element relative" overlay positioning code (e.g., to avoid needing a version
bump that would reset a player's custom positioning).

.. _lua-plugins:

=======
Expand Down
96 changes: 96 additions & 0 deletions docs/dev/overlay-dev-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,99 @@ screen (by default, but the player can move it wherever).
},
}
end

Widget example 4: positioning relative to DF UI
-----------------------------------------------

The player-controllable positioning provided by the overlay framework is
relative to a corner of the interface area (technically, the positioning is
edge-relative, but the anchors are always two adjacent edges, so it is
equivalent to corner-relative positioning). This corner-relative positioning
works well to adapt overlay positions for interface areas of varying sizes (due
to DF window size changes, or because the DF interface percentage has been
reconfigured).

However, some overlays are most useful when they are drawn near a DF UI element
that does not maintain a fixed offset from any particular corner. For example,
the central toolbar (i.e., the fort mode tools for designations, etc.) is not
always at the same offset from either the left or the right interface edges (to
maintain a centered position, its left and right offsets each (mostly) grow in
proportion to the width of the interface area). The positions of the "secondary"
toolbars (that "open from" the central toolbar) are similarly variable (but in a
slightly more complicated way).

Such an overlay could eschew player-customizable positioning, but giving up that
flexibility should not be done lightly.

Instead, with some clever manipulation, an overlay's corner-relative positioning
can be "translated" to be relative to some other UI element.

The ``gui.dflayout`` module provides the ``getOverlayPlacementInfo`` helper
function that provides helper values that can automatically adapt an overlay's
corner-relative, player-customizable position to be relative to a UI element
while still being player-customizable::

local overlay = require('plugins.overlay')
local Label = require('gui.widgets.labels.label')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a local commit that changes this to the normal local widgets = require('gui.widgets').
I can push it whenever, but thought I might hold on to it (avoiding a possible Windows-build CI hiccup) until I also have something substantive from my attempts to further generalize the overlay placement code.

local dflayout = require('gui.dflayout')

local WIDTH, HEIGHT = 20, 1 -- whatever static size the overlay needs
local PLACEMENT = dflayout.getOverlayPlacementInfo{
size = { w = WIDTH, h = HEIGHT },
ui_element = dflayout.elements.fort.secondary_toolbar_buttons.DIG.DIG_DIG,
h_placement = 'align left edges',
v_placement = 'above',
}

local dig_dig_button = dflayout.element_layouts.fort.secondary_toolbars.DIG.buttons.DIG_DIG

UIRelativeOverlay = defclass(UIRelativeOverlay, overlay.OverlayWidget)
UIRelativeOverlay.ATTRS{
name = 'Can you dig it?',
desc = 'A overlay that has UI-relative positioning.',
default_enabled = true,
default_pos = PLACEMENT.default_pos,
-- frame and frame_inset are managed in preUpdateLayout
viewscreens = { 'dwarfmode/Designate/DIG_DIG' },
}

function UIRelativeOverlay:init()
self:addviews{
Label{
text_pen = { fg = COLOR_BLACK, bg = COLOR_GREY },
text = string.char(25):rep(dig_dig_button.width) .. ' I can dig it!',
},
}
end

UIRelativeOverlay.preUpdateLayout = PLACEMENT.preUpdateLayout_fn

OVERLAY_WIDGETS = { overlay = UIRelativeOverlay }

Consequences
************

The generated ``preUpdateLayout_fn`` function works by "inflating" the overlay
size (setting ``frame.w`` and ``frame.h``) and "floating" the overlay content
(setting ``frame_inset``). This has some drawbacks when the interface area is
much larger than the minimum size.

* The overlay outlines drawn by `gui/overlay` reflect the inflated size, and
thus can grow quite large.

* When the mouse is over an "inflated" overlay outline, the area will be
filled, obscuring a potentially large portion of the interface area.

* Since the overlay content is inset to "float" inside the inflated overlay
size, it can be quite hard to judge where the content will be drawn while an
overlay move is in progress. It may take multiple tries to move the overlay
content to a particular desired location.

* Because the overlay size is inflated, the available area for positioning the
overlay content is effectively reduced. The available area is equivalent to
the area that is available around the targeted UI element in minimum-size
interface area.

An alternate ``getLeftOnlyOverlayPlacementInfo`` function is available that only
"inflates" and "floats" on the left side, which is compatible with the way
several existing overlays overlays are positioned relative to DF toolbars.
Loading