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.
5.9 KiB
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.
- Some things can only be said in a UI file. The shortcut overview is the proof: GTK's
GtkShortcutsShortcuthas exactly one gesture type that draws anicon, and libadwaita'sAdwShortcutsItemhas no icon property at all -- so a mouse gesture can only be shown as a glyph from a.ui.src/ui/shortcuts.uiis that, and it is the reason the file exists. - Structure is data. A window's arrangement rewritten in C++ is a hundred lines of
gtk_box_appendthat 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. - Current GTK and libadwaita have better widgets than this app uses. The forms here are
GtkEntry/GtkSpinButtonin hand-built label rows. The modern equivalents --AdwEntryRow,AdwSpinRow,AdwSwitchRow,AdwComboRow, insideAdwPreferencesPage/AdwPreferencesGroupin anAdwPreferencesDialog-- 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)
.uifiles live insrc/ui/and are compiled by the third GResource (udpscope-ui.gresource.xml,prefix=/org/udpscope/ui), loaded withgtk_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
GIconproperty takes a nested object, not a name. - A
.uiover 4095 characters needs-Wno-overlength-stringson 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
GtkWindowcannot 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: anAdwDialogofAdwPreferencesGrouprows, 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: anAdwWindowwith a header bar and anAdwNavigationSplitViewwhose 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 anAdwPreferencesDialogwithAdwEntryRow,AdwSpinRow,AdwSwitchRow,AdwComboRowandAdwButtonRowrows. 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]'sui=/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.