Files
udpscope/src/ui_app.hpp
T
Martino Ferrari 8b2dae7655 fix: the overlaid sidebar steps aside for a drag, and the compact view follows the trigger
Two reports, both real, and neither was about the lock itself.

**A drag out of an overlaid sidebar had nowhere to land.** In a window too narrow to show both columns,
`AdwOverlaySplitView` puts the sidebar *on* the plot — that is what `collapsed` means — so the pointer
stays over the list and never over a drop target: a signal could be dragged but not dropped, which reads
as "dragging does not work". `row_drag_sidebar()` now hides the sidebar on `drag-begin` and restores it on
`drag-end`, and only when collapsed: side by side the plot is already visible, and hiding the list the drag
started in would be startling. The reading begins a drag through the *source* with the state forced
collapsed, the way the sidebar probe forces it:

    pane drag: sidebar hidden for the drag=1 restored after it=1

**The compact view did not follow the trigger switch.** The locked line's first branch reads
`tc.enabled` — with trigger mode off it says `stream (trigger off)` and nothing else — and
`on_trigger_toggled()` never re-synced it, so the line kept saying whichever it had been. One call to
`sync_trigger_summary()` beside the switch sync. The dump's two `panes:` lines were already the test and
now pass it: with a session whose trigger is off, the line as found reads `stream (trigger off)` and the
live one reads the settings, because the exit probe turns the trigger on in between. Both reading `stream`
was the defect.

Verified: the two lines above; `panes: the same line as the probe found it: 'mode: stream (trigger off)'`
against the live `'signal: square | condition: window, rising | level: 0.3 .. 0.6 | mode: normal'`;
`make test-ui` 14 files; zero `Gtk-CRITICAL`. Docs: the `pane drag:` reading and the invariants bullet for
the overlay, and the `panes:` pair's second job — the refresh test — written down so the next reader knows
what both lines reading `stream` means.
2026-10-14 15:04:10 +02:00

1623 lines
87 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;
/// 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 sidebar's tab stack and its switcher. The stack is what chooses which panels are on
/// screen; a feature's own on/off never does (it sets its panel's sensitivity instead).
GtkWidget* side_stack = nullptr;
GtkWidget* side_switcher = nullptr;
/// The page name the stack is showing, and the one value the session file persists about the
/// sidebar. "signals" on a first run.
std::string side_tab = "signals";
GtkWidget* arm_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
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 two sidebar pages' own icons and whether the theme resolved them, read on the first tick
/// like the two lines above.
std::string dbg_page_icons;
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The label
/// count is the one that says the pages are icons rather than words: `narrow` hides them, and the
/// titles stay set because they are the pages' names for a screen reader and for the tooltip.
int dbg_switcher_pills = -1;
int dbg_switcher_labels = -1;
int dbg_switcher_names = -1;
/// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what
/// keeps its 2px bars crisp.
int dbg_switcher_icon_w = -1;
int dbg_switcher_icon_h = -1;
/// The switcher's own size, beside it: the pills carry 32px icon boxes now, so the tab bar's height
/// is what it costs.
int dbg_switcher_w = -1;
int dbg_switcher_h = -1;
/// 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;
int dbg_switcher_policy = -1;
/// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped.
bool side_tabs_named = false;
/// 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;
/// True while a drag has the overlaid sidebar hidden: what it was showing before, so `drag-end` puts it
/// back rather than guessing.
bool sidebar_hidden_for_drag = 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 two panes' modes. *Locked* is the one a returning user gets: the trigger pane shows a line
/// that says what it is set to, the signal list shows only the signals that are on, and neither
/// offers a 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 flags last for the
/// session: a lock is a mode, not a setting, so it is not persisted.
bool trigger_locked = false;
bool signals_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] = {};
GtkWidget* trigger_edit = nullptr;
GtkWidget* signals_edit = nullptr;
int dbg_trigger_locked = -1;
int dbg_signals_locked = -1;
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.
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;
/// 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;
/// 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;
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;
/// 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;
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_page_current = 0;
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 channels 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`: `rows - 1`, the divider not being draggable.
int dbg_signal_drag_sources = -1;
/// The drag-with-an-overlaid-sidebar pair: the sidebar hid for the drag, and came back after it.
int dbg_drag_hid_sidebar = -1;
int dbg_drag_restored_sidebar = -1;
std::size_t dbg_signal_plain = 0;
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_switcher_visible = 0;
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:
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;
/// 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 tab-switch probe: the accumulator counts recorded while the Measure page was
/// still current, so the report can say whether any of them advanced afterwards.
inline std::vector<std::uint64_t> g_side_tab_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);
/// The `MATH` divider row in the one channel list, and the `+` it carries.
GtkWidget* make_math_divider(App& app, bool with_add);
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);
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);
void on_side_tab_changed(GObject*, GParamSpec*, gpointer user);
void select_side_tab(App& app, const char* name);
/// 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);
/// Hides the sidebar for the length of a drag when it is *overlaid*, and puts it back after. With the
/// split view collapsed the sidebar covers the plot, so a drag that starts in the signal list has nowhere
/// to land: the pointer is over the sidebar, not over a drop target.
void row_drag_sidebar(App& app, bool dragging);
/// The compact line the trigger pane shows while it is locked, and the lock itself: the panes'
/// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and
/// `signals_locked`.
void sync_trigger_summary(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 measure_probe(gpointer user);
gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user);
gboolean side_tab_probe_report(gpointer user);
gboolean side_tab_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