.. _overlay-dev-guide:

DFHack overlay dev guide
=========================

.. highlight:: lua

This guide walks you through how to build overlay widgets and register them with
the `overlay` framework for injection into Dwarf Fortress viewscreens.

Why would I want to create an overlay widget?
---------------------------------------------

There are both C++ and Lua APIs for creating viewscreens and drawing to the
screen. If you need very specific low-level control, those APIs might be the
right choice for you. However, here are some reasons you might want to implement
an overlay widget instead:

#. You can draw directly to an existing viewscreen instead of creating an
   entirely new screen on the viewscreen stack. This allows the original
   viewscreen to continue processing uninterrupted and keybindings bound to
   that viewscreen will continue to function. This was previously only
   achievable by C++ plugins.
#. You'll get a free UI for enabling/disabling your widget and repositioning it
   on the screen. Widget state is saved for you and is automatically restored
   when the game is restarted.
#. You don't have to manage the C++ interposing logic yourself and can focus on
   the business logic, writing purely in Lua if desired.

In general, if you are writing a plugin or script and have anything you'd like
to add to an existing screen (including live updates of map tiles while the game
is unpaused), an overlay widget is probably your easiest path to get it done. If
your plugin or script doesn't otherwise need to be enabled to function, using
the overlay allows you to avoid writing any of the enable management code that
would normally be required for you to show info in the UI.

Overlay widget API
------------------

Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget``
(which itself inherits from `widgets.Panel <panel>`). The regular
``onInput(keys)``, ``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)``
functions work as normal, and they are called when the viewscreen that the
widget is associated with does its usual input and render processing. The widget
gets first dibs on input processing. If a widget returns ``true`` from its
``onInput()`` function, the viewscreen will not receive the input.

Overlay widgets can contain other Widgets and be as simple or complex as you
need them to be, just like you're building a regular UI element.

There are a few extra capabilities that overlay widgets have that take them
beyond your everyday ``Widget``:

- If an ``overlay_onupdate(viewscreen)`` function is defined, it will be called
    just after the associated viewscreen's ``logic()`` function is called (i.e.
    a "tick" or a (non-graphical) "frame"). For hotspot widgets, this function
    will also get called after the top viewscreen's ``logic()`` function is
    called, regardless of whether the widget is associated with that viewscreen.
    If this function returns ``true``, then the widget's ``overlay_trigger()``
    function is immediately called. Note that the ``viewscreen`` parameter will
    be ``nil`` for hotspot widgets that are not also associated with the current
    viewscreen.
- If an ``overlay_trigger()`` function is defined, will be called when the
    widget's ``overlay_onupdate`` callback returns true or when the player uses
    the CLI (or a keybinding calling the CLI) to trigger the widget. The
    function must return either ``nil`` or the ``gui.Screen`` object that the
    widget code has allocated, shown, and now owns. Hotspot widgets will receive
    no callbacks from unassociated viewscreens until the returned screen is
    dismissed. Unbound hotspot widgets **must** allocate a Screen with this
    function if they want to react to the ``onInput()`` feed or be rendered. The
    widgets owned by the overlay framework must not be attached to that new
    screen, but the returned screen can instantiate and configure any new views
    that it wants to.

If the widget can take up a variable amount of space on the screen, and you want
the widget to adjust its position according to the size of its contents, you can
modify ``self.frame.w`` and ``self.frame.h`` at any time -- in ``init()`` or in
any of the callbacks -- to indicate a new size. The overlay framework will
detect the size change and adjust the widget position and layout.

If you don't need to dynamically resize, just set ``self.frame.w`` and
``self.frame.h`` once in ``init()``.

Widget attributes
*****************

The ``overlay.OverlayWidget`` superclass defines the following class attributes:

- ``name``
    This will be filled in with the display name of your widget, in case you
    have multiple widgets with the same implementation but different
    configurations.
- ``default_pos`` (default: ``{x=-2, y=-2}``)
    Override this attribute with your desired default widget position. See
    the `overlay` docs for information on what positive and negative numbers
    mean for the position. Players can change the widget position at any time
    via the `overlay position <overlay>` command, so don't assume that your
    widget will always be at the default position.
- ``viewscreens`` (default: ``{}``)
    The list of viewscreens that this widget should be associated with. When
    one of these viewscreens is on top of the viewscreen stack, your widget's
    callback functions for update, input, and render will be interposed into the
    viewscreen's call path. The name of the viewscreen is the name of the DFHack
    class that represents the viewscreen, minus the ``viewscreen_`` prefix and
    ``st`` suffix. For example, the fort mode main map viewscreen would be
    ``dwarfmode`` and the adventure mode map viewscreen would be
    ``dungeonmode``. If there is only one viewscreen that this widget is
    associated with, it can be specified as a string instead of a list of
    strings with a single element.
