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
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

  • 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.