working on todo.md features

This commit is contained in:
Martino Ferrari committed 2026-10-10 22:52:12 +02:00
1 parent 210d124814
commit 8f54841c2c
110 files changed
+15932 -6576

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
//
// The cursors pane: what the two vertical markers and the per-row rulers read, in the four cursor\n// modes, rebuilt only when the rows or the mode change.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
namespace ui_impl {
/// `%.6g s` is exact enough for a cursor readout and short enough for the panel.
void update_cursors_pane(App& app) {
if (app.cursors_body == nullptr || !app.rows->cursors_visible()) return;
if (app.cursors_pane_mode != static_cast<int>(app.rows->cursor_mode()) ||
app.cursors_pane_rows != app.rows->row_count() ||
app.cursors_pane_generation != app.model->generation() ||
app.cursors_pane_bound != bound_slot_mask(app)) {
rebuild_cursors_pane(app);
}
const double t0 = app.rows->cursor_a_x();
const double t1 = app.rows->cursor_b_x();
const auto put = [](GtkWidget* w, const std::string& text) {
if (w != nullptr) gtk_label_set_text(GTK_LABEL(w), text.c_str());
};
const RowSet::CursorMode mode = app.rows->cursor_mode();
const bool rulers = mode == RowSet::CursorMode::Horizontal || mode == RowSet::CursorMode::Both;
const bool crossings = mode == RowSet::CursorMode::Mix;
for (const App::CursorLine& line : app.cursors_lines) {
if (line.a == nullptr) continue;
// One measurement per cell, each with the unit its own axis declares: the pane is a
// readout beside a plot, where the values are the point.
std::string va;
std::string vb;
std::string vd;
if (line.slot == -2) {
va = format_measure(t0, "s");
vb = format_measure(t1, "s");
vd = format_measure(t1 - t0, "s");
} else if (line.slot == -1 && rulers) {
const std::string unit = row_unit(app, line.row);
const double y1 = app.rows->ruler_y(line.row);
const double y2 = app.rows->ruler_b_y(line.row);
va = format_measure(y1, unit);
vb = format_measure(y2, unit);
vd = format_measure(y2 - y1, unit);
} else if (crossings && line.slot >= 0) {
// Where each cursor crosses this trace: the value the trace has at that time,
// read the way a crosshair reads it.
// The same call the crossing markers use, on the same trace: the pane and the
// marks on the plot cannot disagree about what the value is.
const RowSet::Row& row = app.rows->row(line.row);
double y1 = 0.0;
double y2 = 0.0;
const bool a = row.plot != nullptr &&
row.plot->crosshair_value(static_cast<plot::u32>(line.slot), t0, y1);
const bool b = row.plot != nullptr &&
row.plot->crosshair_value(static_cast<plot::u32>(line.slot), t1, y2);
if (a && b) {
const std::string unit = slot_unit(app, line.row, line.slot);
va = format_measure(y1, unit);
vb = format_measure(y2, unit);
vd = format_measure(y2 - y1, unit);
} else {
va = vb = vd = a || b ? "partial" : "--";
}
} else {
va = vb = vd = "--";
}
put(line.a, va);
put(line.b, vb);
put(line.d, vd);
}
}
GtkWidget* build_cursors_panel(App& app) {
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
gtk_widget_set_margin_top(box, 6);
gtk_widget_set_margin_bottom(box, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget* title = gtk_label_new("Cursors");
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_set_hexpand(title, true);
gtk_box_append(GTK_BOX(head), title);
const char* names[5] = {kCursorModeNames[0], kCursorModeNames[1], kCursorModeNames[2],
kCursorModeNames[3], nullptr};
app.cursors_mode_drop = gtk_drop_down_new_from_strings(names);
gtk_widget_set_tooltip_text(app.cursors_mode_drop,
"vertical: two times, shared by every row\n"
"horizontal: two rulers per row\n"
"both: the two together\n"
"mix: the two times, read off each trace where they cross");
g_signal_connect(app.cursors_mode_drop, "notify::selected",
G_CALLBACK(on_cursor_mode_changed), &app);
gtk_box_append(GTK_BOX(head), app.cursors_mode_drop);
// Reset lives here, beside the mode it resets, rather than in the overflow menu: it acts on this
// pane's instruments and is only visible while they are on, both of which are this pane's facts.
app.cursors_reset = gtk_button_new_from_icon_name("edit-clear-symbolic");
gtk_widget_add_css_class(app.cursors_reset, "flat");
gtk_widget_set_tooltip_text(app.cursors_reset, "re-place the cursors at the window's edges");
g_signal_connect(app.cursors_reset, "clicked", G_CALLBACK(on_cursors_reset), &app);
gtk_box_append(GTK_BOX(head), app.cursors_reset);
gtk_box_append(GTK_BOX(box), head);
app.cursors_body = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(app.cursors_body), 2);
gtk_grid_set_column_spacing(GTK_GRID(app.cursors_body), 4);
gtk_box_append(GTK_BOX(box), app.cursors_body);
return box;
}
void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user);
const bool on = gtk_toggle_button_get_active(button);
app->rows->set_cursors_visible(on);
// The reset button exists for the cursors: with them hidden there is nothing to
// reset, and a button that does nothing reads as broken.
gtk_widget_set_visible(app->cursors_reset, on);
// The pane is the cursors' readout: with them off there is nothing to read.
if (app->cursors_revealer != nullptr) {
gtk_revealer_set_reveal_child(GTK_REVEALER(app->cursors_revealer), on);
}
if (on) {
app->rows->reset_cursors();
rebuild_cursors_pane(*app);
update_cursors_pane(*app);
}
}
void on_cursors_reset(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
// A reset the user cannot see reads as a dead button, so it also switches the
// cursors on.
if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->cursors_toggle))) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->cursors_toggle), true);
}
app->rows->reset_cursors();
}
} // namespace ui_impl
} // namespace udpscope