Originally, as a next step in (documented) development, I wanted to deal with the various application states (main menu, overworld-idle, targetting, etc) but I realized that the application states, in my mind at least, have a very strong link with ... widgets! Textboxes, tile grids, containers, minimaps, that sort of thing. Therefore, I thought it will be prudent to nail that down first, as it's a lower-level system. Here is a first stab at a specification, with a working example.
Widgets are objects responsible for rendering and input handling. Widget examples:
- Container1D: Container of other widgets, laid out horizontally or vertically (listed top to bottom).
- Container2D: Container of other widgets, laid out in a grid.
- TextBox: A rectangular area of text. Used for labels.
- ListBox: The typical vertical arrangement of e.g. 1. "Blah" 2. "Bloop" of which the user selects one
- Button: A button with optional text. Can be implemented as a special textbox,
- TileGrid: View in a a 2D grid of cells, that are highlightable and selectable
- PixelGrid: For minimaps and zoomed out overworld something something
Here is a current example. I have a vertical Container1D with 4 elements: a textbox (with some sort of rich text format), a simple box (it's a dummy single color widget), a horizontal Container1D with three simple boxes inside, and a simple box with margins (left 1px, bot 2px, right 3px, top 4px). All that is driven via the JSON text below.
- Widgets are highly hierarchical: A container can contain containers and so on.
- A widget can be modal or not.
- In-focus modal widgets don't allow any other widget to get focus/input, modal or otherwise
- Each widget can spawn a maximum of one modal widget
- Modal, in-focus widgets are rendered last. So, for example, we can darken everything else a bit before overlaying the widget, to communicate the modality.
- Widgets can spawn an arbitrary number of floating widgets (floaters). This for example could be damage numbers above creatures when hit.
- Non-modal floaters don't handle input or be considered for focus
- Use-case: Damage number (non-modal)
- when activated, we specify the tile location that it originates from
- when updating, reduce transparency and increase .y slightly
- no input handling
- Container widget never overlap each other. Only floaters can overlap
- Widgets are not resizable. Too much hassle to make sure it always looks nice. Presets will have to be created for each supported resolution, or at least for each aspect ratio.
- Widgets need to provide intersection testing against the mouse pointer. Simplest way using a bounding rectangle.
- Widgets control their dimensions
- Widgets do not control their location: their parents do!
- How does the GUI system get input? The application passes the input events to the active root widget. The widget handles input and passes the events to children in case the input was not handled.
- Input handling functions for: mouse wheel, mouse pointer, mouse buttons and keyboard. Later on could also add controller.
- Widgets store a vector of (key, command) pairs for handling key presses. When populating that vector, always assert that a key is only used once.
- Some widgets have constant bindings. For example, some modal dialogs bind Esc for back/cancel, while listboxes bind 1,2,3,etc for option index. Similarly, arrow keys are used for listbox option navigation and tilegrid tile selection
- Order of handling input:
- The modal+in-focus widget, if one exists.
- Otherwise: root widget and non-floater children, recursively.
- Effectively, depth-first handling.
- Widgets input interaction with current running state (decoupled)
- Example: TileGrid widget, SelectTile state
- widget::OnMouseMotion: emits event TileHighlighted
- LMB: emits event TileActionMain
- RMB: emits event TileActionSecondary
- Arrow keys emit TileHighlighted.
- Enter emits TileActionMain
- The state listens to the events and processes them accordingly
- The state adjusts the TileGrid widget rendering configuration so that it renders some flashing transparent dark tile over the areas that can be selected (e.g. within attack range)
- Example: TileGrid widget, SelectTile state
- Use pixel coordinates, no normalized (0,1) space
- Bottom-left is (0,0)
- Use a selection of (flyweight) renderers: colored rect, textured rect, font, etc
- Widgets own the configuration for the renderers, not the renderers themselves
- Basic widget rendering involves:
- Rendering its own area using simple color, simple texture, or a fancy shader
- Rendering its margins (if any) using simple color, simple texture, or a fancy shader
- The widget hierarchy has an effect in rendering order.
- Simple rendering method: render everything on the fly, every frame
- Advanced rendering method (later on): Instead of rendering, just schedule parameterized rendering commands, and let the renderer do the batching/ordering.
- z depth would be related to the hierarchy level: Children are always rendered in front of parents
- The hierarchical widget model can be used for caching rendering results. (later!)
- A sidebar full of textboxes, lifebars and whatnot, will only be rendered when one of its elements changed. So, no font rendering all the time
- TileGrid will be rendered all the time
- Separate rendertargets may be employed for rendering some widgets.
- Floating damage textboxes! they are pre-rendered in their own rendertarget, and every frame just blit some rects onto the appropriate location on the tilegrid
- Have presets for each resolution/aspect ratio
- Widgets can be initialized but not activated.
- Only activated widgets are used in the game
- Maintain a globally accessible mapping of widget names to widgets. When a state needs to adjust a widget, use the mapping to get access to it (widgets are not owned by states, but they are used by them)
- Inactive widgets can be used as prototypes: When I need to create a widget, lookup an already created one (using a certain configuration) and clone it.
... And that's it for now. Next time, I'll have some more widget types implemented (as listed above), with some fancier rendering and input handling. The first test case will be a main menu and the overworld screen, so when it's time for the application states, I can connect these two.