- ``hotspot`` (default: ``false``)
    If set to ``true``, your widget's ``overlay_onupdate`` function will be
    called whenever the `overlay` plugin's ``plugin_onupdate()`` function is
    called (which corresponds to one call per call to the current top
    viewscreen's ``logic()`` function). This call to ``overlay_onupdate`` is in
    addition to any calls initiated from associated interposed viewscreens and
    will come after calls from associated viewscreens.
- ``overlay_only`` (default: ``false``)
    If set to ``true``, no widget frame will be drawn in `gui/overlay` for drag
    and drop repositioning. Overlay widgets that don't have a "widget" to
    reposition should set this to ``true``.
- ``overlay_onupdate_max_freq_seconds`` (default: ``5``)
    This throttles how often a widget's ``overlay_onupdate`` function can be
    called (from any source). Set this to the largest amount of time (in
    seconds) that your widget can take to react to changes in information and
    not annoy the player. Set to 0 to be called at the maximum rate. Be aware
    that running more often than you really need to will impact game FPS,
    especially if your widget can run while the game is unpaused.
- ``always_enabled`` (default: ``false``)
    Set this to ``true`` if you don't want to let the user disable the widget.
    This is useful for widgets that are controlled purely through their
    triggers. See `gui/pathable` for an example.

Registering a widget with the overlay framework
***********************************************

Anywhere in your code after the widget classes are declared, define a table
named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and
the values are the widget classes. For example, the `dwarfmonitor` widgets are
declared like this::

    OVERLAY_WIDGETS = {
        cursor=CursorWidget,
        date=DateWidget,
        misery=MiseryWidget,
        weather=WeatherWidget,
    }

When the `overlay` plugin is enabled, it scans all plugins and scripts for
this table and registers the widgets on your behalf. Plugin lua code is loaded
with ``require()`` and script lua code is loaded with `reqscript`. If your
widget is in a script, ensure your script can be
`loaded as a module <reqscript>`, or else the widget will not be discoverable.
The widget is enabled on load if it was enabled the last time the `overlay`
plugin was loaded, and the widget's position is restored according to the state
saved in the :file:`dfhack-config/overlay.json` file.

The overlay framework will instantiate widgets from the named classes and own
the resulting objects. The instantiated widgets must not be added as subviews to
any other View, including the Screen views that can be returned from the
``overlay_trigger()`` function.

Development workflows
---------------------

When you are developing an overlay widget, you will likely need to reload your
widget many times as you make changes. The process for this differs slightly
depending on whether your widget is attached to a plugin or is implemented in a
script.

Note that reloading a script does not clear its global environment. This is fine
if you are changing existing functions or adding new ones. If you remove a
global function or other variable from the source, though, it will stick around
in your script's global environment until you restart DF.

Scripts
*******

#. Edit the widget source
#. If the script is not in your `script-paths`, install your script (see the
   `modding-guide` for help setting up a dev environment so that you don't need
   to reinstall your scripts after every edit).
#. Call ``:lua require('plugins.overlay').reload()`` to reload your overlay
   widget

Plugins
*******

#. Edit the widget source
#. Install the plugin so that the updated code is available in
   :file:`hack/lua/plugins/`
#. If you have changed the compiled plugin, `reload` it
#. If you have changed the lua code, run ``:lua reload('plugins.mypluginname')``
#. Call ``:lua require('plugins.overlay').reload()`` to reload your overlay
   widget

Troubleshooting
---------------

**If your widget is not getting discovered by the overlay framework, double
check that:**

#. ``OVERLAY_WIDGETS`` is declared, is global (not ``local``), and contains your
   widget class
