layout: detach a figure to its own window, and attach it back

Beside each figure row's full-screen toggle -- same corner, same show-on-approach reveal, same
one-figure/never-maximized rules -- a **detach** button moves the figure into a window of its
own: copied into a new scope window (titled "detached") and removed from the main window's rows,
which then save the change. The detached window is the one scope window with an **attach**
control: an overlay wrapping its GL area, a button in the top-right corner revealed by the same
pointer-proximity rule from the overlay's own motion controller, and one click appends the
figure back to the main window's rows (cells, weights, every slot's signal in its colour) and
closes the window.

`detach_figure()` / `attach_scope_window()`; the reveal and placement share the maximize
buttons' geometry and pointer state, per row. A detached window's rows participate everywhere a
scope window's do (drops, the signal page's *Plot on* groups), which the previous commit gave
them.

Probe: `[maximize-probe]` now counts the detach buttons (`detach_single=0` for one figure,
`detach=2` for two, absent while maximized) and reads their corner reveal
(`detach_near=1 detach_other=0`, the pointer parked on the detach button's own centre -- the
strictest zone of the pair). Real-pointer smoke: hover reveals the pair, clicking detach opens
the window, clicking attach closes it with the figure back. Docs: README, the Mallard layout
page, the probe readings, the layout invariant, and the tour's plot stop.
This commit is contained in:
Martino Ferrari committed 2026-11-05 23:35:26 +01:00
1 parent 51b4fad386
commit 5ed75eb470
7 files changed
+303 -28

No files matched your search

+27 -1
View File
@@ -221,6 +221,18 @@ struct App {
/// The drop target wired onto `widget`: the handler reads the *window* off it (each window's
/// target names its own scope), and the probe calls the handler with it directly.
GtkDropTarget* drop = nullptr;
/// This window came from a **detach** -- the figure left the main window to live here -- so it
/// carries the attach control and can be put back. A copied or empty window is a plain second
/// view, and has no attach control.
bool detached = false;
/// The attach control: an overlay wrapping the GL widget, its button in the top-right corner
/// (revealed by pointer proximity, the same rule the main window's toggles follow), and the
/// pointer state the overlay's own motion controller keeps.
GtkWidget* attach_overlay = nullptr;
std::vector<GtkWidget*> attach_buttons;
double pointer_x = 0.0;
double pointer_y = 0.0;
bool pointer_valid = false;
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
/// outlives the widget, and sharing the main window's placeholder would hand two areas
/// one plot the moment the figure mode is ever left.
@@ -243,6 +255,10 @@ struct App {
/// count changes (`maximize_buttons_rows` is the count they were built for).
std::vector<GtkWidget*> maximize_buttons;
int maximize_buttons_rows = -1;
/// The **detach** buttons: one per figure row beside its full-screen toggle, same reveal,
/// same rebuild cadence -- but only while the layout holds two or more figures (detaching the
/// only figure would leave the main window with nothing to draw) and never while maximized.
std::vector<GtkWidget*> detach_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;
@@ -878,6 +894,7 @@ struct App {
int dbg_maximize_step = 0;
std::size_t dbg_maximize_single = 0;
int dbg_maximize_hidden = -1;
std::size_t dbg_detach_single = 0;
/// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`).
int dbg_scope_step = 0;
int cursors_pane_mode = -1;
@@ -2161,7 +2178,16 @@ gboolean scope_probe_step(gpointer user);
/// A second top-level window showing the same stream through its own figure: `figure_row` >= 0
/// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an
/// empty one-row scope. The main window's tick binds, acquires and repaints it.
void open_scope_window(App& app, int figure_row);
void open_scope_window(App& app, int figure_row, bool detached = false);
/// Detaches figure row `fr` into a window of its own: the figure is copied into a new scope
/// window -- which is what makes the window *detached*, the one kind with an attach control --
/// and removed from the main window's rows. Refused, changing nothing, for a one-figure layout
/// or a maximized one: detaching then would leave the main window with nothing to draw.
void detach_figure(App& app, int fr);
/// Puts a detached window's figure back: a figure row is appended to the main window carrying
/// the window's arrangement (cells, weights, every slot's signal in its colour) and the window
/// closes. The window must be a detached one; anything else returns without changing.
void attach_scope_window(App& app, App::ScopeWindow& scope);
gboolean measurepin_probe(gpointer user);
gboolean measurepin_held_wait(gpointer user);
gboolean measurepin_pause_wait(gpointer user);