ui: the cursors and measurements panels are slide-over pages; the dock is the readouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-11-03 06:24:52 +01:00
1 parent a0fc0762d5
commit 3d307a0185
5 files changed
+91 -154

No files matched your search

+54 -102
View File
@@ -18,6 +18,7 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <tuple>
namespace udpscope {
namespace ui_impl {
@@ -183,6 +184,18 @@ gboolean exit_probe_step3b(gpointer user) {
a->dbg_dock_shown = sidebar_shown;
a->dbg_dock_sensitive = gtk_widget_get_sensitive(a->side_box) ? 1 : 0;
a->dbg_dock_w = a->dbg_side_w;
{
// The dock holds only the readouts box; the full cursors and measurements panels are slide-over pages,
// registered whether or not either is open.
int children = 0;
for (GtkWidget* c = gtk_widget_get_first_child(a->dock_page); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
++children;
}
std::fprintf(stderr, "[readouts] pages cursors=%d measure=%d dock_children=%d dock_w=%d\n",
slideover_has_page(*a, "cursors") ? 1 : 0, slideover_has_page(*a, "measure") ? 1 : 0,
children, a->dbg_side_w);
}
a->dbg_dock_scrollers = 0;
for (GtkWidget* c = gtk_widget_get_first_child(a->side_box); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
@@ -1792,24 +1805,11 @@ gboolean stats_probe(gpointer user) {
return G_SOURCE_REMOVE;
}
/// Which section the dock's scroller is showing at its top: "measure" once it has been scrolled to the
/// measurements' heading or past it, "cursors" above that (the cursors panel is the dock's first section;
/// the signal list is a slide-over page). Read from the adjustment, so it is the state the user would see
/// and not the last call made.
static const char* dock_section_in_view(App& a) {
if (a.dock_scroller == nullptr || a.measures_head == nullptr || a.dock_page == nullptr) return "none";
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(a.measures_head, a.dock_page, &zero, &at)) return "none";
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a.dock_scroller));
return gtk_adjustment_get_value(adj) > 0.5 * at.y ? "measure" : "cursors";
}
/// Phase two of the dock-section probe: the accumulators must have advanced while the Measure section was
/// scrolled out of view. A statistics readout is *history*, so min/max/avg would gain a gap for exactly as
/// long as the section was off screen -- which is the one defect the design's "what runs on a hidden
/// page" rule is there to prevent, and the reason no frame-rate code may be gated on which section is in
/// view. (It was a tab once; the dock is one scroller, and the rule outlived the tabs.)
/// Phase two of the dock-section probe: the accumulators must have advanced while the `measure` page was
/// closed. A statistics readout is *history*, so min/max/avg would gain a gap for exactly as long as the
/// page was off screen -- which is the one defect the design's "what runs on a hidden page" rule is there
/// to prevent, and the reason no frame-rate code may be gated on which page is open. (It was a tab once,
/// then a dock section; the rule outlived both.)
gboolean dock_section_probe_report(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
@@ -1824,34 +1824,27 @@ gboolean dock_section_probe_report(gpointer user) {
first = false;
}
}
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
if (a->cursors_panel != nullptr) gtk_widget_set_size_request(a->cursors_panel, -1, -1);
std::fprintf(stderr,
"[side-tab-probe] section=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
" min_n=%" G_GUINT64_FORMAT "\n",
dock_section_in_view(*a), a->measures.size(), a->measure_stats.size(), advanced, min_n);
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
/// Phase one: four measurements on the first channel, statistics on, then scroll the dock *away* from the
/// Measure section and come back in two seconds to read what ran while it was not in view. (The probe
/// keeps its old environment variable, `UDPSCOPE_PROBE_SIDE_TAB_MS`, and its `[side-tab-probe]` tag.)
/// Phase one: four measurements on the first channel, statistics on, then the `measure` page opened for a
/// forced pass and *closed* again; it comes back in two seconds to read what ran while the page was not on
/// screen. (The probe keeps its old environment variable, `UDPSCOPE_PROBE_SIDE_TAB_MS`, and its
/// `[side-tab-probe]` tag.) Run it against a config that has seen the first-run tour, which would close the page.
gboolean dock_section_probe(gpointer user) {
App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE;
// A dock whose content fits has nothing to scroll away from, and a tall window does that. The first
// pass makes the page taller than any window (a probe-only minimum height, undone in the report) and
// comes back once it has been laid out, so "scrolled out of view" is a state this run really reaches.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
// ... and the cursors panel (the dock's first section, above the measurements) taller than any window
// too, which is what pushes the measurements below the viewport at offset 0 (padding the page's end only
// makes it scrollable, and on a window taller than the section's y the section would stay in view).
// Undone in the report.
if (a->cursors_panel != nullptr) gtk_widget_set_size_request(a->cursors_panel, -1, 3000);
// The first pass only waits 400 ms: the enable-all probe switches signals on and off around this time,
// and a buffer rebuild between the baseline and the report would drop the accumulators and read as a
// gated pass. (It also gives the layout the pass this probe's own row assignment needs.)
static bool waited = false;
if (!waited) {
waited = true;
g_timeout_add(400, dock_section_probe, a);
return G_SOURCE_REMOVE;
}
@@ -1892,32 +1885,21 @@ gboolean dock_section_probe(gpointer user) {
if (a->measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
}
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
// From the top, is the measure section (the measurements' heading) below the viewport? Only then is scrolling to it a move *into* view and back out a move *away*; a
// section already in view at offset 0 would make the whole probe a reading of nothing.
gtk_adjustment_set_value(dock_adj, 0.0);
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t measure_at = GRAPHENE_POINT_INIT(-1.f, -1.f);
const bool located =
a->measures_head != nullptr && gtk_widget_compute_point(a->measures_head, a->dock_page, &zero, &measure_at);
const bool outside_at_top = located && static_cast<double>(measure_at.y) >= gtk_adjustment_get_page_size(dock_adj);
dock_show_section(*a, "measure"); // lands on the cursors panel, the dock's top: a no-op scroll
if (located) gtk_adjustment_set_value(dock_adj, measure_at.y); // ... so go to the measurements themselves
// Where the dock went: `scrolled` is its offset and `reach` the most it can scroll. A reach of 0 would
// mean the content fits and "away from the section" is not a state this run can reach.
const double scrolled = gtk_adjustment_get_value(dock_adj);
const double reach = gtk_adjustment_get_upper(dock_adj) - gtk_adjustment_get_page_size(dock_adj);
// On the page: the section is where the user reads the measurements in full.
slideover_open(*a, "measure");
const bool was_open = slideover_page(*a) == "measure";
update_measures(*a, /*force=*/true, /*accumulate=*/true);
g_dock_section_n_before.clear();
for (const MeasureStatsAccum& s : a->measure_stats) g_dock_section_n_before.push_back(s.n);
// Away from the section the measurements are in: they are scrolled out of the viewport (the widgets
// stay mapped, only clipped), and the tick keeps calling `update_measures()` regardless.
gtk_adjustment_set_value(dock_adj, 0.0);
// Away from the page: it is closed (its widgets stay in the tree, unmapped), and the tick keeps calling
// `update_measures()` regardless.
slideover_close(*a);
const bool hidden = slideover_page(*a).empty();
std::fprintf(stderr,
"[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f, "
"outside_at_top=%d)\n",
dock_section_in_view(*a), g_dock_section_n_before.size(), scrolled, reach,
outside_at_top ? 1 : 0);
"[side-tab-probe] section=%s accums=%zu baseline recorded (measure page was_open=%d, "
"now closed=%d)\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
g_dock_section_n_before.size(), was_open ? 1 : 0, hidden ? 1 : 0);
std::fflush(stderr);
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
g_timeout_add(2000, dock_section_probe_report, a);
@@ -4687,7 +4669,7 @@ gboolean measures_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m));
}
dock_show_section(*a, "measure");
slideover_open(*a, "measure"); // the page the probe reads and edits
rebuild_measures_pane(*a);
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
signal.c_str());
@@ -4849,7 +4831,7 @@ gboolean bandavg_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m));
}
dock_show_section(*a, "measure");
slideover_open(*a, "measure"); // the page the probe reads and edits
rebuild_measures_pane(*a);
g_timeout_add(250, measure_trace, a);
bandavg_probe_next(*a, 1, 1500);
@@ -6414,16 +6396,6 @@ gboolean defaults_probe(gpointer user) {
gboolean keys_probe(gpointer user) {
App* a = static_cast<App*>(user);
// `m` scrolls the dock, and a dock whose content fits has nothing to scroll: a run on a tall window
// would read 0->0 and prove nothing. So the first pass only makes the page taller than any window
// (a probe-only minimum height, undone below) and comes back once it has been laid out.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
g_timeout_add(400, keys_probe, a);
return G_SOURCE_REMOVE;
}
std::string line = "[keys]";
const auto before_after = [&](const char* name, GtkWidget* button, AppAction action) {
@@ -6459,39 +6431,23 @@ gboolean keys_probe(gpointer user) {
before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel);
before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane);
// `m` and `g` scroll the dock: the reading is the scroller's offset before and after, and `m` must land
// on the cursors panel's own y (clamped to what the page can scroll, which is why the reachable maximum
// is printed beside it), `g` back on 0.
// `m` and `g` open a slide-over page: `m` the measurements, `g` the signal list, and a second press of
// either closes it again. The dock does not move (it holds only the readouts).
const auto dock_adjustment = [a]() {
return gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
};
const auto dock_value = [&]() { return gtk_adjustment_get_value(dock_adjustment()); };
{
for (const auto& [key, action, page] :
{std::tuple<const char*, AppAction, const char*>{"m", AppAction::ShowMeasure, "measure"},
{"g", AppAction::ShowSignals, "signals"}}) {
const double before = dock_value();
run_app_action(*a, AppAction::ShowMeasure);
run_app_action(*a, action);
const bool opened = slideover_page(*a) == page;
const double after = dock_value();
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at = GRAPHENE_POINT_INIT(-1.f, -1.f);
if (!gtk_widget_compute_point(a->cursors_panel, a->dock_page, &zero, &at)) at.y = -1.f;
GtkAdjustment* adj = dock_adjustment();
const double reach = gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj);
char buf[160];
std::snprintf(buf, sizeof(buf), " m: dock %.0f->%.0f (cursors_y=%.0f reach=%.0f at_cursors=%d) |", before,
after, static_cast<double>(at.y), reach,
std::fabs(after - std::min(static_cast<double>(at.y), reach)) < 1.0 ? 1 : 0);
line += buf;
}
{
// `g` is the signal list's slide-over page now: it opens it, the dock does not move, and a second `g`
// closes it again.
const double before = dock_value();
run_app_action(*a, AppAction::ShowSignals);
const bool opened = slideover_page(*a) == "signals";
const double after = dock_value();
run_app_action(*a, AppAction::ShowSignals);
run_app_action(*a, action);
const bool closed = slideover_page(*a).empty();
char buf[96];
std::snprintf(buf, sizeof(buf), " g: page=%d dock_still=%d closed=%d |", opened ? 1 : 0,
std::snprintf(buf, sizeof(buf), " %s: page=%d dock_still=%d closed=%d |", key, opened ? 1 : 0,
std::fabs(after - before) < 1.0 ? 1 : 0, closed ? 1 : 0);
line += buf;
}
@@ -6512,21 +6468,17 @@ gboolean keys_probe(gpointer user) {
}
// An accel must be refused while text is being typed. Focus the window entry, send 'm'
// through the real handler, and require the dock not to scroll.
// through the real handler, and require no page to open.
int refused = -1;
if (a->window_entry != nullptr) {
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
gtk_widget_grab_focus(a->window_entry);
// The dock has to be somewhere other than where `m` would take it, or "did not scroll" proves nothing.
gtk_adjustment_set_value(dock_adjustment(), 0.0);
const double before_dock = dock_value();
const std::string page_before = slideover_page(*a);
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0,
static_cast<GdkModifierType>(0), a);
const double after_dock = dock_value();
refused = (handled == FALSE && before_dock == after_dock) ? 1 : 0;
refused = (handled == FALSE && slideover_page(*a) == page_before) ? 1 : 0;
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
}
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
char tail[64];
std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused);
line += tail;