ui: the sidebar's tabs become one dock; Measure and Signals by keyboard section; session keeps the dock's state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
31ff778e07
commit
4b65197f56
19 files changed
+421
-419
No files matched your search
+79
-109
@@ -2864,35 +2864,38 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
|
||||
open_row_menu(*app, row, x, y, widget);
|
||||
}
|
||||
|
||||
void on_side_tab_changed(GObject* o, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const char* name = adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(o));
|
||||
app->side_tab = name != nullptr ? name : "signals";
|
||||
// Not while the layout is being applied: the restore would schedule a save of what it just read.
|
||||
if (!app->updating_panels) schedule_layout_save(*app);
|
||||
/// Scrolls the dock to a section. The dock is one scroller (`App::dock_scroller`) holding the signal list,
|
||||
/// the cursors and the measurements top to bottom, so "measure" is the cursors panel's offset in the
|
||||
/// page, and "signals" the top. Nothing is hidden or removed: both stay in the page. An unknown name is a
|
||||
/// no-op, so a key bound to a section a later version drops cannot blank anything.
|
||||
void dock_show_section(App& app, const char* section) {
|
||||
if (app.dock_scroller == nullptr || app.dock_page == nullptr || section == nullptr) return;
|
||||
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(app.dock_scroller));
|
||||
if (std::strcmp(section, "signals") == 0) {
|
||||
// The list is the first section: its own offset in the page is the page's margin, and "go to Signals"
|
||||
// means the top of the dock.
|
||||
gtk_adjustment_set_value(adj, 0.0);
|
||||
return;
|
||||
}
|
||||
if (std::strcmp(section, "measure") != 0 || app.cursors_panel == nullptr) return;
|
||||
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
|
||||
graphene_point_t at;
|
||||
if (!gtk_widget_compute_point(app.cursors_panel, app.dock_page, &zero, &at)) return;
|
||||
gtk_adjustment_set_value(adj, at.y);
|
||||
}
|
||||
|
||||
void select_side_tab(App& app, const char* name) {
|
||||
if (app.side_stack == nullptr || name == nullptr) return;
|
||||
// An unknown name leaves the stack where it is -- a session file from a later version must not
|
||||
// be able to blank the sidebar.
|
||||
if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(app.side_stack), name) == nullptr) return;
|
||||
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app.side_stack), name);
|
||||
app.side_tab = name;
|
||||
}
|
||||
|
||||
/// The side column is on when it has something to show *and* the user's sidebar
|
||||
/// button is in; the two panel buttons appear as soon as a CONFIG describes any
|
||||
/// signal at all.
|
||||
/// The dock is on when the user's sidebar button is in -- from the start, with or without signals; the
|
||||
/// two panel buttons appear as soon as a CONFIG describes any signal at all.
|
||||
void update_side_visibility(App& app) {
|
||||
if (app.side_stack == nullptr) return;
|
||||
if (app.side_box == nullptr) return;
|
||||
const bool have_signals = !app.model->signals().empty();
|
||||
// `have_signals` decides whether the sidebar *can* be on; `sidebar_wanted` is the user's last word,
|
||||
// which is why it is kept apart from the button's own state -- that state follows the panel now, and
|
||||
// the panel is off while there is nothing to show. A swipe that hides the sidebar fires the button's
|
||||
// handler (the binding is bidirectional), so a gesture is a choice too.
|
||||
gtk_widget_set_sensitive(app.sidebar_toggle, have_signals);
|
||||
const bool want_column = have_signals && app.sidebar_wanted;
|
||||
// `sidebar_wanted` is the user's last word, which is why it is kept apart from the button's own state:
|
||||
// a swipe that hides the sidebar fires the button's handler (the binding is bidirectional), so a gesture
|
||||
// is a choice too. "Disable, do not hide": the dock is on screen from the start and does nothing until
|
||||
// the stream has signals, so the signals decide its *sensitivity* and never whether it is there.
|
||||
gtk_widget_set_sensitive(app.side_box, have_signals);
|
||||
gtk_widget_set_sensitive(app.sidebar_toggle, TRUE);
|
||||
const bool want_column = app.sidebar_wanted;
|
||||
// The split view owns the sidebar's visibility: it animates it, and when the window is too narrow it
|
||||
// shows it as an overlay rather than letting the column be squeezed to nothing.
|
||||
if (app.side_view != nullptr) {
|
||||
@@ -2908,7 +2911,12 @@ void on_sidebar_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
// Only a state we did not write is a wish: `update_side_visibility` sets this button (and the panel)
|
||||
// from `sidebar_wanted`, so an unguarded change here is the user's.
|
||||
if (!app->sidebar_syncing) app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE;
|
||||
if (!app->sidebar_syncing) {
|
||||
app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE;
|
||||
// The wish is persisted (`SessionState::dock_shown`), so a change of it is a change of the session --
|
||||
// but not one made while a layout is being applied, which would save what it just read.
|
||||
if (!app->updating_panels) schedule_layout_save(*app);
|
||||
}
|
||||
update_side_visibility(*app);
|
||||
}
|
||||
|
||||
@@ -2994,8 +3002,8 @@ void note_panel_controls(App& app) {
|
||||
// `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent.
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text),
|
||||
"cursors=%d in the toolbar, switcher=%d, reset=%d in the cursors pane",
|
||||
in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0,
|
||||
"cursors=%d in the toolbar, dock=%d, reset=%d in the cursors pane",
|
||||
in_bar(app.cursors_toggle), app.dock_page != nullptr ? 1 : 0,
|
||||
in_cursors_pane(app.cursors_reset));
|
||||
// ... and the order the bar actually shows, which is what "the menu should be last" is about. A
|
||||
// widget tree walked in child order *is* the layout order, so this is the reading order of the
|
||||
@@ -3051,42 +3059,6 @@ void note_panel_controls(App& app) {
|
||||
" " +
|
||||
icon_report(theme, "org.udpscope.app-symbolic",
|
||||
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
|
||||
|
||||
// What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
|
||||
// icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
|
||||
// reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
|
||||
// *parent* is hidden still reads visible; `mapped` requires every ancestor, which is what "on
|
||||
// screen" means -- and it is why the pill count is read the same way: if nothing is mapped the
|
||||
// pill count reads 0 and the label count means nothing, rather than passing vacuously.
|
||||
int pills = 0;
|
||||
int drawn_labels = 0;
|
||||
int drawn_images = 0;
|
||||
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
|
||||
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
|
||||
// A *mapped* image is an icon (or its missing-icon placeholder) on a tab: tabs are words only.
|
||||
if (GTK_IS_IMAGE(w) && gtk_widget_get_mapped(w)) ++drawn_images;
|
||||
// A *mapped* label with text: an empty label would be counted by "mapped" alone.
|
||||
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
|
||||
++drawn_labels;
|
||||
}
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
count_drawn(c);
|
||||
}
|
||||
};
|
||||
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
|
||||
app.dbg_switcher_pills = pills;
|
||||
app.dbg_switcher_labels = drawn_labels;
|
||||
app.dbg_switcher_images = drawn_images;
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
|
||||
app.dbg_switcher_pages =
|
||||
app.side_stack != nullptr
|
||||
? static_cast<int>(g_list_model_get_n_items(
|
||||
G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
|
||||
: 0;
|
||||
if (app.side_switcher != nullptr) {
|
||||
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window
|
||||
@@ -4022,7 +3994,7 @@ gboolean tick(gpointer user) {
|
||||
// follow a resize and the gutter settling without being told.
|
||||
if (app->layout_editing) sync_layout_editor(*app);
|
||||
|
||||
// The one thing that has to be decided *per frame* about the sidebar: whether there is room for it
|
||||
// The one thing that has to be decided *per frame* about the dock: whether there is room for it
|
||||
// beside the plot. Collapsed, the split view puts it *over* the plot rather than giving it a share of
|
||||
// a width that is not there -- so a small window shows fewer pixels of plot, never a panel squeezed
|
||||
// to nothing. `kSidebarRoom` is the sidebar's 320 plus the width below which the plot stops being
|
||||
@@ -4030,8 +4002,12 @@ gboolean tick(gpointer user) {
|
||||
if (app->side_view != nullptr && app->window != nullptr) {
|
||||
// The split view writes `show-sidebar` itself (at startup and when it collapses), so the wish is
|
||||
// re-asserted here -- but only when they disagree, so a settled frame touches nothing. This is also
|
||||
// why a hide *gesture* does not stick: the panel is where the button says it is.
|
||||
const bool want = !app->model->signals().empty() && app->sidebar_wanted;
|
||||
// why a hide *gesture* does not stick: the panel is where the button says it is. The wish alone decides
|
||||
// whether the dock is there; having signals decides only whether it does anything ("disable, do not
|
||||
// hide"): a dock that is on screen from the start does not jump in when the first CONFIG arrives.
|
||||
const bool want = app->sidebar_wanted;
|
||||
gtk_widget_set_sensitive(app->side_box, !app->model->signals().empty());
|
||||
gtk_widget_set_sensitive(app->sidebar_toggle, TRUE);
|
||||
if (adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view)) != want) {
|
||||
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view), want);
|
||||
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->sidebar_toggle)) != want) {
|
||||
@@ -4702,8 +4678,8 @@ void run_app_action(App& app, AppAction action) {
|
||||
case AppAction::CursorsPane:
|
||||
toggle(app.cursors_toggle);
|
||||
break;
|
||||
case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break;
|
||||
case AppAction::SideTabSignals: select_side_tab(app, "signals"); break;
|
||||
case AppAction::ShowMeasure: dock_show_section(app, "measure"); break;
|
||||
case AppAction::ShowSignals: dock_show_section(app, "signals"); break;
|
||||
case AppAction::LayoutEditor:
|
||||
toggle(app.layout_toggle);
|
||||
break;
|
||||
@@ -4745,6 +4721,9 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi
|
||||
tour_dismiss(*app);
|
||||
return TRUE;
|
||||
}
|
||||
// ... and then the slide-over: Escape closes it, ahead of the editable check for the same reason -- the
|
||||
// key that dismisses a panel must work from inside one of its own spin buttons.
|
||||
if (keyval == GDK_KEY_Escape && slideover_escape(*app)) return TRUE;
|
||||
GtkWidget* focus = gtk_window_get_focus(GTK_WINDOW(app->window));
|
||||
if (focus != nullptr && GTK_IS_EDITABLE(focus)) return FALSE;
|
||||
|
||||
@@ -4998,8 +4977,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
|
||||
install_css();
|
||||
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status
|
||||
// line under it, and the sidebar's tab switcher and two-page stack. Everything that depends on the
|
||||
// stream -- the events pane, the two plot areas, the sidebar's panels -- is built below and put
|
||||
// line under it, and the dock's scroller. Everything that depends on the
|
||||
// stream -- the events pane, the two plot areas, the dock's panels -- is built below and put
|
||||
// into the slots by id.
|
||||
g_clear_object(&app->window_ui);
|
||||
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
|
||||
@@ -5018,10 +4997,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
// help's annotated figures label.
|
||||
app->status_row = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_row"));
|
||||
GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column"));
|
||||
GtkWidget* side_stack = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_stack"));
|
||||
GtkWidget* side_switcher = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_switcher"));
|
||||
GtkWidget* measure_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "measure_page"));
|
||||
GtkWidget* signals_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "signals_page"));
|
||||
app->dock_scroller = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_scroller"));
|
||||
app->dock_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_page"));
|
||||
app->badge_bar = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_bar"));
|
||||
app->badge_signals = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_signals"));
|
||||
app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system"));
|
||||
@@ -5033,8 +5010,7 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
app->status_label == nullptr || app->status_info_button == nullptr ||
|
||||
app->status_row == nullptr || app->badge_bar == nullptr || app->badge_signals == nullptr ||
|
||||
app->badge_system == nullptr ||
|
||||
side == nullptr || side_stack == nullptr || side_switcher == nullptr ||
|
||||
measure_page == nullptr || signals_page == nullptr) {
|
||||
side == nullptr || app->dock_scroller == nullptr || app->dock_page == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The window's actions live on the application, so it has to know which one it belongs to.
|
||||
@@ -5177,28 +5153,28 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
|
||||
}
|
||||
|
||||
// The right-hand side is two tabs, not five stacked panels: one tab is on screen at a time, so
|
||||
// no section competes with four others for height. Each page scrolls on its own -- the two are
|
||||
// different heights, and one shared scroller would leave a switch scrolled where the new page
|
||||
// never asked to be.
|
||||
// `side` and `side_stack`/`side_switcher`/`measure_page`/`signals_page` are resolved above with
|
||||
// the rest of the skeleton and checked by the null guard; assign the members here.
|
||||
// The right-hand side is the dock: one scroller, the signal list first (it is what is dragged onto the
|
||||
// plot), then the cursors and the measurements. Every section is always there -- "go to Measure" scrolls
|
||||
// to it instead of switching a tab, so the measurements are as live as the list above them.
|
||||
// `side` and `dock_page`/`dock_scroller` are resolved above with the rest of the skeleton and checked
|
||||
// by the null guard; assign the member here.
|
||||
app->side_box = side;
|
||||
app->side_stack = side_stack;
|
||||
app->side_switcher = side_switcher;
|
||||
|
||||
// Measure: what the cursors bracket, and the figures derived from it.
|
||||
// Built in the order the panels have always been (cursors, measurements, trigger, signals) and placed in
|
||||
// the dock's: the trigger form goes to the slide-over (reached by the Trigger badge).
|
||||
app->cursors_panel = build_cursors_panel(*app);
|
||||
gtk_box_append(GTK_BOX(measure_page), app->cursors_panel);
|
||||
gtk_box_append(GTK_BOX(measure_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(measure_page), build_measures_panel(*app));
|
||||
|
||||
// The trigger form lives in the slide-over (reached by the Trigger badge); the signal list stays in the
|
||||
// sidebar until the dock replaces it.
|
||||
GtkWidget* measures_panel = build_measures_panel(*app);
|
||||
app->trigger_panel = build_trigger_panel(*app);
|
||||
// Top-aligned: the page's scroller gives the child the page's height, and a form that stretched to it
|
||||
// would spread its rows (or draw its frame) over empty space under the last one.
|
||||
gtk_widget_set_valign(app->trigger_panel, GTK_ALIGN_START);
|
||||
slideover_add_page(*app, "trigger", "Trigger", app->trigger_panel);
|
||||
app->signal_panel = build_signal_panel(*app);
|
||||
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
|
||||
gtk_box_append(GTK_BOX(app->dock_page), app->signal_panel);
|
||||
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(app->dock_page), app->cursors_panel);
|
||||
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(app->dock_page), measures_panel);
|
||||
|
||||
// The cursors start off, so their readout starts insensitive -- stated here by the one place that
|
||||
// knows the default, and the *body* and not the panel, because the panel's heading carries the
|
||||
@@ -5210,15 +5186,8 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
sync_arm_buttons(*app);
|
||||
gtk_widget_set_sensitive(app->cursors_reset, FALSE);
|
||||
|
||||
if (app->side_stack != nullptr) {
|
||||
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app->side_stack), "signals");
|
||||
g_signal_connect(app->side_stack, "notify::visible-child-name",
|
||||
G_CALLBACK(on_side_tab_changed), app);
|
||||
}
|
||||
// The source kind's own tabs (a generator, digital I/O), added before the first tick so the
|
||||
// pages-versus-pills reading below counts them too. The tabs are words, so the switcher's icons
|
||||
// are hidden first and kept hidden for the pages this call adds.
|
||||
keep_tabs_icon_free(*app);
|
||||
// The source kind's own panels (a generator, digital I/O), pages of the slide-over, added before the
|
||||
// first tick.
|
||||
build_source_panels(*app);
|
||||
// The badge bar reads `trigger_toggle` (made by `build_toolbar()` above) and the source's pages, which
|
||||
// `build_source_panels()` has just registered: the Source badge opens the first of them.
|
||||
@@ -5915,7 +5884,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), dock_section_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SLIDEOVER_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), slideover_probe, &app);
|
||||
@@ -6028,6 +5997,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MENUS_MS")) {
|
||||
menus_probe_next(app, 0, static_cast<guint>(std::atoi(env)));
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_DOCK_SESSION_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), dock_session_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||
}
|
||||
@@ -6237,12 +6209,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
inside(app.events_list, app.events_revealer));
|
||||
}
|
||||
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
|
||||
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d "
|
||||
"(page_current=%d) trigger=%dx%d (sensitive=%d) "
|
||||
"cursors=%d window=%.3fs window_cap=%.1fs\n",
|
||||
app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w,
|
||||
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current,
|
||||
app.dbg_trigger_w, app.dbg_trigger_h,
|
||||
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
||||
app.dbg_trigger_sensitive,
|
||||
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
||||
app.window_limit_s);
|
||||
{
|
||||
@@ -6267,10 +6239,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
|
||||
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
|
||||
app.dbg_panel_drove_toolbar);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d images=%d pages=%d size=%dx%d\n",
|
||||
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_images,
|
||||
app.dbg_switcher_pages, app.dbg_switcher_w, app.dbg_switcher_h);
|
||||
std::printf("[udpscope] dock: shown=%d sensitive=%d w=%d scroller_pages=%d\n", app.dbg_dock_shown,
|
||||
app.dbg_dock_sensitive, app.dbg_dock_w, app.dbg_dock_scrollers);
|
||||
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the
|
||||
// guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
|
||||
// so neither asks a destroyed widget anything.
|
||||
|
||||
Reference in new issue
Block a user