// 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 #include "acquisition.hpp" #include "build_info.hpp" #include "capture.hpp" #include "config.hpp" #include "event_store.hpp" #include "export.hpp" #include "help_window.hpp" #include "layout_store.hpp" #include "measure.hpp" #include "model.hpp" #include "plot_host.hpp" #include "search.hpp" #include "settings.hpp" #include "source/source_config.hpp" #include "ui_prefs.hpp" #include "shortcuts.hpp" #include "status_text.hpp" #include "waveform.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace udpscope { namespace ui_impl { enum class EventGroupKind { None, ///< nothing: streaming, one event replayed, or a file opened Compare, ///< two events in two plots side by side, at one scale, with synchronised axes Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness }; // --- the guided tour, shared by both windows (ui_tour.cpp) ------------------- // // The tour is *two* tours now -- the main window's and the source chooser's -- because the chooser is // the window a first run meets first and the surfaces it must explain are its own. The machinery is // one implementation: a `TourView` is the overlay children (the four dim rectangles, the spotlight // outline and the card) plus where the walk stands in it, and either window holds one. What differs // is only the *stops*: a stop's target is a widget the owner resolves, so each window keeps its own // table (the chooser runs before `App` exists and could not name one of `App`'s fields). /// One stop of a guided tour: the widget to spotlight, the page it stands on, and the two paragraphs /// of text beside it. The target is resolved when the tour starts, so the table may name widgets that /// do not exist yet. struct TourStop { GtkWidget* target; /// The page the target lives on, or nullptr when the stop needs none. What a page *is* depends on /// the view: a named page of its `page_stack` (the chooser), or whatever its `on_page` hook opens /// (the main window: a slide-over page id). The tour selects it as the stop is entered, so a target /// on a page the user has left is made visible instead of being walked over or spotlighted where it /// is not. A stop with no page *closes* what an `on_page` hook opened. const char* page; const char* title; const char* body; }; /// A tour's widgets and its place in the walk. `hole` is the spotlight's rectangle in the overlay's /// own coordinates, kept from the last frame that could measure the target: a frame can come between /// two layout passes, where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied /// there would make the spotlight and the card jump. struct TourView { GtkWidget* overlay = nullptr; GtkWidget* dim[4] = {}; GtkWidget* spot = nullptr; GtkWidget* card = nullptr; GtkWidget* title = nullptr; GtkWidget* body = nullptr; GtkWidget* counter = nullptr; GtkWidget* back = nullptr; GtkWidget* next = nullptr; int hole[4] = {0, 0, 0, 0}; bool running = false; int index = 0; std::vector plan; /// The stack whose pages the stops' `page` names select. A window has exactly one such surface (the /// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk /// begins, when the widgets exist; null leaves every stop on the page it finds. GtkWidget* page_stack = nullptr; /// The alternative to `page_stack` for a surface that is not a stack: when set it *replaces* the /// stack mechanism for this view, and is called with each stop's `page` as the stop is entered -- /// `nullptr` meaning "no page needed" -- and with `nullptr` as the walk ends. The main window's opens /// and closes the slide-over through it; the chooser keeps `page_stack`. void (*on_page)(void* user, const char* page) = nullptr; void* page_user = nullptr; /// Optional, with `on_page`: whether the surface `on_page` opened has finished appearing. A stop that /// names a page is not spotlit while this answers false -- a panel that is still sliding in is a /// target that moves, and a spotlight that follows it for 120 ms reads as a glitch. Null means "ready". bool (*page_settled)(void* user) = nullptr; /// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour /// on to the application through `Config::tour_pending`. void (*on_done)(void* user) = nullptr; void* done_user = nullptr; }; /// One capability panel a source kind contributed to the slide-over. `content` is the kind's own widget /// (made insensitive while the source is disconnected); the page wrapping it is named `name`. struct SourcePanelPage { std::string name; ///< slide-over page id: "source-" + the kind's panel id std::string title; ///< the slide-over page's title, and the segmented switch's label GtkWidget* content = nullptr; }; /// The slide-over panel's widgets and state (src/ui_slideover.cpp). `page` is the open page's id, empty when closed. struct SlideOver { GtkWidget* overlay = nullptr; ///< the GtkOverlay around the plot (main-window.ui `plot_overlay`) GtkWidget* revealer = nullptr; ///< the one overlay child; halign END, valign FILL, 360 px GtkWidget* title = nullptr; ///< GtkLabel in the panel header GtkWidget* stack = nullptr; ///< GtkStack: one scroller per page GtkWidget* source_switch = nullptr; ///< AdwToggleGroup, shown on a source page when there are >= 2 std::vector source_pages; std::string page; bool syncing = false; ///< true while we write the toggle group ourselves }; constexpr int kSlideOverWidth = 360; /// One badge: a button with a name and a state line. `page` is the slide-over page it opens. struct Badge { GtkWidget* button = nullptr; GtkWidget* name = nullptr; GtkWidget* state = nullptr; std::string page; std::string signal_name; ///< a signal badge: the signal it stands for; empty for the others std::size_t signal = 0; ///< a signal badge: its index in `ScopeModel::signals()` when it was built }; /// The most signal badges the bar shows; beyond that one "N signals" badge stands for them all. inline constexpr std::size_t kMaxSignalBadges = 6; /// The badge bar under the plot (src/ui_badges.cpp). struct BadgeBar { GtkWidget* bar = nullptr; ///< main-window.ui `badge_bar`, fixed height GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it GtkWidget* system_slot = nullptr; ///< right: Trigger, Source Badge trigger; Badge source; ///< opens the first source page; a read-out when the source has none GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+" /// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds /// a pointer to its `Badge`, which must not move when the vector does. std::vector> signal_badges; Badge overflow; ///< the "N signals" badge, used only beyond `kMaxSignalBadges` Badge plus; ///< the "+" in the signals slot: opens the `signals` page }; struct App { ScopeModel* model = nullptr; Acquisition* acq = nullptr; RowSet* rows = nullptr; GtkWidget* window = nullptr; GtkWidget* plot_box = nullptr; /// The `GtkOverlay` around `plot_box` (`main-window.ui`), which the slide-over panel is a child of. GtkWidget* plot_overlay = nullptr; SlideOver slideover; /// The badge bar under the plot (`main-window.ui` `badge_bar`, with its two slots). BadgeBar badges; GtkWidget* badge_bar = nullptr; GtkWidget* badge_signals = nullptr; /// Signal names in the order the user left the badges (memory only; names not in it sort last). std::vector badge_order; GtkWidget* badge_system = nullptr; /// The one GL area, drawing the `RowSet`'s figure: every row is a row of that /// figure, so there is one widget, one context and one `::render` whatever the row /// count is. GtkWidget* area_widget = nullptr; plot::gtk4::PlotArea* area = nullptr; /// The GL area wrapped in a `GtkOverlay`, which is what the plot box actually holds. /// The layout editor's buttons are overlay children: each is a small widget placed by /// its margins over the row it acts on, so it intercepts pointer events only where it /// is -- a single transparent child covering the area would swallow every pan, drop and /// hover the editor is not interested in. GtkWidget* area_overlay = nullptr; /// On while the layout editor is up. Nothing else about the plot changes: the editor is /// a set of overlay buttons, not a mode the figure knows about. bool layout_editing = false; GtkWidget* layout_toggle = nullptr; /// The editor's buttons, in the order they were added, so they can be taken off the /// overlay again. Rebuilt when the row count changes, repositioned when the rows move. std::vector 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 layout_drag_start; /// The editor's way out: one `Done` button in the plot area's top-right corner while /// the editor is up. Not part of `layout_buttons`, which is one button per row and per /// boundary -- the count the layout probe asserts. GtkWidget* layout_done_button = nullptr; /// The row count and figure geometry the buttons were placed for. Their positions are /// restated only when one of these moves: a `gtk_widget_set_margin_top()` per frame is a /// relayout per frame, and overlay children are measured out of the main child's /// allocation, so the churn would reach the plot. int layout_buttons_rows = 0; /// Which step of the layout probe runs next (`UDPSCOPE_PROBE_LAYOUT_MS`). The steps are /// a frame apart, because the buttons are placed from the rects the last frame was laid /// out with, so the sequence cannot be one function. int dbg_layout_step = 0; /// Which step of the cursors probe runs next (`UDPSCOPE_PROBE_CURSORS_MS`). int dbg_cursors_step = 0; /// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`). int dbg_fit_step = 0; /// Which step of the x-label-form probe runs next (`UDPSCOPE_PROBE_TIMELABELS_MS`). int dbg_timelabels_step = 0; /// The cursors probe's own bookkeeping: the stored time before a gestureless move, so the /// report can assert the move was ignored. double dbg_gestureless_before = 0.0; /// What `PlotArea::create()` is handed. The area is in figure mode from the start and /// never draws this, but it keeps a reference to the plot it was created with (its /// single-plot controller holds one) -- and every plot of the figure is destroyed by a /// rebuild, so handing it a row would leave that reference dangling. plot::Plot placeholder_plot; GtkWidget* status_label = nullptr; /// The activity badge: one glyph whose *colour* is the whole signal, so that blinking /// it cannot change its width and drive a relayout (see the status label's comment). GtkWidget* status_badge = nullptr; /// What the badge was last set to (`BadgePhase`), so the markup is rewritten on a /// change of phase rather than on every one of the 60 frames a second. int badge_phase = -1; /// The display-window control: a header-bar button whose label is the current window /// and whose popover holds the number, its unit and the two decade steppers. The label /// is width-locked (see `kWindowLabelChars`) because the header bar sits above the plot /// and a label that grew with the text would relayout everything under it. GtkWidget* window_button = nullptr; /// The span the control shows, pinned to `kWindowLabelChars` and ellipsized: the bar sits above the /// plot, so a label that grew with its text would relayout everything under it on a change of window. GtkWidget* window_label = nullptr; GtkWidget* window_entry = nullptr; GtkWidget* window_unit_drop = nullptr; /// The unit `sync_window_display()` last wrote into the dropdown. `GtkDropDown`'s /// `notify::selected` arrives *after* the call that caused it returns, so the /// `updating_panels` flag is already clear by then and cannot tell our own write from the /// user's pick; the entry the sync wrote is rounded for reading, so re-parsing it made the /// rounding the window (a 500 s request came back as 499.98 s). A selection that matches /// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it. int window_sync_unit = -1; GtkWidget* trigger_toggle = nullptr; /// The trigger panel's own box, kept so the defaults probe can read its spin buttons without /// walking the window for them. GtkWidget* trigger_panel = nullptr; GtkWidget* signal_panel = nullptr; GtkWidget* side_box = nullptr; /// The dock: the sidebar's one scroller and the box inside it, which holds the cursors and /// the measurements (`dock_show_section()` scrolls it). A feature's own on/off never /// removes a section (it sets its panel's sensitivity instead). GtkWidget* dock_page = nullptr; GtkWidget* dock_scroller = nullptr; GtkWidget* arm_button = nullptr; /// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post /// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has /// (sensitivity, from trigger mode), so the two buttons beside each other cannot disagree about /// whether the trigger has an engine at all. GtkWidget* force_button = nullptr; GtkWidget* trigger_signal_drop = nullptr; GtkWidget* sidebar_toggle = nullptr; GtkWidget* trigger_edge_drop = nullptr; GtkWidget* trigger_type_drop = nullptr; GtkWidget* trigger_cmp_drop = nullptr; GtkWidget* trigger_width_spin = nullptr; GtkWidget* trigger_width2_spin = nullptr; GtkWidget* trigger_level2_spin = nullptr; GtkWidget* trigger_timeout_spin = nullptr; /// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can /// hide them. A hidden row keeps its widget's value, so switching condition and back /// does not lose what was typed. GtkWidget* trigger_cmp_row = nullptr; GtkWidget* trigger_width_row = nullptr; GtkWidget* trigger_width2_row = nullptr; GtkWidget* trigger_level2_row = nullptr; GtkWidget* trigger_timeout_row = nullptr; GtkWidget* trigger_mode_drop = nullptr; GtkWidget* trigger_level_spin = nullptr; GtkWidget* trigger_pre_spin = nullptr; GtkWidget* trigger_holdoff_spin = nullptr; GtkWidget* trigger_band_spin = nullptr; ///< EventCondition::noise_band GtkWidget* trigger_hf_spin = nullptr; ///< EventCondition::hf_fc, 0 is off /// The persistence spin: how many of the most recent triggered acquisitions stay on screen, drawn /// dimmer with age (1 = the event alone, which is the display without the feature). Its row is held /// for a `.ui` that lost it, as the other rows are. GtkWidget* trigger_persist_spin = nullptr; GtkWidget* trigger_persist_row = nullptr; /// What this frame's persistence plan drew, against what the depth asked for: `sync_layers()` /// returns the former and fills the latter, and the two differ exactly when a row had no free /// slot left for a layer or the retention budget refused an acquisition. Read by the exit dump and /// by the persistence probe, and what the "only some layers fit" notice is keyed on. int persist_want = 0; int persist_placed = 0; /// The last plan a notice was raised for, so a plan that stays short says so once rather than /// every frame. int persist_noted_depth = 0; int persist_noted_want = -1; int persist_noted_placed = -1; /// The persistence probe's own state: the depth its first leg asks for (kept between steps, /// because the steps are separate `GSourceFunc`s), how many polls it has spent waiting for the /// retention to fill, and whether that set has stopped growing (which is how a budget that keeps /// fewer acquisitions than the depth asks for still gets a report). int dbg_persist_depth = 4; int dbg_persist_polls = 0; std::size_t dbg_persist_last_kept = 0; int dbg_persist_stable_polls = 0; bool dbg_persist_settled = false; /// The phosphor density's controls -- the trigger pane's *Advanced* block: the switch, the /// dropdown that chooses what the image fades by, and the two parameter spins, only one of which /// is on screen at a time (`sync_trigger_rows()` states that, as it does for the condition rows). /// The rows and the expander are held too, so the probe can read what the `.ui` holds and the dump /// can walk into the block -- an expander's child is *not* in the widget-parent chain. GtkWidget* trigger_density_switch = nullptr; GtkWidget* trigger_density_decay_drop = nullptr; GtkWidget* trigger_density_tau_spin = nullptr; GtkWidget* trigger_density_factor_spin = nullptr; GtkWidget* trigger_density_row = nullptr; GtkWidget* trigger_density_decay_row = nullptr; GtkWidget* trigger_density_tau_row = nullptr; GtkWidget* trigger_density_factor_row = nullptr; GtkWidget* trigger_advanced = nullptr; /// A new acquisition has reached the screen and has not been deposited into the density grid yet. /// Set where a snapshot is *framed* -- the held-event branch of the tick -- and when the display /// is switched on under an acquisition already on screen, and consumed by the next tick's /// `apply_density()`: the deposit needs the plot laid out for the acquisition it deposits, and /// that layout is the frame's, one frame after the bind. bool density_pending = false; /// The previous tick's clock (`g_get_monotonic_time()`), for the density's per-frame fade; 0 /// before the first frame, which is what makes the first frame's `dt` zero rather than since-boot. gint64 density_last_us = 0; /// Whether the density was on the last time the tick looked, so switching it on under an /// acquisition already on screen deposits that one at once instead of waiting for a latch that /// `single` mode may never send. bool density_was_on = false; /// What the last tick left on screen, for the dump: the rows the density was on, the total ink /// and the peak over them, and the bytes the last *frame* uploaded. Captured on the tick because /// the dump runs after the window is gone. /// The **last tick the density was on**, captured there and not read back from the plots at /// exit: the dump runs after the window is gone and after the exit probe has switched the trigger /// off, which turns the display off with it -- a dump that asked the plots would say `rows=0` /// about a run whose whole tail had the image up. int dbg_density_rows = 0; int dbg_density_traces = 0; unsigned long long dbg_density_cells = 0; unsigned long long dbg_density_grid_bytes = 0; double dbg_density_ink = 0.0; double dbg_density_peak = 0.0; /// The most any one frame **uploaded** of the grid: one byte a cell, so this is the resolution /// read back as the frame's own cost. A maximum rather than the last value, because the exit /// probe changes the display before the dump runs and the last painted frame is then not a frame /// of this display. long long dbg_density_upload_bytes = 0; /// How many ticks it was on for: 0 is "no image was ever drawn", which is what separates a real /// reading from a default. int dbg_density_frames = 0; /// The frame-cost accumulators as of the tick the probe switched the display on, so the cost can /// be read **before and after** it inside one run -- the same stream, the same layout, the same /// mock. Two runs would differ in everything but the density. double dbg_density_tick_sum0 = 0.0; int dbg_density_tick_n0 = 0; double dbg_density_draw_sum0 = 0.0; int dbg_density_draw_n0 = 0; /// The density probe's own state: the time constant its second half uses, and how many polls it /// has spent waiting for an event to be on screen. double dbg_density_tau = 1.0; int dbg_density_polls = 0; /// The density probe's step, so its sequence is a state machine rather than nested timers. int dbg_density_stage = 0; GtkWidget* signal_list = nullptr; GtkWidget* pause_button = nullptr; /// When the pause began, on the **monotonic wall clock**, or 0 while running. The elapsed time /// the status bar shows has to be wall time rather than stream time: the producer never stops /// while paused (it keeps filling the live ring, which is what makes the resume seamless), so /// the stream's own clock advances whether the view is frozen or not and would say nothing /// about how long the user has been looking at a still picture. gint64 pause_started_us = 0; /// The buffer search. Driven a few milliseconds at a time from the frame tick, because its /// samples live behind the model's mutex and the producer needs it back between chunks. BufferSearch search; GtkWidget* search_run_button = nullptr; GtkWidget* search_cancel_button = nullptr; GtkWidget* search_status_label = nullptr; GtkWidget* search_pos_label = nullptr; /// "16 of 38 marks shown" -- the pool is smaller than a scan's result, and a display that is /// showing a sixth of what it found has to say so. GtkWidget* search_marks_label = nullptr; GtkWidget* search_prev_button = nullptr; GtkWidget* search_next_button = nullptr; /// The expander the block lives in, so the probe can read its collapsed state when it reads the /// header hint. GtkWidget* search_exp = nullptr; /// Set true when the last `search_jump()` produced an empty cut — the hit was so close to the /// oldest retained sample that `publish_pause_view()` held nothing. Cleared at every jump. bool search_jump_empty = false; GtkWidget* menu_button = nullptr; GtkWidget* cursors_toggle = nullptr; /// --- the help affordance layer ------------------------------------------- /// /// The overflow menu's two views. The popover's child is a `GtkStack` holding both, and the /// *visible child* is the view: swapping the stack rather than rebuilding the box is what keeps /// `layout_toggle` — which points into the main view and is read by the tick — a live widget. GtkWidget* menu_stack = nullptr; GtkWidget* menu_main_view = nullptr; GtkWidget* menu_help_view = nullptr; /// The Help view's own rows, so the probe can read them and drive them without walking the popover: /// the two existing dialogs' entry points and the two new actions. GtkWidget* menu_shortcuts_row = nullptr; GtkWidget* menu_docs_row = nullptr; GtkWidget* menu_tour_row = nullptr; /// The two navigation rows: the main view's door into the Help view, and the Back row out of it. GtkWidget* menu_help_row = nullptr; GtkWidget* menu_back_row = nullptr; /// "Enable contextual help": a stateful menu row whose check *is* the setting. GtkWidget* contextual_toggle = nullptr; /// The "?" each surface carries, one per row of the table in `ui_context_help.cpp`. These are the /// heading rows the buttons join — not the surfaces themselves — so a hint is a sibling of a /// title rather than an overlay on top of a control. `math_head` only exists while the math /// editor is open, which is why the row is skipped when it is null. GtkWidget* trigger_head = nullptr; GtkWidget* cursors_head = nullptr; GtkWidget* measures_head = nullptr; GtkWidget* signals_head = nullptr; GtkWidget* events_head = nullptr; GtkWidget* math_head = nullptr; /// The status line's row, and the header bar's slot for the window control: the two surfaces that /// are not a panel with a heading. GtkWidget* status_row = nullptr; GtkWidget* window_control_slot = nullptr; /// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits /// beside the thing it explains -- the "record events" switch, the pre-trigger share row, and the /// signals pane's hint line (whose box exists so a hint can join it beside the text). GtkWidget* trigger_record_row = nullptr; GtkWidget* trigger_pre_row = nullptr; /// The search block's expander *header* -- the box the contextual help's "trigger_search" hint joins /// as its last child. The header, not the block: the expander's child is out of the widget-parent /// chain until the block is expanded, so a hint there is invisible while it is collapsed. GtkWidget* trigger_search = nullptr; GtkWidget* signals_note = nullptr; /// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the /// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the /// steps whose target was on screen when the tour started, so a surface that is not there is not /// walked over. The machinery is shared with the source chooser's tour -- see `TourView`. TourView tour; /// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump. /// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before". bool tour_auto_started = false; /// The chooser's own "Guided tour" request (`Config::tour_pending`), recorded when its timer runs. /// A separate flag from the auto-start because it is a *different* thing: the user asked for the /// tour before the window existed, rather than never having seen it. bool tour_requested = false; /// The self-check's reading of the manual's launch (`UDPSCOPE_PROBE_HELP_MS`). The launch is /// asynchronous and the viewer is another process, so the outcome arrives in a callback and the /// probe prints it from a timer afterwards; `attempted` is set at the call, because a launch that /// never called back is itself a reading. bool dbg_help_launch_attempted = false; bool dbg_help_launch_done = false; bool dbg_help_launch_ok = false; std::string dbg_help_launch_error; /// The help layer's own dump readings, captured on the tick while the window is alive (nothing /// here may be read in the dump -- see `docs/claude/verification.md`). std::string dbg_context_help; std::string dbg_help_figures; int dbg_tour_steps = -1; int dbg_tour_step = -1; int dbg_tour_visible = -1; /// Which step of the help-affordance probe runs next (`UDPSCOPE_PROBE_HELPUI_MS`), and how many /// event controllers the window carried before the tour opened -- the reading that says the tour /// left nothing behind. int dbg_helpui_step = 0; int dbg_helpui_controllers = -1; /// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: which step of that probe runs next. int dbg_tour_surface_step = 0; /// Where the panel controls ended up, decided once at build time: the dump runs after the window /// is destroyed and cannot ask GTK anything about a widget. std::string dbg_panel_controls; /// The icon each toolbar button carries, and whether the theme resolved it. Recorded at the /// same moment and for the same reason, but it guards a different thing: four of the icons are /// drawn for this app and shipped inside the binary, and a resource path that never got /// registered is *silent* -- GTK substitutes a missing-image placeholder, the button still /// works, and every other line of the dump still passes. std::string dbg_toolbar_icons; std::string dbg_app_icons; /// The panes' own switches and the header bar's buttons, read together by the exit probe: the two /// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button. int dbg_panel_switch_trigger = -1; int dbg_toolbar_switch_trigger = -1; int dbg_panel_switch_cursors = -1; int dbg_toolbar_switch_cursors = -1; int dbg_panel_drove_toolbar = -1; /// Both panes' switches being *sensitive* while their panes are off -- read before anything turns /// one on. It is the whole point of putting them in the heading: a control inside an insensitive /// parent cannot be clicked, so a switch that went insensitive with its own pane could never turn /// it back on. int dbg_panel_switch_live = -1; /// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one /// thing in the body that *is* disabled with the mode. int dbg_arm_sensitive = -1; /// The header bar, so the dump can report the order the controls are in: a header bar is a /// *titlebar*, not a child of the window, so walking down from the window does not reach it. GtkWidget* toolbar = nullptr; GtkWidget* cursors_reset = nullptr; /// The cursors panel's root, kept because cursors-off disables it rather than hiding it. GtkWidget* cursors_panel = nullptr; /// The panes' *own* switches, in their header rows, and the panel bodies those switches disable. /// The switch and the header bar's button are one control with two places to click: each runs the /// same action, and `sync_panel_switches()` re-reads both from the toolbar's state after any /// change, so they cannot disagree. GtkWidget* trigger_switch = nullptr; GtkWidget* cursors_switch = nullptr; GtkWidget* trigger_body = nullptr; /// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own /// `notify::active`, and without this the program's own write would come back as a click. bool panel_switch_sync = false; /// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the /// file stores a *name* and the trigger needs an index into the acquired channels, which do not /// exist until the stream has been described. Empty means "nothing pending". std::string pending_trigger_channel; /// The trigger pane's mode. *Locked* is the one a returning user gets: the pane shows a line that /// says what it is set to and offers no control that changes the configuration. A first run -- no /// session file for this source -- starts in configuration mode, because that is what a first run is /// for. The flag lasts for the session: a lock is a mode, not a setting, so it is not persisted. /// (The signals have no lock: the badges and the signal pages are direct manipulation.) bool trigger_locked = false; /// The locked trigger pane's content: a small definition list, because one line cannot hold the /// signal, the condition, the level and the mode, and those are what a user reads the pane for. GtkWidget* trigger_summary = nullptr; static constexpr int kTriggerSummaryRows = 8; GtkWidget* trigger_summary_keys[kTriggerSummaryRows] = {}; GtkWidget* trigger_summary_values[kTriggerSummaryRows] = {}; /// The compact line's arm control. It lives in the pane's *heading*, the part that stays on screen when /// the body is hidden, and `sync_arm_buttons()` states it: present only while trigger mode is on and the /// pane is locked, which is the one combination where the body's own Arm button has gone with the body. GtkWidget* trigger_summary_arm = nullptr; GtkWidget* trigger_edit = nullptr; /// The dim label of the `signals` page that says nothing is left to add. GtkWidget* signals_empty = nullptr; int dbg_trigger_locked = -1; int dbg_summary_arm_off = -1; ///< the compact line's arm control with the trigger *off* int dbg_summary_arm_visible = -1; ///< the compact line's arm control, locked and with the trigger on int dbg_summary_arm_sensitive = -1; std::string dbg_summary_arm_label; ///< its text, which is the armed state in the user's words int dbg_summary_arm_width = -1; ///< its allocation a frame later: 0 means "not laid out" int dbg_trigger_body_visible = -1; /// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it /// is read rather than assumed: empty here is a locked pane showing nothing at all. std::string dbg_trigger_summary; /// The same line as the exit probe found it, captured before the probe touches the trigger. The locked /// pane says one thing about a trigger that is running and another about one that is not, and only a run /// whose session has it off shows the second -- so the dump carries both rather than assuming either. std::string dbg_trigger_summary_early; /// The two [edit] buttons' icons, read in each mode by clicking into it and back: a button whose icon /// does not resolve is *blank*, and that is how the missing "done" icon was found. std::string dbg_pane_edit_icon; std::string dbg_pane_view_icon; /// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY /// where rulers are up, and in `mix` mode the value each cursor crosses each trace at. GtkWidget* cursors_mode_drop = nullptr; GtkWidget* cursors_body = nullptr; /// One line per row (rulers) and one per bound trace (`mix`), in the order the body /// was built with. The texts are rewritten each frame; the widgets are rebuilt only /// when the rows or the mode change. /// One row of the pane's table: a `who` cell and the three value cells, so the numbers /// line up as columns the way a table does. struct CursorLine { GtkWidget* label = nullptr; ///< the `who` cell (a chip's markup, or text) GtkWidget* a = nullptr; GtkWidget* b = nullptr; GtkWidget* d = nullptr; int row = -1; int slot = -1; }; std::vector 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 measures; std::vector 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 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 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 measure_text; /// The window the current results were computed over, and whether a capture was held then: /// a recomputation is owed when either moves (see `update_measures`). /// /// It is kept in **stream** seconds (through `ScopeModel::stream_at_x0()`), not in the display x /// it was read as, because a live view is newest-relative: the axis there is a fixed /// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates /// and the pane would compute once and then never again -- the readout frozen on the samples /// that happened to be on screen when the pane opened. /// /// **Which** window that is depends on the view: the viewport while the view is live, and the /// **pin** (`RowSet::pinned_range()`, captured in `measure_pin_window` below) while it is /// pinned. A still frame is measured as a frame, so a gesture inside the pin moves neither this /// nor any number. plot::Range measure_window{}; bool measure_window_valid = false; bool measure_was_held = false; /// The window **on screen**, in the same stream seconds, as of the last *tick* -- which is the /// only thing `measure_window` is comparable with. Captured here rather than read back in the /// dump for the reason every other dump field is (see verification.md): the dump runs from the /// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by /// then answers for a mode no frame was ever drawn in -- which read as a 45000 % drift on a run /// whose every frame was exact. /// /// While the view is pinned this and `measure_window` are *allowed* to differ: the pane /// describes the pin, and a gesture may have narrowed what is on screen to a slice of it. plot::Range measure_view_window{}; /// The pin's own extent in the same stream seconds, and whether the view was pinned when the /// tick read it. This is the window a pinned view's results describe, so the dump measures its /// drift against it -- otherwise a zoomed held event would read as a pane describing a window /// the plot has left, which is precisely what it must do there. plot::Range measure_pin_window{}; bool measure_pin_valid = false; /// What the two walks over one channel's visible window produced, shared by every measurement /// on that signal: the window's statistics, and one accumulator per *distinct level* any of /// them asked for. Several measurements on one signal must cost two walks, not two each -- and /// the window itself is never stored, which is what the chunked walks bought (see /// `MeasureChunk`). /// One channel's window, at one HF corner, with every level accumulator the pane asked for. /// /// Keyed by `(channel, fc)` and not by channel alone: the corner is a per-measurement setting, so two /// measurements on one signal at different corners are reading two different signals and cannot share a /// walk. struct MeasureWindow { int channel = -1; double fc = 0.0; ///< the HF corner this window was walked at; 0 is unfiltered MeasureStats stats; std::vector pct; ///< the level shares asked for, deduplicated std::vector noise; ///< each entry's band, as a percentage; parallel to `pct` std::vector level; ///< one accumulator per entry of `pct` MeasureFilter scan_filter; ///< pass one's state MeasureFilter acc_filter; ///< pass two's; a separate walk, so a separate state /// Some measurement on this window reads a flat level, so pass one folds the bucket /// histogram the flat-level types are answered from. Only then: a pane of means and extremes /// should not pay for a density estimate it never reads. bool flat_wanted = false; bool ok = false; }; std::vector 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 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 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 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 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 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 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 measure_marks_display; /// What the dump prints for the header and the panels. Captured while the window is alive: /// the dump runs from the destroy handler, where every widget call is a GTK-CRITICAL and /// reads nothing. int dbg_win_btn_w = -1; int dbg_bar_w = -1; /// The measurements pane's allocation, for the same reason and more urgently: the dump used /// to ask the widget itself, which is where the two documented `Gtk-CRITICAL`s came from -- /// and a critical is the lucky outcome. The pointer is freed memory by then, so a run that /// built and destroyed other windows first (`UDPSCOPE_PROBE_HELP_MS`, three of them) has had /// that memory reused and the read is a **SIGSEGV** in the middle of the dump. int dbg_measures_pane_w = -1; int dbg_measures_pane_h = -1; /// The measurement dialog while one is open (null otherwise). A probe needs a handle on it: /// it is a window of its own, so the main window's widget tree does not reach its buttons. GtkWidget* measure_dialog = nullptr; int dbg_measure_step = 0; /// The band-average probe's own step counter. Its own rather than the measurement pane's: the two /// probes add measurements, so they must not be scheduled in one run (see verification.md), and a /// shared counter would make that a silent mis-step rather than a documented prohibition. int dbg_bandavg_step = 0; int cursors_pane_mode = -1; int cursors_pane_rows = -1; unsigned cursors_pane_generation = ~0u; /// Which slots were *bound* when the pane was last built, one bit per row/slot /// (`kMaxRows * kSlotsPerRow` is 32). `mix` draws a line per bound trace, and a slot's /// channel is re-resolved by `bind()` every frame -- so the pane has to follow the bind /// and not just the model's generation, or a trace that appears without a structural /// change gets no line until something else rebuilds the table. std::uint32_t cursors_pane_bound = ~0u; /// The persisted-layout file this data source maps to (--server/--port), a /// debounced save handle, and whether the file has been applied once the /// CONFIG arrived. std::string source_host; std::uint16_t source_port = 0; /// The configuration of that source this run is on, and its note. A source may have several /// named sessions; this is the one its saves go to and the one the dialog marks as current. std::string config_name; std::string config_comment; /// The cache of captured events, and whether latches are being written to it. The store is created /// once the source is known (it is keyed on `-`, 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 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 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 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> sync_last_a; std::vector> 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 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 `_.toml` can say neither, and the startup /// chooser needs both to reconnect to a multicast source from a stored row. std::string source_multicast; std::string source_interface; /// The source kind and its settings, so the session can say which kind it belongs to, and the /// words that name the source in dialogs (`host:port` for UDPS, ` ` otherwise). std::string source_kind = "udps"; SourceConfig source_opts; std::string source_title; /// The panels the source kind added to the slide-over, in page order. Empty for UDPS and in file mode. std::vector source_panels; bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled guint layout_save_source = 0; bool layout_loaded = false; unsigned signal_generation = ~0u; /// Signal name -> the content box of its `signal:` slide-over page (src/ui_signal_page.cpp). std::map signal_pages; bool updating_panels = false; ///< guards signal handlers during a rebuild bool relayout_pending = false; /// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not /// exist until the area is realized, so the one-shot has to be a tick's job. bool style_stated = false; /// Set once the user edits the trigger level, so the panel stops defaulting it /// into the middle of the declared range. bool trigger_level_touched = false; /// A short-lived message shown in the status line (a drop's outcome), so an /// action that changes nothing still says why. std::string notice; gint64 notice_until_us = 0; /// Trigger-mode state for the status line: how many latches were on screen last /// frame, and whether an event is held (which is what "frozen" means). std::uint64_t seen_latches = 0; /// Which snapshot was framed last. Separate from `seen_latches` because a Disarm during /// a capture publishes a snapshot without an event having fired. std::uint64_t seen_event_seq = 0; bool event_held = false; /// Whether a capture is open on screen: the edge has been accepted and the post window /// is filling in view, on the coming event's own clock. It is what keeps /// `clamp_to_data()` off the pinned axis and tells the status line apart from "armed". bool capturing = false; /// The edge of the capture `capturing` refers to, so a second edge accepted before the /// first capture closed re-frames instead of being mistaken for the same one. double capture_edge = 0.0; /// Monotonic time of the last event, and the edge that started the clock. The clock /// starts when the *edge* is accepted, not when the capture closes -- that is the /// instant the user saw -- and the edge is kept so the latch that ends a live-updated /// capture does not restart it. /// Wall time, not stream time: "how long since the last event" is a question about the /// user's wait, and a stream whose clock runs at any other speed would answer it wrong. /// 0 until the first event of the session. gint64 last_latch_us = 0; double timed_edge = 0.0; /// The render gate's memory (see the end of `tick()`). A `Held` or `Paused` view is bound to /// an immutable snapshot on a pinned axis, so a frame whose picture cannot have changed does /// not need to be drawn again -- and on llvmpipe, or on a battery, re-drawing a frozen event /// sixty times a second is the whole cost of the application. The first four are the coarse /// signature of what a frozen picture can be re-framed by; a change to any of them opens /// `render_open` for `kSettleRenders` frames, so the figure's layout floor has the passes it /// needs to converge. `render_skipped` is the floor that makes everything the signature does /// *not* name render soon anyway (within `kStaticRenderEvery` ticks), which is what keeps the /// gate from being able to stall the picture. std::uint64_t render_event_seq = 0; double render_x_min = 0.0; double render_x_max = 0.0; int render_rows = -1; int render_open = 0; int render_skipped = 0; /// Self-check: ticks that queued no render, so the gate is a number rather than a claim. int dbg_render_skips = 0; double window_limit_s = -1.0; ///< the cap the window control was last clamped against /// The rate each channel actually *stored* over the last second, and the declared rate /// beside it. The two differ for two reasons now and both are worth seeing: the declared /// rate is a claim the stream may not keep, and a window too long to hold at the wire /// rate is resampled on ingest (`Partition::time_stride`), so this is also the readout /// of how much resolution the current window costs. Re-measured once a second (the same /// tick as the channel dump), never per frame. std::vector stored_rate; std::vector 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 last_stored; std::uint64_t dbg_latches_at_disarm = 0; std::uint64_t dbg_seq_at_disarm = 0; bool dbg_capturing_at_disarm = false; int dbg_disarm_polls = 0; int dbg_disarm_in_capture = 0; /// The force probe's state: the button's stated state on both sides of the trigger toggle (a /// width read a frame after the state it belongs to), and the two legs of its press. See /// `force_probe()` in the probes' translation unit. int dbg_force_off_sensitive = -1; int dbg_force_off_visible = -1; int dbg_force_off_width = -1; ///< its allocation with the trigger *off* int dbg_force_on_sensitive = -1; int dbg_force_on_width = -1; ///< ... and with the trigger on: the size the user can press int dbg_force_arm_width = -1; ///< the Arm button beside it, so the pair's widths can be compared int dbg_force_label_found = -1; ///< `find_labelled_button("Force")` *is* the panel's button int dbg_force_leg = 0; ///< 1 while leg A is in flight, 2 for leg B int dbg_force_polls = 0; int dbg_force_capture_seen = 0; ///< the axis was read while the forced capture was filling double dbg_force_capture_lo = 0.0; ///< that axis: the *requested* window, before the latch double dbg_force_capture_hi = 0.0; double dbg_force_want_pre = 0.0; ///< the request the engine will cut by, at the moment of the press double dbg_force_want_post = 0.0; std::uint64_t dbg_force_latches_before = 0; int dbg_swaps = 0; int dbg_axes_w_min = 1 << 20; int dbg_axes_w_max = -(1 << 20); int dbg_axes_w_prev = -1; int dbg_axes_w_changes = 0; /// What the pan probe asked for, so the frame after it can be compared against it: a capture /// during the probe is not a value either side can recompute, and a capturing lambda cannot be /// a `GSourceFunc`. double dbg_pan_asked_lo = 0.0; double dbg_pan_asked_hi = 0.0; bool dbg_pan_asked = false; /// Which gesture the probe is on: 1 = the pan was asked and its verdict is due, 2 = the /// zoom-out was asked and its verdict is due. int dbg_gesture_stage = 0; /// A verdict the *tick* owes, one frame after a gesture was asked: 1 pan, 2 zoom-out, 3 /// zoom-in. Reported from the frame loop rather than a `g_timeout`, because a 300 ms timer /// slips past the state it was meant to observe as soon as the frames are heavy -- which is /// exactly the load this probe runs under. int dbg_gesture_verdict = 0; /// The event-zoom probe's remaining steps, counted down by the *tick* for the same reason /// the gesture verdicts are: each step's reading has to be taken after the frame's pin and /// clamp have run, or it reports the ask rather than the view. int dbg_zoom_steps = 0; int dbg_zoom_step_no = 0; int dbg_axes_w_change_frame = -1; int dbg_axes_w_change_side = -1; float dbg_scale_min = 1e9f; float dbg_scale_max = -1e9f; int dbg_vp_w_min = 1 << 20; int dbg_vp_w_max = -(1 << 20); int dbg_xtick_min = 1 << 20; int dbg_xtick_max = -(1 << 20); int dbg_gutter_min = 1 << 20; int dbg_gutter_max = -(1 << 20); int dbg_side_w = 0; int dbg_side_h = 0; int dbg_signal_w = 0; int dbg_signal_h = 0; int dbg_signal_row_h = 0; int dbg_trigger_w = 0; int dbg_trigger_h = 0; /// The signal list has rows (the dock has no tab to be "current" on any more). int dbg_signal_page_current = 0; /// The dock as the exit probe found it: the split view showing the sidebar, the column sensitive (it is /// not until the stream has signals), its width, and how many `GtkScrolledWindow`s it holds directly. int dbg_dock_shown = -1; int dbg_dock_sensitive = -1; int dbg_dock_w = -1; int dbg_dock_scrollers = -1; int dbg_trigger_sensitive = 0; int dbg_trigger_items = 0; int dbg_trigger_selected = -1; /// The signal list's shape, captured where the list is built: how many rows it holds, and how many /// of them are streamed signals. The `MATH` divider and the math rows account for the rest, and /// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`. int dbg_signal_rows = 0; /// How many rows of the list carry a `GtkDragSource`: 0 -- the signal badges are the drag handles now. int dbg_signal_drag_sources = -1; /// A drag begun through the first signal badge's source closed the open slide-over (1), or did not (0); /// -1 when there is no badge to drag. int dbg_drag_closed_slideover = -1; std::size_t dbg_signal_plain = 0; /// The math channels among those rows, and the number of signals the page should be proposing (counted from /// the model, not from the rows built), and whether the "every signal is acquired" label was on screen. std::size_t dbg_signal_math_rows = 0; std::size_t dbg_signal_addable = 0; int dbg_signal_empty_label = -1; int dbg_side_column_after_hide = 1; std::string dbg_mode_triggered; std::string dbg_window_text; double dbg_marker_x = 0.0; double dbg_marker_max = 0.0; double dbg_trigger_time = 0.0; int dbg_trigger_time_valid = -1; std::string dbg_mode_live; int dbg_trigger_sensitive_default = 0; /// The detail string the popover and the exit dump read. The dump used to read the label, /// which no longer carries these fields. std::string status_detail; std::size_t status_detail_max = 0; /// The widest glance line this run produced. The bar sits above the plot, so this is the /// "layout must not depend on content" reading for the status row. std::size_t glance_max_chars = 0; /// The last glance line, for the exit dump's `status bar:` line — the dump runs after the /// window is destroyed, so it cannot ask the label. std::string dbg_glance_last; /// Which step of the menu probe runs next (`UDPSCOPE_PROBE_MENUS_MS`). int dbg_menus_step = 0; /// Which step of the capture probe runs next (`UDPSCOPE_PROBE_CAPTURE_MS`). int dbg_capture_step = 0; /// Which step of the event-cache probe runs next (`UDPSCOPE_PROBE_EVENTS_MS`), and the bounded wait /// it keeps for a second latch (it is counting retries, not time). int dbg_events_step = 0; int dbg_events_wait = 0; /// The event row the probe selected before it activated one, and how many rows were selected. /// Both are read back *after* the activation: a row pointer that is no longer the list's row 0, /// or a smaller count, says the activation rebuilt the list and so threw away the user's scroll /// position and their comparison selection. GtkListBoxRow* dbg_events_row0 = nullptr; int dbg_events_selected = 0; /// What the *live* view was set to just before the probe opened an event: the time window, the /// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after /// each way out of a file view, because a file view overwrites all four -- the window is pinned to /// the event's own span and the trigger is switched off, since a file has no edge to wait for -- /// and going live has to put them back. The probe keeps its own expectation rather than asking /// `App`: a reading taken from the mechanism under test asserts nothing. double dbg_live_window = 0.0; bool dbg_live_trigger_on = false; std::string dbg_live_trigger_channel; double dbg_live_level = 0.0; /// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the /// popover it is measuring. int dbg_rowmenu_step = 0; GtkWidget* row_menu_popover = nullptr; GtkWidget* status_info_button = nullptr; ///< the ⓘ /// The packet-capture toggle, in the detail popover. `toggled` sets the producer's /// atomic and nothing else; the tick keeps it honest about connection and failures. GtkWidget* capture_button = nullptr; /// What the toggle was last set to from this thread, so a producer-side failure /// (a full disk) can return the button to off without re-entering its handler. bool capture_want_ui = false; GtkWidget* status_popover = nullptr; /// The popover's thirteen labels: five row headings and eight value lines. Built once; the /// tick sets their text. A rebuild here would be a per-frame relayout of a visible widget. GtkWidget* popover_label[13] = {}; int popover_rebuilds = 0; }; struct ConfigDialog { App* app = nullptr; GtkWidget* window = nullptr; GtkWidget* list = nullptr; GtkWidget* name = nullptr; GtkWidget* comment = nullptr; }; struct WindowUnit { const char* name; double seconds; }; enum class BadgePhase { Lit = 0, Dim = 1, Idle = 2, Connecting = 3 }; struct CalibContext { App* app; std::size_t signal; GtkWidget* gain; GtkWidget* offset; GtkWidget* window; }; struct DragContext { App* app; std::size_t signal; }; struct ColorPick { App* app; std::string signal; }; class MeasureWindowReader { public: /// `window` is the x range to read, **in the coordinates the axes read** -- the viewport for a /// live view, the pin for a pinned one -- and it is handed in rather than read off the plot /// here, because *which* window a measurement describes is a decision (`update_measures()`) /// that also has to be booked: the pane compares the window it last measured against this one /// to decide whether a recompute is owed, and a reader that chose its own would leave the two /// disagreeing -- a pinned view recomputing on every gesture frame for a number that cannot /// change. MeasureWindowReader(App& app, std::size_t channel, plot::Range window) { const RowSet::Row& r0 = app.rows->row(0); if (r0.plot == nullptr) return; if ((snap_ = app.model->display_snapshot()) != nullptr) { if (channel >= snap_->values.size()) return; const std::vector& st = snap_->stamps; if (snap_->values[channel].size() != st.size()) return; const auto lo = std::lower_bound(st.begin(), st.end(), window.min); const auto hi = std::upper_bound(st.begin(), st.end(), window.max); if (hi <= lo) return; off_ = static_cast(lo - st.begin()); count_ = static_cast(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(app.model->mutex()); if (channel >= app.model->channel_count()) return; epoch_ = app.model->display_epoch(); u64 first = 0; u64 last = 0; app.model->live_window_locked(window.min - epoch_, window.max - epoch_, first, last); if (last <= first) return; store_ = &app.model->live(); channel_ = channel; first_ = first; count_ = static_cast(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(origin_, pyr_->window_base()); const u64 end = std::min(origin_ + count_, pyr_->window_end()); if (end <= begin) return false; const u64 stored = pyr_->level_base(0); first = std::max(stored, (begin + plot::data::kBucket - 1) / plot::data::kBucket); last = std::min(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(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(mm.min); out.hi = static_cast(mm.max); out.sum = pyr_->bucket_sum(0, k); out.count = static_cast(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 snap_; const std::vector* snap_vals_ = nullptr; std::size_t off_ = 0; std::unique_lock 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 scratch_; }; struct MeasureDialog { App* app = nullptr; int edit = -1; GtkWidget* signal_drop = nullptr; GtkWidget* type_drop = nullptr; GtkWidget* threshold = nullptr; /// `Between`'s *other* edge: the same unit as `threshold`, shown only for the one type that reads /// two shares (see `measure_uses_band()`). GtkWidget* band = nullptr; GtkWidget* above = nullptr; GtkWidget* show = nullptr; GtkWidget* window = nullptr; /// The option rows' boxes and labels, so the dialog can *retitle* them per type: the same /// switch means "the pulse is above the level" for a pulse and nothing at all for a period, /// and the difference has to be in words, not just in visibility. GtkWidget* threshold_row = nullptr; GtkWidget* threshold_label = nullptr; GtkWidget* band_row = nullptr; GtkWidget* band_label = nullptr; GtkWidget* side_row = nullptr; GtkWidget* side_label = nullptr; /// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in /// Hz with 0 off; both are shown only where they do something (see `measure_dialog_sync`). GtkWidget* noise = nullptr; GtkWidget* noise_row = nullptr; GtkWidget* hf = nullptr; GtkWidget* hf_row = nullptr; /// The names the signal dropdown was built from, in order. `apply` resolves an index /// through *this*, not through the model's signal list: the dropdown lists the plotted /// traces, so index `i` here is the i-th trace -- indexing the CONFIG with it selected /// whatever signal happened to sit at that position there (the stream's timestamp, for the /// first entry, which is what picking the first trace used to store). std::vector 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 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 names; std::vector times; std::vector> columns; ///< [column][row] }; using EventThumbCache = std::unordered_map; 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 g_stats_n_before; /// The same for the dock-section probe: the accumulator counts recorded while the Measure section was /// still scrolled into view, so the report can say whether any of them advanced after it scrolled out. inline std::vector g_dock_section_n_before; inline double g_trigrenum_edge = 0.0; inline std::uint64_t g_trigrenum_latches = 0; inline ConditionProbe g_condition; inline double g_search_t0 = 0.0; constexpr int kMenuRowMaxPx = 30; inline RowMenuProbeState g_rowmenu_probe; void install_css(); void on_menu_row_toggled(GtkToggleButton* button, gpointer); GtkWidget* menu_row(const char* label, const char* accel, bool with_check, GtkWidget** out_check); GtkWidget* menu_button(const char* label, const char* accel); GtkWidget* menu_toggle(const char* label, const char* accel, bool active); GtkWidget* menu_separator(); GtkWidget* labeled_box(const char* label, GtkWidget* child); /// Loads a UI file into the window's builders and returns the builder, so the caller can resolve the /// objects it needs by id. See `ui_widgets.cpp`. GtkBuilder* ui_load(App& app, const char* resource); LayoutState collect_layout(App& app); void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user); void on_config_forget_clicked(GtkButton* button, gpointer user); void on_config_save_as(GtkButton*, gpointer user); void config_dialog_refresh(ConfigDialog& d); void open_configurations(App& app); void save_layout_now(App& app); /// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as /// pages of the slide-over (`source-`), once, before the first tick. `apply_source_panels` sets every panel's /// sensitivity from `connected` -- disabled, never hidden. /// `sync_source_panels` is the per-tick call: it applies the model's own connection flag. void build_source_panels(App& app); void apply_source_panels(App& app, bool connected); void sync_source_panels(App& app); bool save_configuration_as(App& app, const std::string& name, const std::string& comment); gboolean layout_save_tick(gpointer user); void schedule_layout_save(App& app); void apply_layout(App& app, const LayoutState& st); GtkWidget* labeled_box_fixed(const char* label, GtkWidget* child); GtkWidget* spin(double min, double max, double step, int digits, double value); GtkWidget* dialog_window(GtkWidget* parent, const char* title); void dialog_add_button(GtkWidget* window, GtkWidget* button); void on_popover_closed(GtkPopover* popover, gpointer); int window_unit_for(double s); std::string format_window_value(double s, int unit); std::string format_window(double s); const char* badge_markup(BadgePhase phase); std::string format_measure(double v, const std::string& unit); void set_notice(App& app, std::string what, int seconds = 4); void set_row_key(GtkWidget* widget, int row); int row_key(GtkWidget* widget); int row_at(const App& app, double x, double y); void cancel_drag_on(App& app, int row); void report_wide_widgets(GtkWidget* root, int threshold, int depth); const char* display_mode_name(DisplayMode m); void check_frame_ink(App& app); int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user); void on_before_paint(GdkFrameClock*, gpointer user); void on_after_paint(GdkFrameClock*, gpointer user); GtkWidget* make_compare_area(App& app); GtkWidget* make_area(App& app); void relayout_areas(App& app); void clear_layout_editor(App& app); void on_layout_row_remove(GtkButton* button, gpointer user); void on_layout_row_insert(GtkButton* button, gpointer user); void sync_layout_editor(App& app); void rebuild_layout_editor(App& app); void on_layout_editor_done(GtkButton*, gpointer user); void on_layout_editor_toggled(GtkToggleButton* button, gpointer user); void on_signal_switch(GObject* obj, GParamSpec*, gpointer user); void on_arm_clicked(GtkButton* button, gpointer user); TriggerType trigger_type_at(guint i); WidthCmp width_cmp_at(guint i); void publish_trigger_fields(App* app); void sync_trigger_rows(App& app); void on_trigger_field_changed(GtkWidget*, gpointer user); void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user); void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user); void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user); void show_info_dialog(GtkWindow* parent, const SignalInfo& sig); void on_info_clicked(GtkButton* button, gpointer user); void on_calibrate_apply(GtkButton*, gpointer user); void open_calibration(App& app, GtkWindow* parent, std::size_t signal); GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user); int signal_color_index(const std::vector& 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& 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& signals); /// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order. /// The one rule the list and the badges share. std::vector listed_signals(const std::vector& signals); /// Whether more signals are acquired than `kMaxSignalBadges`: the badge bar then shows one "N signals" badge, /// and the `signals` page lists the acquired signals as well, since their rows are the only way to a page. /// The one rule the bar and the page share. bool signal_badges_overflowed(App& app, const std::vector& signals); /// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if /// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a /// signal page's "Plot on" button share. bool assign_signal_to_row(App& app, std::size_t signal, int row); /// Back to the palette colour for the signal's position. void reset_signal_color(App& app, std::size_t signal); /// Holds `widget` alive across the current emission: a handler that rebuilds the list or a page destroys the /// widget it was emitted from. void outlive_the_rebuild(GtkWidget* widget); /// Creates the page `signal:` if it is missing, fills it, and returns whether it exists afterwards. bool signal_page_ensure(App& app, const std::string& name); /// Called at the end of `rebuild_signal_panels`: refills every existing signal page, drops the pages of /// signals the model no longer has (closing the slide-over first if one of them is open). void signal_pages_sync(App& app); gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS /// The `MATH` divider row in the one channel list, and the `+` it carries. GtkWidget* make_math_divider(App& app); void on_math_add_clicked(GtkButton* button, gpointer user); /// The index in `App::math` of the channel a synthetic signal is named after, or `-1`. int math_index_for_signal(App& app, const std::string& name); void on_math_edit_clicked(GtkButton* button, gpointer user); void on_math_delete_clicked(GtkButton* button, gpointer user); void rebuild_trigger_signals(App& app); double window_limit(const App& app); void sync_window_display(App& app, double seconds); void reset_measure_stats(App& app); void apply_time_window(App& app, double seconds); void on_window_entry_activate(GtkEntry* entry, gpointer user); void on_window_unit_changed(GObject* obj, GParamSpec*, gpointer user); void on_window_step(GtkButton* button, gpointer user); void refresh_window_limits(App& app); void on_pause_toggled(GtkToggleButton* button, gpointer user); void search_jump(App& app, double t); void update_search_panel(App& app); void sync_events_availability(App& app); /// Restates the events pane's divider position once a frame; see the definition. void sync_events_divider(App& app); /// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate /// because the theme is the one preference that takes effect without a restart. void open_preferences(App& app); gboolean prefs_probe(gpointer user); gboolean sidebar_probe(gpointer user); gboolean boxzoom_probe(gpointer user); /// A labelled row whose control keeps its natural width -- what a `GtkSwitch` needs and what /// `labeled_box()`'s expanding child denies it. GtkWidget* labeled_switch_row(const char* label, GtkWidget* control); /// The layout editor's boundary drag: the press starts it, movement sizes the two rows it separates. void on_layout_drag_begin(GtkGestureDrag* gesture, double x, double y, gpointer user); void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user); void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user); void on_trigger_toggled(GtkToggleButton* button, gpointer user); plot::u32 measure_color(std::size_t index); /// One line naming what a measurement's type is measuring, with the shares it reads them at: the /// pane's row tooltip. The row's own label is the type alone (as for every type), so this is where /// a user sees the share an `above`/`below`/`between` row is averaging over. std::string measure_row_summary(const Measure& m); bool visible_samples(App& app, std::size_t channel, std::vector& time, std::vector& 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 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 disable_dependent_math_channels(App& app, const std::string& name); void on_remove_clicked(GtkButton* button, gpointer user); GtkWidget* menu_icon_button(const char* icon, const char* label); void on_row_menu_show_remove(GtkButton*, gpointer user); void on_row_menu_back(GtkButton*, gpointer user); void row_menu_fill(RowMenuState& st); GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true); void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user); gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user); /// Scrolls the dock to a section: "signals" (the top) or "measure" (the cursors panel, which the /// measurements follow). An unknown name is a no-op. void dock_show_section(App& app, const char* section); /// Reads each pane's own switch back off the header bar's button, and the other way round: the two are /// one control in two places, and `panel_switch_sync` keeps the write from looking like a click. void sync_panel_switches(App& app); /// The trigger's four enumerations as words: the panel's dropdown labels and the session file's /// vocabulary, in one place so the two cannot drift. const char* trigger_type_text(TriggerType t); TriggerType trigger_type_from_text(const std::string& s); const char* trigger_edge_text(TriggerEdge e); TriggerEdge trigger_edge_from_text(const std::string& s); const char* width_cmp_text(WidthCmp c); WidthCmp width_cmp_from_text(const std::string& s); const char* trigger_mode_text(TriggerMode m); TriggerMode trigger_mode_from_text(const std::string& s); /// The panel's controls from the model, and the session's trigger into both. void write_trigger_widgets(App& app); /// The compact line the trigger pane shows while it is locked, and the lock itself: the pane's body and /// its [edit] button follow from `trigger_locked`. void sync_trigger_summary(App& app); /// Both arm buttons -- the panel's and the compact line's -- from the one armed state the producer keeps in /// `model->trigger_armed()`. The label was written from four places (the click, the toolbar toggle, the /// curator's re-read, the start-up default) and a second button would have needed a fifth copy of the same /// two lines; the drift that costs is a button reading "Arm" while the engine is armed. Sensitivity, and /// the compact button's visibility, are stated here too and read from the toolbar toggle -- the control /// that owns `enabled`, per the restore rules -- so no caller needs the model lock to state any of it. void sync_arm_buttons(App& app); void apply_pane_lock(App& app); void on_pane_edit_clicked(GtkButton* button, gpointer user); void apply_trigger_layout(App& app, const LayoutState::Trigger& t); void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user); void update_side_visibility(App& app); void on_sidebar_toggled(GtkToggleButton* button, gpointer user); void rebuild_signal_panels(App& app); void svg_declared_size(const char* data, gsize size, int* w, int* h); std::string icon_report(GtkIconTheme* theme, const char* name, const char* resource); void note_panel_controls(App& app); void report_event_zoom(App& a, const char* what); void update_status_popover(App& app, const udpscope::StatusSnapshot& s); std::uint64_t capture_file_bytes(std::uint32_t slots); std::string abbrev_home(const std::string& path); void update_capture_tooltip(App& app); void update_capture_row(App& app); bool record_event(App& app); bool replay_event(App& app, const EventRecord& record); bool go_live_again(App& app); void snapshot_live_state(App& app); void restore_live_state(App& app); bool read_event_samples(const std::string& path, EventSamples* out, std::string* error); std::vector selected_events(App& app); void update_group_buttons(App& app); bool build_event_group(App& app, const std::vector& records, EventGroupKind kind, std::string* error); void sync_compare_axes(App& app); void on_group_clicked(GtkButton* button, gpointer user); GdkTexture* scale_texture(GdkTexture* source, int width, int height); EventThumbCache* event_thumb_cache(App& app); GdkTexture* event_preview_texture(App& app, const std::string& path); void refresh_events_pane(App& app); /// Re-marks which row is the event currently on screen, **without rebuilding the list**. /// /// The mark is the only thing about a row that a replay changes -- the picture and the numbers /// are the file's -- and rebuilding the list to restate it threw away the scroll position and /// the selection with it, so reaching the thirtieth event meant scrolling to it again after /// every click, and a selection made for a comparison was lost by the act of looking at one of /// its members. Call this for a change of what is on screen, and `refresh_events_pane()` only /// for a change of what is *cached*. void mark_events_on_screen(App& app); void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user); GtkWidget* build_events_pane(App& app); gboolean tick(gpointer user); void on_events_record_toggled(GObject* o, GParamSpec*, gpointer user); GtkWidget* build_trigger_panel(App& app); GtkWidget* build_signal_panel(App& app); void note_starvation(App& app); GtkWidget* build_window_control(App& app); void run_app_action(App& app, AppAction action); gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user); GtkWidget* build_toolbar(App& app); void on_activate(GtkApplication* application, gpointer user); gboolean exit_probe_step3(gpointer user); gboolean exit_probe_step2(gpointer user); GtkWidget* find_labelled_button(GtkWidget* w, const char* label); gboolean trig_trace_probe(gpointer user); gboolean eventzoom_wait(gpointer user); gboolean eventzoom_probe(gpointer user); gboolean math_probe(gpointer user); gboolean mathwrap_probe(gpointer user); gboolean mathwrap_probe_wait(gpointer user); gboolean mathwrap_probe_report(gpointer user); gboolean measure_probe(gpointer user); gboolean levels_probe(gpointer user); gboolean measurepin_probe(gpointer user); gboolean measurepin_held_wait(gpointer user); gboolean measurepin_pause_wait(gpointer user); gboolean measurepin_live_wait(gpointer user); gboolean measure_trace(gpointer user); gboolean stats_probe_report(gpointer user); gboolean stats_probe(gpointer user); gboolean dock_section_probe_report(gpointer user); gboolean dock_section_probe(gpointer user); gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS /// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page /// per setup surface, each page in a scroller of its own. void build_slideover(App& app); // once, after the plot_overlay exists bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content); bool slideover_has_page(const App& app, const std::string& id); /// The inverse of `slideover_add_page`: false when the id is unknown or is the open page. bool slideover_remove_page(App& app, const std::string& id); bool slideover_open(App& app, const std::string& id); // false when the id is unknown void slideover_close(App& app); bool slideover_toggle(App& app, const std::string& id); // open if closed or on another page, else close const std::string& slideover_page(const App& app); // "" when closed bool slideover_escape(App& app); // closes if open; true when it did /// x, y in the overlay's coordinates; closes unless the point is inside the panel; true when it closed. bool slideover_click_away(App& app, double x, double y); gboolean slideover_probe(gpointer user); // UDPSCOPE_PROBE_SLIDEOVER_MS /// The badge bar (`src/ui_badges.cpp`): setup is reached by the object it configures. /// The Trigger badge's state word. Pure, so the probe can assert the whole table. const char* trigger_badge_state(bool enabled, bool armed, bool pending, bool has_event); void build_badge_bar(App& app); // after build_slideover, the toolbar (it reads trigger_toggle) and build_source_panels (the Source badge's page) void sync_badge_bar(App& app); // per tick and on every slide-over open/close; only writes what changed gboolean badges_probe(gpointer user); // UDPSCOPE_PROBE_BADGES_MS /// Rebuilds the signal badges (one per enabled signal, or the "N signals" overflow) from the model. void sync_signal_badges(App& app); gboolean signal_badges_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_BADGES_MS gboolean source_kinds_probe_report(gpointer user); gboolean source_kinds_probe(gpointer user); /// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and /// their three controls, the capture button and a hand-checked decode. `_report` is the second phase. /// `_reconnect` reads the state after the mock dropped the link; `_resumed` shows the stream moved on. gboolean e1608_probe(gpointer user); gboolean e1608_probe_report(gpointer user); gboolean e1608_probe_reconnect(gpointer user); gboolean e1608_probe_resumed(gpointer user); /// The generator's update rate: starts a Sine on DAC 0 through the panel, reads the achieved rate the /// source measured and the Analog out panel's "Update rate" row (`_report`, 2.5 s later), then stops /// it and checks both read "off" (`_stopped`). gboolean e1608_gen_probe(gpointer user); gboolean e1608_gen_probe_report(gpointer user); gboolean e1608_gen_probe_stopped(gpointer user); /// The E-1608 form's "Scan network" row: presses Scan against the mock, then (phases `_fill`, /// `_rescan`) clicks a result to fill the address, "Use MAC" to fill the MAC, scans again and checks /// the results were replaced, not added to. gboolean e1608_scan_probe(gpointer user); gboolean e1608_scan_probe_fill(gpointer user); gboolean e1608_scan_probe_rescan(gpointer user); /// The flat UDP (MARTe2 UDPSender) source: the signal-list codec and a hand-computed decode, then, /// against `tools/mock_flatudp.py`, the stream, the value continuity, the wrong-size accounting and /// the capture button (`_report`, a second later). `_gap` starts a 100 ms poll that waits for the /// stream, then for the mock's silence to drop the source (whenever it falls), then (`_resumed`) for /// the stream to come back. gboolean flatudp_probe(gpointer user); gboolean flatudp_probe_report(gpointer user); gboolean flatudp_probe_gap(gpointer user); gboolean flatudp_probe_resumed(gpointer user); gboolean pan_probe(gpointer user); gboolean arm_probe(gpointer user); gboolean trigrenum_probe_step3(gpointer user); gboolean trigrenum_probe_step2(gpointer user); gboolean trigrenum_probe(gpointer user); gboolean fields_probe(gpointer user); gboolean condition_probe_report(gpointer user); gboolean condition_probe_arm(gpointer user); gboolean condition_probe(gpointer user); gboolean search_probe_marks(gpointer user); gboolean search_probe_report(gpointer user); gboolean search_probe_run(gpointer user); gboolean search_probe(gpointer user); gboolean window_probe(gpointer user); GtkWidget* layout_button(App& a, bool insert, int index); void layout_probe_report(App& a, const char* what); void layout_click(GtkWidget* b); void layout_probe_next(App& a, int step, guint delay_ms); gboolean layout_probe_step(gpointer user); gboolean enable_all_probe(gpointer user); gboolean disarm_probe_step2(gpointer user); gboolean disarm_probe_step3(gpointer user); gboolean disarm_probe_wait(gpointer user); gboolean disarm_probe_step1(gpointer user); int persist_probe_depth(); void persist_probe_report(App& a, const char* what); void persist_probe_set_depth(App& a, int depth); gboolean persist_probe_wait(gpointer user); gboolean persist_probe(gpointer user); /// The phosphor density's one step for a frame: state every row's `plot::Plot` with the /// configuration the trigger holds, advance its fade by the frame's own `dt`, and deposit the /// acquisition on screen if one has arrived since the last step. `display` and `live_display` are /// passed in rather than re-read, so the step, the bind and the status line all describe one frame. /// /// The density is on while an acquisition is on screen -- `Held`, and `Capturing` as it fills -- /// and off on a live view, a paused one, or a file: the display the user asked for is a *trigger* /// display. Off is not "paused": switching it off (and leaving trigger mode, and opening a file) /// drops the image at once, so a fresh presentation starts empty rather than showing a stale image /// at another scale. void apply_density(App& a, DisplayMode display, bool live_display); /// The density probe (`UDPSCOPE_PROBE_DENSITY_MS`): the display's own numbers read back off the /// plots -- total ink and peak with it off, on, and after one decay step of each kind. void density_probe_report(App& a, const char* what); /// States the switch and/or the fade through the panel's own widgets, the way a user would. void density_probe_set(App& a, bool on, bool by_time); gboolean density_probe(gpointer user); gboolean density_probe_step(gpointer user); void force_probe_report(App& a, const char* leg); gboolean force_probe_press(gpointer user); gboolean force_probe_poll(gpointer user); gboolean force_probe_step2(gpointer user); gboolean force_probe(gpointer user); void pause_probe_report(App& a, const char* what); gboolean pause_probe(gpointer user); gboolean export_probe(gpointer user); gboolean exit_probe_step1(gpointer user); void cursors_probe_report(App& a, const char* what); void fit_probe_report(App& a, const char* what); gboolean timelabels_probe_step(gpointer user); void fit_probe_zoom_in(App& a); gboolean fit_probe_step(gpointer user); void cursors_probe_next(App& a, int step, guint delay_ms); gboolean cursors_probe_step(gpointer user); void measure_dialog_click(App& a, const char* label); void measure_dialog_report(App& a, const char* what); void measures_probe_next(App& a, int step, guint delay_ms); gboolean measures_probe_step(gpointer user); void bandavg_probe_next(App& a, int step, guint delay_ms); void bandavg_report(App& a, const char* what); int bandavg_index(App& a, MeasureType type, const char* signal); gboolean bandavg_probe_step(gpointer user); gboolean zoomkeys_probe(gpointer user); void menus_probe_next(App& a, int step, guint delay_ms); void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel); gboolean menus_probe_step(gpointer user); gboolean windowpop_probe_step(gpointer user); /// Closes the time-window popover when a press lands elsewhere in the window (see ui.cpp). Declared for /// the probe, which drives it and presses the button to check the one case that must *not* close it. void on_window_popover_press(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user); gboolean color_probe(gpointer user); gboolean reseq_probe(gpointer user); void capture_probe_next(App& a, int step, guint delay_ms); gboolean capture_probe_step(gpointer user); gboolean configs_probe(gpointer user); void rowmenu_probe_next(App& a, int step, guint delay_ms); GtkWidget* row_menu_find(GtkWidget* container, const char* want); void row_menu_measure(GtkWidget* box, const char* tag, int* rows_out, int* bad_out); gboolean rowmenu_probe_step(gpointer user); int signal_tick_visible(App& app); int group_visible_traces(App& app, int row); void report_events_availability(App& a, const char* what); void events_probe_next(App& a, int step, guint delay_ms); gboolean events_probe_step(gpointer user); gboolean events_group_probe_step(gpointer user); void events_group_probe_next(App& a, int step, guint delay_ms); gboolean drop_probe(gpointer user); gboolean noise_probe(gpointer user); gboolean noise_probe_setup(gpointer user); gboolean noise_probe_math(gpointer user); gboolean noise_probe_denoise(gpointer user); void collect_spins(GtkWidget* w, std::vector* out); gboolean defaults_probe(gpointer user); gboolean keys_probe(gpointer user); gboolean help_probe(gpointer user); void helpui_probe_next(App& a, int step, guint delay_ms); gboolean helpui_probe_step(gpointer user); /// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: a stop that stands on another page of a stack, entered from the /// page it is not on -- the sidebar put on Measure, the walk to the signals stop, the reading of the /// page and of the spotlight, and then the same walk with the sidebar *collapsed*. void tour_surface_probe_next(App& a, guint delay_ms); // --- the help affordance layer (ui_context_help.cpp, ui_tour.cpp) ---------- // // Two things live here and they are not the same thing: the *contextual help* is a "?" on every // surface the app can explain, and the *guided tour* is one walk over five of them. Both read the // one setting the Help menu's toggle writes. /// Adds or removes the "?" affordances so they match `settings.contextual_help`, and does nothing /// else. Idempotent, so it can be called after the window is built, on every change of the setting /// and once more when a dialog that carries one of the hosts is opened. void context_help_sync(App& app); /// How many "?" affordances are on screen, writing their ids space-separated into `ids`. Found by /// the data key rather than kept in a list, so a host that died with a dialog cannot leave a stale /// pointer behind. int context_help_count(App& app, std::string* ids); /// The menu row's handler: writes the setting, saves the file and re-states the layer. void on_contextual_help_toggled(GtkToggleButton* button, gpointer user); /// The "?" for one surface by id, for a host that only exists while a dialog is open (the math /// editor is rebuilt on every open, so it cannot be a row the window's own build visits). void context_help_add(App& app, const char* id, GtkWidget* host); /// The same affordance for a surface that is *not* in `App`'s table -- the source chooser's, which /// runs before `App` exists and so cannot name one of its fields. `host` is a `GtkBox` and the button /// joins it as its last child, which is the rule the table's own rows follow; `on` false takes any /// hint off `host` instead. `shortcut` may be "". void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body, const char* shortcut, bool on); /// How many hints a widget subtree carries, and their ids, for a window that has no `App` (the /// chooser's probe). `context_help_count()` is this with `App::window` as the root. int context_help_count_in(GtkWidget* root, std::string* ids); /// The size of the first *visible* hint button in a subtree, read the way the probe's geometry is /// (`gtk_widget_compute_bounds()`), and the number a shrinking affordance is checked against. Both /// are 0 when the layer is off or nothing on screen carries one. void context_help_size_in(GtkWidget* root, int* width, int* height); /// `context_help_size_in()` with `App::window` as the root. void context_help_size(App& app, int* width, int* height); /// Builds a tour's overlay children (the four dim rectangles, the spotlight outline and the card) /// inside `overlay`. Called once, as the overlay is inserted, by either window that draws a tour. void tour_build(TourView& view, GtkWidget* overlay); /// Starts at the first stop. `stops` is the whole table, in walk order: the stops whose surface is /// not on screen are filtered out here, by the one rule both windows use (a wrong page is kept and /// selected, a collapsed pane is dropped). `on_done` (may be null) runs when the walk passes its last /// stop, and is how the chooser hands off. void tour_begin(TourView& view, std::vector stops, void (*on_done)(void* user), void* done_user); /// Moves `delta` stops, finishing the walk (and calling `on_done`) on the way past the last one. void tour_advance(TourView& view, int delta); /// Ends the walk without finishing it: every child of the overlay is hidden and nothing is left on /// screen. Skip and Escape. void tour_end(TourView& view); /// Called from the tick while the walk runs: re-measures the target each frame, so a window resize /// keeps the spotlight on the widget, and places the card where it does not cover it. void tour_update(TourView& view); /// The widget the current stop points at, or nullptr when the walk is not on a stop. GtkWidget* tour_target(TourView& view); /// The part of `target` that is on screen, in `overlay`'s coordinates: its bounds intersected with every /// scrolled window above it. A target scrolled out of sight entirely is measured and *empty*: true with a /// zero-size rectangle. False only when it cannot be measured yet (no allocation, not in the overlay). /// The spotlight is drawn from it, and the probe reads the spotlight against it. bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out); /// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted. void build_tour(App& app); /// Starts the window's tour at its first step and writes `tour_seen`. Safe to call while it already /// runs -- it restarts at step 0. void start_tour(App& app); /// Moves `delta` steps, ending the tour on the way past the last one. void tour_step(App& app, int delta); /// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen. void tour_dismiss(App& app); /// The window's tour, re-measured each frame. void update_tour(App& app); bool tour_active(const App& app); /// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe /// reads the spotlight against it. GtkWidget* tour_target(App& app); /// The steps this run actually walks (targets that were on screen when it started), and where it is. int tour_step_count(const App& app); int tour_step_index(const App& app); /// The first-run auto-start's callback, scheduled only when `settings.tour_seen` is false. gboolean tour_autostart(gpointer user); /// The chooser's "Guided tour" request (`Config::tour_pending`): the same delayed start, but it /// sets `tour_requested` rather than `tour_auto_started`, so the probe and the dump can tell the two /// apart. Scheduled only when the config carries the request. gboolean tour_start_requested(gpointer user); /// Captures the layer's own readings and the bounds of the surfaces the help's figures label, while /// the window is alive -- the dump runs after it is destroyed and may not ask GTK anything. void capture_help_state(App& app); int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq, RowSet& rows); } // namespace ui_impl } // namespace udpscope