Files
udpscope/src/ui_app.hpp
T
Martino Ferrari c38630643d test: the noise probe reads the de-noise node, and the math probe builds one
The probe gains a staged chain rather than one phase, and the schedule moves with it:
`UDPSCOPE_PROBE_NOISE_MS` now times the *setup* stage, which installs the de-noise graph through
`apply_math_channels()` -- a structural rebuild that replaces the buffer and drops its history --
five seconds before anything scans it. The legs and the five measurements then run at about
10.5 s, and the reading that says the median rejected every impulse at 15.5 s, so the documented
run needs a 20 s mock and an 18 s exit.

The de-noise channel is installed **disabled** and enabled only at 13 s, for its own reading. An
acquired channel is a share of the ring, so a fifth one shortens every channel's retained span --
and the six `BufferSearch` leg counts and `pkpk bare` are all peaks and counts *of that window*.
Installing it enabled (as the plan had it) moved `bare` from 49 to 21, `ratio` from 7.00 to 3.00
and `pkpk bare` from 2.988 to 2.488, i.e. it would have rewritten the older feature's documented
proof to record a regression that is not one. With it enabled only at the end, every reading from
Tasks 4 and 8 is bit-for-bit what the doc records, and the new line reads
`denoise raw=2.987696/1 denoised=2.000000/1 ratio=0.669 channels=1` -- `denoised` exactly the
clean sine's 2.0, which is the claim the node makes.

Two other findings, both folded in: a derived channel whose source is switched off through the
panel's checkbox follows it off and does *not* come back when the source returns (the enable-all
probe's last act switches `sine` off at ~3 s), and the probes reload leftover math channels from
`$XDG_CONFIG_HOME/udpscope`, which is why a `setup channels=` count above 1 means an earlier run's
session, not a defect.

`math_probe` gains a third channel with a `DeNoise` node so the editor's median row is built and
synced on a baseline run: `3 math channels on 'sine' and 'noise'`, every other line as before,
`math cost: 103.3 ns/slot` (one reading of the same path).

verification.md: the recipe, and why the two stages may not be collapsed -- a probe that rebuilt
the buffer under its own legs reads `bare=0` and looks like a broken comparator. Also that the
noise and math probes may not share a run: both write `app.math`.
2026-10-13 16:01:44 +02:00

1438 lines
75 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 "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 <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
};
struct App {
ScopeModel* model = nullptr;
Acquisition* acq = nullptr;
RowSet* rows = nullptr;
GtkWidget* window = nullptr;
GtkWidget* plot_box = 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;
GtkWidget* trigger_revealer = 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_toggle = nullptr;
GtkWidget* signal_revealer = nullptr;
GtkWidget* signal_panel = nullptr;
GtkWidget* side_box = nullptr;
GtkWidget* arm_button = nullptr;
GtkWidget* trigger_signal_drop = nullptr;
GtkWidget* sidebar_toggle = nullptr;
GtkWidget* side_scroll = 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
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;
/// 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;
/// 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 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 pane: revealed with the toggle, and 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_revealer = nullptr;
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_revealer = nullptr;
GtkWidget* measures_list = nullptr;
GtkWidget* measures_toggle = nullptr;
// --- math channels --------------------------------------------------------
/// The app's copy of the math channels, and the source of truth for the pane. Every change goes
/// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own
/// copy is what the producer evaluates.
std::vector<MathChannel> math;
GtkWidget* math_toggle = nullptr;
GtkWidget* math_revealer = nullptr;
GtkWidget* math_list = nullptr;
/// 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.
plot::Range measure_window{};
bool measure_window_valid = false;
bool measure_was_held = 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
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;
bool measures_revealed = false;
/// 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;
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;
/// 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;
guint layout_save_source = 0;
bool layout_loaded = false;
unsigned signal_generation = ~0u;
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;
bool signals_panel_initialised = 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;
/// 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_visible = -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;
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;
int dbg_signal_revealed = 0;
int dbg_trigger_revealed = 0;
int dbg_side_visible = 0;
int dbg_trigger_items = 0;
int dbg_trigger_selected = -1;
int dbg_side_scroll_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_toggles_visible = 0;
int dbg_trigger_revealed_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;
/// 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:
MeasureWindowReader(App& app, std::size_t channel) {
const RowSet::Row& r0 = app.rows->row(0);
if (r0.plot == nullptr) return;
const plot::Range view = r0.plot->x_range(r0.axis_x);
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(), view.min);
const auto hi = std::upper_bound(st.begin(), st.end(), view.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(view.min - epoch_, view.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;
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* 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;
constexpr int kEventThumbH = 46;
inline int g_stats_phase = 0;
inline std::vector<std::uint64_t> g_stats_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);
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);
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);
void on_go_live(GtkButton*, gpointer user);
plot::u32 measure_color(std::size_t index);
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_math_panel(App& app);
void rebuild_math_pane(App& app);
GtkWidget* build_measures_panel(App& app);
GtkWidget* build_cursors_panel(App& app);
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
void on_signals_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);
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);
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);
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 measure_probe(gpointer user);
gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user);
gboolean stats_probe(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);
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);
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);
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
RowSet& rows);
} // namespace ui_impl
} // namespace udpscope