feat: a search's hits are marked on every row, and the panel says how many of them it is showing

This commit is contained in:
Martino Ferrari committed 2026-10-08 10:11:50 +02:00
1 parent 8ee4effb70
commit db81ab2700
3 files changed
+140

No files matched your search

+74
View File
@@ -32,6 +32,7 @@
#include <algorithm>
#include <cmath>
#include <utility>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -165,6 +166,9 @@ struct App {
GtkWidget* search_cancel_button = nullptr;
GtkWidget* search_status_label = nullptr;
GtkWidget* search_pos_label = nullptr;
/// "16 of 38 marks shown" -- the pool is smaller than a scan's result, and a display that is
/// showing a sixth of what it found has to say so.
GtkWidget* search_marks_label = nullptr;
GtkWidget* search_prev_button = nullptr;
GtkWidget* search_next_button = nullptr;
GtkWidget* menu_button = nullptr;
@@ -3341,6 +3345,62 @@ void place_measure_marks(App& app) {
app.rows->set_measure_marks(app.measure_marks_display);
}
/// Puts the search hits nearest the view on the plots, converting each from the stream seconds
/// `BufferSearch` records to the x the axis is reading this frame -- the same conversion
/// `place_measure_marks()` makes, and for the same reason. Called every frame: that is what
/// makes a mark read-only and what keeps it on its own sample as a live view rolls.
///
/// Only the hits **inside the visible window** are candidates, nearest the view's centre first, so
/// that a zoomed-out view with more hits than marks shows the middle of the cluster rather than an
/// arbitrary end of it. The selected hit is placed first regardless, so stepping to a hit never
/// leaves it unmarked, and it is the one drawn in the current colour.
void place_search_marks(App& app) {
std::vector<double> xs;
int current = -1;
std::size_t in_view = 0;
const std::vector<double>& hits = app.search.hits();
if (!hits.empty()) {
const double s0 = app.model->stream_at_x0();
const plot::Range view = app.rows->view_range();
const double centre = 0.5 * (view.min + view.max);
const int sel = app.search.cursor();
// Candidates: the display x of every hit in view, paired with its distance from the centre.
std::vector<std::pair<double, double>> cand;
for (std::size_t i = 0; i < hits.size(); ++i) {
const double x = hits[i] - s0;
if (x < view.min || x > view.max) continue;
++in_view;
if (sel >= 0 && i == static_cast<std::size_t>(sel)) continue; // placed first, below
cand.push_back({std::fabs(x - centre), x});
}
std::sort(cand.begin(), cand.end());
const std::size_t cap = static_cast<std::size_t>(RowSet::kSearchMarks);
xs.reserve(cap);
if (sel >= 0 && sel < static_cast<int>(hits.size())) {
current = 0;
xs.push_back(hits[static_cast<std::size_t>(sel)] - s0);
}
for (std::size_t i = 0; i < cand.size() && xs.size() < cap; ++i) xs.push_back(cand[i].second);
}
if (app.search_marks_label != nullptr) {
char text[128];
if (hits.empty()) {
std::snprintf(text, sizeof(text), " ");
} else if (in_view > xs.size()) {
// A truncated display says so: "16 of 38" with no qualifier reads as "there are 38 and you
// are seeing 16 of them *everywhere*", which is not the claim.
std::snprintf(text, sizeof(text), "%zu of %zu marks shown \xE2\x80\x94 zoom in to see the rest",
xs.size(), in_view);
} else {
std::snprintf(text, sizeof(text), "%zu of %zu in view", xs.size(), hits.size());
}
gtk_label_set_text(GTK_LABEL(app.search_marks_label), text);
}
app.rows->set_search_marks(xs, current);
}
/// Recomputes every measurement over the visible window and writes the results into the pane,
/// collecting the marks the ones asking to be shown need.
///
@@ -6128,6 +6188,9 @@ gboolean tick(gpointer user) {
app->rows->sync_cursors(cursor_drag_row);
update_cursors_pane(*app);
update_measures(*app, false);
// Every frame, whether or not a scan is running: the marks are in stream seconds and the axis
// is newest-relative, so a live window that has rolled since the last frame needs them moved.
place_search_marks(*app);
// The first CONFIG names the signals, which is what the persisted layout is
// written against: apply it once, here, and never again (a later CONFIG only
@@ -7026,6 +7089,11 @@ GtkWidget* build_trigger_panel(App& app) {
gtk_box_append(GTK_BOX(snav), app.search_next_button);
gtk_box_append(GTK_BOX(sbox), snav);
app.search_marks_label = gtk_label_new(" ");
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START);
gtk_widget_add_css_class(app.search_marks_label, "dim-label");
gtk_box_append(GTK_BOX(sbox), app.search_marks_label);
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
gtk_box_append(GTK_BOX(box), search_exp);
@@ -10967,6 +11035,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
rows.measure_marks_placed(), app.measure_labels.size(),
app.dbg_measures_pane_w, app.dbg_measures_pane_h);
}
// `locked` is re-reported here rather than only in the readouts line: this task added a
// whole new cursor pool, and a pool created selectable is exactly the regression that a
// readout-vs-instrument check is for.
std::printf("[udpscope] search marks: placed=%d hits=%zu locked=%d\n",
rows.search_marks_placed(), app.search.hits().size(),
rows.readouts_locked() ? 1 : 0);
std::printf("[udpscope] header: window_button_w=%d window_label_w=%d (bar_w=%d)\n",
app.dbg_win_btn_w, app.dbg_win_label_w, app.dbg_bar_w);
std::printf("[udpscope] layout: rows=%d window_s=%.6g", rows.row_count(),