# Crossword Editor Panel Components | | | |----------------|-------------------------| | **Status:** | Implemented | | **Date:** | October, 2023 | | **Reviewers:** | @federico | ## Summary The crossword editor has migrated to over to use libpanel. As a result, we have a large collection of `PanelWidgets` planned to go in the frames around the outside of the dock, as well as some central widgets. This document describes what they are and how they interoperate. ### Scope This is focused on the interface for Standard, Cryptic, and Barred Crosswords. We'll use a similar approach but have a different set of widgets for the other crossword types. ## Proposal As of 0.3.11, we have a new user interface for `EditWindow`. ![Panel Widgets Overview](panel-widgets-overview.png)
_Mockup of `EditWindow`_ Visually, the window consists of a grid **(1)** in the center displaying the current puzzle. There are `PanelFrames` on the left **(2)** and bottom **(3)** of the window. Each frame contains a number of `PanelWidgets`, depending on the mode and puzzle type. On the top, there is a `GtkStackSwitcher` **(4)** to switch between the different stages of editing a puzzle, represented by `PuzzleStackStage`. Currently, four stages are planned and all except **STYLE** have some of its implementation written. The first three stages (**GRID**, **EDIT**, and **STYLE**) all have a similar looking grid in the middle. There's a slight change in background color to give a quick visual hint to the current stage. The final stage (**METADATA**) looks very different and doesn't use either of the side frames. The left frame has tools for mutating the puzzle — primarily editors and navigation controls. It currently has a primary and secondary area, and lets the user switch between different tools. On the other hand, the bottom grid is purely informational. It contains information about the current state, such as the definition of the current word, or details about the grid. There may be many of these during the `CLUE` stage, as they can be extremely useful for writing clues. ### Editor components | **Widget** | **Stage** | **Location** | **Description** | |------------------------|-------------------|--------------|--------------------------------------------------------------------------------------------| | EditGrid | GRID | Center | Displays the current puzzle grid and edit answers. **Changes cursor** | | EditGrid | EDIT | Center | Displays the current puzzle grid. **Changes cursor** | | _**EditGrid**_ | STYLE | Center | Displays the current puzzle grid. **Changes cursor** | | EditWordList | GRID | Left | Shows potential words that fit in the grid | | _**EditAutofill**_ | GRID | Left | Autofill the grid depending on | | EditClueList | CLUE | Left | Selects the list of answers to set a clue | | EditSymmetry | GRID | Left | Sets the symmetry of bars and blocks. **Changes symmetry** | | EditBars | GRID & IPuzBarred | Left | Sets barred lines between cells | | _**EditCellType**_ | GRID | Left | Selects a cell type and shape for the current cell | | EditClueDetails | CLUE | Left | Edits the details of the currently selected clue. **changes clue substring** | | _**EditStyleList**_ | STYLE | Left | Shows the list of current named styles | | _**EditStyleDetails**_ | STYLE | Left | Edits the style of the current cell | | _**EditGridInfo**_ | GRID | Bottom | Shows statistics about the current grid, such as distribution of clue lengths and letters. | | _**EditDict**_ | CLUE | Bottom | Gives a definition of the selected clue | | _**EditDict**_ | GRID | Bottom | Gives a definition of the clue substring | | _**EditAnagram**_ | CLUE | Bottom | Shows anagrams of the selected clue or subclue | | _**EditThesaurus**_ | CLUE | Bottom | Gives a synonym of the selected clue or subclue | | _**EditHint**_ | CLUE | Bottom | Shows potential clue fragments for selected clue or subclue | **NOTE:** In the table above, _**Widgets**_ written as bold/italic are just proposed and not written yet (as of Sept. 2023) ## Implementation Notes With this shift, our old approach of using the `PuzzleStack` to contain the full state is no longer sufficient. We need other transient state — such as the cursor position — in order for everything to update correctly. Different `PanelWidgets` can affect the behaviors of others, as can the central dock widget. This change is correct: we don't expect the **undo** command to undo each movement around the grid or change tools. As an example of this transient state, `EditSymmetry` modifies the current selected symmetry of the puzzle, and other widgets can read from it for making changes. Similarly, `EditClueDetails` produces a substring of a clue that can be queried. The main grid's cursor will also affect things. To handle this at scale and keep things straight forward in the code, we've taken the following approach: * All state for a given `EditWindow` is stored in the `EditState` struct. This is the cannonical state of the application and crossword. It has a few constraints that are checked with the `VALIDATE_EDIT_STATE()` macro. * Each `PanelWidget` does three things: * It can update its display when given state from `EditState` * It will emit events when aspect of the `EditState` need changing. * It can optionally push transient state on the puzzle-stack. This is ppredmoninantly focus data and cursor postition. In addition, each widget can be told to commit any unsaved changes due to external events. As an example, widgets are committed before we save the puzzle to disk. * The only object that modifies `EditState` is the `EditWindow`. This happens exclusively in callbacks for _edit-window-controls.h_. Each `PanelWidget` is relatively simple as a result. * The only widget that deals with the undo/redo stack is `EditWindow`. This also happens in _edit-window-controls.h_. The exception to this is that `PanelWidgets` are asked to save/restore transient state but the window. * Each panel widget is defined in the `EditWindow.ui`, but not put in a widget tree. The `EditWindow` has an additional ref count for each widget beyond what the widget tree contains. This is because we have to add/remove the widgets to the panel when the Editor switches modes. `PanelFrame` does not handle widgets being hidden and shown gracefully, so we have to create it from scratch on each stage change. See [libpanel issue #31](https://gitlab.gnome.org/GNOME/libpanel/-/issues/31) * Finally, during the course of writing this doc, it's become clear that `XwordState` is named incorrectly. We will rename it to be `GridState` in the future. This approach is an example of a pattern we've adopted in writing Crosswords: _Concentrate the complexity_. Where possible, we put all the complexity in one area. That lets us test that particular area, and limit side-effects across the code base. ## Open Questions Some open questions about this approach: * [X] There is no common base class or interface for the `PanelWidgets`. I started writing an `EditPanelWidget` for that role, and realized that other than sharing the PuzzleStack, it didn't have a lot of value. Different widgets need different things, and I didn't want to add a union of property types -- mostly empty. However, the result of this is a lot of duplicate code as each widget has to add its own property handling for the stack, at a minimum. * This base type doesn't have a ton of value, but we can centralize an `::update()` function as well as the `puzzle-stack` and `grid` properties. It also gives us flexibility in case we need it for future libpanel interactions. * **UPDATE:** With the shared `puzzle-stack` not being used, we don't need `EditPanelWidget`. This can go away for good. * [ ] One of the main strengths of libpanel and gnome-builder is that you can reorder / resize the frames and panel widgets within the application. This breaks pretty badly with the stages, and is currently fully disabled. That means that we're not taking advantage of libpanel. * **UPDATE:** After talking with Christian, it's not clear libpanel will be able to provide us what we need. It's a small enough amount of work that its worth looking to see if we can modify it, but it may be more work than its worth. `AdwSplitView` may be a better approach.