The sidebar's width was a GtkPaned divider position: `window - 940` in pixels. On any window narrower than 940 px the divider landed past the right edge and the sidebar came up 0 px wide, reachable only by dragging the divider back. Every attempt that kept the paned failed for that reason -- deriving the position from the paned's own width, re-parenting the column, property-form children, code-set size requests. The mechanism was the fault, so it is gone: the row is an AdwOverlaySplitView, which is what a utility sidebar is in GNOME, with min-sidebar-width = max-sidebar-width = 320 in pixels. Its width is therefore its own at any window size, and when there is no room beside the plot it *overlays* the plot (collapsed, set from the window's width in tick()) instead of being squeezed to nothing. Two things found on the way, both recorded in the invariants: - sidebar-width-fraction is a 0..1 fraction of the window whatever sidebar-width-unit says. Setting a pixel count there is a GObject critical and it falls back to 0.25, which is why the sidebar first read 560. The width is pinned with min/max instead. - the window opened at its content minimum (756 px, the plot column plus the sidebar) because the explicit gtk_window_set_default_size() was dropped when the window moved into the .ui. Restored. [sidebar-probe] reads both states and is the guard: window=756 sidebar=320 under a 2x scale, which is the case that used to read 0.
85 lines
5.9 KiB
Markdown
85 lines
5.9 KiB
Markdown
# UI files and current GTK practice
|
|
|
|
Where the application's interface should be a `.ui` resource instead of widget construction, what is
|
|
already there, and the order to finish it in. Written 2026-10-11.
|
|
|
|
## Why
|
|
|
|
Three reasons, in the order they bite.
|
|
|
|
1. **Some things can only be said in a UI file.** The shortcut overview is the proof: GTK's
|
|
`GtkShortcutsShortcut` has exactly one gesture type that draws an `icon`, and libadwaita's
|
|
`AdwShortcutsItem` has no icon property at all -- so a mouse gesture can only be shown as a glyph
|
|
from a `.ui`. `src/ui/shortcuts.ui` is that, and it is the reason the file exists.
|
|
2. **Structure is data.** A window's arrangement rewritten in C++ is a hundred lines of
|
|
`gtk_box_append` that no one can read at a glance, cannot be compared with a diff, and cannot be
|
|
previewed. A UI file is the same arrangement in the notation the GNOME ecosystem reads.
|
|
3. **Current GTK and libadwaita have better widgets than this app uses.** The forms here are
|
|
`GtkEntry`/`GtkSpinButton` in hand-built label rows. The modern equivalents -- `AdwEntryRow`,
|
|
`AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow`, inside `AdwPreferencesPage`/`AdwPreferencesGroup` in an
|
|
`AdwPreferencesDialog` -- are what the preferences dialog should be, and they come from the same
|
|
move.
|
|
|
|
## Conventions (established, and in `docs/claude/invariants-gtk-and-chrome.md`)
|
|
|
|
- `.ui` files live in `src/ui/` and are compiled by the third GResource (`udpscope-ui.gresource.xml`,
|
|
`prefix=/org/udpscope/ui`), loaded with `gtk_builder_new_from_resource()`.
|
|
- Every file carries `preprocess="xml-stripblanks"`, which is the only thing that catches a malformed
|
|
file at build time.
|
|
- A file that is valid XML but wrong for GTK **aborts the process** (`Gtk-ERROR`). The probe that opens
|
|
every window is what catches that; there is no build-time substitute.
|
|
- A `GIcon` property takes a nested object, not a name.
|
|
- A `.ui` over 4095 characters needs `-Wno-overlength-strings` on its generated source.
|
|
- A window built from a builder is replaced (not reused) on the next opening: it belongs to the
|
|
builder's object list, and a destroyed `GtkWindow` cannot be presented again.
|
|
- A deprecated *widget* is acceptable when no deprecated *function* is called.
|
|
|
|
## Acceptance for every slice
|
|
|
|
The same as any other change here: the build stays at **four warnings**, a run stays at **no
|
|
`Gtk-CRITICAL`**, and the slice's own surface is read back by a probe rather than asserted. A new `.ui`
|
|
needs a probe that proves it *loads* and that its contents match whatever drives it -- `[help]`'s
|
|
`ui=` against `tables=` is the model.
|
|
|
|
## Already done
|
|
|
|
- `src/ui/main-window.ui` -- the main window's **skeleton**: the window, its two panes, the plot column
|
|
with the status line under it, and the sidebar's scroller. The four subtrees that depend on the stream
|
|
(the events pane, the two plot areas, the sidebar's panels, the status popover) are still built by
|
|
code and put into the slots by id.
|
|
- `src/ui/source-form.ui` -- the form a source is added or edited with: an `AdwDialog` of
|
|
`AdwPreferencesGroup` rows, with its action in the header bar where the label says which mode it is in.
|
|
- `src/ui/help.ui` -- the help window's chrome: an `AdwWindow` with a header bar and an
|
|
`AdwNavigationSplitView` whose sidebar is the page list. The rows stay in C++ because the fourteen
|
|
pages are data, not template.
|
|
- `src/ui/preferences.ui` -- the preferences, as an `AdwPreferencesDialog` with `AdwEntryRow`,
|
|
`AdwSpinRow`, `AdwSwitchRow`, `AdwComboRow` and `AdwButtonRow` rows. The Apply button is gone: a row
|
|
applies as it changes, which is what those widgets are designed around, and `[prefs-probe]` reads the
|
|
file back after every change.
|
|
- `src/ui/shortcuts.ui` -- the shortcut overview, with the mouse rows drawn as an icon cell.
|
|
- The `src/ui/` GResource and its build block.
|
|
- `[help]`'s `ui=`/`ui_accel=`/`ui_icon=`/`ui_bare=` reading, which checks the file against the tables
|
|
and asserts that no row is left without a visual.
|
|
|
|
## Remaining, smallest first
|
|
|
|
| where | what | why it is the right next step |
|
|
|---|---|---|
|
|
| `src/source_dialog.cpp` (the chooser's own window) | the chooser: `.ui` for the window, its two pages and its action row. Its **host form is done**; what is left is the list, the notebook and the buttons around it | the last hand-built UI outside `ui.cpp`, and the file is already open |
|
|
| `build_measures_panel` (`src/ui.cpp`, ~82 lines) | the measurements panel: `.ui` + `tools/check_ui.py` after | small, self-contained, and its own probe |
|
|
| `src/ui/ui_trigger.cpp` (~22 kB) | the trigger panel: the `.ui` holds the panel and its groups; the signal chips and the condition rows stay in code because they follow CONFIG | the largest user-visible panel |
|
|
| `src/ui/ui_signals.cpp` (~20 kB) | the signals panel | mostly a list of data-driven rows, so the `.ui` holds the frame |
|
|
| `src/ui/ui_math.cpp` (~48 kB) | the math panel and the node editor's chrome (the canvas itself is drawn, not declared) | the largest file, and the one where least of it is template |
|
|
| `src/ui/ui_events.cpp` (~44 kB) | the events pane | the rows are events, the frame is not |
|
|
| `src/ui/ui_layout.cpp` (~20 kB) | the layout editor's overlay buttons, which are positioned by code | only the buttons are template |
|
|
| `build_toolbar` (`src/ui.cpp`, ~212 lines) | the header bar's layout, with every button still wired in code | delicate: the `[udpscope] header:` probe reads its geometry |
|
|
|
|
**Done since:** the sidebar is an `AdwOverlaySplitView` (`src/ui/main-window.ui`) rather than a paned's end
|
|
child, so its width is its own 320 px and it overlays the plot instead of collapsing to zero — see
|
|
`[sidebar-probe]`.
|
|
|
|
Each of these is one change on the pattern `src/ui/ui_cursors.cpp` now follows: `ui_load()` the file, resolve
|
|
the ids, keep every value, signal and rebuild in code, and run the panel's own probe before committing.
|
|
|
|
Nothing in this list should be done in one pass. Each row is its own change, verified on its own.
|