#. (if a script> your script is `declared as a module <reqscript>`
   (``--@ module = true``) and it does not have side effects when loaded as a
   module (i.e. you check ``dfhack_flags.module`` and return before executing
   any statements if the value is ``true``
#. your code does not have syntax errors -- run
   ``:lua ~reqscript('myscriptname')`` (if a script) or
   ``:lua ~require('plugins.mypluginname')`` (if a plugin) and make sure there
   are no errors and the global environment contains what you expect.

**If your widget is not running when you expect it to be running,** run
`gui/overlay` when on the target screen and check to see if your widget is
listed when showing overlays for the current screen. If it's not there, verify
that this screen is included in the ``viewscreens`` list in the widget class
attributes.

Widget example 1: adding text to a DF screen
--------------------------------------------

This is a simple widget that displays a message at its position. The message
text is retrieved from the host script or plugin every ~20 seconds or when
the :kbd:`Alt`:kbd:`Z` hotkey is hit::

    local overlay = require('plugins.overlay')
    local widgets = require('gui.widgets')

    MessageWidget = defclass(MessageWidget, overlay.OverlayWidget)
    MessageWidget.ATTRS{
        default_pos={x=5,y=-2},
        viewscreens={'dwarfmode', 'dungeonmode'},
        overlay_onupdate_max_freq_seconds=20,
    }

    function MessageWidget:init()
        self.label = widgets.Label{text=''}
        self:addviews{self.label}
    end

    function MessageWidget:overlay_onupdate()
        local text = getImportantMessage() -- defined in the host script/plugin
        self.label:setText(text)
        self.frame.w = #text
    end

    function MessageWidget:onInput(keys)
        if keys.CUSTOM_ALT_Z then
            self:overlay_onupdate()
            return true
        end
    end

    OVERLAY_WIDGETS = {message=MessageWidget}

Widget example 2: highlighting artifacts on the live game map
-------------------------------------------------------------

This widget is not rendered at its "position" at all, but instead monitors the
map and overlays information about where artifacts are located. Scanning for
which artifacts are visible on the map can slow, so that is only done every 10
seconds to avoid slowing down the entire game on every frame.

::

    local overlay = require('plugins.overlay')
    local widgets = require('gui.widgets')

    ArtifactRadarWidget = defclass(ArtifactRadarWidget, overlay.OverlayWidget)
    ArtifactRadarWidget.ATTRS{
        viewscreens={'dwarfmode', 'dungeonmode'},
        overlay_onupdate_max_freq_seconds=10,
    }

    function ArtifactRadarWidget:overlay_onupdate()
        self.visible_artifacts_coords = getVisibleArtifactCoords()
    end

    function ArtifactRadarWidget:onRenderFrame()
        for _,pos in ipairs(self.visible_artifacts_coords) do
            -- highlight tile at given coordinates
        end
    end

    OVERLAY_WIDGETS = {radar=ArtifactRadarWidget}

Widget example 3: corner hotspot
--------------------------------

This hotspot reacts to mouseover events and launches a screen that can react to
input events. The hotspot area is a 2x2 block near the lower right corner of the
screen (by default, but the player can move it wherever).

::

    local overlay = require('plugins.overlay')
    local widgets = require('gui.widgets')

    HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
    HotspotMenuWidget.ATTRS{
        default_pos={x=-3,y=-3},
        frame={w=2, h=2},
        hotspot=true,
        viewscreens='dwarfmode',
        overlay_onupdate_max_freq_seconds=0, -- check for mouseover every tick
    }

    function HotspotMenuWidget:init()
        -- note this label only gets rendered on the associated viewscreen
        -- (dwarfmode), but the hotspot is active on all screens
        self:addviews{widgets.Label{text={'!!', NEWLINE, '!!'}}}
        self.mouseover = false
    end

    function HotspotMenuWidget:overlay_onupdate()
        local hasMouse = self:getMousePos()
        if hasMouse and not self.mouseover then -- only trigger on mouse entry
            self.mouseover = true
            return true
        end
        self.mouseover = hasMouse
    end

    function HotspotMenuWidget:overlay_trigger()
        return MenuScreen{hotspot_frame=self.frame}:show()
    end

    OVERLAY_WIDGETS = {menu=HotspotMenuWidget}

    MenuScreen = defclass(MenuScreen, gui.Screen)
    MenuScreen.ATTRS{
        focus_path='hotspot/menu',
        hotspot_frame=DEFAULT_NIL,
    }

    function MenuScreen:init()
        self.mouseover = false

        -- derrive the menu frame from the hotspot frame so it
        -- can appear in a nearby location
        local frame = copyall(self.hotspot_frame)
        -- ...

        self:addviews{
            widgets.ResizingPanel{
                autoarrange_subviews=true,
                frame=frame,
                frame_style=gui.GREY_LINE_FRAME,
                frame_background=gui.CLEAR_PEN,
                subviews={
                    -- ...
                    },
                },
            },
        }
    end

    function MenuScreen:onInput(keys)
        if keys.LEAVESCREEN then
            self:dismiss()
            return true
        end
        return self:inputToSubviews(keys)
    end

    function MenuScreen:onRenderFrame(dc, rect)
        self:renderParent()
    end
