Files
udpscope/src/ui_app.hpp
T
Martino FerrariandClaude Opus 4.6 b98a3478ff badges: source badge shows the name only; drop the signals lock; the signals page proposes what can be added
The link state is the status bar's, so the Source badge loses its state text. The signals have no lock
(the badges and signal pages are direct manipulation), and the signals page lists only unacquired
signals and uncomputed math channels (plus the acquired ones while the badge bar has overflowed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-11-03 02:14:37 +01:00

2206 lines
124 KiB
C++

// 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; a read-out 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;
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;
/// 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 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;
/// 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 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 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 the cursors and
/// the measurements (`dock_show_section()` scrolls it). A feature's own on/off never
/// removes a section (it sets its panel's 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 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 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 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)` and not by channel alone: the corner is a per-measurement setting, so two
/// measurements on one signal at different corners are reading two different signals and cannot share a
/// walk.
struct MeasureWindow {
int channel = -1;
double fc = 0.0; ///< the HF corner this window was walked at; 0 is unfiltered
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;
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 defaults on -- the cache is only useful if
/// it was already on when the event you wanted happened.
std::unique_ptr<EventStore> event_store;
/// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar
/// for a source whose events are already written, even with recording switched off.
bool events_cached = false;
bool event_recording = false;
/// The events pane: its list, the revealer that holds it, 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 pane does not put anywhere special.
GtkWidget* events_revealer = nullptr;
GtkWidget* events_list = nullptr;
GtkWidget* events_thumb = nullptr;
GtkWidget* events_record_switch = nullptr;
/// The toolbar's event-history toggle: the pane is opened *on demand* from here (the item's own
/// arrangement), and the toggle only exists while trigger mode does -- there are no events without
/// a trigger, and a button that can never have anything to show is a button spent on nothing.
GtkWidget* events_toggle = nullptr;
/// The paned that holds the events pane and the plot, and the width the divider was last dragged
/// to. 240 by default: enough for the preview and the numbers beside it, and not a plot's worth.
GtkWidget* events_divider = nullptr;
int events_pane_width = 240;
/// 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;
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 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;
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;
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;
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";
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 section was
/// still scrolled into view, so the report can say whether any of them advanced after it scrolled out.
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);
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);
void open_calibration(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);
/// Restates the events pane's divider position once a frame; see the definition.
void sync_events_divider(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);
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);
/// Scrolls the dock to a section: "signals" (the top) or "measure" (the cursors panel, which the
/// measurements follow). An unknown name is a no-op.
void dock_show_section(App& app, const char* section);
/// 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);
/// 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 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);
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);
void layout_probe_report(App& a, const char* what);
void layout_click(GtkWidget* b);
void layout_probe_next(App& a, int step, guint delay_ms);
gboolean layout_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