Files
udpscope/src/ui_app.hpp
T
Martino Ferrari 91ca29d181 scope windows: the same instruments as the main window
A scope or detached window drew its traces and nothing else: no cursors, no gate band, no trigger
marker, no measurement marks. They are not per-window state to be recreated -- the cursors are one
pair of times, the gate one band, the trigger one instant -- so the main window's row set stays the
**owner** and every window is written from it.

`sync_scope_instruments()` runs in the main tick after the main's own `sync_cursors()`/
`sync_gate()`: a gesture inside a scope window is read back from *that* window's controllers first
(those two calls only adopt while their own plot holds the grab, which is exactly "the drag
happened here"), adopted by the owner, and then every window -- the dragged one included -- is
written from the owner with the same cursor pair, mode and visibility, gate bounds and visibility,
and trigger marker. `RowSet::set_cursors()` is the new pair setter; `trigger_marker_x()`/`_visible()`
are the getters the mirror reads. The measurement marks travel by signal name
(`RowSet::MeasureMark::signal`, filled by the measures pass), so each window finds the row that
plots the signal and draws its ruler or mark there -- or none, when it does not plot it. The search
marks are times on the window and are placed on every window by the same per-frame path, and the
crosshair-axis-labels preference is restated to open windows when it changes.

Probe: `[scope-probe] instruments windows=N cursors=N gate=N trigger=N marks=N (main trigger
on=..)`, after adding a real gated `Top` measurement on `sine` -- the gate is restated visible each
frame by `update_measures()` and marks come only from `show_on_plot` measurements, so the leg uses
the app's own path. Docs: README, the cursors invariant, and the probe readings.
2026-11-06 12:21:38 +01:00

2535 lines
147 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The front end's *internal* interface.
//
// `ui.cpp` was one 13-thousand-line translation unit. The window is assembled from several, and this
// header is what they share: the application state (`App`) and the free functions the front end is
// built from. Nothing here is part of the app's public surface -- `ui.hpp` is -- and nothing here owns
// sample data: the model does, and `RowSet` owns the figure.
//
// The *definitions* carry the default arguments (they were written that way), so these declarations
// are generated without them: a default in both places is an error.
#pragma once
#include <gtk4/gtk_plot_area.hpp>
#include "acquisition.hpp"
#include "build_info.hpp"
#include "capture.hpp"
#include "config.hpp"
#include "event_store.hpp"
#include "export.hpp"
#include "help_window.hpp"
#include "layout_store.hpp"
#include "measure.hpp"
#include "model.hpp"
#include "plot_host.hpp"
#include "search.hpp"
#include "settings.hpp"
#include "source/source_config.hpp"
#include "ui_prefs.hpp"
#include "shortcuts.hpp"
#include "status_text.hpp"
#include "waveform.hpp"
#include <adwaita.h>
#include <gtk/gtk.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace udpscope {
namespace ui_impl {
enum class EventGroupKind {
None, ///< nothing: streaming, one event replayed, or a file opened
Compare, ///< two events in two plots side by side, at one scale, with synchronised axes
Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness
};
// --- the guided tour, shared by both windows (ui_tour.cpp) -------------------
//
// The tour is *two* tours now -- the main window's and the source chooser's -- because the chooser is
// the window a first run meets first and the surfaces it must explain are its own. The machinery is
// one implementation: a `TourView` is the overlay children (the four dim rectangles, the spotlight
// outline and the card) plus where the walk stands in it, and either window holds one. What differs
// is only the *stops*: a stop's target is a widget the owner resolves, so each window keeps its own
// table (the chooser runs before `App` exists and could not name one of `App`'s fields).
/// One stop of a guided tour: the widget to spotlight, the page it stands on, and the two paragraphs
/// of text beside it. The target is resolved when the tour starts, so the table may name widgets that
/// do not exist yet.
struct TourStop {
GtkWidget* target;
/// The page the target lives on, or nullptr when the stop needs none. What a page *is* depends on
/// the view: a named page of its `page_stack` (the chooser), or whatever its `on_page` hook opens
/// (the main window: a slide-over page id). The tour selects it as the stop is entered, so a target
/// on a page the user has left is made visible instead of being walked over or spotlighted where it
/// is not. A stop with no page *closes* what an `on_page` hook opened.
const char* page;
const char* title;
const char* body;
};
/// A tour's widgets and its place in the walk. `hole` is the spotlight's rectangle in the overlay's
/// own coordinates, kept from the last frame that could measure the target: a frame can come between
/// two layout passes, where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied
/// there would make the spotlight and the card jump.
struct TourView {
GtkWidget* overlay = nullptr;
GtkWidget* dim[4] = {};
GtkWidget* spot = nullptr;
GtkWidget* card = nullptr;
GtkWidget* title = nullptr;
GtkWidget* body = nullptr;
GtkWidget* counter = nullptr;
GtkWidget* back = nullptr;
GtkWidget* next = nullptr;
int hole[4] = {0, 0, 0, 0};
bool running = false;
int index = 0;
std::vector<TourStop> plan;
/// The stack whose pages the stops' `page` names select. A window has exactly one such surface (the
/// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk
/// begins, when the widgets exist; null leaves every stop on the page it finds.
GtkWidget* page_stack = nullptr;
/// The alternative to `page_stack` for a surface that is not a stack: when set it *replaces* the
/// stack mechanism for this view, and is called with each stop's `page` as the stop is entered --
/// `nullptr` meaning "no page needed" -- and with `nullptr` as the walk ends. The main window's opens
/// and closes the slide-over through it; the chooser keeps `page_stack`.
void (*on_page)(void* user, const char* page) = nullptr;
void* page_user = nullptr;
/// Optional, with `on_page`: whether the surface `on_page` opened has finished appearing. A stop that
/// names a page is not spotlit while this answers false -- a panel that is still sliding in is a
/// target that moves, and a spotlight that follows it for 120 ms reads as a glitch. Null means "ready".
bool (*page_settled)(void* user) = nullptr;
/// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour
/// on to the application through `Config::tour_pending`.
void (*on_done)(void* user) = nullptr;
void* done_user = nullptr;
};
/// One capability panel a source kind contributed to the slide-over. `content` is the kind's own widget
/// (made insensitive while the source is disconnected); the page wrapping it is named `name`.
struct SourcePanelPage {
std::string name; ///< slide-over page id: "source-" + the kind's panel id
std::string title; ///< the slide-over page's title, and the segmented switch's label
GtkWidget* content = nullptr;
};
/// The slide-over panel's widgets and state (src/ui_slideover.cpp). `page` is the open page's id, empty when closed.
struct SlideOver {
GtkWidget* overlay = nullptr; ///< the GtkOverlay around the plot (main-window.ui `plot_overlay`)
GtkWidget* revealer = nullptr; ///< the one overlay child; halign END, valign FILL, 360 px
GtkWidget* title = nullptr; ///< GtkLabel in the panel header
GtkWidget* stack = nullptr; ///< GtkStack: one scroller per page
GtkWidget* source_switch = nullptr; ///< AdwToggleGroup, shown on a source page when there are >= 2
std::vector<std::string> source_pages;
std::string page;
bool syncing = false; ///< true while we write the toggle group ourselves
};
constexpr int kSlideOverWidth = 360;
/// One badge: a button with a name and a state line. `page` is the slide-over page it opens.
struct Badge {
GtkWidget* button = nullptr;
GtkWidget* name = nullptr;
GtkWidget* state = nullptr;
std::string page;
std::string signal_name; ///< a signal badge: the signal it stands for; empty for the others
std::size_t signal = 0; ///< a signal badge: its index in `ScopeModel::signals()` when it was built
};
/// The most signal badges the bar shows; beyond that one "N signals" badge stands for them all.
inline constexpr std::size_t kMaxSignalBadges = 6;
/// The badge bar under the plot (src/ui_badges.cpp).
struct BadgeBar {
GtkWidget* bar = nullptr; ///< main-window.ui `badge_bar`, fixed height
GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
Badge trigger;
Badge source; ///< opens the first source page; hidden when the source has none
GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+"
/// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds
/// a pointer to its `Badge`, which must not move when the vector does.
std::vector<std::unique_ptr<Badge>> signal_badges;
Badge overflow; ///< the "N signals" badge, used only beyond `kMaxSignalBadges`
Badge plus; ///< the "+" in the signals slot: opens the `signals` page
};
struct App {
ScopeModel* model = nullptr;
Acquisition* acq = nullptr;
RowSet* rows = nullptr;
GtkWidget* window = nullptr;
GtkWidget* plot_box = nullptr;
/// The `GtkOverlay` around `plot_box` (`main-window.ui`), which the slide-over panel is a child of.
GtkWidget* plot_overlay = nullptr;
SlideOver slideover;
/// The badge bar under the plot (`main-window.ui` `badge_bar`, with its two slots).
BadgeBar badges;
GtkWidget* badge_bar = nullptr;
GtkWidget* badge_signals = nullptr;
/// Signal names in the order the user left the badges (memory only; names not in it sort last).
std::vector<std::string> badge_order;
GtkWidget* badge_system = nullptr;
/// The one GL area, drawing the `RowSet`'s figure: every row is a row of that
/// figure, so there is one widget, one context and one `::render` whatever the row
/// count is.
GtkWidget* area_widget = nullptr;
/// The pointer's last position over the plot area, in the area's coordinates (the overlay's,
/// for the toggles' corner), and whether one is over it at all: the full-screen toggles are
/// revealed by proximity, so the frame tick reads this. The overlay's own motion controller
/// keeps it current, which is why a pointer *on* a revealed toggle still counts as over the
/// area -- the toggle is an overlay child, and the event still travels the ancestor.
double area_pointer_x = 0.0;
double area_pointer_y = 0.0;
bool area_pointer_valid = false;
plot::gtk4::PlotArea* area = nullptr;
/// The GL area wrapped in a `GtkOverlay`, which is what the plot box actually holds.
/// The layout editor's buttons are overlay children: each is a small widget placed by
/// its margins over the row it acts on, so it intercepts pointer events only where it
/// is -- a single transparent child covering the area would swallow every pan, drop and
/// hover the editor is not interested in.
GtkWidget* area_overlay = nullptr;
/// One extra top-level scope window: its own figure and `RowSet`, bound to the same
/// `ScopeModel` by the main window's tick -- which is also its repaint pump -- so a scope
/// is a second *view* of one acquisition, never a second acquisition. The main window's
/// x window (or its pin) is mirrored onto it every frame, so the two look at the same
/// span; its own controllers can still pan and zoom, and the mirror reclaims the span on
/// the next frame.
struct ScopeWindow {
GtkWindow* window = nullptr;
plot::gtk4::PlotArea* area = nullptr;
GtkWidget* widget = nullptr;
/// The window's short name, the one the signal page's *Plot on* groups carry ("plot 2" for a
/// copy, "scope 3" for an empty one): a button must say which window it plots into.
std::string name;
/// The drop target wired onto `widget`: the handler reads the *window* off it (each window's
/// target names its own scope), and the probe calls the handler with it directly.
GtkDropTarget* drop = nullptr;
/// This window came from a **detach** -- the figure left the main window to live here -- so it
/// carries the attach control and can be put back. A copied or empty window is a plain second
/// view, and has no attach control.
bool detached = false;
/// False while attaching: the window is closing *because* its figure went back to the main
/// window, so there is nothing to remember (remembering it would offer to reopen a window
/// whose figure is on screen again).
bool remember_on_close = true;
/// The overlay wrapping the GL widget: the corner controls (attach, the layout toggle, the
/// layout editor's own buttons) all live on it. Its motion controller keeps the pointer state
/// they are revealed by.
GtkWidget* overlay = nullptr;
/// Layout editing in this window: its own editor and toggle. A scope is a view with its own
/// arrangement, so it holds as many figure rows as it is given and edits them with the same
/// `+` / `×` / Done controls the main window's editor has -- its own, never the main window's.
bool layout_editing = false;
GtkWidget* layout_toggle = nullptr;
std::vector<GtkWidget*> layout_buttons;
GtkWidget* layout_done = nullptr;
int layout_buttons_rows = -1;
std::vector<GtkWidget*> attach_buttons;
double pointer_x = 0.0;
double pointer_y = 0.0;
bool pointer_valid = false;
/// Frames this window's area was watched for its first paint: a window whose GL context failed
/// or that never paints says so in its own title (and on stderr) instead of staying a blank
/// rectangle nobody can diagnose.
int paint_checks = 0;
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
/// outlives the widget, and sharing the main window's placeholder would hand two areas
/// one plot the moment the figure mode is ever left.
plot::Plot placeholder;
std::unique_ptr<RowSet> rows;
};
std::vector<std::unique_ptr<ScopeWindow>> scopes;
/// The **closed** extra windows, as the session remembers them: a window's arrangement is kept
/// when it is closed (and across a restart), and the *Reopen closed windows* entry opens them --
/// a launch never does, because a window is a view the user closed on purpose.
std::vector<LayoutState::Window> closed_windows;
/// The menu entry that reopens them, so its label can carry the count.
GtkWidget* reopen_scopes_item = nullptr;
/// Every empty scope window is numbered from this, so two of them never share a name.
int scope_seq = 0;
/// On while the layout editor is up. Nothing else about the plot changes: the editor is
/// a set of overlay buttons, not a mode the figure knows about.
bool layout_editing = false;
GtkWidget* layout_toggle = nullptr;
/// The editor's buttons, in the order they were added, so they can be taken off the
/// overlay again. Rebuilt when the row count changes, repositioned when the rows move.
std::vector<GtkWidget*> layout_buttons;
/// The full-screen toggles: one per figure row, on the overlay **always** (not only while
/// the editor is up), placed in each row's top-right corner. Rebuilt when the figure row
/// count changes (`maximize_buttons_rows` is the count they were built for).
std::vector<GtkWidget*> maximize_buttons;
int maximize_buttons_rows = -1;
/// The **detach** buttons: one per figure row beside its full-screen toggle, same reveal,
/// same rebuild cadence -- but only while the layout holds two or more figures (detaching the
/// only figure would leave the main window with nothing to draw) and never while maximized.
std::vector<GtkWidget*> detach_buttons;
/// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it
/// began, so the beginning has to be kept -- reading the weights while dragging would compound.
int layout_drag_row = -1;
std::vector<float> layout_drag_start;
/// Column drag state: which plot (right-hand cell), the two starting *plot* widths in logical px
/// (each plot's `layout().axes.w`, the area the plotter places a cell by -- not the cell rect, which
/// adds the y and right gutters and the gap), and the cell weights snapshot at drag begin
/// (`layout_col_apply()` redistributes the pair's weight sum from these, so a drag stays relative to
/// where it started).
int layout_col_drag_plot = -1;
double layout_col_drag_left0 = 0.0;
double layout_col_drag_right0 = 0.0;
/// The drag handle's x in the overlay's coordinates (logical px) when the drag began. The handle is
/// re-placed on the divider every frame, and a gesture reports its offset in the handle's own, moving,
/// coordinates; `layout_col_total_offset()` adds the handle's displacement since this back.
double layout_col_drag_handle_x0 = 0.0;
std::vector<float> layout_col_drag_start;
/// The editor's way out: one `Done` button in the plot area's top-right corner while
/// the editor is up. Not part of `layout_buttons`, which is one button per row and per
/// boundary -- the count the layout probe asserts.
GtkWidget* layout_done_button = nullptr;
/// The row count and figure geometry the buttons were placed for. Their positions are
/// restated only when one of these moves: a `gtk_widget_set_margin_top()` per frame is a
/// relayout per frame, and overlay children are measured out of the main child's
/// allocation, so the churn would reach the plot.
int layout_buttons_rows = 0;
/// Which step of the layout probe runs next (`UDPSCOPE_PROBE_LAYOUT_MS`). The steps are
/// a frame apart, because the buttons are placed from the rects the last frame was laid
/// out with, so the sequence cannot be one function.
int dbg_layout_step = 0;
/// Which step of the split probe runs next (`UDPSCOPE_PROBE_SPLIT_MS`).
int dbg_split_step = 0;
/// The split probe's round trip: the arrangement saved, and the legacy copy of its file.
std::string dbg_split_expect;
std::string dbg_split_legacy_path;
/// Which step of the cursors probe runs next (`UDPSCOPE_PROBE_CURSORS_MS`).
int dbg_cursors_step = 0;
/// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`).
int dbg_fit_step = 0;
/// Which step of the x-label-form probe runs next (`UDPSCOPE_PROBE_TIMELABELS_MS`).
int dbg_timelabels_step = 0;
/// The cursors probe's own bookkeeping: the stored time before a gestureless move, so the
/// report can assert the move was ignored.
double dbg_gestureless_before = 0.0;
/// What `PlotArea::create()` is handed. The area is in figure mode from the start and
/// never draws this, but it keeps a reference to the plot it was created with (its
/// single-plot controller holds one) -- and every plot of the figure is destroyed by a
/// rebuild, so handing it a row would leave that reference dangling.
plot::Plot placeholder_plot;
GtkWidget* status_label = nullptr;
/// The activity badge: one glyph whose *colour* is the whole signal, so that blinking
/// it cannot change its width and drive a relayout (see the status label's comment).
GtkWidget* status_badge = nullptr;
/// What the badge was last set to (`BadgePhase`), so the markup is rewritten on a
/// change of phase rather than on every one of the 60 frames a second.
int badge_phase = -1;
/// The trigger status circle beside it (`main-window.ui` `trigger_status`): green while an
/// event is held or a capture is filling, flashing green while armed and waiting, red while
/// disarmed, hidden while the trigger is off. Same width-locked logic as the badge: one
/// "●" glyph in every state, markup rewritten on a change of the state key only, and the
/// flash runs off the same half-second square the badge blinks on.
GtkWidget* trigger_status = nullptr;
int trigger_circle_state = -1;
/// The circle's state as of the last tick, for the exit dump (which cannot read widgets).
int dbg_trigger_circle = -1;
/// The display-window control: a header-bar button whose label is the current window
/// and whose popover holds the number, its unit and the two decade steppers. The label
/// is width-locked (see `kWindowLabelChars`) because the header bar sits above the plot
/// and a label that grew with the text would relayout everything under it.
GtkWidget* window_button = nullptr;
/// The span the control shows, pinned to `kWindowLabelChars` and ellipsized: the bar sits above the
/// plot, so a label that grew with its text would relayout everything under it on a change of window.
GtkWidget* window_label = nullptr;
GtkWidget* window_entry = nullptr;
GtkWidget* window_unit_drop = nullptr;
/// The unit `sync_window_display()` last wrote into the dropdown. `GtkDropDown`'s
/// `notify::selected` arrives *after* the call that caused it returns, so the
/// `updating_panels` flag is already clear by then and cannot tell our own write from the
/// user's pick; the entry the sync wrote is rounded for reading, so re-parsing it made the
/// rounding the window (a 500 s request came back as 499.98 s). A selection that matches
/// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it.
int window_sync_unit = -1;
GtkWidget* trigger_toggle = nullptr;
/// The trigger panel's own box, kept so the defaults probe can read its spin buttons without
/// walking the window for them.
GtkWidget* trigger_panel = nullptr;
GtkWidget* signal_panel = nullptr;
GtkWidget* side_box = nullptr;
/// The dock: the sidebar's one scroller and the box inside it, which holds only the compact readouts
/// (`build_readouts_dock()`). The full cursors and measurements panels are the slide-over's `cursors`
/// and `measure` pages. A feature's own on/off never removes a panel (it sets its sensitivity instead).
GtkWidget* dock_page = nullptr;
GtkWidget* dock_scroller = nullptr;
GtkWidget* arm_button = nullptr;
/// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post
/// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has
/// (sensitivity, from trigger mode), so the two buttons beside each other cannot disagree about
/// whether the trigger has an engine at all.
GtkWidget* force_button = nullptr;
GtkWidget* trigger_signal_drop = nullptr;
GtkWidget* sidebar_toggle = nullptr;
GtkWidget* trigger_edge_drop = nullptr;
GtkWidget* trigger_type_drop = nullptr;
GtkWidget* trigger_cmp_drop = nullptr;
GtkWidget* trigger_width_spin = nullptr;
GtkWidget* trigger_width2_spin = nullptr;
GtkWidget* trigger_level2_spin = nullptr;
GtkWidget* trigger_timeout_spin = nullptr;
/// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can
/// hide them. A hidden row keeps its widget's value, so switching condition and back
/// does not lose what was typed.
GtkWidget* trigger_cmp_row = nullptr;
GtkWidget* trigger_width_row = nullptr;
GtkWidget* trigger_width2_row = nullptr;
GtkWidget* trigger_level2_row = nullptr;
GtkWidget* trigger_timeout_row = nullptr;
GtkWidget* trigger_mode_drop = nullptr;
GtkWidget* trigger_level_spin = nullptr;
GtkWidget* trigger_pre_spin = nullptr;
GtkWidget* trigger_holdoff_spin = nullptr;
GtkWidget* trigger_band_spin = nullptr; ///< EventCondition::noise_band
GtkWidget* trigger_hf_spin = nullptr; ///< EventCondition::hf_fc, 0 is off
/// The persistence spin: how many of the most recent triggered acquisitions stay on screen, drawn
/// dimmer with age (1 = the event alone, which is the display without the feature). Its row is held
/// for a `.ui` that lost it, as the other rows are.
GtkWidget* trigger_persist_spin = nullptr;
GtkWidget* trigger_persist_row = nullptr;
/// What this frame's persistence plan drew, against what the depth asked for: `sync_layers()`
/// returns the former and fills the latter, and the two differ exactly when a row had no free
/// slot left for a layer or the retention budget refused an acquisition. Read by the exit dump and
/// by the persistence probe, and what the "only some layers fit" notice is keyed on.
int persist_want = 0;
int persist_placed = 0;
/// The last plan a notice was raised for, so a plan that stays short says so once rather than
/// every frame.
int persist_noted_depth = 0;
int persist_noted_want = -1;
int persist_noted_placed = -1;
/// The persistence probe's own state: the depth its first leg asks for (kept between steps,
/// because the steps are separate `GSourceFunc`s), how many polls it has spent waiting for the
/// retention to fill, and whether that set has stopped growing (which is how a budget that keeps
/// fewer acquisitions than the depth asks for still gets a report).
int dbg_persist_depth = 4;
int dbg_persist_polls = 0;
std::size_t dbg_persist_last_kept = 0;
int dbg_persist_stable_polls = 0;
bool dbg_persist_settled = false;
/// The phosphor density's controls -- the trigger pane's *Advanced* block: the switch, the
/// dropdown that chooses what the image fades by, and the two parameter spins, only one of which
/// is on screen at a time (`sync_trigger_rows()` states that, as it does for the condition rows).
/// The rows and the expander are held too, so the probe can read what the `.ui` holds and the dump
/// can walk into the block -- an expander's child is *not* in the widget-parent chain.
GtkWidget* trigger_density_switch = nullptr;
GtkWidget* trigger_density_decay_drop = nullptr;
GtkWidget* trigger_density_tau_spin = nullptr;
GtkWidget* trigger_density_factor_spin = nullptr;
GtkWidget* trigger_density_row = nullptr;
GtkWidget* trigger_density_decay_row = nullptr;
GtkWidget* trigger_density_tau_row = nullptr;
GtkWidget* trigger_density_factor_row = nullptr;
GtkWidget* trigger_advanced = nullptr;
/// A new acquisition has reached the screen and has not been deposited into the density grid yet.
/// Set where a snapshot is *framed* -- the held-event branch of the tick -- and when the display
/// is switched on under an acquisition already on screen, and consumed by the next tick's
/// `apply_density()`: the deposit needs the plot laid out for the acquisition it deposits, and
/// that layout is the frame's, one frame after the bind.
bool density_pending = false;
/// The previous tick's clock (`g_get_monotonic_time()`), for the density's per-frame fade; 0
/// before the first frame, which is what makes the first frame's `dt` zero rather than since-boot.
gint64 density_last_us = 0;
/// Whether the density was on the last time the tick looked, so switching it on under an
/// acquisition already on screen deposits that one at once instead of waiting for a latch that
/// `single` mode may never send.
bool density_was_on = false;
/// What the last tick left on screen, for the dump: the rows the density was on, the total ink
/// and the peak over them, and the bytes the last *frame* uploaded. Captured on the tick because
/// the dump runs after the window is gone.
/// The **last tick the density was on**, captured there and not read back from the plots at
/// exit: the dump runs after the window is gone and after the exit probe has switched the trigger
/// off, which turns the display off with it -- a dump that asked the plots would say `rows=0`
/// about a run whose whole tail had the image up.
int dbg_density_rows = 0;
int dbg_density_traces = 0;
unsigned long long dbg_density_cells = 0;
unsigned long long dbg_density_grid_bytes = 0;
double dbg_density_ink = 0.0;
double dbg_density_peak = 0.0;
/// The most any one frame **uploaded** of the grid: one byte a cell, so this is the resolution
/// read back as the frame's own cost. A maximum rather than the last value, because the exit
/// probe changes the display before the dump runs and the last painted frame is then not a frame
/// of this display.
long long dbg_density_upload_bytes = 0;
/// How many ticks it was on for: 0 is "no image was ever drawn", which is what separates a real
/// reading from a default.
int dbg_density_frames = 0;
/// The frame-cost accumulators as of the tick the probe switched the display on, so the cost can
/// be read **before and after** it inside one run -- the same stream, the same layout, the same
/// mock. Two runs would differ in everything but the density.
double dbg_density_tick_sum0 = 0.0;
int dbg_density_tick_n0 = 0;
double dbg_density_draw_sum0 = 0.0;
int dbg_density_draw_n0 = 0;
/// The density probe's own state: the time constant its second half uses, and how many polls it
/// has spent waiting for an event to be on screen.
double dbg_density_tau = 1.0;
int dbg_density_polls = 0;
/// The density probe's step, so its sequence is a state machine rather than nested timers.
int dbg_density_stage = 0;
GtkWidget* signal_list = nullptr;
GtkWidget* pause_button = nullptr;
/// When the pause began, on the **monotonic wall clock**, or 0 while running. The elapsed time
/// the status bar shows has to be wall time rather than stream time: the producer never stops
/// while paused (it keeps filling the live ring, which is what makes the resume seamless), so
/// the stream's own clock advances whether the view is frozen or not and would say nothing
/// about how long the user has been looking at a still picture.
gint64 pause_started_us = 0;
/// The buffer search. Driven a few milliseconds at a time from the frame tick, because its
/// samples live behind the model's mutex and the producer needs it back between chunks.
BufferSearch search;
GtkWidget* search_run_button = nullptr;
GtkWidget* search_cancel_button = nullptr;
GtkWidget* search_status_label = nullptr;
GtkWidget* search_pos_label = nullptr;
/// "16 of 38 marks shown" -- the pool is smaller than a scan's result, and a display that is
/// showing a sixth of what it found has to say so.
GtkWidget* search_marks_label = nullptr;
GtkWidget* search_prev_button = nullptr;
GtkWidget* search_next_button = nullptr;
/// The expander the block lives in, so the probe can read its collapsed state when it reads the
/// header hint.
GtkWidget* search_exp = nullptr;
/// Set true when the last `search_jump()` produced an empty cut — the hit was so close to the
/// oldest retained sample that `publish_pause_view()` held nothing. Cleared at every jump.
bool search_jump_empty = false;
GtkWidget* menu_button = nullptr;
GtkWidget* cursors_toggle = nullptr;
/// --- the help affordance layer -------------------------------------------
///
/// The overflow menu's two views. The popover's child is a `GtkStack` holding both, and the
/// *visible child* is the view: swapping the stack rather than rebuilding the box is what keeps
/// `layout_toggle` — which points into the main view and is read by the tick — a live widget.
GtkWidget* menu_stack = nullptr;
GtkWidget* menu_main_view = nullptr;
GtkWidget* menu_help_view = nullptr;
/// The Help view's own rows, so the probe can read them and drive them without walking the popover:
/// the two existing dialogs' entry points and the two new actions.
GtkWidget* menu_shortcuts_row = nullptr;
GtkWidget* menu_docs_row = nullptr;
GtkWidget* menu_tour_row = nullptr;
/// The two navigation rows: the main view's door into the Help view, and the Back row out of it.
GtkWidget* menu_help_row = nullptr;
GtkWidget* menu_back_row = nullptr;
/// "Enable contextual help": a stateful menu row whose check *is* the setting.
GtkWidget* contextual_toggle = nullptr;
/// The "?" each surface carries, one per row of the table in `ui_context_help.cpp`. These are the
/// heading rows the buttons join — not the surfaces themselves — so a hint is a sibling of a
/// title rather than an overlay on top of a control. `math_head` only exists while the math
/// editor is open, which is why the row is skipped when it is null.
GtkWidget* trigger_head = nullptr;
GtkWidget* cursors_head = nullptr;
GtkWidget* measures_head = nullptr;
GtkWidget* signals_head = nullptr;
GtkWidget* events_head = nullptr;
GtkWidget* math_head = nullptr;
/// The status line's row, and the header bar's slot for the window control: the two surfaces that
/// are not a panel with a heading.
GtkWidget* status_row = nullptr;
GtkWidget* window_control_slot = nullptr;
/// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits
/// beside the thing it explains -- the "record events" switch, the pre-trigger share row, and the
/// signals pane's hint line (whose box exists so a hint can join it beside the text).
GtkWidget* trigger_record_row = nullptr;
GtkWidget* trigger_pre_row = nullptr;
/// The search block's expander *header* -- the box the contextual help's "trigger_search" hint joins
/// as its last child. The header, not the block: the expander's child is out of the widget-parent
/// chain until the block is expanded, so a hint there is invisible while it is collapsed.
GtkWidget* trigger_search = nullptr;
GtkWidget* signals_note = nullptr;
/// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the
/// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the
/// steps whose target was on screen when the tour started, so a surface that is not there is not
/// walked over. The machinery is shared with the source chooser's tour -- see `TourView`.
TourView tour;
/// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump.
/// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before".
bool tour_auto_started = false;
/// The chooser's own "Guided tour" request (`Config::tour_pending`), recorded when its timer runs.
/// A separate flag from the auto-start because it is a *different* thing: the user asked for the
/// tour before the window existed, rather than never having seen it.
bool tour_requested = false;
/// The self-check's reading of the manual's launch (`UDPSCOPE_PROBE_HELP_MS`). The launch is
/// asynchronous and the viewer is another process, so the outcome arrives in a callback and the
/// probe prints it from a timer afterwards; `attempted` is set at the call, because a launch that
/// never called back is itself a reading.
bool dbg_help_launch_attempted = false;
bool dbg_help_launch_done = false;
bool dbg_help_launch_ok = false;
std::string dbg_help_launch_error;
/// The help layer's own dump readings, captured on the tick while the window is alive (nothing
/// here may be read in the dump -- see `docs/claude/verification.md`).
std::string dbg_context_help;
std::string dbg_help_figures;
int dbg_tour_steps = -1;
int dbg_tour_step = -1;
int dbg_tour_visible = -1;
/// Which step of the help-affordance probe runs next (`UDPSCOPE_PROBE_HELPUI_MS`), and how many
/// event controllers the window carried before the tour opened -- the reading that says the tour
/// left nothing behind.
int dbg_helpui_step = 0;
int dbg_helpui_controllers = -1;
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: which step of that probe runs next.
int dbg_tour_surface_step = 0;
/// Where the panel controls ended up, decided once at build time: the dump runs after the window
/// is destroyed and cannot ask GTK anything about a widget.
std::string dbg_panel_controls;
/// The icon each toolbar button carries, and whether the theme resolved it. Recorded at the
/// same moment and for the same reason, but it guards a different thing: four of the icons are
/// drawn for this app and shipped inside the binary, and a resource path that never got
/// registered is *silent* -- GTK substitutes a missing-image placeholder, the button still
/// works, and every other line of the dump still passes.
std::string dbg_toolbar_icons;
std::string dbg_app_icons;
/// The panes' own switches and the header bar's buttons, read together by the exit probe: the two
/// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button.
int dbg_panel_switch_trigger = -1;
int dbg_toolbar_switch_trigger = -1;
int dbg_panel_switch_cursors = -1;
int dbg_toolbar_switch_cursors = -1;
int dbg_panel_drove_toolbar = -1;
/// Both panes' switches being *sensitive* while their panes are off -- read before anything turns
/// one on. It is the whole point of putting them in the heading: a control inside an insensitive
/// parent cannot be clicked, so a switch that went insensitive with its own pane could never turn
/// it back on.
int dbg_panel_switch_live = -1;
/// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one
/// thing in the body that *is* disabled with the mode.
int dbg_arm_sensitive = -1;
/// The header bar, so the dump can report the order the controls are in: a header bar is a
/// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr;
GtkWidget* cursors_reset = nullptr;
/// The cursors panel's root, kept because cursors-off disables it rather than hiding it.
GtkWidget* cursors_panel = nullptr;
/// The measurements page's root: like `cursors_panel`, insensitive until the stream has signals.
GtkWidget* measures_panel = nullptr;
/// The panes' *own* switches, in their header rows, and the panel bodies those switches disable.
/// The switch and the header bar's button are one control with two places to click: each runs the
/// same action, and `sync_panel_switches()` re-reads both from the toolbar's state after any
/// change, so they cannot disagree.
GtkWidget* trigger_switch = nullptr;
GtkWidget* cursors_switch = nullptr;
GtkWidget* trigger_body = nullptr;
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
/// `notify::active`, and without this the program's own write would come back as a click.
bool panel_switch_sync = false;
/// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the
/// file stores a *name* and the trigger needs an index into the acquired channels, which do not
/// exist until the stream has been described. Empty means "nothing pending".
std::string pending_trigger_channel;
/// The trigger pane's mode. *Locked* is the one a returning user gets: the pane shows a line that
/// says what it is set to and offers no control that changes the configuration. A first run -- no
/// session file for this source -- starts in configuration mode, because that is what a first run is
/// for. The flag lasts for the session: a lock is a mode, not a setting, so it is not persisted.
/// (The signals have no lock: the badges and the signal pages are direct manipulation.)
bool trigger_locked = false;
/// The locked trigger pane's content: a small definition list, because one line cannot hold the
/// signal, the condition, the level and the mode, and those are what a user reads the pane for.
GtkWidget* trigger_summary = nullptr;
static constexpr int kTriggerSummaryRows = 8;
GtkWidget* trigger_summary_keys[kTriggerSummaryRows] = {};
GtkWidget* trigger_summary_values[kTriggerSummaryRows] = {};
/// The compact line's arm control. It lives in the pane's *heading*, the part that stays on screen when
/// the body is hidden, and `sync_arm_buttons()` states it: present only while trigger mode is on and the
/// pane is locked, which is the one combination where the body's own Arm button has gone with the body.
GtkWidget* trigger_summary_arm = nullptr;
GtkWidget* trigger_edit = nullptr;
/// The dim label of the `signals` page that says nothing is left to add.
GtkWidget* signals_empty = nullptr;
int dbg_trigger_locked = -1;
int dbg_summary_arm_off = -1; ///< the compact line's arm control with the trigger *off*
int dbg_summary_arm_visible = -1; ///< the compact line's arm control, locked and with the trigger on
int dbg_summary_arm_sensitive = -1;
std::string dbg_summary_arm_label; ///< its text, which is the armed state in the user's words
int dbg_summary_arm_width = -1; ///< its allocation a frame later: 0 means "not laid out"
int dbg_trigger_body_visible = -1;
/// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it
/// is read rather than assumed: empty here is a locked pane showing nothing at all.
std::string dbg_trigger_summary;
/// The same line as the exit probe found it, captured before the probe touches the trigger. The locked
/// pane says one thing about a trigger that is running and another about one that is not, and only a run
/// whose session has it off shows the second -- so the dump carries both rather than assuming either.
std::string dbg_trigger_summary_early;
/// The two [edit] buttons' icons, read in each mode by clicking into it and back: a button whose icon
/// does not resolve is *blank*, and that is how the missing "done" icon was found.
std::string dbg_pane_edit_icon;
std::string dbg_pane_view_icon;
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
GtkWidget* cursors_mode_drop = nullptr;
GtkWidget* cursors_body = nullptr;
/// One line per row (rulers) and one per bound trace (`mix`), in the order the body
/// was built with. The texts are rewritten each frame; the widgets are rebuilt only
/// when the rows or the mode change.
/// One row of the pane's table: a `who` cell and the three value cells, so the numbers
/// line up as columns the way a table does.
struct CursorLine {
GtkWidget* label = nullptr; ///< the `who` cell (a chip's markup, or text)
GtkWidget* a = nullptr;
GtkWidget* b = nullptr;
GtkWidget* d = nullptr;
int row = -1;
int slot = -1;
};
std::vector<CursorLine> cursors_lines;
/// The dock's compact copy of the cursors table (`src/ui_dock.cpp`): one line per `cursors_lines`
/// entry, same order, written from the same strings by `update_cursors_pane()`.
struct DockCursorLine {
GtkWidget* label = nullptr;
GtkWidget* a = nullptr;
GtkWidget* b = nullptr;
GtkWidget* d = nullptr;
};
std::vector<DockCursorLine> dock_cursor_lines;
/// The Readouts section at the top of the dock, and the parts of it the rebuilds and the Edit buttons
/// reach.
GtkWidget* dock_readouts = nullptr;
GtkWidget* dock_cursor_grid = nullptr;
GtkWidget* dock_measures_box = nullptr;
GtkWidget* dock_cursors_edit = nullptr;
GtkWidget* dock_measures_edit = nullptr;
/// One value label per measurement, in `measures` order (the dock's copy of `measure_labels`).
std::vector<GtkWidget*> dock_measure_labels;
/// The readouts strip under the plot (`main-window.ui` `readouts_strip`, `src/ui_dock.cpp`): shown
/// whenever the dock is not beside the plot (`!show-sidebar || collapsed`) *and* there is something to
/// read (the cursors on, or a measurement; `sync_readouts_strip()`). Its value labels mirror the dock's one for one -- `strip_cursor_deltas` the
/// `d` cell of each `dock_cursor_lines` entry, `strip_measure_labels` each of `dock_measure_labels` --
/// and `strip_cursor_who` the cursor lines' `who` cells, all written from the dock setters' arguments.
GtkWidget* readouts_strip = nullptr;
GtkWidget* strip_box = nullptr;
GtkWidget* strip_cursor_box = nullptr; ///< the cursor chips; insensitive with the cursors off
GtkWidget* strip_measure_box = nullptr; ///< the measurement chips
GtkWidget* strip_cursor_group = nullptr; ///< the Cursors button and its chips; shown with the cursors on
GtkWidget* strip_measure_group = nullptr; ///< the Measures button and its chips; shown with measurements
GtkWidget* strip_sep = nullptr; ///< between the groups; shown when both are
std::vector<GtkWidget*> strip_cursor_who;
std::vector<GtkWidget*> strip_cursor_deltas;
std::vector<GtkWidget*> strip_measure_labels;
/// The measurements the user set up, and what they last computed. The list is the state;
/// the results are recomputed on a throttle (the numbers are window statistics, and the
/// window scrolls every frame -- 60 recomputations a second of every sample is not a
/// price the frame tick should pay).
std::vector<Measure> measures;
std::vector<MeasureResult> measure_results;
GtkWidget* measures_list = nullptr;
// --- math channels --------------------------------------------------------
/// The app's copy of the math channels: what the signal list's ticks mirror into and what the
/// session file is written from. Every change goes through `apply_math_channels()`, which hands it
/// to the model -- the model's own copy is what the producer evaluates.
std::vector<MathChannel> math;
/// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is
/// held for as long as the window is up and cleared on the *next* opening rather than with the window --
/// freeing it under a live window frees the window with it. That is the source form's pattern.
GtkBuilder* math_dialog_ui = nullptr;
GtkWidget* math_dialog = nullptr;
/// One value label per measure, in `measures` order; rebuilt with the list.
std::vector<GtkWidget*> measure_labels;
/// The text each label was last set to. The dump runs from the destroy handler, where the
/// labels are gone, so it prints these instead of reading freed widgets.
std::vector<std::string> measure_text;
/// The window the current results were computed over, and whether a capture was held then:
/// a recomputation is owed when either moves (see `update_measures`).
///
/// It is kept in **stream** seconds (through `ScopeModel::stream_at_x0()`), not in the display x
/// it was read as, because a live view is newest-relative: the axis there is a fixed
/// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates
/// and the pane would compute once and then never again -- the readout frozen on the samples
/// that happened to be on screen when the pane opened.
///
/// **Which** window that is depends on the view: the viewport while the view is live, and the
/// **pin** (`RowSet::pinned_range()`, captured in `measure_pin_window` below) while it is
/// pinned. A still frame is measured as a frame, so a gesture inside the pin moves neither this
/// nor any number.
plot::Range measure_window{};
bool measure_window_valid = false;
bool measure_was_held = false;
/// The **gate band** the last pass saw, in the same stream seconds, clipped to the window the
/// pane was about. The gate is per measurement, but the band is one: when any measurement is
/// gated, a band that moves -- a drag, a seed, a mode change -- changes what *those*
/// measurements describe without the viewport or the pin moving, so the "it moved" question
/// is asked of this as well. Booked even when nothing is gated (then it is the pane's own
/// window, and the comparison below repeats one the viewport has already answered).
plot::Range measure_gate_window{};
bool measure_gate_window_valid = false;
/// The window **on screen**, in the same stream seconds, as of the last *tick* -- which is the
/// only thing `measure_window` is comparable with. Captured here rather than read back in the
/// dump for the reason every other dump field is (see verification.md): the dump runs from the
/// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by
/// then answers for a mode no frame was ever drawn in -- which read as a 45000 % drift on a run
/// whose every frame was exact.
///
/// While the view is pinned this and `measure_window` are *allowed* to differ: the pane
/// describes the pin, and a gesture may have narrowed what is on screen to a slice of it.
plot::Range measure_view_window{};
/// The pin's own extent in the same stream seconds, and whether the view was pinned when the
/// tick read it. This is the window a pinned view's results describe, so the dump measures its
/// drift against it -- otherwise a zoomed held event would read as a pane describing a window
/// the plot has left, which is precisely what it must do there.
plot::Range measure_pin_window{};
bool measure_pin_valid = false;
/// What the two walks over one channel's visible window produced, shared by every measurement
/// on that signal: the window's statistics, and one accumulator per *distinct level* any of
/// them asked for. Several measurements on one signal must cost two walks, not two each -- and
/// the window itself is never stored, which is what the chunked walks bought (see
/// `MeasureChunk`).
/// One channel's window, at one HF corner, with every level accumulator the pane asked for.
///
/// Keyed by `(channel, fc, gated)` and not by channel alone: the corner and the gate are
/// per-measurement settings, so two measurements on one signal at different corners -- or one
/// gated and one not -- are reading two different windows and cannot share a walk. A gated
/// window is walked over the gate band clipped to the pane's window; an ungated one over the
/// pane's window whole.
struct MeasureWindow {
int channel = -1;
double fc = 0.0; ///< the HF corner this window was walked at; 0 is unfiltered
bool gated = false; ///< this window is the gate band, not the pane's window whole
MeasureStats stats;
std::vector<double> pct; ///< the level shares asked for, deduplicated
std::vector<double> noise; ///< each entry's band, as a percentage; parallel to `pct`
std::vector<MeasureLevel> level; ///< one accumulator per entry of `pct`
MeasureFilter scan_filter; ///< pass one's state
MeasureFilter acc_filter; ///< pass two's; a separate walk, so a separate state
/// Some measurement on this window reads a flat level, so pass one folds the bucket
/// histogram the flat-level types are answered from. Only then: a pane of means and extremes
/// should not pay for a density estimate it never reads.
bool flat_wanted = false;
bool ok = false;
};
std::vector<MeasureWindow> measure_samples;
/// The window's LOD buckets, reused across recomputes: a 1.7 M-sample window is 26 k of them,
/// and both passes walk the same list.
std::vector<MeasureBucket> measure_bucket_scratch;
/// The ink probe's gather buffer, reused for the same reason: it is refilled on every
/// painted frame and its size is O(axis pixels).
std::vector<plot::f32> measure_ink_scratch;
/// When the pane was last recomputed, for the refresh cadence above.
gint64 measure_stamp_us = 0;
bool measure_stamp_valid = false;
/// What the pane cost: the recomputes it ran and the wall time they took. A readout that is
/// recomputed per frame over a live window is exactly the kind of thing that is invisible in a
/// frame mean, so it is measured where it happens and reported in the dump.
gint64 measure_cost_us = 0;
int measure_passes = 0;
/// How much of the window the pane actually *read*: the samples covered, against the LOD
/// buckets the two walks answered from and the samples they still had to open. That ratio is
/// the whole question for this pane -- a bucket is 64 samples not read, and the raw tally is
/// the window's partial edges plus the buckets that straddle a level, which on a clean
/// waveform is one per crossing. A `raw` near `covered` means the pyramid bought nothing
/// (a noisy signal, or a window shorter than a bucket) and the cost is back to O(window).
gint64 measure_fetch_us = 0;
std::uint64_t measure_covered = 0;
std::uint64_t measure_bucketed = 0;
std::uint64_t measure_raw = 0;
/// The last pass' own duration, which the cadence adapts to.
gint64 measure_last_pass_us = 0;
/// The last *valid* result per measurement, which the marks fall back to: a level is a property
/// of the signal and survives the window it was measured in, so the rulers do not blink off when
/// a pass lands on a window with no samples (a rebuild, a capture boundary, a mode change).
std::vector<MeasureResult> measure_last_valid;
/// Running statistics, one per entry of `measures` and reset whenever that vector is
/// rebuilt. Not reset by a pause, a latch or a pan: those change what is measured *now*,
/// not what the measurement means, and a statistic the user cannot keep across them is
/// not worth having.
std::vector<MeasureStatsAccum> measure_stats;
/// The second line of each measurement's row, one per entry of `measures`. Hidden rather
/// than removed when the toggle is off, so turning it on costs no rebuild -- and a rebuild
/// is what would reset the statistics the user just asked to see.
std::vector<GtkWidget*> measure_stats_labels;
bool measure_stats_on = false;
GtkWidget* measures_stats_check = nullptr;
GtkWidget* measures_stats_reset = nullptr;
/// The marks the last computation produced: re-placed every frame, which is what makes the
/// measurement cursors read-only.
///
/// A **time** mark's `value` is held in stream seconds, like `measure_window`, and converted to
/// the axis' own x by `place_measure_marks()` on the frame it is placed. That is what keeps a
/// mark glued to the sample it measured while a live view rolls the data leftwards under it --
/// a mark is a *result*, so it travels with its datum, which is the opposite of a cursor (an
/// instrument the user put at a position and that stays there). A level mark's `value` is a y
/// and has nothing to do with either clock.
std::vector<RowSet::MeasureMark> measure_marks;
/// `measure_marks` with the time marks converted to display x, rebuilt in place each frame
/// rather than allocated: this is on the frame tick's path.
std::vector<RowSet::MeasureMark> measure_marks_display;
/// What the dump prints for the header and the panels. Captured while the window is alive:
/// the dump runs from the destroy handler, where every widget call is a GTK-CRITICAL and
/// reads nothing.
int dbg_win_btn_w = -1;
int dbg_bar_w = -1;
/// The measurements pane's allocation, for the same reason and more urgently: the dump used
/// to ask the widget itself, which is where the two documented `Gtk-CRITICAL`s came from --
/// and a critical is the lucky outcome. The pointer is freed memory by then, so a run that
/// built and destroyed other windows first (`UDPSCOPE_PROBE_HELP_MS`, three of them) has had
/// that memory reused and the read is a **SIGSEGV** in the middle of the dump.
int dbg_measures_pane_w = -1;
int dbg_measures_pane_h = -1;
/// The measurement dialog while one is open (null otherwise). A probe needs a handle on it:
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
GtkWidget* measure_dialog = nullptr;
int dbg_measure_step = 0;
/// The band-average probe's own step counter. Its own rather than the measurement pane's: the two
/// probes add measurements, so they must not be scheduled in one run (see verification.md), and a
/// shared counter would make that a silent mis-step rather than a documented prohibition.
int dbg_bandavg_step = 0;
/// The gate probe's (`UDPSCOPE_PROBE_GATE_MS`) step counter and bookkeeping: the bounds it
/// set and the `s0` it read when it set them, so the second leg can say whether the band
/// stayed still while the stream's clock moved under it.
int dbg_gate_step = 0;
double dbg_gate_x0 = 0.0;
double dbg_gate_x1 = 0.0;
double dbg_gate_s0 = 0.0;
/// The gate probe's settle polls (`dbg_gate_step == 1` re-enters while the ring is younger
/// than the window, which is what its whole-periods arithmetic needs), bounded so a stalled
/// stream still measures and prints.
int dbg_gate_wait = 0;
/// The row a gate-region drag was on last frame, or -1. The fall to -1 is the drag's end,
/// and the tick saves the session there: the bounds are session state, and a drag that is
/// not saved is a session that lies about where the gate is.
int gate_drag_last = -1;
/// Which step of the crosshair probe runs next (`UDPSCOPE_PROBE_CROSSHAIR_MS`).
int dbg_crosshair_step = 0;
/// Which step of the maximize probe runs next (`UDPSCOPE_PROBE_MAXIMIZE_MS`).
int dbg_maximize_step = 0;
std::size_t dbg_maximize_single = 0;
int dbg_maximize_hidden = -1;
std::size_t dbg_detach_single = 0;
/// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`).
int dbg_scope_step = 0;
int cursors_pane_mode = -1;
int cursors_pane_rows = -1;
unsigned cursors_pane_generation = ~0u;
/// Which slots were *bound* when the pane was last built, one bit per row/slot
/// (`kMaxRows * kSlotsPerRow` is 32). `mix` draws a line per bound trace, and a slot's
/// channel is re-resolved by `bind()` every frame -- so the pane has to follow the bind
/// and not just the model's generation, or a trace that appears without a structural
/// change gets no line until something else rebuilds the table.
std::uint32_t cursors_pane_bound = ~0u;
/// The persisted-layout file this data source maps to (--server/--port), a
/// debounced save handle, and whether the file has been applied once the
/// CONFIG arrived.
std::string source_host;
std::uint16_t source_port = 0;
/// The configuration of that source this run is on, and its note. A source may have several
/// named sessions; this is the one its saves go to and the one the dialog marks as current.
std::string config_name;
std::string config_comment;
/// The cache of captured events, and whether latches are being written to it. The store is created
/// once the source is known (it is keyed on `<host>-<port>`, like the packet capture);
/// `event_recording` is the trigger panel's switch and starts off; the cache is only useful if
/// it was already on when the event you wanted happened, so switch it on before the events you want.
std::unique_ptr<EventStore> event_store;
/// Whether the cache holds anything, refreshed once a second: what keeps the dock's Events block and the
/// `events` page live for a source whose events are already written, even with recording switched off.
bool events_cached = false;
bool event_recording = false;
/// The `events` page of the slide-over: its content box, its list, the recording switch (in the
/// trigger panel, where the trigger's options live), and `events_thumb` -- the picture of the newest
/// event. Every row carries the picture of *its* event; this is the first one's, which is the one
/// the probe measures and which the page does not put anywhere special.
GtkWidget* events_box = nullptr;
GtkWidget* events_list = nullptr;
GtkWidget* events_thumb = nullptr;
GtkWidget* events_record_switch = nullptr;
/// The Events block of the dock: the box, its list (compact rows, one per `EventRecord`, the record
/// attached to each row as `udpscope-event`) and the Manage button.
GtkWidget* dock_events_box = nullptr;
GtkWidget* dock_events_list = nullptr;
GtkWidget* dock_events_manage = nullptr;
/// The comparison's second plot: its own placeholder, area and `RowSet`, and the paned that holds
/// the two side by side. `rows_b` exists only once the second area is built, and the area is hidden
/// (so the paned gives it no room and no handle) unless a comparison is up.
plot::Plot placeholder_plot_b;
plot::gtk4::PlotArea* area_b = nullptr;
GtkWidget* area_b_widget = nullptr;
/// The figure the second area was last pointed at. `set_figure()` rebuilds its per-row input
/// controllers, so it is only called when the `RowSet` really has a new figure.
plot::Figure* area_b_figure = nullptr;
std::unique_ptr<RowSet> rows_b;
GtkWidget* plot_split = nullptr;
/// The builders of the panels inside the window. A widget belongs to the builder that built it, and a
/// panel lives as long as the window, so these do too -- unlike the dialogs', which are replaced.
std::vector<GtkBuilder*> ui_builders;
/// The main window's builder. Held because the window belongs to its object list: it is replaced on
/// the next build rather than reused, and everything found by id comes out of it.
/// The split view the sidebar lives in. Its width, its collapse and its overlay are that widget's
/// business -- `AdwOverlaySplitView` -- and not a paned position's.
GtkWidget* side_view = nullptr;
/// Whether the user wants the sidebar: their last word on the toolbar's button. Kept apart from the
/// button's own state because that state now *follows* the panel, and the panel is forced off while
/// there is nothing to show -- without this, connecting a source would leave the sidebar closed.
bool sidebar_wanted = true;
/// Set while we are writing the split view's or the button's state ourselves. A *binding* cannot
/// express this: the widget moves `show-sidebar` on its own when it collapses, the binding pushes that
/// into the button, and the button's handler then records it as the user's wish -- so the sidebar never
/// comes back when a source connects. An explicit flag is reliable precisely because our writes go
/// through `update_side_visibility`, which sets the flag around them.
bool sidebar_syncing = false;
GtkBuilder* window_ui = nullptr;
bool comparing = false;
/// The last x/y range seen on each side of a comparison, one entry per row: a frame compares them
/// with the plots' own to tell *which* side the user moved, and copies from that side to the other.
/// Without it the mirror would be unconditional and the first copy would undo the gesture.
std::vector<std::array<double, 4>> sync_last_a;
std::vector<std::array<double, 4>> sync_last_b;
/// The two group actions under the pane's header, and what they last put on screen.
GtkWidget* events_compare = nullptr;
GtkWidget* events_accumulate = nullptr;
EventGroupKind group_kind = EventGroupKind::None;
std::vector<EventRecord> group;
/// True while a stored event is on screen instead of the live stream.
bool replaying = false;
/// What the live view was set to when a file view was opened over it, so that *Go live* can put it
/// back. A file view overwrites all of it and for good reasons -- the window is pinned to the
/// event's own span because that is what framing an event means, and the trigger goes off because a
/// file has no edge to wait for -- but none of those reasons survive the return to the stream, and
/// what the user set is theirs. Taken on the way *in*, and only when the view being left is the live
/// one, so a replay opened from inside a group does not snapshot the group's settings as "live".
///
/// The trigger's channel is kept by **name**: the index is a position in a channel list that a file
/// view rebuilds (a group renames its channels outright), so putting the index back would select a
/// different signal, or none.
struct LiveState {
bool valid = false;
double window_s = 0.0;
bool trigger_on = false;
TriggerConfig trigger;
std::string trigger_channel;
bool level_touched = false;
};
LiveState live_state;
/// Set by *Go live*, cleared by the tick that performs the restore, with the model generation it
/// has to see move first -- the stream's own channel list is what the stored trigger channel is
/// resolved against.
bool live_restore_pending = false;
unsigned live_restore_generation = 0;
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
bool record_pending = false;
/// The waveform file this run opened (`--open`, or the chooser's Waveforms page), or empty for a
/// live source. Nothing arrives in this mode, so everything that would report on a stream --
/// the popover's Stream row, the starvation notice -- reports on the file instead.
std::string waveform_name;
/// The preferences, as read at start-up and written by the dialog. Held here rather than read where
/// they are used: the dialog edits a copy, saves it and hands it back, so the file and the running
/// program cannot disagree about what a preference is.
Settings settings;
/// The preferences window while it is up, and its controls: one at a time, like the configurations
/// dialog -- a second window editing the same settings would let the two disagree.
/// The preferences window. A `PrefsForm` rather than fields here because the source chooser opens
/// the same dialog and runs before `App` exists -- see `ui_prefs.hpp`.
PrefsForm prefs;
/// The configurations dialog, while it is up: one at a time, and this is what makes a second
/// `Configurations…` a present rather than a second window.
GtkWidget* config_dialog = nullptr;
/// How that source was reached. Unused at runtime -- `Acquisition` has its own copy -- but
/// written into the session file, because `<host>_<port>.toml` can say neither, and the startup
/// chooser needs both to reconnect to a multicast source from a stored row.
std::string source_multicast;
std::string source_interface;
/// The source kind and its settings, so the session can say which kind it belongs to, and the
/// words that name the source in dialogs (`host:port` for UDPS, `<kind> <identity>` otherwise).
std::string source_kind = "udps";
SourceConfig source_opts;
std::string source_title;
/// The panels the source kind added to the slide-over, in page order. Empty for UDPS and in file mode.
std::vector<SourcePanelPage> source_panels;
bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled
guint layout_save_source = 0;
bool layout_loaded = false;
unsigned signal_generation = ~0u;
/// Signal name -> the content box of its `signal:<name>` slide-over page (src/ui_signal_page.cpp).
std::map<std::string, GtkWidget*> signal_pages;
bool updating_panels = false; ///< guards signal handlers during a rebuild
bool relayout_pending = false;
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
/// exist until the area is realized, so the one-shot has to be a tick's job.
bool style_stated = false;
/// Set once the user edits the trigger level, so the panel stops defaulting it
/// into the middle of the declared range.
bool trigger_level_touched = false;
/// A short-lived message shown in the status line (a drop's outcome), so an
/// action that changes nothing still says why.
std::string notice;
gint64 notice_until_us = 0;
/// Trigger-mode state for the status line: how many latches were on screen last
/// frame, and whether an event is held (which is what "frozen" means).
std::uint64_t seen_latches = 0;
/// Which snapshot was framed last. Separate from `seen_latches` because a Disarm during
/// a capture publishes a snapshot without an event having fired.
std::uint64_t seen_event_seq = 0;
bool event_held = false;
/// Whether a capture is open on screen: the edge has been accepted and the post window
/// is filling in view, on the coming event's own clock. It is what keeps
/// `clamp_to_data()` off the pinned axis and tells the status line apart from "armed".
bool capturing = false;
/// The edge of the capture `capturing` refers to, so a second edge accepted before the
/// first capture closed re-frames instead of being mistaken for the same one.
double capture_edge = 0.0;
/// Monotonic time of the last event, and the edge that started the clock. The clock
/// starts when the *edge* is accepted, not when the capture closes -- that is the
/// instant the user saw -- and the edge is kept so the latch that ends a live-updated
/// capture does not restart it.
/// Wall time, not stream time: "how long since the last event" is a question about the
/// user's wait, and a stream whose clock runs at any other speed would answer it wrong.
/// 0 until the first event of the session.
gint64 last_latch_us = 0;
double timed_edge = 0.0;
/// The render gate's memory (see the end of `tick()`). A `Held` or `Paused` view is bound to
/// an immutable snapshot on a pinned axis, so a frame whose picture cannot have changed does
/// not need to be drawn again -- and on llvmpipe, or on a battery, re-drawing a frozen event
/// sixty times a second is the whole cost of the application. The first four are the coarse
/// signature of what a frozen picture can be re-framed by; a change to any of them opens
/// `render_open` for `kSettleRenders` frames, so the figure's layout floor has the passes it
/// needs to converge. `render_skipped` is the floor that makes everything the signature does
/// *not* name render soon anyway (within `kStaticRenderEvery` ticks), which is what keeps the
/// gate from being able to stall the picture.
std::uint64_t render_event_seq = 0;
double render_x_min = 0.0;
double render_x_max = 0.0;
int render_rows = -1;
int render_open = 0;
int render_skipped = 0;
/// Self-check: ticks that queued no render, so the gate is a number rather than a claim.
int dbg_render_skips = 0;
double window_limit_s = -1.0; ///< the cap the window control was last clamped against
/// The rate each channel actually *stored* over the last second, and the declared rate
/// beside it. The two differ for two reasons now and both are worth seeing: the declared
/// rate is a claim the stream may not keep, and a window too long to hold at the wire
/// rate is resampled on ingest (`Partition::time_stride`), so this is also the readout
/// of how much resolution the current window costs. Re-measured once a second (the same
/// tick as the channel dump), never per frame.
std::vector<double> stored_rate;
std::vector<double> declared_rate;
/// Self-check: how much the axes' left edge moves between frames.
int dbg_axes_x_min = 1 << 20;
int dbg_axes_x_max = -(1 << 20);
int dbg_axes_frames = 0;
/// Frames on which some row's x window did not match row 0's. The rows share the
/// link group's one window, so this is an assertion: it must read 0.
int dbg_x_desync = 0;
gint64 last_channels_meas = 0;
/// Starvation detector: the delivered-frame count and the client's reconnect count a
/// second ago. A source that serves one client at a time shows up as a reconnect plus
/// no frames for the second between two samples (see `note_starvation`).
std::uint64_t starve_frames = 0;
bool starve_have_prev = false;
/// Consecutive one-second samples with `frames` unchanged while connected and running.
int starve_still = 0;
/// Set while the starvation notice is up, so the stderr line prints on the edges.
bool starve_reported = false;
/// Frames drawn with every bound trace slot empty although its channel has
/// samples: the view was swapped and not yet published.
int dbg_blank_frames = 0;
/// The *renderer's* blank: a frame that staged no trace geometry while every bound trace held
/// samples. `dbg_blank_frames` cannot see this one -- it asks the rings, and a ring that holds
/// data while the renderer draws nothing is exactly the "the data disappears for a frame" glitch.
int dbg_render_blank_frames = 0;
/// The last frame's staged segment count, so the tick can judge the frame the renderer drew.
unsigned long long dbg_seg_last = 0;
/// Frames the one area staged: a figure is one `::render` for every row.
int dbg_frames_painted = 0;
/// Where a frame's time goes, split at the two boundaries that can be observed from
/// here: `tick` is this function (bind, acquire, clamp, status), `draw` is everything
/// the plot renderer did (measured from the frame clock's `before-paint` to the return
/// of `PlotArea`'s own `::render` handler, which is why the adapter returns FALSE from
/// it), and `paint` is the whole GTK paint including the toolkit compositing the GL
/// area's framebuffer. A complaint about lag names one of the three; a summary that
/// does not separate them cannot say which.
int dbg_tick_n = 0;
double dbg_tick_ms_sum = 0.0;
double dbg_tick_ms_max = 0.0;
int dbg_paint_n = 0;
double dbg_paint_ms_sum = 0.0;
double dbg_paint_ms_max = 0.0;
double dbg_draw_ms_sum = 0.0;
double dbg_draw_ms_max = 0.0;
/// Which paint the worst `draw` was. A mean far below the max says the cost is
/// concentrated in a few frames but not *which*, and the two answers need opposite
/// responses: an early frame is the warm-up (the glyph atlas bake and the first
/// vertex-buffer growth land on it), a late one is a frame whose own geometry was
/// expensive and is the thing to chase.
int dbg_draw_max_frame = -1;
gint64 dbg_paint_start_us = 0;
GdkFrameClock* dbg_clock = nullptr;
/// The renderer's own account of what each frame cost it: whether the traces went
/// through the LOD pyramid or were drawn raw, and how much geometry that staged. The
/// *path* is the first question to ask of a slow frame -- `raw` frames are O(visible
/// samples) by construction.
std::uint64_t dbg_seg_sum = 0;
std::uint32_t dbg_seg_max = 0;
std::uint64_t dbg_visible_max = 0;
int dbg_raw_frames = 0;
int dbg_lod_frames = 0;
/// Frames on which at least one trace's geometry was reduced onto the pixel grid (one
/// vertical segment per column). It is orthogonal to `raw`/`lod` on purpose -- the
/// reduction is gated on the *density* of the gathered points, not on which level
/// produced them -- and it is the only reading that distinguishes "the segment count
/// collapsed because the reduction ran" from "the window got narrower".
int dbg_column_frames = 0;
/// Frames on which at least one reduced column was drawn **graded by dwell** rather than
/// as one flat band (`Style::intensity_grading`, which this app turns on). A subset of
/// `columns`: grading is a way of drawing the reduction, not a third path, so the two
/// counts being equal is what says the look is reaching every reduced frame.
int dbg_graded_frames = 0;
/// Painted frames on which `draw_traces` drew **no trace at all**, and the last one's index.
/// This is the observable for "the plot flickers as if there were no data": `staged_segments`
/// cannot see it, because the grid and the instruments stage geometry of their own, so a frame
/// that drew every chrome line and not one sample still reports tens of thousands of segments.
/// A count that is only the frames before the first bind is startup; one that keeps growing
/// afterwards is the glitch.
int dbg_notrace_frames = 0;
int dbg_notrace_last = -1;
/// Times the file view has *stated* its pin rather than only refreshing its numbers. A file's
/// extent does not change while it is open, so this must read **1** for a whole run: stating the
/// pin writes the group's range, which discards whatever gesture the user had left on the axis,
/// so a count that grows with the frames is both the flicker and the reason no interaction took.
int dbg_file_repins = 0;
/// The widest *minimum* width the side column or either pane has ever demanded. The side
/// column is a fixed 320 px by design, so a figure far above that is a label whose text drove
/// its own minimum -- the "layout must not depend on content" rule broken, and the only known
/// mechanism by which the plot's allocation can exceed the window.
int dbg_min_w_worst = 0;
int dbg_min_w_reports = 0;
/// Buffer rebuilds the tick has seen. Each one drops the history behind every trace.
unsigned dbg_generation_prev = 0;
bool dbg_generation_seen = false;
int dbg_generation_changes = 0;
/// Set by the tick once a bind has put samples on a trace, so the counter above can tell the
/// frames before any data from the ones after.
bool dbg_ever_bound = false;
int dbg_notrace_after_bound = 0;
/// The frame's one `DisplayMode`, recorded by the tick so the paint probe can name the
/// state a frame was drawn in without taking a second read of it (the producer can latch
/// between any two, which is why the mode is decided once per frame).
DisplayMode dbg_display = DisplayMode::Live;
/// The **ink** account, recomputed per painted frame the renderer's own way (see
/// `check_frame_ink`). `staged_segments` and `lod_traces` cannot see a waveform that
/// vanished for a frame -- the grid and the instruments stage geometry of their own, and a
/// trace whose gathered points all collapse onto one x is still "drawn" -- so these count
/// the polyline the filter actually leaves and how much of the axis it spans.
int dbg_ink_checked = 0;
int dbg_ink_collapse = 0; ///< fewer than two distinct positions left: nothing to draw
int dbg_ink_staircase = 0; ///< the filter dropped over three quarters of the points
int dbg_ink_short = 0; ///< the ink reached under a quarter of the axis' width
double dbg_ink_worst_cover = 1.0;
/// The worst distance, in pixels, between where a sample is *drawn* and the x a readout
/// quotes for the same sample. `gather()` stages x as an f32 relative to an origin the
/// renderer names, while `Plot::crosshair_point()` reads `x_at()` in exact f64, so this is
/// the quantisation of that f32 expressed in the unit the user sees it in. Measured on the
/// raw levels only, where a gathered point *is* a sample; above them a point is a bucket's
/// envelope and has no single sample to be off from.
double dbg_ink_worst_offset_px = 0.0;
int dbg_ink_raw_checked = 0; ///< frames the figure above was measured over
int dbg_ink_reports = 0;
/// Frames the badge spent in each `BadgePhase`. Both `lit` and `dim` have to be non-zero
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
/// stuck lit looks identical to a working one in a screenshot.
int dbg_badge_frames[4] = {0, 0, 0, 0};
int dbg_reset_sensitive = -1;
std::vector<std::uint64_t> last_stored;
std::uint64_t dbg_latches_at_disarm = 0;
std::uint64_t dbg_seq_at_disarm = 0;
bool dbg_capturing_at_disarm = false;
int dbg_disarm_polls = 0;
int dbg_disarm_in_capture = 0;
/// The force probe's state: the button's stated state on both sides of the trigger toggle (a
/// width read a frame after the state it belongs to), and the two legs of its press. See
/// `force_probe()` in the probes' translation unit.
int dbg_force_off_sensitive = -1;
int dbg_force_off_visible = -1;
int dbg_force_off_width = -1; ///< its allocation with the trigger *off*
int dbg_force_on_sensitive = -1;
int dbg_force_on_width = -1; ///< ... and with the trigger on: the size the user can press
int dbg_force_arm_width = -1; ///< the Arm button beside it, so the pair's widths can be compared
int dbg_force_label_found = -1; ///< `find_labelled_button("Force")` *is* the panel's button
int dbg_force_leg = 0; ///< 1 while leg A is in flight, 2 for leg B
int dbg_force_polls = 0;
int dbg_force_capture_seen = 0; ///< the axis was read while the forced capture was filling
double dbg_force_capture_lo = 0.0; ///< that axis: the *requested* window, before the latch
double dbg_force_capture_hi = 0.0;
double dbg_force_want_pre = 0.0; ///< the request the engine will cut by, at the moment of the press
double dbg_force_want_post = 0.0;
std::uint64_t dbg_force_latches_before = 0;
int dbg_swaps = 0;
int dbg_axes_w_min = 1 << 20;
int dbg_axes_w_max = -(1 << 20);
int dbg_axes_w_prev = -1;
int dbg_axes_w_changes = 0;
/// What the pan probe asked for, so the frame after it can be compared against it: a capture
/// during the probe is not a value either side can recompute, and a capturing lambda cannot be
/// a `GSourceFunc`.
double dbg_pan_asked_lo = 0.0;
double dbg_pan_asked_hi = 0.0;
bool dbg_pan_asked = false;
/// Which gesture the probe is on: 1 = the pan was asked and its verdict is due, 2 = the
/// zoom-out was asked and its verdict is due.
int dbg_gesture_stage = 0;
/// A verdict the *tick* owes, one frame after a gesture was asked: 1 pan, 2 zoom-out, 3
/// zoom-in. Reported from the frame loop rather than a `g_timeout`, because a 300 ms timer
/// slips past the state it was meant to observe as soon as the frames are heavy -- which is
/// exactly the load this probe runs under.
int dbg_gesture_verdict = 0;
/// The event-zoom probe's remaining steps, counted down by the *tick* for the same reason
/// the gesture verdicts are: each step's reading has to be taken after the frame's pin and
/// clamp have run, or it reports the ask rather than the view.
int dbg_zoom_steps = 0;
int dbg_zoom_step_no = 0;
int dbg_axes_w_change_frame = -1;
int dbg_axes_w_change_side = -1;
float dbg_scale_min = 1e9f;
float dbg_scale_max = -1e9f;
int dbg_vp_w_min = 1 << 20;
int dbg_vp_w_max = -(1 << 20);
int dbg_xtick_min = 1 << 20;
int dbg_xtick_max = -(1 << 20);
int dbg_gutter_min = 1 << 20;
int dbg_gutter_max = -(1 << 20);
int dbg_side_w = 0;
int dbg_side_h = 0;
int dbg_signal_w = 0;
int dbg_signal_h = 0;
int dbg_signal_row_h = 0;
int dbg_trigger_w = 0;
int dbg_trigger_h = 0;
/// The signal list has rows (the dock has no tab to be "current" on any more).
int dbg_signal_page_current = 0;
/// The dock as the exit probe found it: the split view showing the sidebar, the column sensitive (it is
/// not until the stream has signals), its width, and how many `GtkScrolledWindow`s it holds directly.
int dbg_dock_shown = -1;
int dbg_dock_sensitive = -1;
/// The Cursors and Measurements pages' sensitivity (1 only if both are), read on the tick.
int dbg_pages_sensitive = -1;
int dbg_dock_w = -1;
int dbg_dock_scrollers = -1;
int dbg_trigger_sensitive = 0;
int dbg_trigger_items = 0;
int dbg_trigger_selected = -1;
/// The signal list's shape, captured where the list is built: how many rows it holds, and how many
/// of them are streamed signals. The `MATH` divider and the math rows account for the rest, and
/// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
int dbg_signal_rows = 0;
/// How many rows of the list carry a `GtkDragSource`: 0 -- the signal badges are the drag handles now.
int dbg_signal_drag_sources = -1;
/// A drag begun through the first signal badge's source closed the open slide-over (1), or did not (0);
/// -1 when there is no badge to drag.
int dbg_drag_closed_slideover = -1;
std::size_t dbg_signal_plain = 0;
/// The math channels among those rows, and the number of signals the page should be proposing (counted from
/// the model, not from the rows built), and whether the "every signal is acquired" label was on screen.
std::size_t dbg_signal_math_rows = 0;
std::size_t dbg_signal_addable = 0;
int dbg_signal_empty_label = -1;
int dbg_side_column_after_hide = 1;
std::string dbg_mode_triggered;
std::string dbg_window_text;
double dbg_marker_x = 0.0;
double dbg_marker_max = 0.0;
double dbg_trigger_time = 0.0;
int dbg_trigger_time_valid = -1;
std::string dbg_mode_live;
int dbg_trigger_sensitive_default = 0;
/// The detail string the popover and the exit dump read. The dump used to read the label,
/// which no longer carries these fields.
std::string status_detail;
std::size_t status_detail_max = 0;
/// The widest glance line this run produced. The bar sits above the plot, so this is the
/// "layout must not depend on content" reading for the status row.
std::size_t glance_max_chars = 0;
/// The last glance line, for the exit dump's `status bar:` line — the dump runs after the
/// window is destroyed, so it cannot ask the label.
std::string dbg_glance_last;
/// Which step of the menu probe runs next (`UDPSCOPE_PROBE_MENUS_MS`).
int dbg_menus_step = 0;
/// Which step of the capture probe runs next (`UDPSCOPE_PROBE_CAPTURE_MS`).
int dbg_capture_step = 0;
/// Which step of the event-cache probe runs next (`UDPSCOPE_PROBE_EVENTS_MS`), and the bounded wait
/// it keeps for a second latch (it is counting retries, not time).
int dbg_events_step = 0;
int dbg_events_wait = 0;
/// The plot area's allocated width before the first event was latched (the events probe), compared with
/// its width after: the events list is a slide-over page and no longer a column beside the plot.
int dbg_events_plot_w = 0;
/// The event row the probe selected before it activated one, and how many rows were selected.
/// Both are read back *after* the activation: a row pointer that is no longer the list's row 0,
/// or a smaller count, says the activation rebuilt the list and so threw away the user's scroll
/// position and their comparison selection.
GtkListBoxRow* dbg_events_row0 = nullptr;
int dbg_events_selected = 0;
/// What the *live* view was set to just before the probe opened an event: the time window, the
/// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after
/// each way out of a file view, because a file view overwrites all four -- the window is pinned to
/// the event's own span and the trigger is switched off, since a file has no edge to wait for --
/// and going live has to put them back. The probe keeps its own expectation rather than asking
/// `App`: a reading taken from the mechanism under test asserts nothing.
double dbg_live_window = 0.0;
bool dbg_live_trigger_on = false;
std::string dbg_live_trigger_channel;
double dbg_live_level = 0.0;
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
/// popover it is measuring.
int dbg_rowmenu_step = 0;
GtkWidget* row_menu_popover = nullptr;
GtkWidget* status_info_button = nullptr; ///< the ⓘ
/// The packet-capture toggle, in the detail popover. `toggled` sets the producer's
/// atomic and nothing else; the tick keeps it honest about connection and failures.
GtkWidget* capture_button = nullptr;
/// What the toggle was last set to from this thread, so a producer-side failure
/// (a full disk) can return the button to off without re-entering its handler.
bool capture_want_ui = false;
GtkWidget* status_popover = nullptr;
/// The popover's thirteen labels: five row headings and eight value lines. Built once; the
/// tick sets their text. A rebuild here would be a per-frame relayout of a visible widget.
GtkWidget* popover_label[13] = {};
int popover_rebuilds = 0;
};
struct ConfigDialog {
App* app = nullptr;
GtkWidget* window = nullptr;
GtkWidget* list = nullptr;
GtkWidget* name = nullptr;
GtkWidget* comment = nullptr;
};
struct WindowUnit {
const char* name;
double seconds;
};
enum class BadgePhase { Lit = 0, Dim = 1, Idle = 2, Connecting = 3 };
struct CalibContext {
App* app;
std::size_t signal;
GtkWidget* gain;
GtkWidget* offset;
GtkWidget* window;
};
struct DragContext {
App* app;
std::size_t signal;
};
struct ColorPick {
App* app;
std::string signal;
};
class MeasureWindowReader {
public:
/// `window` is the x range to read, **in the coordinates the axes read** -- the viewport for a
/// live view, the pin for a pinned one -- and it is handed in rather than read off the plot
/// here, because *which* window a measurement describes is a decision (`update_measures()`)
/// that also has to be booked: the pane compares the window it last measured against this one
/// to decide whether a recompute is owed, and a reader that chose its own would leave the two
/// disagreeing -- a pinned view recomputing on every gesture frame for a number that cannot
/// change.
MeasureWindowReader(App& app, std::size_t channel, plot::Range window) {
const RowSet::Row& r0 = app.rows->row(0);
if (r0.plot == nullptr) return;
if ((snap_ = app.model->display_snapshot()) != nullptr) {
if (channel >= snap_->values.size()) return;
const std::vector<f64>& st = snap_->stamps;
if (snap_->values[channel].size() != st.size()) return;
const auto lo = std::lower_bound(st.begin(), st.end(), window.min);
const auto hi = std::upper_bound(st.begin(), st.end(), window.max);
if (hi <= lo) return;
off_ = static_cast<std::size_t>(lo - st.begin());
count_ = static_cast<std::size_t>(hi - lo);
snap_vals_ = &snap_->values[channel];
// The snapshot's pyramid is built from sample 0 and nothing is ever dropped from it, so
// its absolute index space *is* the snapshot's.
if (channel < snap_->pyramids.size()) pyr_ = &snap_->pyramids[channel];
origin_ = off_;
return;
}
lock_ = std::unique_lock<std::mutex>(app.model->mutex());
if (channel >= app.model->channel_count()) return;
epoch_ = app.model->display_epoch();
u64 first = 0;
u64 last = 0;
app.model->live_window_locked(window.min - epoch_, window.max - epoch_, first, last);
if (last <= first) return;
store_ = &app.model->live();
channel_ = channel;
first_ = first;
count_ = static_cast<std::size_t>(last - first);
// The channel's own pyramid, in the value ring's absolute index space -- the same space
// `live_window_locked()` answers in, since a channel's stride is 1. It is current as of this
// frame's `acquire_all()`; the samples produced since are past `window_end()` and are read
// raw, which bounds that tail at one frame of production.
pyr_ = &store_->channel(channel).pyramid();
origin_ = first;
}
std::size_t size() const noexcept { return count_; }
/// The whole level-0 LOD buckets lying inside this window, as absolute bucket indices
/// `[first, last)`. False when the window has none -- a window shorter than a bucket, or one
/// the pyramid does not reach.
bool bucket_range(u64& first, u64& last) const {
if (pyr_ == nullptr || count_ == 0) return false;
const u64 begin = std::max<u64>(origin_, pyr_->window_base());
const u64 end = std::min<u64>(origin_ + count_, pyr_->window_end());
if (end <= begin) return false;
const u64 stored = pyr_->level_base(0);
first = std::max<u64>(stored, (begin + plot::data::kBucket - 1) / plot::data::kBucket);
last = std::min<u64>(stored + pyr_->level_size(0), end / plot::data::kBucket);
return last > first;
}
/// Window offset of absolute bucket `k`'s first sample.
std::size_t bucket_offset(u64 k) const noexcept {
return static_cast<std::size_t>(k * plot::data::kBucket - origin_);
}
MeasureBucket bucket(u64 k) const {
const plot::data::MinMax mm = pyr_->bucket(0, k);
MeasureBucket out;
out.lo = static_cast<double>(mm.min);
out.hi = static_cast<double>(mm.max);
out.sum = pyr_->bucket_sum(0, k);
out.count = static_cast<std::size_t>(pyr_->bucket_count(0, k));
out.offset = bucket_offset(k);
out.span = plot::data::kBucket;
return out;
}
/// `n` values from window offset `at`. The pointer is valid until the next call.
const f32* values(std::size_t at, std::size_t n) {
if (snap_vals_ != nullptr) return snap_vals_->data() + off_ + at;
if (scratch_.size() < n) scratch_.resize(n);
store_->values(channel_).copy_from(first_ + at, n, scratch_.data());
return scratch_.data();
}
/// Window sample `i`'s stream time. One sample at a time on purpose: see the class comment.
double time_of(std::size_t i) const {
if (snap_vals_ != nullptr) return snap_->stamps[off_ + i];
f64 stamp = 0.0;
store_->time().copy_from(first_ + i, 1, &stamp);
return epoch_ + stamp;
}
private:
std::shared_ptr<const EventSnapshot> snap_;
const std::vector<f32>* snap_vals_ = nullptr;
std::size_t off_ = 0;
std::unique_lock<std::mutex> lock_;
BufferStore* store_ = nullptr;
std::size_t channel_ = 0;
u64 first_ = 0;
std::size_t count_ = 0;
f64 epoch_ = 0.0;
const plot::data::MinMaxPyramid* pyr_ = nullptr;
/// Absolute sample index of window offset 0, so a bucket's absolute span becomes an offset.
u64 origin_ = 0;
std::vector<f32> scratch_;
};
struct MeasureDialog {
App* app = nullptr;
int edit = -1;
GtkWidget* signal_drop = nullptr;
GtkWidget* type_drop = nullptr;
GtkWidget* threshold = nullptr;
/// `Between`'s *other* edge: the same unit as `threshold`, shown only for the one type that reads
/// two shares (see `measure_uses_band()`).
GtkWidget* band = nullptr;
GtkWidget* above = nullptr;
GtkWidget* show = nullptr;
/// The per-measurement gate: measure only inside the one shared band. Type-independent --
/// the gate is about *which samples* answer, and every type reads samples -- so unlike the
/// rows above it is shown for every type and never retitled.
GtkWidget* gate = nullptr;
GtkWidget* window = nullptr;
/// The option rows' boxes and labels, so the dialog can *retitle* them per type: the same
/// switch means "the pulse is above the level" for a pulse and nothing at all for a period,
/// and the difference has to be in words, not just in visibility.
GtkWidget* threshold_row = nullptr;
GtkWidget* threshold_label = nullptr;
GtkWidget* band_row = nullptr;
GtkWidget* band_label = nullptr;
GtkWidget* side_row = nullptr;
GtkWidget* side_label = nullptr;
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
/// Hz with 0 off; both are shown only where they do something (see `measure_dialog_sync`).
GtkWidget* noise = nullptr;
GtkWidget* noise_row = nullptr;
GtkWidget* hf = nullptr;
GtkWidget* hf_row = nullptr;
/// The names the signal dropdown was built from, in order. `apply` resolves an index
/// through *this*, not through the model's signal list: the dropdown lists the plotted
/// traces, so index `i` here is the i-th trace -- indexing the CONFIG with it selected
/// whatever signal happened to sit at that position there (the stream's timestamp, for the
/// first entry, which is what picking the first trace used to store).
std::vector<std::string> signals;
};
struct MathWireRef {
int node = -1;
int which = 0;
};
enum class MathDrag { None, Node, Wire, Pan };
struct MathDialog {
App* app = nullptr;
int edit = -1;
std::vector<MathNode> nodes;
int output = 0;
int selected = 0;
/// What a drag is doing, decided once at `drag-begin` from what is under the press and not
/// revisited: a gesture that changes its mind mid-drag is unusable. `Wire` carries a loose end
/// in graph units, which the draw shows so the user can see what is being connected.
MathDrag drag_mode = MathDrag::None;
int wire_from = -1;
int wire_detach = -1;
int wire_detach_which = 0;
double wire_x = 0.0;
double wire_y = 0.0;
double pan_x0 = 0.0;
double pan_y0 = 0.0;
/// `Wire`: the press point in canvas coordinates. The gesture's `dx`/`dy` is relative to this
/// point, so `press_cx + dx` is always the pointer, even on a detach where `wire_from` is a
/// different node from the one the pointer touched.
double press_cx = 0.0;
double press_cy = 0.0;
/// Where the box being dragged *was* when the drag began, and which box it is. The gesture
/// reports the total offset from its own start point, so adding it to a value that the previous
/// update already moved is a runaway: the box accelerates away from the pointer and leaves the
/// canvas. `-1` means the drag did not begin on a box.
double grab_x = 0.0;
double grab_y = 0.0;
int drag_node = -1;
/// The wire `Delete` would remove, or `{-1, 0}`.
MathWireRef selected_wire{-1, 0};
/// The view: canvas pixels per graph unit, and where graph (0,0) is drawn. Applied once to the
/// cairo context by `math_canvas_draw()`; every hit test runs the inverse (`math_to_graph()`)
/// rather than comparing a pointer position against a graph one, and sizes its tolerance in
/// *screen* pixels (`kPort / view_scale`) -- a tolerance left in graph units shrinks with the
/// view and makes a port unhittable zoomed out.
double view_scale = 1.0;
double view_x = 0.0;
double view_y = 0.0;
GtkWidget* window = nullptr;
GtkWidget* canvas = nullptr;
GtkWidget* name = nullptr;
GtkWidget* unit = nullptr;
GtkWidget* op_drop = nullptr;
GtkWidget* source_drop = nullptr;
GtkWidget* gain = nullptr;
GtkWidget* offset = nullptr;
GtkWidget* fc = nullptr;
GtkWidget* q = nullptr;
GtkWidget* med = nullptr; ///< MathNode::med_n
GtkWidget* lo = nullptr;
GtkWidget* hi = nullptr;
GtkWidget* rows[7] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
GtkWidget* inputs = nullptr;
/// The validation line and the button it governs. Both are set from `math_dialog_sync()` and from
/// nowhere else, so what the line says and what the button allows cannot disagree.
GtkWidget* problem = nullptr;
GtkWidget* apply = nullptr;
/// Why the last gesture was *refused* -- "a node cannot read itself", or a cycle. A refusal has no
/// standing state to be read back from: it is a thing that did not happen, so `math_dialog_problem()`
/// cannot describe it and a refused wire otherwise just fails to appear, which reads as the editor
/// being broken. `math_dialog_sync()` shows this in preference to the graph's own problem and
/// **consumes it as it writes**, so the next sync -- and every mutation syncs -- replaces it with
/// the graph's state. `Apply`'s sensitivity still follows `math_dialog_problem()` alone: a refused
/// wire changed nothing, so a graph that was saveable still is.
std::string refusal;
/// True while the form is being written. `gtk_drop_down_set_selected()` emits `notify::selected`
/// for a programmatic set exactly as for a user one, and the op handler answers a notify by
/// syncing -- so writing the form **re-entered** `math_dialog_sync()`, and the inner call rewrote
/// the validation label the outer one had just written. That is how a refusal vanished in the same
/// call that put it up (the probe's `wire: self through drag-end` read an empty line). A
/// programmatic set is not a user edit.
bool syncing = false;
};
struct ExportContext {
App* app = nullptr;
ExportTable table;
};
struct FixedRangeContext {
App* app;
/// The row set the range applies to: `nullptr` is the main window's. A scope window's own
/// dialog writes its window's rows.
RowSet* rows = nullptr;
int row;
GtkWidget* lo;
GtkWidget* hi;
GtkWidget* window;
};
struct RemoveTarget {
static constexpr int kAllSlots = -1;
App* app;
int row;
int slot;
};
enum class RowMenuView { Main, Remove };
struct RowMenuState {
App* app = nullptr;
int row = 0;
GtkWidget* popover = nullptr;
GtkWidget* box = nullptr;
RowMenuView view = RowMenuView::Main;
};
struct EventSamples {
std::vector<std::string> names;
std::vector<f64> times;
std::vector<std::vector<f32>> columns; ///< [column][row]
};
using EventThumbCache = std::unordered_map<std::string, GdkTexture*>;
struct ConditionProbe {
double timeout = 0.300;
/// 0 = pulse width `<` (the glitch), 1 = pulse width `>`, 2 = window, 3 = dropout. Selected
/// by `UDPSCOPE_PROBE_CONDITION_SHAPE`; the default keeps Task 3's glitch leg.
int shape = 0;
int waited = 0;
};
struct RowMenuProbeState {
int main_rows = 0;
int main_bad = 0;
int remove_rows = 0;
int remove_bad = 0;
int back_rows = 0;
int back_bad = 0;
int second_rows = 0;
int second_bad = 0;
int remove_row_in_main = 0;
int remove_row_gone = 0;
int can_open_again = 0;
};
constexpr int kTickMs = 16;
/// How often a frozen view is re-drawn anyway, in ticks: the render gate's floor (see `tick()`
/// and `App::render_skipped`). Four is ~15 Hz, so anything the gate's signature does not name --
/// a recolour, a measurement's mark, a y range a fit changed -- reaches the screen within ~64 ms
/// of a still picture, which is below noticing. It is deliberately a small number: the gate's
/// purpose is to stop spending a GPU on an unchanging frame, not to shave the last frame off.
constexpr int kStaticRenderEvery = 4;
/// How many frames one change to the gate's signature is worth (see `tick()` and
/// `App::render_open`). More than one because the figure lays itself out *per render* and its left
/// gutter is a monotone floor that takes two passes to converge (the `layout over N frames`
/// reading): a re-framed frozen view drawn once would sit a pass short of its final gutter until
/// the floor above fired. Three leaves a pass in hand without being a frame budget.
constexpr int kSettleRenders = 3;
/// Below this window width the sidebar has no room beside the plot, and the split view overlays it
/// instead of squeezing it. See `tick()`.
constexpr int kSidebarRoom = 900;
constexpr int kWindowUnitCount = 3;
constexpr double kWindowMinS = 1.0e-6;
constexpr int kWindowLabelChars = 9;
constexpr int kRowKeyBias = 1;
constexpr int kLayoutButtonPx = 24;
constexpr int kLayoutButtonPad = 4;
/// How far from its corner a full-screen toggle comes out of hiding. The toggles are on the
/// screen only while the pointer is near them; this is the slack around the button's own rect
/// (a square corner zone a few button-widths wide), large enough that a pointer arrives without
/// having to find the exact pixel and small enough that crossing a plot's top edge leaves it
/// hidden.
constexpr int kMaximizeRevealPx = 48;
constexpr const char* kLayoutKindKey = "udpscope-layout-insert";
/// The ↕ handles' own key: what they are is *not* what the `+` is, and `sync_layout_editor()`
/// places them differently -- one boundary, two controls.
constexpr const char* kLayoutResizeKey = "udpscope-layout-resize";
/// Cell `+` buttons carry this key (fr * 8 + pos + 1) so `layout_button()` never matches them.
constexpr const char* kLayoutCellKey = "udpscope-layout-cell";
/// Column resize handles carry the plot index of the right-hand cell.
constexpr const char* kLayoutColKey = "udpscope-layout-col";
/// The full-screen toggle's key: the figure row it acts on, +1 (0 is not a valid row). Its
/// own key because the button is on the overlay *always*, not only while the editor is up.
constexpr const char* kMaximizeKey = "udpscope-maximize-row";
/// The minimum width a cell may be dragged to, in logical pixels.
constexpr double kMinCellPx = 96.0;
/// The plotter's own per-cell floor, `kMinCellWidthLogical` in `deps/plotter/src/plot/figure.cpp`
/// (`place_cells()`): a cell is `floor + extra * weight / total`. Both it and `kMinCellPx` are
/// *logical* px -- the plotter scales its floor by the `ui_scale` the renderer installs, which is
/// `PlotArea::device_scale()`, the same divisor that turns a layout rect into logical px here.
constexpr double kPlotterMinCellPx = 64.0;
constexpr gint64 kMeasureRefreshUs = 100 * 1000;
constexpr gint64 kMeasureHeavyUs = 5 * 1000;
constexpr const char* kCursorModeNames[] = {"vertical", "horizontal", "both", "mix"};
constexpr double kNodeW = 132.0;
constexpr double kNodeH = 46.0;
constexpr double kPort = 10.0;
constexpr int kPortOut = -1;
constexpr double kMathScaleMin = 0.25;
constexpr double kMathScaleMax = 4.0;
constexpr double kMathZoomStep = 1.1;
constexpr double kWirePick = 6.0;
constexpr int kZoomGestureSteps = 8;
constexpr int kZoomWindowSteps = 8;
constexpr int kEventThumbW = 138;
/// The row thumbnail's height floor and ceiling. A floor and not a fixed size: the stored preview is
/// `44 px` per signal, so its proportions depend on how many signals the event has, and a row shows
/// it at *its* aspect -- a one-signal event is a short row and a six-signal one a tall one. The
/// ceiling is what keeps a wide event from making a row taller than the pane; the full picture is a
/// hover away.
constexpr int kEventThumbH = 46;
constexpr int kEventThumbMaxH = 104;
inline int g_stats_phase = 0;
inline std::vector<std::uint64_t> g_stats_n_before;
/// The same for the dock-section probe: the accumulator counts recorded while the `measure` page was
/// still open, so the report can say whether any of them advanced after it was closed.
inline std::vector<std::uint64_t> g_dock_section_n_before;
inline double g_trigrenum_edge = 0.0;
inline std::uint64_t g_trigrenum_latches = 0;
inline ConditionProbe g_condition;
inline double g_search_t0 = 0.0;
constexpr int kMenuRowMaxPx = 30;
inline RowMenuProbeState g_rowmenu_probe;
void install_css();
void on_menu_row_toggled(GtkToggleButton* button, gpointer);
GtkWidget* menu_row(const char* label, const char* accel, bool with_check, GtkWidget** out_check);
GtkWidget* menu_button(const char* label, const char* accel);
GtkWidget* menu_toggle(const char* label, const char* accel, bool active);
GtkWidget* menu_separator();
GtkWidget* labeled_box(const char* label, GtkWidget* child);
/// Loads a UI file into the window's builders and returns the builder, so the caller can resolve the
/// objects it needs by id. See `ui_widgets.cpp`.
GtkBuilder* ui_load(App& app, const char* resource);
LayoutState collect_layout(App& app);
void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
void on_config_forget_clicked(GtkButton* button, gpointer user);
void on_config_save_as(GtkButton*, gpointer user);
void config_dialog_refresh(ConfigDialog& d);
void open_configurations(App& app);
void save_layout_now(App& app);
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
/// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's
/// sensitivity from `connected` -- disabled, never hidden.
/// `sync_source_panels` is the per-tick call: it applies the model's own connection flag.
void build_source_panels(App& app);
void apply_source_panels(App& app, bool connected);
void sync_source_panels(App& app);
bool save_configuration_as(App& app, const std::string& name, const std::string& comment);
gboolean layout_save_tick(gpointer user);
void schedule_layout_save(App& app);
void apply_layout(App& app, const LayoutState& st);
GtkWidget* labeled_box_fixed(const char* label, GtkWidget* child);
GtkWidget* spin(double min, double max, double step, int digits, double value);
GtkWidget* dialog_window(GtkWidget* parent, const char* title);
void dialog_add_button(GtkWidget* window, GtkWidget* button);
void on_popover_closed(GtkPopover* popover, gpointer);
int window_unit_for(double s);
std::string format_window_value(double s, int unit);
std::string format_window(double s);
const char* badge_markup(BadgePhase phase);
/// The trigger status circle's markup, by state (see `trigger_circle_markup` in
/// `ui_status.cpp`): 0 solid green, 1/2 the flash's halves, 3 red.
const char* trigger_circle_markup(int state);
std::string format_measure(double v, const std::string& unit);
void set_notice(App& app, std::string what, int seconds = 4);
void set_row_key(GtkWidget* widget, int row);
int row_key(GtkWidget* widget);
int row_at(const App& app, double x, double y);
void cancel_drag_on(App& app, int row);
void report_wide_widgets(GtkWidget* root, int threshold, int depth);
const char* display_mode_name(DisplayMode m);
void check_frame_ink(App& app);
int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user);
void on_before_paint(GdkFrameClock*, gpointer user);
void on_after_paint(GdkFrameClock*, gpointer user);
GtkWidget* make_compare_area(App& app);
GtkWidget* make_area(App& app);
void relayout_areas(App& app);
void clear_layout_editor(App& app);
void on_layout_row_remove(GtkButton* button, gpointer user);
void on_layout_row_insert(GtkButton* button, gpointer user);
void sync_layout_editor(App& app);
void rebuild_layout_editor(App& app);
void on_layout_editor_done(GtkButton*, gpointer user);
void on_layout_editor_toggled(GtkToggleButton* button, gpointer user);
void on_signal_switch(GObject* obj, GParamSpec*, gpointer user);
void on_arm_clicked(GtkButton* button, gpointer user);
TriggerType trigger_type_at(guint i);
WidthCmp width_cmp_at(guint i);
void publish_trigger_fields(App* app);
void sync_trigger_rows(App& app);
void on_trigger_field_changed(GtkWidget*, gpointer user);
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user);
void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user);
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user);
void show_info_dialog(GtkWindow* parent, const SignalInfo& sig);
void on_info_clicked(GtkButton* button, gpointer user);
void on_calibrate_apply(GtkButton*, gpointer user);
/// Which of the calibration's two numbers a dialog edits; `Both` is the full dialog.
enum class CalibField { Both, Gain, Offset };
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field = CalibField::Both);
/// The one-entry editor for a streamed signal's unit (an empty entry restores the declared one).
void open_unit_editor(App& app, GtkWindow* parent, std::size_t signal);
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user);
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
std::string color_hex(plot::Color c);
std::string chip_markup(plot::Color c);
bool signal_plotted(App& app, const std::string& name);
std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals, std::size_t signal);
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
void open_signal_color(App& app, std::size_t signal);
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
/// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order.
/// The one rule the list and the badges share.
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals);
/// Whether more signals are acquired than `kMaxSignalBadges`: the badge bar then shows one "N signals" badge,
/// and the `signals` page lists the acquired signals as well, since their rows are the only way to a page.
/// The one rule the bar and the page share.
bool signal_badges_overflowed(App& app, const std::vector<SignalInfo>& signals);
/// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if
/// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a
/// signal page's "Plot on" button share.
bool assign_signal_to_row(App& app, std::size_t signal, int row);
/// Back to the palette colour for the signal's position.
void reset_signal_color(App& app, std::size_t signal);
/// Holds `widget` alive across the current emission: a handler that rebuilds the list or a page destroys the
/// widget it was emitted from.
void outlive_the_rebuild(GtkWidget* widget);
/// Creates the page `signal:<name>` if it is missing, fills it, and returns whether it exists afterwards.
bool signal_page_ensure(App& app, const std::string& name);
/// Called at the end of `rebuild_signal_panels`: refills every existing signal page, drops the pages of
/// signals the model no longer has (closing the slide-over first if one of them is open).
void signal_pages_sync(App& app);
gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS
/// The `MATH` divider row in the one channel list, and the `+` it carries.
GtkWidget* make_math_divider(App& app);
void on_math_add_clicked(GtkButton* button, gpointer user);
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
int math_index_for_signal(App& app, const std::string& name);
void on_math_edit_clicked(GtkButton* button, gpointer user);
void on_math_delete_clicked(GtkButton* button, gpointer user);
void rebuild_trigger_signals(App& app);
double window_limit(const App& app);
void sync_window_display(App& app, double seconds);
void reset_measure_stats(App& app);
void apply_time_window(App& app, double seconds);
void on_window_entry_activate(GtkEntry* entry, gpointer user);
void on_window_unit_changed(GObject* obj, GParamSpec*, gpointer user);
void on_window_step(GtkButton* button, gpointer user);
void refresh_window_limits(App& app);
void on_pause_toggled(GtkToggleButton* button, gpointer user);
void search_jump(App& app, double t);
void update_search_panel(App& app);
void sync_events_availability(App& app);
/// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate
/// because the theme is the one preference that takes effect without a restart.
void open_preferences(App& app);
gboolean prefs_probe(gpointer user);
gboolean sidebar_probe(gpointer user);
gboolean boxzoom_probe(gpointer user);
/// A labelled row whose control keeps its natural width -- what a `GtkSwitch` needs and what
/// `labeled_box()`'s expanding child denies it.
GtkWidget* labeled_switch_row(const char* label, GtkWidget* control);
/// The layout editor's boundary drag: the press starts it, movement sizes the two rows it separates.
void on_layout_drag_begin(GtkGestureDrag* gesture, double x, double y, gpointer user);
void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
plot::u32 measure_color(std::size_t index);
/// One line naming what a measurement's type is measuring, with the shares it reads them at: the
/// pane's row tooltip. The row's own label is the type alone (as for every type), so this is where
/// a user sees the share an `above`/`below`/`between` row is averaging over.
std::string measure_row_summary(const Measure& m);
bool visible_samples(App& app, std::size_t channel, std::vector<double>& time, std::vector<double>& value);
int measure_channel(App& app, const std::string& signal);
int measure_row(App& app, const std::string& signal);
std::string measure_unit(App& app, const std::string& signal);
GtkWidget* measures_row(App& app, std::size_t index, const Measure& m);
void rebuild_measures_pane(App& app);
void on_cursor_mode_changed(GObject* obj, GParamSpec*, gpointer user);
App::CursorLine cursors_line(App& app, const char* who, plot::Color chip, bool has_chip);
GtkWidget* cursors_header(App& app);
/// The compact readouts at the top of the dock (`src/ui_dock.cpp`). The strings are the pages' own: the
/// pages' writers call the `dock_*` setters from the same variable, so a number is formatted once.
GtkWidget* build_readouts_dock(App& app);
void dock_rebuild_cursors(App& app);
void dock_rebuild_measures(App& app);
void dock_cursor_cell(App& app, std::size_t line, const std::string& a, const std::string& b,
const std::string& d);
void dock_measure_value(App& app, std::size_t i, const std::string& text);
/// The readouts strip under the plot (`src/ui_dock.cpp`): the third reader of the same strings. It is
/// filled by the `dock_*` rebuilds and written by the `dock_*` setters above (so nothing here formats),
/// and shown whenever the dock is not beside the plot (`!show-sidebar || collapsed`, a collapsed dock being
/// an overlay on it) and there is something to read (cursors on or a measurement; each group follows its own
/// feature, and with neither the strip is hidden): `sync_readouts_strip()` is called by the tick after the show-sidebar reconciliation
/// and before `set_collapsed()`, so it reads the previous frame's `collapsed`. `dock_set_cursors_sensitive()` is the one writer of the cursor readouts'
/// sensitivity, for the dock's grid and the strip's chips together.
void sync_readouts_strip(App& app);
void dock_set_cursors_sensitive(App& app, bool on);
/// The Events block under the readouts (`src/ui_dock.cpp`): compact rows (marker, title, time; no
/// thumbnails) from `event_store->list()`, newest first, and a Manage button for the `events` page.
/// `dock_rebuild_events()` follows every change of what is *cached*, `dock_mark_events()` every change
/// of what is *on screen* (in place, no rebuild), `dock_events_set_sensitive()` is the one writer of the
/// block's sensitivity. All three are no-ops before the block is built.
GtkWidget* build_events_dock(App& app);
/// The dock shows at most this many events (the newest), then one dim "... N more, see Manage" row.
inline constexpr std::size_t kDockEventsMax = 24;
/// The first lists the store itself; the second takes a list the caller already holds (so one refresh
/// reads the cache directory once, not twice). Both keep the list's vertical scroll across the rebuild.
void dock_rebuild_events(App& app);
void dock_rebuild_events(App& app, const std::vector<EventRecord>& records);
void dock_mark_events(App& app);
void dock_events_set_sensitive(App& app, bool available);
/// A row's first line, and the two questions its mark is made of (`ui_events.cpp`): the one function
/// for the pane and the dock, so the two cannot disagree about which event is on screen.
std::string event_row_title(App& app, const EventRecord& r);
bool event_on_screen(const App& app, const EventRecord& r);
bool event_in_group(const App& app, const EventRecord& r);
/// The signal chip of a measurement's row, in the colour every other view names that signal with.
std::string measure_chip_markup(App& app, const std::string& signal);
/// The 1-based number of an active channel (its position among the acquired channels), or 0
/// when the signal is not acquired. The number the badge bar and the measurements pane show.
int measure_channel_number(App& app, const std::string& signal);
std::string row_unit(App& app, int row);
std::string slot_unit(App& app, int row, int slot);
std::uint32_t bound_slot_mask(App& app);
void rebuild_cursors_pane(App& app);
void update_cursors_pane(App& app);
GtkWidget* labelled_row(const char* label, GtkWidget* child, GtkWidget** label_out);
void measure_dialog_sync(MeasureDialog* d);
void on_measure_type_changed(GObject* obj, GParamSpec*, gpointer user);
void on_measure_dialog_apply(GtkButton*, gpointer user);
void on_measure_dialog_delete(GtkButton*, gpointer user);
void open_measure_dialog(App& app, int edit_index);
void place_measure_marks(App& app);
void place_search_marks(App& app);
void update_measures(App& app, bool force, bool accumulate);
void apply_math_channels(App& app);
/// Re-reads the file a file view is showing after a structural change rebuilt the buffer and dropped
/// its history -- see the definition in ui.cpp. True when something was reloaded.
bool reload_file_view(App& app);
void add_math_channel(App& app);
std::vector<std::string> math_source_names(App& app);
std::string math_dialog_problem(const MathDialog* d);
GdkRectangle math_node_rect(const MathNode& n);
void math_input_pos(const MathNode& n, int which, double& x, double& y);
void math_output_pos(const MathNode& n, double& x, double& y);
int math_node_at(const MathDialog* d, double x, double y);
int math_port_at(const MathDialog* d, double x, double y, double tol, int& which);
void math_to_graph(const MathDialog* d, double cx, double cy, double& gx, double& gy);
double math_set_scale(MathDialog* d, double want);
void math_zoom_at(MathDialog* d, double px, double py, double dy);
void math_view_reset(MathDialog* d);
void math_view_fit(MathDialog* d);
void math_tidy(MathDialog* d);
int math_port_at_canvas(const MathDialog* d, double cx, double cy, int& which);
int math_node_at_canvas(const MathDialog* d, double cx, double cy);
double math_seg_dist2(double px, double py, double ax, double ay, double bx, double by);
MathWireRef math_wire_at(const MathDialog* d, double gx, double gy, double tol);
MathWireRef math_wire_at_canvas(const MathDialog* d, double cx, double cy);
bool math_connect(MathDialog* d, int from, int to, int which, std::string* why);
void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpointer user);
void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer user);
void math_canvas_drag_begin(GtkGestureDrag*, double x, double y, gpointer user);
void math_canvas_drag(GtkGestureDrag*, double dx, double dy, gpointer user);
void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user);
gboolean math_canvas_scrolled(GtkEventControllerScroll* scroll, double, double dy, gpointer user);
gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user);
void math_add_node(MathDialog* d, MathOp op);
void on_math_node_added(GtkButton*, gpointer user);
void on_math_node_deleted(GtkButton*, gpointer user);
void on_math_output_set(GtkButton*, gpointer user);
void on_math_param_changed(GtkWidget*, gpointer user);
void on_math_source_changed(GObject*, GParamSpec*, gpointer user);
void on_math_op_changed(GObject* object, GParamSpec*, gpointer user);
void math_dialog_sync_form(MathDialog* d);
void math_dialog_sync(MathDialog* d);
void on_math_apply(GtkButton*, gpointer user);
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
void open_math_dialog(App& app, int edit);
GtkWidget* build_measures_panel(App& app);
GtkWidget* build_cursors_panel(App& app);
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
void on_fit_clicked(GtkButton*, gpointer user);
void on_fit_x_clicked(GtkButton*, gpointer user);
void on_cursors_reset(GtkButton*, gpointer user);
void on_export_response(GObject* source, GAsyncResult* result, gpointer user);
void on_export_clicked(GtkButton*, gpointer user);
void on_about_clicked(GtkButton*, gpointer user);
void on_help_clicked(GtkButton*, gpointer user);
void on_shortcuts_clicked(GtkButton*, gpointer user);
void on_y_auto_clicked(GtkButton* button, gpointer user);
void on_fixed_range_apply(GtkButton*, gpointer user);
void on_y_fixed_clicked(GtkButton* button, gpointer user);
void remove_trace(App& app, int row, int slot);
int remove_signal_from_plots(App& app, const std::string& name);
std::vector<std::string> disable_dependent_math_channels(App& app, const std::string& name);
void on_remove_clicked(GtkButton* button, gpointer user);
GtkWidget* menu_icon_button(const char* icon, const char* label);
void on_row_menu_show_remove(GtkButton*, gpointer user);
void on_row_menu_back(GtkButton*, gpointer user);
void row_menu_fill(RowMenuState& st);
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
/// The same drop, onto a **scope window's** plot: the target carries the window, the row is the
/// window's own cell under the pointer, and the assignment lands in that window's row set.
gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
/// Plots `signal` on `row` of a scope window's row set (the signal page's *Plot on* buttons and the
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
/// holds four signals already.
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
/// Runs one keyboard accelerator. Scope windows carry the same shortcuts as the main window: the
/// view-level ones (`L` -- the layout editor) act on the window the key arrived in; the rest reach
/// the application through `run_app_action()`.
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint keycode,
GdkModifierType state, gpointer user);
/// The session record of a window: its name, whether it was detached, and its plot rows.
LayoutState::Window scope_window_state(const App::ScopeWindow& scope);
/// Remembers a window as closed (`closed_windows`, replaced by name) -- what its destroy handler
/// does, and what keeps a closed window reopenable.
void remember_closed_window(App& app, const App::ScopeWindow& scope);
/// Opens a window from a remembered state: an empty scope window, then its rows, weights and
/// signals. Returns the opened window (nullptr when it could not be made).
App::ScopeWindow* open_scope_window_from_state(App& app, const LayoutState::Window& state);
/// Writes the main window's instruments -- the two cursors, the gate band, the trigger marker --
/// into every scope window, and adopts one dragged *there* back into the main window first. Called
/// once per frame from the main tick, after the main's own `sync_cursors()`/`sync_gate()`.
void sync_scope_instruments(App& app);
/// The main window's measurement marks, translated to each scope window's own rows (by signal
/// name) and placed there: a window that plots a measured signal draws its mark.
void mirror_measure_marks(App& app);
/// Refreshes the *Reopen closed windows* entry: its count and whether it is sensitive.
void sync_reopen_scopes_item(App& app);
/// Turns a scope window's own layout editor on or off, building or taking down its buttons.
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
/// The scope window's own right-click menu: the same row menu's actions, aimed at **that
/// window's** row set.
void on_scope_right_click(GtkGestureClick* gesture, int n_press, double x, double y,
gpointer user);
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
void sync_panel_switches(App& app);
/// The trigger's four enumerations as words: the panel's dropdown labels and the session file's
/// vocabulary, in one place so the two cannot drift.
const char* trigger_type_text(TriggerType t);
TriggerType trigger_type_from_text(const std::string& s);
const char* trigger_edge_text(TriggerEdge e);
TriggerEdge trigger_edge_from_text(const std::string& s);
const char* width_cmp_text(WidthCmp c);
WidthCmp width_cmp_from_text(const std::string& s);
const char* trigger_mode_text(TriggerMode m);
TriggerMode trigger_mode_from_text(const std::string& s);
/// The panel's controls from the model, and the session's trigger into both.
void write_trigger_widgets(App& app);
/// The compact line the trigger pane shows while it is locked, and the lock itself: the pane's body and
/// its [edit] button follow from `trigger_locked`.
void sync_trigger_summary(App& app);
/// Both arm buttons -- the panel's and the compact line's -- from the one armed state the producer keeps in
/// `model->trigger_armed()`. The label was written from four places (the click, the toolbar toggle, the
/// curator's re-read, the start-up default) and a second button would have needed a fifth copy of the same
/// two lines; the drift that costs is a button reading "Arm" while the engine is armed. Sensitivity, and
/// the compact button's visibility, are stated here too and read from the toolbar toggle -- the control
/// that owns `enabled`, per the restore rules -- so no caller needs the model lock to state any of it.
void sync_arm_buttons(App& app);
void apply_pane_lock(App& app);
void on_pane_edit_clicked(GtkButton* button, gpointer user);
void apply_trigger_layout(App& app, const LayoutState::Trigger& t);
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
void update_side_visibility(App& app);
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
void rebuild_signal_panels(App& app);
void svg_declared_size(const char* data, gsize size, int* w, int* h);
std::string icon_report(GtkIconTheme* theme, const char* name, const char* resource);
void note_panel_controls(App& app);
void report_event_zoom(App& a, const char* what);
void update_status_popover(App& app, const udpscope::StatusSnapshot& s);
std::uint64_t capture_file_bytes(std::uint32_t slots);
std::string abbrev_home(const std::string& path);
void update_capture_tooltip(App& app);
void update_capture_row(App& app);
bool record_event(App& app);
bool replay_event(App& app, const EventRecord& record);
bool go_live_again(App& app);
void snapshot_live_state(App& app);
void restore_live_state(App& app);
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
std::vector<EventRecord> selected_events(App& app);
void update_group_buttons(App& app);
bool build_event_group(App& app, const std::vector<EventRecord>& records, EventGroupKind kind, std::string* error);
void sync_compare_axes(App& app);
void on_group_clicked(GtkButton* button, gpointer user);
GdkTexture* scale_texture(GdkTexture* source, int width, int height);
EventThumbCache* event_thumb_cache(App& app);
GdkTexture* event_preview_texture(App& app, const std::string& path);
void refresh_events_pane(App& app);
/// Points the event cache at the configuration the session is on -- the active one when
/// `App::config_name` is empty -- and refreshes the two views of it when the directory changed.
/// Every configuration switch calls it next to setting `App::config_name`, so the list on screen
/// is always the configuration's own.
void retarget_event_store(App& app);
/// Opens the `events` page. **Every route that opens that page must use this** and never `slideover_open()`
/// directly: the page's list is rebuilt only while the page is open (see `record_event()`), and the
/// slide-over has no on-open hook, so the list is refreshed here, once, before the page shows. The refresh
/// is skipped when the page is already open -- it is current then, and a rebuild would drop the
/// Compare/Accumulate selection and the scroll position. Returns what `slideover_open()` returns.
bool open_events_page(App& app);
/// Re-marks which row is the event currently on screen, **without rebuilding the list**.
///
/// The mark is the only thing about a row that a replay changes -- the picture and the numbers
/// are the file's -- and rebuilding the list to restate it threw away the scroll position and
/// the selection with it, so reaching the thirtieth event meant scrolling to it again after
/// every click, and a selection made for a comparison was lost by the act of looking at one of
/// its members. Call this for a change of what is on screen, and `refresh_events_pane()` only
/// for a change of what is *cached*.
void mark_events_on_screen(App& app);
void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
GtkWidget* build_events_pane(App& app);
gboolean tick(gpointer user);
void on_events_record_toggled(GObject* o, GParamSpec*, gpointer user);
GtkWidget* build_trigger_panel(App& app);
GtkWidget* build_signal_panel(App& app);
void note_starvation(App& app);
GtkWidget* build_window_control(App& app);
void run_app_action(App& app, AppAction action);
gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user);
GtkWidget* build_toolbar(App& app);
void on_activate(GtkApplication* application, gpointer user);
gboolean exit_probe_step3(gpointer user);
gboolean exit_probe_step2(gpointer user);
GtkWidget* find_labelled_button(GtkWidget* w, const char* label);
gboolean trig_trace_probe(gpointer user);
gboolean eventzoom_wait(gpointer user);
gboolean eventzoom_probe(gpointer user);
gboolean math_probe(gpointer user);
gboolean mathwrap_probe(gpointer user);
gboolean mathwrap_probe_wait(gpointer user);
gboolean mathwrap_probe_report(gpointer user);
gboolean measure_probe(gpointer user);
gboolean levels_probe(gpointer user);
gboolean gate_probe_step(gpointer user);
gboolean crosshair_probe(gpointer user);
gboolean maximize_probe_step(gpointer user);
gboolean scope_probe_step(gpointer user);
/// A second top-level window showing the same stream through its own figure: `figure_row` >= 0
/// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an
/// empty one-row scope. The main window's tick binds, acquires and repaints it.
void open_scope_window(App& app, int figure_row, bool detached = false);
/// Detaches figure row `fr` into a window of its own: the figure is copied into a new scope
/// window -- which is what makes the window *detached*, the one kind with an attach control --
/// and removed from the main window's rows. Refused, changing nothing, for a one-figure layout
/// or a maximized one: detaching then would leave the main window with nothing to draw.
void detach_figure(App& app, int fr);
/// Puts a detached window's figure back: a figure row is appended to the main window carrying
/// the window's arrangement (cells, weights, every slot's signal in its colour) and the window
/// closes. The window must be a detached one; anything else returns without changing.
void attach_scope_window(App& app, App::ScopeWindow& scope);
gboolean measurepin_probe(gpointer user);
gboolean measurepin_held_wait(gpointer user);
gboolean measurepin_pause_wait(gpointer user);
gboolean measurepin_live_wait(gpointer user);
gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user);
gboolean dock_section_probe_report(gpointer user);
gboolean dock_section_probe(gpointer user);
gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS
/// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page
/// per setup surface, each page in a scroller of its own.
void build_slideover(App& app); // once, after the plot_overlay exists
bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content);
/// Retitles a page in place (`slideover_add_page`'s title, for a page whose subject changed: a signal
/// page carries its channel's number, and the stream can renumber). False when the id is unknown.
bool slideover_set_title(App& app, const std::string& id, const std::string& title);
bool slideover_has_page(const App& app, const std::string& id);
/// The inverse of `slideover_add_page`: false when the id is unknown or is the open page.
bool slideover_remove_page(App& app, const std::string& id);
bool slideover_open(App& app, const std::string& id); // false when the id is unknown
void slideover_close(App& app);
bool slideover_toggle(App& app, const std::string& id); // open if closed or on another page, else close
const std::string& slideover_page(const App& app); // "" when closed
bool slideover_escape(App& app); // closes if open; true when it did
/// x, y in the overlay's coordinates; closes unless the point is inside the panel; true when it closed.
bool slideover_click_away(App& app, double x, double y);
gboolean slideover_probe(gpointer user); // UDPSCOPE_PROBE_SLIDEOVER_MS
/// The badge bar (`src/ui_badges.cpp`): setup is reached by the object it configures.
/// The Trigger badge's state word. Pure, so the probe can assert the whole table.
const char* trigger_badge_state(bool enabled, bool armed, bool pending, bool has_event);
void build_badge_bar(App& app); // after build_slideover, the toolbar (it reads trigger_toggle) and build_source_panels (the Source badge's page)
void sync_badge_bar(App& app); // per tick and on every slide-over open/close; only writes what changed
gboolean badges_probe(gpointer user); // UDPSCOPE_PROBE_BADGES_MS
/// Rebuilds the signal badges (one per enabled signal, or the "N signals" overflow) from the model.
void sync_signal_badges(App& app);
gboolean signal_badges_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_BADGES_MS
gboolean source_kinds_probe_report(gpointer user);
gboolean source_kinds_probe(gpointer user);
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
/// their three controls, the capture button and a hand-checked decode. `_report` is the second phase.
/// `_reconnect` reads the state after the mock dropped the link; `_resumed` shows the stream moved on.
gboolean e1608_probe(gpointer user);
gboolean e1608_probe_report(gpointer user);
gboolean e1608_probe_reconnect(gpointer user);
gboolean e1608_probe_resumed(gpointer user);
/// The generator's update rate: starts a Sine on DAC 0 through the panel, reads the achieved rate the
/// source measured and the Analog out panel's "Update rate" row (`_report`, 2.5 s later), then stops
/// it and checks both read "off" (`_stopped`).
gboolean e1608_gen_probe(gpointer user);
gboolean e1608_gen_probe_report(gpointer user);
gboolean e1608_gen_probe_stopped(gpointer user);
/// The E-1608 form's "Scan network" row: presses Scan against the mock, then (phases `_fill`,
/// `_rescan`) clicks a result to fill the address, "Use MAC" to fill the MAC, scans again and checks
/// the results were replaced, not added to.
gboolean e1608_scan_probe(gpointer user);
gboolean e1608_scan_probe_fill(gpointer user);
gboolean e1608_scan_probe_rescan(gpointer user);
/// The flat UDP (MARTe2 UDPSender) source: the signal-list codec and a hand-computed decode, then,
/// against `tools/mock_flatudp.py`, the stream, the value continuity, the wrong-size accounting and
/// the capture button (`_report`, a second later). `_gap` starts a 100 ms poll that waits for the
/// stream, then for the mock's silence to drop the source (whenever it falls), then (`_resumed`) for
/// the stream to come back.
gboolean flatudp_probe(gpointer user);
gboolean flatudp_probe_report(gpointer user);
gboolean flatudp_probe_gap(gpointer user);
gboolean flatudp_probe_resumed(gpointer user);
gboolean pan_probe(gpointer user);
gboolean arm_probe(gpointer user);
gboolean trigrenum_probe_step3(gpointer user);
gboolean trigrenum_probe_step2(gpointer user);
gboolean trigrenum_probe(gpointer user);
gboolean fields_probe(gpointer user);
gboolean condition_probe_report(gpointer user);
gboolean condition_probe_arm(gpointer user);
gboolean condition_probe(gpointer user);
gboolean search_probe_marks(gpointer user);
gboolean search_probe_report(gpointer user);
gboolean search_probe_run(gpointer user);
gboolean search_probe(gpointer user);
gboolean window_probe(gpointer user);
GtkWidget* layout_button(App& a, bool insert, int index);
GtkWidget* layout_cell_button(App& a, int fr, int pos);
GtkWidget* layout_col_handle(App& a, int plot);
/// Records the starting widths (logical px) and weights of the cell pair a column drag on `plot`
/// (the right-hand cell) begins with. False when `plot` has no left neighbour.
bool layout_col_drag_begin(App& app, int plot);
void layout_col_apply(App& app, int plot, double offset_px);
/// A column drag's pointer travel from what its gesture reports: `offset` is relative to the handle, which
/// has moved from `handle_begin` to `handle_now` (same units, the overlay's) since the drag began.
double layout_col_total_offset(double offset, double handle_now, double handle_begin);
void layout_probe_report(App& a, const char* what);
/// The split in the report lines, from the `RowSet` alone (no widget is asked, so the exit dump can
/// use them): cells per figure row (`1,2,1`) and each figure row's cell weights, rows divided by `|`
/// and empty for an unsplit row (`|0.3,0.7|`).
std::string layout_cells_text(const RowSet& rs);
std::string layout_colweights_text(const RowSet& rs);
void layout_click(GtkWidget* b);
void layout_probe_next(App& a, int step, guint delay_ms);
gboolean layout_probe_step(gpointer user);
void split_probe_report(App& a, const char* tag);
void split_probe_next(App& a);
gboolean split_probe_step(gpointer user);
gboolean enable_all_probe(gpointer user);
gboolean disarm_probe_step2(gpointer user);
gboolean disarm_probe_step3(gpointer user);
gboolean disarm_probe_wait(gpointer user);
gboolean disarm_probe_step1(gpointer user);
int persist_probe_depth();
void persist_probe_report(App& a, const char* what);
void persist_probe_set_depth(App& a, int depth);
gboolean persist_probe_wait(gpointer user);
gboolean persist_probe(gpointer user);
/// The phosphor density's one step for a frame: state every row's `plot::Plot` with the
/// configuration the trigger holds, advance its fade by the frame's own `dt`, and deposit the
/// acquisition on screen if one has arrived since the last step. `display` and `live_display` are
/// passed in rather than re-read, so the step, the bind and the status line all describe one frame.
///
/// The density is on while an acquisition is on screen -- `Held`, and `Capturing` as it fills --
/// and off on a live view, a paused one, or a file: the display the user asked for is a *trigger*
/// display. Off is not "paused": switching it off (and leaving trigger mode, and opening a file)
/// drops the image at once, so a fresh presentation starts empty rather than showing a stale image
/// at another scale.
void apply_density(App& a, DisplayMode display, bool live_display);
/// The density probe (`UDPSCOPE_PROBE_DENSITY_MS`): the display's own numbers read back off the
/// plots -- total ink and peak with it off, on, and after one decay step of each kind.
void density_probe_report(App& a, const char* what);
/// States the switch and/or the fade through the panel's own widgets, the way a user would.
void density_probe_set(App& a, bool on, bool by_time);
gboolean density_probe(gpointer user);
gboolean density_probe_step(gpointer user);
void force_probe_report(App& a, const char* leg);
gboolean force_probe_press(gpointer user);
gboolean force_probe_poll(gpointer user);
gboolean force_probe_step2(gpointer user);
gboolean force_probe(gpointer user);
void pause_probe_report(App& a, const char* what);
gboolean pause_probe(gpointer user);
gboolean export_probe(gpointer user);
gboolean exit_probe_step1(gpointer user);
void cursors_probe_report(App& a, const char* what);
void fit_probe_report(App& a, const char* what);
gboolean timelabels_probe_step(gpointer user);
void fit_probe_zoom_in(App& a);
gboolean fit_probe_step(gpointer user);
void cursors_probe_next(App& a, int step, guint delay_ms);
gboolean cursors_probe_step(gpointer user);
void measure_dialog_click(App& a, const char* label);
void measure_dialog_report(App& a, const char* what);
void measures_probe_next(App& a, int step, guint delay_ms);
gboolean measures_probe_step(gpointer user);
void bandavg_probe_next(App& a, int step, guint delay_ms);
void bandavg_report(App& a, const char* what);
int bandavg_index(App& a, MeasureType type, const char* signal);
gboolean bandavg_probe_step(gpointer user);
gboolean zoomkeys_probe(gpointer user);
void menus_probe_next(App& a, int step, guint delay_ms);
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
gboolean menus_probe_step(gpointer user);
gboolean windowpop_probe_step(gpointer user);
/// Closes the time-window popover when a press lands elsewhere in the window (see ui.cpp). Declared for
/// the probe, which drives it and presses the button to check the one case that must *not* close it.
void on_window_popover_press(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user);
gboolean color_probe(gpointer user);
gboolean reseq_probe(gpointer user);
void capture_probe_next(App& a, int step, guint delay_ms);
gboolean capture_probe_step(gpointer user);
gboolean configs_probe(gpointer user);
void rowmenu_probe_next(App& a, int step, guint delay_ms);
GtkWidget* row_menu_find(GtkWidget* container, const char* want);
void row_menu_measure(GtkWidget* box, const char* tag, int* rows_out, int* bad_out);
gboolean rowmenu_probe_step(gpointer user);
int signal_tick_visible(App& app);
int group_visible_traces(App& app, int row);
void report_events_availability(App& a, const char* what);
void events_probe_next(App& a, int step, guint delay_ms);
gboolean events_probe_step(gpointer user);
gboolean events_group_probe_step(gpointer user);
void events_group_probe_next(App& a, int step, guint delay_ms);
gboolean drop_probe(gpointer user);
gboolean noise_probe(gpointer user);
gboolean noise_probe_setup(gpointer user);
gboolean noise_probe_math(gpointer user);
gboolean noise_probe_denoise(gpointer user);
void collect_spins(GtkWidget* w, std::vector<double>* out);
gboolean defaults_probe(gpointer user);
gboolean keys_probe(gpointer user);
gboolean help_probe(gpointer user);
void helpui_probe_next(App& a, int step, guint delay_ms);
gboolean helpui_probe_step(gpointer user);
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: a stop that stands on another page of a stack, entered from the
/// page it is not on -- the sidebar put on Measure, the walk to the signals stop, the reading of the
/// page and of the spotlight, and then the same walk with the sidebar *collapsed*.
void tour_surface_probe_next(App& a, guint delay_ms);
// --- the help affordance layer (ui_context_help.cpp, ui_tour.cpp) ----------
//
// Two things live here and they are not the same thing: the *contextual help* is a "?" on every
// surface the app can explain, and the *guided tour* is one walk over five of them. Both read the
// one setting the Help menu's toggle writes.
/// Adds or removes the "?" affordances so they match `settings.contextual_help`, and does nothing
/// else. Idempotent, so it can be called after the window is built, on every change of the setting
/// and once more when a dialog that carries one of the hosts is opened.
void context_help_sync(App& app);
/// How many "?" affordances are on screen, writing their ids space-separated into `ids`. Found by
/// the data key rather than kept in a list, so a host that died with a dialog cannot leave a stale
/// pointer behind.
int context_help_count(App& app, std::string* ids);
/// The menu row's handler: writes the setting, saves the file and re-states the layer.
void on_contextual_help_toggled(GtkToggleButton* button, gpointer user);
/// The "?" for one surface by id, for a host that only exists while a dialog is open (the math
/// editor is rebuilt on every open, so it cannot be a row the window's own build visits).
void context_help_add(App& app, const char* id, GtkWidget* host);
/// The same affordance for a surface that is *not* in `App`'s table -- the source chooser's, which
/// runs before `App` exists and so cannot name one of its fields. `host` is a `GtkBox` and the button
/// joins it as its last child, which is the rule the table's own rows follow; `on` false takes any
/// hint off `host` instead. `shortcut` may be "".
void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body,
const char* shortcut, bool on);
/// How many hints a widget subtree carries, and their ids, for a window that has no `App` (the
/// chooser's probe). `context_help_count()` is this with `App::window` as the root.
int context_help_count_in(GtkWidget* root, std::string* ids);
/// The size of the first *visible* hint button in a subtree, read the way the probe's geometry is
/// (`gtk_widget_compute_bounds()`), and the number a shrinking affordance is checked against. Both
/// are 0 when the layer is off or nothing on screen carries one.
void context_help_size_in(GtkWidget* root, int* width, int* height);
/// `context_help_size_in()` with `App::window` as the root.
void context_help_size(App& app, int* width, int* height);
/// Builds a tour's overlay children (the four dim rectangles, the spotlight outline and the card)
/// inside `overlay`. Called once, as the overlay is inserted, by either window that draws a tour.
void tour_build(TourView& view, GtkWidget* overlay);
/// Starts at the first stop. `stops` is the whole table, in walk order: the stops whose surface is
/// not on screen are filtered out here, by the one rule both windows use (a wrong page is kept and
/// selected, a collapsed pane is dropped). `on_done` (may be null) runs when the walk passes its last
/// stop, and is how the chooser hands off.
void tour_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
void* done_user);
/// Moves `delta` stops, finishing the walk (and calling `on_done`) on the way past the last one.
void tour_advance(TourView& view, int delta);
/// Ends the walk without finishing it: every child of the overlay is hidden and nothing is left on
/// screen. Skip and Escape.
void tour_end(TourView& view);
/// Called from the tick while the walk runs: re-measures the target each frame, so a window resize
/// keeps the spotlight on the widget, and places the card where it does not cover it.
void tour_update(TourView& view);
/// The widget the current stop points at, or nullptr when the walk is not on a stop.
GtkWidget* tour_target(TourView& view);
/// The part of `target` that is on screen, in `overlay`'s coordinates: its bounds intersected with every
/// scrolled window above it. A target scrolled out of sight entirely is measured and *empty*: true with a
/// zero-size rectangle. False only when it cannot be measured yet (no allocation, not in the overlay).
/// The spotlight is drawn from it, and the probe reads the spotlight against it.
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out);
/// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted.
void build_tour(App& app);
/// Starts the window's tour at its first step and writes `tour_seen`. Safe to call while it already
/// runs -- it restarts at step 0.
void start_tour(App& app);
/// Moves `delta` steps, ending the tour on the way past the last one.
void tour_step(App& app, int delta);
/// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen.
void tour_dismiss(App& app);
/// The window's tour, re-measured each frame.
void update_tour(App& app);
bool tour_active(const App& app);
/// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe
/// reads the spotlight against it.
GtkWidget* tour_target(App& app);
/// The steps this run actually walks (targets that were on screen when it started), and where it is.
int tour_step_count(const App& app);
int tour_step_index(const App& app);
/// The first-run auto-start's callback, scheduled only when `settings.tour_seen` is false.
gboolean tour_autostart(gpointer user);
/// The chooser's "Guided tour" request (`Config::tour_pending`): the same delayed start, but it
/// sets `tour_requested` rather than `tour_auto_started`, so the probe and the dump can tell the two
/// apart. Scheduled only when the config carries the request.
gboolean tour_start_requested(gpointer user);
/// Captures the layer's own readings and the bounds of the surfaces the help's figures label, while
/// the window is alive -- the dump runs after it is destroyed and may not ask GTK anything.
void capture_help_state(App& app);
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
RowSet& rows);
} // namespace ui_impl
} // namespace udpscope