From 9855f68e7f0a2c41be6e918472883460e8bb0ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Stefa=C5=84ski?= Date: Sun, 24 May 2026 19:38:19 +0200 Subject: [PATCH] improvements --- Cargo.lock | 19 +++++++++ svldplotter/Cargo.toml | 1 + svldplotter/src/plot_list.rs | 76 ++++++++++++++++++++++++++++-------- svldplotter/src/tree.rs | 11 ++++-- 4 files changed, 88 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d904de0..ccac53c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,6 +2047,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -5450,6 +5459,7 @@ dependencies = [ "egui", "egui_ltreeview", "egui_plot", + "fuzzy-matcher", "libsvld", ] @@ -5595,6 +5605,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.11.3" diff --git a/svldplotter/Cargo.toml b/svldplotter/Cargo.toml index 74e71c7..a3d6835 100644 --- a/svldplotter/Cargo.toml +++ b/svldplotter/Cargo.toml @@ -16,3 +16,4 @@ eframe = "0.34" egui = "0.34" egui_plot = "0.35" egui_ltreeview = "0.7" +fuzzy-matcher = "0.3" diff --git a/svldplotter/src/plot_list.rs b/svldplotter/src/plot_list.rs index 34d13f9..d685775 100644 --- a/svldplotter/src/plot_list.rs +++ b/svldplotter/src/plot_list.rs @@ -30,6 +30,24 @@ pub fn remove_plot(plots: &mut Vec, plot_id: u64) { plots.retain(|p| p.id != plot_id); } +/// Keep at most one empty plot; drop any further empties (keeps the +/// first one encountered so the user's stable reference doesn't vanish). +fn dedupe_empty(plots: &mut Vec) { + let mut seen_empty = false; + plots.retain(|p| { + if p.signals.is_empty() { + if seen_empty { + false + } else { + seen_empty = true; + true + } + } else { + true + } + }); +} + /// Most common unit in the plot (first-wins on ties). Returns `None` /// when the plot is empty. pub fn majority_unit(signals: &[PlottedSignal]) -> Option<&str> { @@ -50,13 +68,14 @@ pub fn contains_signal(plots: &[PlotGroup], signal_id: u64) -> bool { .any(|p| p.signals.iter().any(|s| s.id == signal_id)) } -pub fn remove_signal(plots: &mut [PlotGroup], signal_id: u64) { - for plot in plots { +pub fn remove_signal(plots: &mut Vec, signal_id: u64) { + for plot in plots.iter_mut() { if let Some(pos) = plot.signals.iter().position(|s| s.id == signal_id) { plot.signals.remove(pos); - return; + break; } } + dedupe_empty(plots); } /// Place `sig` into the first plot whose majority unit matches and that @@ -67,11 +86,18 @@ pub fn add_signal_auto( sig: PlottedSignal, ) { let unit = sig.unit.as_str(); - let slot = plots.iter_mut().find(|p| { - p.signals.len() < MAX_SIGNALS_PER_PLOT && majority_unit(&p.signals) == Some(unit) - }); - if let Some(plot) = slot { - plot.signals.push(sig); + // Prefer an existing empty plot, then a unit-matching plot under cap. + let slot = plots + .iter() + .position(|p| p.signals.is_empty()) + .or_else(|| { + plots.iter().position(|p| { + p.signals.len() < MAX_SIGNALS_PER_PLOT + && majority_unit(&p.signals) == Some(unit) + }) + }); + if let Some(i) = slot { + plots[i].signals.push(sig); } else { plots.push(PlotGroup { id: *next_plot_id, @@ -82,6 +108,9 @@ pub fn add_signal_auto( } pub fn new_plot(plots: &mut Vec, next_plot_id: &mut u64) { + if plots.iter().any(|p| p.signals.is_empty()) { + return; + } plots.push(PlotGroup { id: *next_plot_id, signals: Vec::new(), @@ -111,11 +140,10 @@ pub fn show_plot_list( let unit = majority_unit(&plot.signals).unwrap_or("empty"); let label = format!("Plot {} [{}]", i + 1, unit); let plot_id = plot.id; - let is_empty = plot.signals.is_empty(); - let mut node = - NodeBuilder::dir(plot_node_id(plot.id)).label(label); - if is_empty { - node = node.context_menu(|ui| { + let node = NodeBuilder::dir(plot_node_id(plot.id)) + .label(label) + .activatable(true) + .context_menu(|ui| { if ui.button("Remove plot").clicked() { events .borrow_mut() @@ -123,7 +151,6 @@ pub fn show_plot_list( ui.close(); } }); - } builder.node(node); for sig in &plot.signals { let sig_id = sig.id; @@ -144,8 +171,24 @@ pub fn show_plot_list( builder.close_dir(); }); for action in actions { - if let Action::Move(dd) = action { - events.borrow_mut().push(PlotListAction::Move(dd)); + match action { + Action::Move(dd) => { + events.borrow_mut().push(PlotListAction::Move(dd)); + } + Action::Activate(act) => { + for id in act.selected { + if id & SIGNAL_FLAG != 0 { + events + .borrow_mut() + .push(PlotListAction::RemoveSignal(id)); + } else if let Some(plot_id) = decode_plot_node_id(id) { + events + .borrow_mut() + .push(PlotListAction::RemovePlot(plot_id)); + } + } + } + _ => {} } } }); @@ -164,6 +207,7 @@ pub fn apply_move(plots: &mut Vec, dd: DragAndDrop) { apply_plot_move(plots, source & !PLOT_FLAG, dd.target, &dd.position); } } + dedupe_empty(plots); } fn apply_signal_move( diff --git a/svldplotter/src/tree.rs b/svldplotter/src/tree.rs index c7588e3..fad90b5 100644 --- a/svldplotter/src/tree.rs +++ b/svldplotter/src/tree.rs @@ -2,6 +2,8 @@ use can_dbc::Dbc; use eframe::egui; use egui::ScrollArea; use egui_ltreeview::{Action, TreeView, TreeViewBuilder}; +use fuzzy_matcher::FuzzyMatcher; +use fuzzy_matcher::skim::SkimMatcherV2; use libsvld::DecodeResult; pub struct SigNode { @@ -89,7 +91,8 @@ pub fn show_dbc_tree( .hint_text("Filter messages/signals") .desired_width(f32::INFINITY), ); - let needle = filter.trim().to_lowercase(); + let needle = filter.trim(); + let matcher = SkimMatcherV2::default(); ScrollArea::both() .scroll([true, true]) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded) @@ -100,13 +103,15 @@ pub fn show_dbc_tree( .show(ui, |builder: &mut TreeViewBuilder<'_, u64>| { for msg in tree { let msg_hit = needle.is_empty() - || msg.label.to_lowercase().contains(&needle); + || matcher.fuzzy_match(&msg.label, needle).is_some(); let matching: Vec<&SigNode> = if msg_hit { msg.signals.iter().collect() } else { msg.signals .iter() - .filter(|s| s.name.to_lowercase().contains(&needle)) + .filter(|s| { + matcher.fuzzy_match(&s.name, needle).is_some() + }) .collect() }; if !msg_hit && matching.is_empty() {