Files
udpscope/src/ui/main-window.ui
T
Martino Ferrari e615465366 ui: each tab's glyph is a pair, not one icon
The Measure tab is now `udpscope-tab-measure-symbolic` — the two cursors standing across a ruler — and
the Signals tab is `udpscope-tab-signals-symbolic` — the trigger's rising edge over the channel list it
acts on. Each is a 16px composite rather than one of the icons the toolbar carries: a glyph the toolbar
already uses for one thing means two things the moment a tab shows it too, which is why the ruler alone
and the trigger alone were both wrong here.

The plain ruler stays exactly where it was. `udpscope-measure-symbolic` is the Preferences window's Plot
page icon and is untouched; the tab's pair is a new file beside it, so nothing that already meant "the
measurement ruler" changed meaning. Seven action icons bundled, nine with the two app icons.

Both composites are at the limit of what 16px holds — four features each — so each was drawn and then
*measured*: rasterized and read back as an ink map, since the shapes have to be checked as pixels and the
house range is 25-36% ink (trigger 25, cursors 27, ruler 36). The Signals list is two rows and not three,
so the edge and the list get half the cell each; its bars are all one length, because unequal ones read as
a bar chart and a chart is not a list. The Measure ruler's ends are open on purpose: a closed frame puts
four more pixels of ink at the left and right edges, which is what turns a ruler into a rectangle at this
size. First drafts came out at 39% and 44% ink and were rebalanced to 32% and 36%.

Verified: `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`;
`switcher: policy=narrow pills=2 labels=0 names=2`; `[side-tab-probe] … advanced=4` (switching by page
name still works); the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14
files; zero `Gtk-CRITICAL`. The icons' GResource blob is now 11309 bytes, still over C99's 4095-byte
string-literal limit, so the clean-build warning count is still four.
2026-10-14 13:33:24 +02:00

192 lines
11 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
events pane's divider, the plot column with the status line under it, and the sidebar's tab stack.
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
the reasons it is here:
- its width is *its own*: `sidebar-width-fraction` in **pixels** (the unit says so), floored by
`min-sidebar-width`. A `GtkPaned` kept a child's size in a divider position, which is a number of
pixels *of the window* — so on a window narrower than that number the sidebar came up with no
width at all, which is the bug this widget replaces;
- it resizes by dragging its own edge, within `min-sidebar-width`..`max-sidebar-width`;
- when the window is too narrow for both columns it *overlays* the plot instead of squeezing it,
which is what `collapsed` is for; `tick()` sets it from the window's width.
What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of
the window does not depend on the stream, the trigger or the layout — while the subtrees that do are
built by code and put in place: the events pane, into `events_divider`'s start side; the two plot
areas, into the split inside `plot_box`; the sidebar's panels, appended to `measure_page` and `signals_page`; and the
status popover, attached to `status_info`.
The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()`
because every button on it is wired to an action. The status line is not a bottom bar — it belongs to
the plot column, not to the window, so it stays inside `left_column` where the divider can move past
it.
-->
<interface>
<object class="GtkApplicationWindow" id="main_window">
<property name="title">udpscope</property>
<property name="icon-name">org.udpscope.app</property>
<!-- The size the window asks for, stated in code as well as here: a window whose content has a
larger minimum opens at that minimum instead (see `ui.cpp`). -->
<property name="default-width">1280</property>
<property name="default-height">800</property>
<child>
<object class="AdwOverlaySplitView" id="side_view">
<property name="sidebar-position">end</property>
<!-- Its width in pixels, not as a fraction of the window: 320 on a first run, and never less
than 240 however small the window gets. -->
<!-- Both ends of the range, in pixels: `sidebar-width-fraction` is a 0..1 *fraction* of the
window (setting a pixel count there is a GObject critical and falls back to 0.25), so the
width is pinned by min and max instead. -->
<property name="sidebar-width-unit">px</property>
<property name="min-sidebar-width">320</property>
<property name="max-sidebar-width">320</property>
<property name="sidebar">
<!-- A switcher above a stack, and a scroller *per page*: the two pages are different
heights, so one shared scroller around the stack would leave a tab switch scrolled
to somewhere the new page never asked for. The switcher is the sibling above, not a
child of either scroller: the one control that chooses what you are looking at must
not be the first thing to scroll away.
The policy is `narrow` and the **titles are empty**: `narrow` still draws a label
beside an icon (measured: a 48px "Measure" inside a 54px pill), so an empty title is
what makes a pill an icon. The pages' *names* are what identify them to
`select_side_tab()` and the session file, and the words are re-attached to the pills in
code — tooltip and accessible name — by `name_side_tabs()` on the first tick, so an
icon-only pill is still a named thing rather than an anonymous button.
Each page's icon is a glyph of its own, and each one is a *pair*: the Measure tab is
`udpscope-tab-measure-symbolic` (the cursors and the ruler they read against), and the
Signals tab is `udpscope-tab-signals-symbolic` (the trigger's edge and the channel list
below it). They are drawn for the tabs rather than reusing the icons the toolbar and the
Preferences window already carry: a glyph the toolbar uses for one thing means two things
the moment a tab shows it too. -->
<object class="GtkBox" id="side_column">
<property name="orientation">vertical</property>
<property name="width-request">300</property>
<child>
<object class="AdwViewSwitcher" id="side_switcher">
<property name="stack">side_stack</property>
<property name="policy">narrow</property>
<property name="margin-top">4</property>
<property name="margin-bottom">4</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
</object>
</child>
<child>
<object class="AdwViewStack" id="side_stack">
<property name="vexpand">1</property>
<child>
<object class="AdwViewStackPage">
<property name="name">measure</property>
<property name="title"></property>
<!-- The cursors and the ruler: what the pane brackets, and what it
reads the bracketed window with. -->
<property name="icon-name">udpscope-tab-measure-symbolic</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="measure_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object>
</child>
<child>
<object class="AdwViewStackPage">
<property name="name">signals</property>
<property name="title"></property>
<!-- The trigger and the channel list it acts on. -->
<property name="icon-name">udpscope-tab-signals-symbolic</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="signals_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="content">
<object class="GtkPaned" id="events_divider">
<property name="orientation">horizontal</property>
<!-- Shrinkable, or the pane's own minimum is reserved as an empty column even while it is
closed and the plot pays for a pane nobody opened. -->
<property name="shrink-start-child">1</property>
<property name="resize-start-child">0</property>
<child type="end">
<object class="GtkBox" id="left_column">
<property name="orientation">vertical</property>
<property name="hexpand">1</property>
<child>
<object class="GtkBox" id="plot_box">
<property name="orientation">vertical</property>
<property name="homogeneous">1</property>
<property name="spacing">4</property>
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<child>
<object class="GtkPaned" id="plot_split">
<property name="orientation">horizontal</property>
<property name="shrink-end-child">1</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="status_row">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="margin-start">6</property>
<child>
<object class="GtkLabel" id="status_badge"/>
</child>
<child>
<!-- The line is rewritten every frame and its counters grow, so it is
ellipsized: a label's minimum width is its text, and an unellipsized one
would push the divider around as the numbers change. -->
<object class="GtkLabel" id="status_label">
<property name="label">starting…</property>
<property name="ellipsize">end</property>
<property name="hexpand">1</property>
<property name="halign">fill</property>
</object>
</child>
<child>
<object class="GtkMenuButton" id="status_info">
<property name="icon-name">help-about-symbolic</property>
<property name="tooltip-text" translatable="yes">Stream and buffer detail</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</child>
</object>
</interface>