layout editor: horizontal-resize icon, divider drag follows the pointer; x caption on every labelled plot; probes and docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c228c4822
commit
7972646dd2
14 files changed
+312
-65
No files matched your search
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- A boundary that can be dragged sideways: a horizontal double-headed arrow, the turned twin of
|
||||
udpscope-resize-rows-symbolic. It says *which* edge the handle moves, which a single chevron (a
|
||||
"go next") does not. Every coordinate is a whole pixel of the 16px cell, every subpath is a filled
|
||||
`path` wound the same way (a stroke would keep this colour on a dark theme), and the two heads
|
||||
meet the shaft exactly, at y 5..11 and y 7..9. -->
|
||||
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M4 7 H12 V9 H4 Z
|
||||
M1 8 L5 5 V11 Z
|
||||
M15 8 L11 11 V5 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
@@ -22,6 +22,7 @@
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-cols-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app-symbolic.svg</file>
|
||||
</gresource>
|
||||
|
||||
+18
-7
@@ -1094,10 +1094,11 @@ bool RowSet::update_time_labels() {
|
||||
// millisecond-scale window shows. `x_origin_` is read back below for the caption.
|
||||
r.plot->set_x_axis_origin_from_range(r.axis_x, relative_x_);
|
||||
if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0);
|
||||
// The caption belongs with the labels, which a figure draws once, on the bottom row (the
|
||||
// one whose x axis is the figure's). Every other row gets an empty one, which draws nothing.
|
||||
const bool bottom = i >= first_plot_of_row(figure_row_count() - 1);
|
||||
if (!relative_x_ || !bottom) {
|
||||
// The caption belongs with the labels, so it goes on every plot whose x strip draws them: the
|
||||
// bottom row's, and any row above it that carries a strip of its own (a split row whose columns
|
||||
// differ from the row below's; see `Figure`'s x-axis rule). The plots without labels get an empty
|
||||
// one, which draws nothing.
|
||||
if (!relative_x_ || !plot::x_strip_has_labels(r.plot->x_strip_mode())) {
|
||||
r.plot->set_x_note("");
|
||||
continue;
|
||||
}
|
||||
@@ -1132,11 +1133,21 @@ void RowSet::hold_pinned_window() {
|
||||
// statement, which is what keeps a fresh pin (`pin_event_window()`, `pin_pause_window()`, both
|
||||
// of which write the group in this same tick) from being reverted to the member's stale window.
|
||||
plot::Range now = figure_->x_link_range(x_group_);
|
||||
if (rows_ > 0 && row(0).plot != nullptr) {
|
||||
const plot::Range member = row(0).plot->x_range(row(0).axis_x);
|
||||
// Any plot's member may carry the gesture: the controller writes the plot under the pointer, and an x
|
||||
// label strip is a plot's own (the bottom row's, or a row above it whose columns differ), so reading
|
||||
// plot 0 alone made a zoom or pan on every other plot's axis vanish when the next statement of the pin
|
||||
// went over it. Every member was last written with `pinned_stated_` (the group push), so the first
|
||||
// one that differs is the gesture; the same clamp below applies whichever plot it came from.
|
||||
for (int i = 0; i < rows_; ++i) {
|
||||
Row& r = row(i);
|
||||
if (r.plot == nullptr) continue;
|
||||
const plot::Range member = r.plot->x_range(r.axis_x);
|
||||
const bool member_is_ours = std::abs(member.min - pinned_stated_.min) < 1e-12 &&
|
||||
std::abs(member.max - pinned_stated_.max) < 1e-12;
|
||||
if (!member_is_ours) now = member;
|
||||
if (!member_is_ours) {
|
||||
now = member;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const double pin_span = pin.span();
|
||||
// **A zoom *in* is allowed.** The rule is that the view may not leave the time window, and a
|
||||
|
||||
+45
-13
@@ -753,8 +753,11 @@ void on_layout_drag_end(GtkGestureDrag*, double, double, gpointer user) {
|
||||
|
||||
// --- column (cell) resize drag -----------------------------------------------
|
||||
|
||||
// The two cells' widths here are *plot* widths (a cell's rect without the first cell's y gutter): the
|
||||
// 96 px floor and the plotter's 64 px floor both apply to them, and the weights share them out.
|
||||
// The two cells' widths here are *plot* widths -- `layout().axes.w`, the framed plotting area, which is
|
||||
// what the plotter places cells by (cell rects add the first cell's y gutter, the last cell's right gutter
|
||||
// and the column gap on top): the 96 px floor and the plotter's 64 px floor both apply to them, and the
|
||||
// weights share them out. `offset_px` is the pointer's travel in *device* px (a gesture's logical offset
|
||||
// times the area's scale).
|
||||
void layout_col_apply(App& app, int plot, double offset_px) {
|
||||
RowSet& rs = *app.rows;
|
||||
const int fr = rs.figure_row_of(plot);
|
||||
@@ -791,18 +794,15 @@ bool layout_col_drag_begin(App& app, int plot) {
|
||||
const int fr = app.rows->figure_row_of(plot);
|
||||
const int k = app.rows->cell_of(plot);
|
||||
if (fr < 0 || k < 1) return false;
|
||||
const plot::FigureLayout& fl = app.rows->figure().layout();
|
||||
const double scale =
|
||||
app.area != nullptr && app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0;
|
||||
// The weights share out *plot* widths, so the pair is measured as plots: a cell's rect minus the y
|
||||
// gutter (axis strip + title column) that only the first cell of a row has. The left cell of the pair
|
||||
// is the first one when `plot - 1` starts the row; the right cell never has a gutter, and neither does
|
||||
// a non-first left cell (its plot's strips read 0).
|
||||
// The weights share out *plot* widths, so the pair is measured as plots: each cell's `axes.w`, the
|
||||
// plotting area the plotter places a cell by. Not the cell rect: that adds the y gutter of the first
|
||||
// cell, the right gutter of the last and half a column gap, none of which the weights apply to.
|
||||
(void)app.rows->figure().layout(); // non-const: re-lays the figure, and every plot, on the current weights
|
||||
const auto plot_width = [&](int cell) {
|
||||
const RowSet::Row& r = app.rows->row(cell);
|
||||
const double gutter =
|
||||
r.plot != nullptr ? double(r.plot->layout().y_axis_width + r.plot->layout().y_title_width) : 0.0;
|
||||
return std::max(0.0, double(fl.cells[static_cast<std::size_t>(cell)].w) - gutter);
|
||||
return r.plot != nullptr ? std::max(0.0, static_cast<double>(r.plot->layout().axes.w)) : 0.0;
|
||||
};
|
||||
app.layout_col_drag_left0 = plot_width(plot - 1) / scale;
|
||||
app.layout_col_drag_right0 = plot_width(plot) / scale;
|
||||
@@ -811,15 +811,39 @@ bool layout_col_drag_begin(App& app, int plot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Where the drag handle sits in the overlay's coordinates (logical px), or `fallback` when it cannot be
|
||||
/// asked. The handle follows the divider it drags (`sync_layout_editor()` re-places it every frame), so it
|
||||
/// is *moving* while the gesture runs.
|
||||
static double layout_col_handle_x(App& app, GtkWidget* handle, double fallback) {
|
||||
if (handle == nullptr || app.area_overlay == nullptr) return fallback;
|
||||
graphene_point_t out;
|
||||
const graphene_point_t origin = GRAPHENE_POINT_INIT(0.0f, 0.0f);
|
||||
if (!gtk_widget_compute_point(handle, app.area_overlay, &origin, &out)) return fallback;
|
||||
return static_cast<double>(out.x);
|
||||
}
|
||||
|
||||
double layout_col_total_offset(double offset, double handle_now, double handle_begin) {
|
||||
return offset + (handle_now - handle_begin);
|
||||
}
|
||||
|
||||
void on_layout_col_begin(GtkGestureDrag* gesture, double, double, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
layout_col_drag_begin(
|
||||
app, GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gesture), "udpscope-layout-col-drag")));
|
||||
app.layout_col_drag_handle_x0 = layout_col_handle_x(
|
||||
app, gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)), 0.0);
|
||||
}
|
||||
|
||||
void on_layout_col_update(GtkGestureDrag*, double offset_x, double, gpointer user) {
|
||||
void on_layout_col_update(GtkGestureDrag* gesture, double offset_x, double, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
layout_col_apply(app, app.layout_col_drag_plot, offset_x);
|
||||
// The gesture reports its offset in the *handle's* coordinates, and the handle follows the divider, so
|
||||
// the offset shrinks by the handle's own displacement; adding that back gives the pointer's travel.
|
||||
const double now = layout_col_handle_x(
|
||||
app, gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)), app.layout_col_drag_handle_x0);
|
||||
const double scale =
|
||||
app.area != nullptr && app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0;
|
||||
layout_col_apply(app, app.layout_col_drag_plot,
|
||||
layout_col_total_offset(offset_x, now, app.layout_col_drag_handle_x0) * scale);
|
||||
}
|
||||
|
||||
void on_layout_col_end(GtkGestureDrag*, double, double, gpointer user) {
|
||||
@@ -827,6 +851,7 @@ void on_layout_col_end(GtkGestureDrag*, double, double, gpointer user) {
|
||||
app.layout_col_drag_plot = -1;
|
||||
app.layout_col_drag_left0 = 0.0;
|
||||
app.layout_col_drag_right0 = 0.0;
|
||||
app.layout_col_drag_handle_x0 = 0.0;
|
||||
app.layout_col_drag_start.clear();
|
||||
schedule_layout_save(app);
|
||||
}
|
||||
@@ -948,7 +973,7 @@ void rebuild_layout_editor(App& app) {
|
||||
const int first = app.rows->first_plot_of_row(fr);
|
||||
for (int k = 1; k < n; ++k) {
|
||||
const int plot = first + k;
|
||||
GtkWidget* b = layout_overlay_button(app, "go-next-symbolic", "drag to resize the plots");
|
||||
GtkWidget* b = layout_overlay_button(app, "udpscope-resize-cols-symbolic", "drag to resize the plots");
|
||||
gtk_widget_set_halign(b, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(b, GTK_ALIGN_START);
|
||||
g_object_set_data(G_OBJECT(b), kLayoutColKey, GINT_TO_POINTER(plot));
|
||||
@@ -3320,6 +3345,13 @@ void note_panel_controls(App& app) {
|
||||
app.dbg_toolbar_icons += std::string(app.dbg_toolbar_icons.empty() ? "" : " ") + name + "=" +
|
||||
icon + (gtk_icon_theme_has_icon(theme, icon) ? "(ok)" : "(MISSING)");
|
||||
}
|
||||
// The layout editor's two resize handles are not toolbar buttons (they exist only while the editor is
|
||||
// up), so they are asked of the theme by name: the same silent failure, the same line.
|
||||
for (const auto& [label, icon] : {std::pair<const char*, const char*>{"resize_rows", "udpscope-resize-rows-symbolic"},
|
||||
std::pair<const char*, const char*>{"resize_cols", "udpscope-resize-cols-symbolic"}}) {
|
||||
app.dbg_toolbar_icons += std::string(app.dbg_toolbar_icons.empty() ? "" : " ") + label + "=" + icon +
|
||||
(gtk_icon_theme_has_icon(theme, icon) ? "(ok)" : "(MISSING)");
|
||||
}
|
||||
// The application icon, the same two ways and for the same reason: a resource path that was
|
||||
// never registered is not an error, it is a missing-image placeholder. This one is read on
|
||||
// the first tick like the line above, because the dump runs after the window is destroyed.
|
||||
|
||||
+11
-3
@@ -209,12 +209,17 @@ struct App {
|
||||
int layout_drag_row = -1;
|
||||
std::vector<float> layout_drag_start;
|
||||
/// Column drag state: which plot (right-hand cell), the two starting *plot* widths in logical px
|
||||
/// (a cell's rect minus the y gutter only the first cell of a row has), and the cell weights
|
||||
/// snapshot at drag begin (`layout_col_apply()` redistributes the pair's weight sum from these, so
|
||||
/// a drag stays relative to where it started).
|
||||
/// (each plot's `layout().axes.w`, the area the plotter places a cell by -- not the cell rect, which
|
||||
/// adds the y and right gutters and the gap), and the cell weights snapshot at drag begin
|
||||
/// (`layout_col_apply()` redistributes the pair's weight sum from these, so a drag stays relative to
|
||||
/// where it started).
|
||||
int layout_col_drag_plot = -1;
|
||||
double layout_col_drag_left0 = 0.0;
|
||||
double layout_col_drag_right0 = 0.0;
|
||||
/// The drag handle's x in the overlay's coordinates (logical px) when the drag began. The handle is
|
||||
/// re-placed on the divider every frame, and a gesture reports its offset in the handle's own, moving,
|
||||
/// coordinates; `layout_col_total_offset()` adds the handle's displacement since this back.
|
||||
double layout_col_drag_handle_x0 = 0.0;
|
||||
std::vector<float> layout_col_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
|
||||
@@ -2121,6 +2126,9 @@ GtkWidget* layout_col_handle(App& a, int plot);
|
||||
/// (the right-hand cell) begins with. False when `plot` has no left neighbour.
|
||||
bool layout_col_drag_begin(App& app, int plot);
|
||||
void layout_col_apply(App& app, int plot, double offset_px);
|
||||
/// A column drag's pointer travel from what its gesture reports: `offset` is relative to the handle, which
|
||||
/// has moved from `handle_begin` to `handle_now` (same units, the overlay's) since the drag began.
|
||||
double layout_col_total_offset(double offset, double handle_now, double handle_begin);
|
||||
void layout_probe_report(App& a, const char* what);
|
||||
/// The split in the report lines, from the `RowSet` alone (no widget is asked, so the exit dump can
|
||||
/// use them): cells per figure row (`1,2,1`) and each figure row's cell weights, rows divided by `|`
|
||||
|
||||
+170
-9
@@ -3986,6 +3986,93 @@ struct SharedYProbe {
|
||||
};
|
||||
SharedYProbe g_shared_y;
|
||||
|
||||
/// What the x-axes steps carry from arrangement to arrangement: per arrangement, whether row 0's plots each
|
||||
/// reserve an x strip, whether the bottom row's do, whether neighbouring plots' axes are one `col_gap`
|
||||
/// apart, and whether the `t0` caption sits on exactly the plots that draw labels.
|
||||
struct XAxesProbe {
|
||||
std::string row0;
|
||||
std::string bottom;
|
||||
std::string gap_ok;
|
||||
std::string gap;
|
||||
std::string note;
|
||||
double window_before = 0.0;
|
||||
double col_gap = 0.0;
|
||||
};
|
||||
XAxesProbe g_x_axes;
|
||||
/// The absolute 50 ms span the x-axes steps pin the view to, and the zoom inside it written onto the last plot.
|
||||
constexpr double kXAxesPinLo = 100.0;
|
||||
constexpr double kXAxesPinHi = 100.05;
|
||||
constexpr double kXAxesZoomLo = 100.01;
|
||||
constexpr double kXAxesZoomHi = 100.03;
|
||||
|
||||
/// A trace on every plot: with data bound the live axis spans [newest - window, newest], which is what makes
|
||||
/// the labels relative at a short window (an empty plot frames {-window, 0} and needs no anchor).
|
||||
void split_probe_x_axes_fill(App& a) {
|
||||
for (int p = 0; p < a.rows->row_count(); ++p) split_probe_assign(a, p, 0);
|
||||
}
|
||||
|
||||
/// Reads the arrangement the last frame was laid out with into `g_x_axes`.
|
||||
void split_probe_x_axes_read(App& a) {
|
||||
RowSet& rs = *a.rows;
|
||||
const double scale = (a.area != nullptr && a.area->device_scale() > 0.0f)
|
||||
? static_cast<double>(a.area->device_scale()) : 1.0;
|
||||
const plot::FigureLayout& fl = rs.figure().layout(); // non-const: lays every plot out on the weights
|
||||
const int frows = rs.figure_row_count();
|
||||
// 1 when every plot of the figure row reserves an x strip, 0 when none does, 2 for a mixture (a defect:
|
||||
// a row's cells share their strip decision).
|
||||
const auto strips = [&](int fr) {
|
||||
int with = 0;
|
||||
const int first = rs.first_plot_of_row(fr);
|
||||
const int n = rs.cells_in_row(fr);
|
||||
for (int k = 0; k < n; ++k) {
|
||||
const RowSet::Row& r = rs.row(first + k);
|
||||
if (r.plot != nullptr && r.plot->layout().x_axis_height > 0.0f) ++with;
|
||||
}
|
||||
return with == n ? 1 : (with == 0 ? 0 : 2);
|
||||
};
|
||||
const auto add = [](std::string& out, const std::string& v) { out += (out.empty() ? "" : ",") + v; };
|
||||
add(g_x_axes.row0, std::to_string(strips(0)));
|
||||
add(g_x_axes.bottom, std::to_string(strips(frows - 1)));
|
||||
// The gap between neighbouring plots' axes, over every split row (shared y: only the last cell has a
|
||||
// right gutter, so the distance is the column gap and nothing else).
|
||||
int gap_ok = 1;
|
||||
double gap_min = 1.0e9;
|
||||
double gap_max = -1.0e9;
|
||||
int pairs = 0;
|
||||
for (int fr = 0; fr < frows; ++fr) {
|
||||
const int first = rs.first_plot_of_row(fr);
|
||||
for (int k = 0; k + 1 < rs.cells_in_row(fr); ++k) {
|
||||
const RowSet::Row& l = rs.row(first + k);
|
||||
const RowSet::Row& r = rs.row(first + k + 1);
|
||||
if (l.plot == nullptr || r.plot == nullptr) { gap_ok = 0; continue; }
|
||||
const double d = static_cast<double>(r.plot->layout().axes.x) - l.plot->layout().axes.right();
|
||||
gap_min = std::min(gap_min, d);
|
||||
gap_max = std::max(gap_max, d);
|
||||
++pairs;
|
||||
if (std::fabs(d - static_cast<double>(fl.col_gap)) > 0.5) gap_ok = 0;
|
||||
}
|
||||
}
|
||||
if (pairs == 0) gap_ok = 0;
|
||||
g_x_axes.col_gap = static_cast<double>(fl.col_gap) / scale;
|
||||
add(g_x_axes.gap_ok, std::to_string(gap_ok));
|
||||
char buf[48];
|
||||
std::snprintf(buf, sizeof(buf), "%.1f/%.1f", pairs > 0 ? gap_min / scale : 0.0,
|
||||
pairs > 0 ? gap_max / scale : 0.0);
|
||||
add(g_x_axes.gap, buf);
|
||||
// The caption goes with the labels: a plot has a note exactly when its x strip draws labels. Only
|
||||
// decidable while the labels are relative (otherwise no plot has a note); -1 says so.
|
||||
int note_ok = rs.time_labels_relative() ? 1 : -1;
|
||||
if (note_ok == 1) {
|
||||
for (int p = 0; p < rs.row_count(); ++p) {
|
||||
const RowSet::Row& r = rs.row(p);
|
||||
if (r.plot == nullptr) continue;
|
||||
const bool labelled = plot::x_strip_has_labels(r.plot->x_strip_mode());
|
||||
if (labelled != !r.plot->x_note().empty()) note_ok = 0;
|
||||
}
|
||||
}
|
||||
add(g_x_axes.note, std::to_string(note_ok));
|
||||
}
|
||||
|
||||
void split_probe_clear_all(App& a) {
|
||||
for (int r = 0; r < RowSet::kMaxRows; ++r) {
|
||||
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) a.rows->assign(r, s, "", "", 0);
|
||||
@@ -4203,14 +4290,13 @@ gboolean split_probe_step(gpointer user) {
|
||||
const double scale2 = (a->area != nullptr && a->area->device_scale() > 0.0f)
|
||||
? static_cast<double>(a->area->device_scale()) : 1.0;
|
||||
const int plot1 = rs.first_plot_of_row(0) + 1;
|
||||
// The cells compared are *plot* widths (`axes.w` plus the right gutter, i.e. the cell rect without
|
||||
// the y gutter that only the first cell has): the weights apply to those, so the divider follows the
|
||||
// pointer only when the drag is measured the same way. The rect widths carry the gutter and are
|
||||
// reported separately (`rect=`), they are not what is checked.
|
||||
// The cells compared are *plot* widths (`axes.w`): the plotter places cells by them and the weights
|
||||
// share them out, so the divider follows the pointer only when the drag is measured the same way.
|
||||
// The rect widths also carry the first cell's y gutter, the last cell's right gutter and half a column
|
||||
// gap; the first cell's excess is reported separately (`gutter0=`), it is not what is checked.
|
||||
const auto width = [&](int cell) {
|
||||
(void)rs.figure().layout(); // non-const: re-lays the figure and every plot out on the weights
|
||||
const plot::PlotLayout& pl = rs.row(cell).plot->layout();
|
||||
return static_cast<double>(pl.axes.w + pl.right_gutter) / scale2;
|
||||
return static_cast<double>(rs.row(cell).plot->layout().axes.w) / scale2;
|
||||
};
|
||||
const auto rect_width = [&](int cell) {
|
||||
return static_cast<double>(rs.figure().layout().cells[static_cast<std::size_t>(cell)].w) / scale2;
|
||||
@@ -4260,8 +4346,15 @@ gboolean split_probe_step(gpointer user) {
|
||||
std::fabs((w0_min + w1_max) - pair0) <= 1.0 ? 1 : 0;
|
||||
const int both_above_min = (w0_min >= kMinCellPx - 1.0 && w1_min >= kMinCellPx - 1.0 &&
|
||||
w0_max >= kMinCellPx - 1.0 && w1_max >= kMinCellPx - 1.0) ? 1 : 0;
|
||||
// The gesture's offset is relative to the drag handle, which follows the divider: a handle that has moved
|
||||
// 40 px reports an offset 40 px short, and the helper adds the displacement back. The one piece of
|
||||
// the drag handler a headless run can reach (a real pointer drag cannot be simulated).
|
||||
const int handle_comp = std::fabs(layout_col_total_offset(40.0, 140.0, 100.0) - 80.0) < 1e-9 &&
|
||||
std::fabs(layout_col_total_offset(80.0, 100.0, 100.0) - 80.0) < 1e-9 &&
|
||||
std::fabs(layout_col_total_offset(-25.0, 70.0, 100.0) + 55.0) < 1e-9
|
||||
? 1 : 0;
|
||||
const int ok = left_gt && left_grew && follows_80 && sum_80 && relative && right_floor &&
|
||||
left_floor && sum_ext && both_above_min ? 1 : 0;
|
||||
left_floor && sum_ext && both_above_min && handle_comp ? 1 : 0;
|
||||
// What the rect carries over the plot: the first cell's y gutter (the second has none).
|
||||
const double gutter0 = rect_width(0) - width(0);
|
||||
// Leave the drag ended, as the gesture's end does.
|
||||
@@ -4269,10 +4362,10 @@ gboolean split_probe_step(gpointer user) {
|
||||
a->layout_col_drag_start.clear();
|
||||
std::fprintf(stderr,
|
||||
"[split-probe] col-drag ok=%d left_gt_right=%d follows_80=%d relative=%d "
|
||||
"sum_80=%d right_floor=%d left_floor=%d sum_ext=%d both_above_min=%d "
|
||||
"sum_80=%d right_floor=%d left_floor=%d sum_ext=%d both_above_min=%d handle_comp=%d "
|
||||
"tile=%d d80=[%.1f,%.1f]->[%.1f,%.1f] ext=[%.1f,%.1f|%.1f,%.1f] gutter0=%.1f\n",
|
||||
ok, left_gt, follows_80, relative, sum_80, right_floor, left_floor, sum_ext,
|
||||
both_above_min, split_probe_tile(*a), d80.l0, d80.r0, d80.l, d80.r, w0_max,
|
||||
both_above_min, handle_comp, split_probe_tile(*a), d80.l0, d80.r0, d80.l, d80.r, w0_max,
|
||||
w1_min, w0_min, w1_max, gutter0);
|
||||
}
|
||||
split_probe_next(*a);
|
||||
@@ -4522,6 +4615,7 @@ gboolean split_probe_step(gpointer user) {
|
||||
rs.set_cursor_mode(g_shared_y.was_mode);
|
||||
rs.set_cursors_visible(g_shared_y.was_on);
|
||||
rebuild_cursors_pane(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
const plot::Range a_rng = ra.plot->y_range(ra.axis_y);
|
||||
@@ -4544,6 +4638,73 @@ gboolean split_probe_step(gpointer user) {
|
||||
rs.set_cursor_mode(g_shared_y.was_mode);
|
||||
rs.set_cursors_visible(g_shared_y.was_on);
|
||||
rebuild_cursors_pane(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
// --- Steps 23..27: which rows carry an x axis, and the gap between a split row's plots ---
|
||||
case 23: {
|
||||
// [2]/[1]: row 0's columns differ from row 1's, so row 0 keeps its own x strip. The view is pinned to
|
||||
// an absolute 50 ms span (a paused view's shape) so the labels go relative and the `t0` caption is on
|
||||
// screen for the caption reading; the window is restored at the end.
|
||||
g_x_axes = XAxesProbe{};
|
||||
g_x_axes.window_before = rs.time_window();
|
||||
rs.set_cells_per_row({2, 1});
|
||||
split_probe_clear_all(*a);
|
||||
split_probe_x_axes_fill(*a);
|
||||
rs.pin_pause_window(kXAxesPinLo, kXAxesPinHi);
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 24: {
|
||||
split_probe_x_axes_read(*a);
|
||||
// [2]/[2], equal weights: the columns match, so row 0 hands its axis to row 1.
|
||||
rs.set_cells_per_row({2, 2});
|
||||
split_probe_clear_all(*a);
|
||||
split_probe_x_axes_fill(*a);
|
||||
rs.pin_pause_window(kXAxesPinLo, kXAxesPinHi);
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 25: {
|
||||
split_probe_x_axes_read(*a);
|
||||
// [2]/[2] with weights 1:3 over 1:1: the columns differ, so row 0 is labelled again.
|
||||
rs.set_cell_weights(0, {1.0f, 3.0f});
|
||||
rs.set_cell_weights(1, {1.0f, 1.0f});
|
||||
split_probe_x_axes_fill(*a);
|
||||
rs.pin_pause_window(kXAxesPinLo, kXAxesPinHi);
|
||||
split_probe_restructured(*a);
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
split_probe_x_axes_read(*a);
|
||||
// A zoom written by the controller onto the *last* plot's member axis (what a wheel or a drag on its x
|
||||
// strip does): the pinned view must adopt it, as it adopts one on plot 0. The pin has been held for a
|
||||
// frame by now, so every member reads as the host's own statement.
|
||||
const RowSet::Row& last = rs.row(rs.row_count() - 1);
|
||||
if (last.plot != nullptr) {
|
||||
last.plot->set_x_range(last.axis_x, plot::Range{kXAxesZoomLo, kXAxesZoomHi});
|
||||
}
|
||||
split_probe_next(*a);
|
||||
break;
|
||||
}
|
||||
case 27: {
|
||||
int gesture = 1;
|
||||
for (int p = 0; p < rs.row_count(); ++p) {
|
||||
const RowSet::Row& r = rs.row(p);
|
||||
if (r.plot == nullptr) continue;
|
||||
const plot::Range x = r.plot->x_range(r.axis_x);
|
||||
if (std::fabs(x.min - kXAxesZoomLo) > 1e-6 || std::fabs(x.max - kXAxesZoomHi) > 1e-6) gesture = 0;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[split-probe] x-axes row0_labelled=[%s] bottom_labelled=[%s] gap_ok=[%s] "
|
||||
"gap_px=[%s] col_gap_px=%.1f caption_ok=[%s] last_plot_zoom=%d "
|
||||
"(arrangements: [2]/[1], [2]/[2] equal, [2]/[2] 1:3 over 1:1)\n",
|
||||
g_x_axes.row0.c_str(), g_x_axes.bottom.c_str(), g_x_axes.gap_ok.c_str(),
|
||||
g_x_axes.gap.c_str(), g_x_axes.col_gap, g_x_axes.note.c_str(), gesture);
|
||||
rs.set_time_window(g_x_axes.window_before); // unpins and re-arms the live follow
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user