fix: the sidebar button follows the panel, and the wish survives having no source

The toolbar button was the panel's only input: `update_side_visibility()` derived the
panel from the button and never the other way, so the button went on saying "shown" in
two cases where the panel was off -- while there were no signals to show, and after
anything else moved the split view's state.

Binding the two together is not the fix. AdwOverlaySplitView writes `show-sidebar`
itself, at startup and when it collapses, with no user involved; a binding pushes that
into the button, the button's handler reads it as the user's wish, and the sidebar then
never comes back when a source connects -- a worse bug than the one reported, and one
that a "was this our own write" flag does not catch either, because the toggle's signal
may fire after the write returns.

So the direction is stated instead of inferred: `show-sidebar` is an output, the button
is the only input, and `App::sidebar_wanted` holds the wish between them. The wish is
written by the toggle's handler alone (guarded while we set the button ourselves), the
no-signals case forces the panel off without touching the wish, and tick() re-asserts
both from the wish when the widget disagrees -- so a hide gesture does not stick either.

[sidebar-probe] now reads the pair (shown vs button) in both states and against a hide
that did not come from the button; shown=1 button=1 with a source, shown=0 button=0
without, and the source reconnecting brings both back is the reading this exists for.
This commit is contained in:
Martino Ferrari committed 2026-10-12 13:23:40 +02:00
1 parent bb01278d88
commit 2d9f327da7
5 files changed
+95 -16

No files matched your search

+30 -3
View File
@@ -2751,13 +2751,20 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
void update_side_visibility(App& app) {
if (app.side_scroll == nullptr) return;
const bool have_signals = !app.model->signals().empty();
const bool want_column =
have_signals && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle));
// `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;
// 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. Hiding the scroller
// instead left an empty column where the panels should be.
if (app.side_view != nullptr) {
app.sidebar_syncing = true;
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app.side_view), want_column);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), want_column);
app.sidebar_syncing = false;
}
gtk_widget_set_visible(app.signal_toggle, have_signals);
gtk_widget_set_visible(app.trigger_toggle, have_signals);
@@ -2765,10 +2772,13 @@ void update_side_visibility(App& app) {
void on_sidebar_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
(void)button;
// 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;
update_side_visibility(*app);
}
// --- rebuild of the derived panels -----------------------------------------
@@ -3627,6 +3637,18 @@ gboolean tick(gpointer user) {
// to nothing. `kSidebarRoom` is the sidebar's 320 plus the width below which the plot stops being
// worth looking at.
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;
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) {
app->sidebar_syncing = true;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->sidebar_toggle), want);
app->sidebar_syncing = false;
}
}
const int window_w = gtk_widget_get_width(app->window);
if (window_w > 0) {
adw_overlay_split_view_set_collapsed(ADW_OVERLAY_SPLIT_VIEW(app->side_view),
@@ -4437,6 +4459,11 @@ void on_activate(GtkApplication* application, gpointer user) {
// this is where it went when the window moved into the file, and it is why the window came up small.
gtk_window_set_default_size(GTK_WINDOW(app->window), 1280, 800);
// Nothing listens to the split view's own `show-sidebar`: it is an *output*. `AdwOverlaySplitView`
// writes it itself -- at startup, and when it collapses -- and reading that back as user input is what
// made connecting a source leave the sidebar shut. The button is the only input; `update_side_visibility`
// re-asserts the wish on top of the widget (see the tick).
// The badge and the text share one row so the badge sits at the line's head. It is a
// sibling rather than part of the label's own markup because the label's text is
// rewritten every frame and its markup is ellipsized -- a blinking span inside it would
+10
View File
@@ -408,6 +408,16 @@ struct App {
/// 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;
+27 -7
View File
@@ -1634,7 +1634,8 @@ gboolean boxzoom_probe(gpointer user) {
}
/// `UDPSCOPE_PROBE_SIDEBAR_MS`: the sidebar's width, in both of the split view's states.
/// `UDPSCOPE_PROBE_SIDEBAR_MS`: the sidebar's width and whether the toolbar button agrees with it, in
/// both of the split view's states and against a hide that did not come from the button.
///
/// The small-window case is the one a divider position got wrong: with the sidebar's width derived from
/// the window, a window narrower than that number gave it nothing at all. The split view takes its size
@@ -1644,24 +1645,43 @@ gboolean boxzoom_probe(gpointer user) {
gboolean sidebar_probe(gpointer user) {
App& a = *static_cast<App*>(user);
if (a.side_view == nullptr) return G_SOURCE_REMOVE;
// `shown` is the panel's own state and `button` the toolbar's; they must agree, or the button is
// telling the user something the window does not do.
const auto report = [](App& app, const char* what) {
const int side = app.side_box != nullptr ? gtk_widget_get_width(app.side_box) : -1;
const int win = app.window != nullptr ? gtk_widget_get_width(app.window) : -1;
std::fprintf(stderr, "[sidebar-probe] %-10s window=%d sidebar=%d collapsed=%d\n", what, win,
side, adw_overlay_split_view_get_collapsed(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1
: 0);
std::fprintf(stderr,
"[sidebar-probe] %-10s window=%d sidebar=%d collapsed=%d shown=%d button=%d(sens=%d)\n",
what, win, side,
adw_overlay_split_view_get_collapsed(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1 : 0,
adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1 : 0,
app.sidebar_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle)) ? 1 : 0,
app.sidebar_toggle != nullptr && gtk_widget_get_sensitive(app.sidebar_toggle) ? 1 : 0);
};
report(a, "beside");
// The state a window with no room produces: the sidebar goes over the plot, and its width is still its
// own. If this ever reads 0, the sidebar is being given a share of a width that is not there.
adw_overlay_split_view_set_collapsed(ADW_OVERLAY_SPLIT_VIEW(a.side_view), TRUE);
// A hide that did not come from the button -- an overlay swipe, or the split view's own collapsing --
// must not win: the panel is where the button says it is. `shown` and `button` still reading 1 here is
// the point, and reading 0 is the regression (a button that no longer reflects the panel, or a panel
// that slips out from under it).
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a.side_view), FALSE);
g_timeout_add(400, +[](gpointer u) -> gboolean {
App& app = *static_cast<App*>(u);
const int side = app.side_box != nullptr ? gtk_widget_get_width(app.side_box) : -1;
std::fprintf(stderr, "[sidebar-probe] %-10s window=%d sidebar=%d collapsed=%d\n", "overlaid",
app.window != nullptr ? gtk_widget_get_width(app.window) : -1, side,
adw_overlay_split_view_get_collapsed(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1 : 0);
std::fprintf(stderr,
"[sidebar-probe] %-10s window=%d sidebar=%d collapsed=%d shown=%d button=%d(sens=%d)\n",
"regained", app.window != nullptr ? gtk_widget_get_width(app.window) : -1, side,
adw_overlay_split_view_get_collapsed(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1 : 0,
adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app.side_view)) ? 1 : 0,
app.sidebar_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle)) ? 1 : 0,
app.sidebar_toggle != nullptr && gtk_widget_get_sensitive(app.sidebar_toggle) ? 1 : 0);
adw_overlay_split_view_set_collapsed(ADW_OVERLAY_SPLIT_VIEW(app.side_view), FALSE);
// Back the way a person would: pressing the button is what records the wish, and the panel follows it.
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), TRUE);
return G_SOURCE_REMOVE;
}, &a);
return G_SOURCE_REMOVE;