improvements

This commit is contained in:
2026-05-24 19:38:19 +02:00
parent d6a1bafe47
commit 9855f68e7f
4 changed files with 88 additions and 19 deletions
Generated
+19
View File
@@ -2047,6 +2047,15 @@ dependencies = [
"slab", "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]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -5450,6 +5459,7 @@ dependencies = [
"egui", "egui",
"egui_ltreeview", "egui_ltreeview",
"egui_plot", "egui_plot",
"fuzzy-matcher",
"libsvld", "libsvld",
] ]
@@ -5595,6 +5605,15 @@ dependencies = [
"syn", "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]] [[package]]
name = "tiff" name = "tiff"
version = "0.11.3" version = "0.11.3"
+1
View File
@@ -16,3 +16,4 @@ eframe = "0.34"
egui = "0.34" egui = "0.34"
egui_plot = "0.35" egui_plot = "0.35"
egui_ltreeview = "0.7" egui_ltreeview = "0.7"
fuzzy-matcher = "0.3"
+60 -16
View File
@@ -30,6 +30,24 @@ pub fn remove_plot(plots: &mut Vec<PlotGroup>, plot_id: u64) {
plots.retain(|p| p.id != plot_id); 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<PlotGroup>) {
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` /// Most common unit in the plot (first-wins on ties). Returns `None`
/// when the plot is empty. /// when the plot is empty.
pub fn majority_unit(signals: &[PlottedSignal]) -> Option<&str> { 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)) .any(|p| p.signals.iter().any(|s| s.id == signal_id))
} }
pub fn remove_signal(plots: &mut [PlotGroup], signal_id: u64) { pub fn remove_signal(plots: &mut Vec<PlotGroup>, signal_id: u64) {
for plot in plots { for plot in plots.iter_mut() {
if let Some(pos) = plot.signals.iter().position(|s| s.id == signal_id) { if let Some(pos) = plot.signals.iter().position(|s| s.id == signal_id) {
plot.signals.remove(pos); plot.signals.remove(pos);
return; break;
} }
} }
dedupe_empty(plots);
} }
/// Place `sig` into the first plot whose majority unit matches and that /// Place `sig` into the first plot whose majority unit matches and that
@@ -67,11 +86,18 @@ pub fn add_signal_auto(
sig: PlottedSignal, sig: PlottedSignal,
) { ) {
let unit = sig.unit.as_str(); let unit = sig.unit.as_str();
let slot = plots.iter_mut().find(|p| { // Prefer an existing empty plot, then a unit-matching plot under cap.
p.signals.len() < MAX_SIGNALS_PER_PLOT && majority_unit(&p.signals) == Some(unit) let slot = plots
}); .iter()
if let Some(plot) = slot { .position(|p| p.signals.is_empty())
plot.signals.push(sig); .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 { } else {
plots.push(PlotGroup { plots.push(PlotGroup {
id: *next_plot_id, id: *next_plot_id,
@@ -82,6 +108,9 @@ pub fn add_signal_auto(
} }
pub fn new_plot(plots: &mut Vec<PlotGroup>, next_plot_id: &mut u64) { pub fn new_plot(plots: &mut Vec<PlotGroup>, next_plot_id: &mut u64) {
if plots.iter().any(|p| p.signals.is_empty()) {
return;
}
plots.push(PlotGroup { plots.push(PlotGroup {
id: *next_plot_id, id: *next_plot_id,
signals: Vec::new(), signals: Vec::new(),
@@ -111,11 +140,10 @@ pub fn show_plot_list(
let unit = majority_unit(&plot.signals).unwrap_or("empty"); let unit = majority_unit(&plot.signals).unwrap_or("empty");
let label = format!("Plot {} [{}]", i + 1, unit); let label = format!("Plot {} [{}]", i + 1, unit);
let plot_id = plot.id; let plot_id = plot.id;
let is_empty = plot.signals.is_empty(); let node = NodeBuilder::dir(plot_node_id(plot.id))
let mut node = .label(label)
NodeBuilder::dir(plot_node_id(plot.id)).label(label); .activatable(true)
if is_empty { .context_menu(|ui| {
node = node.context_menu(|ui| {
if ui.button("Remove plot").clicked() { if ui.button("Remove plot").clicked() {
events events
.borrow_mut() .borrow_mut()
@@ -123,7 +151,6 @@ pub fn show_plot_list(
ui.close(); ui.close();
} }
}); });
}
builder.node(node); builder.node(node);
for sig in &plot.signals { for sig in &plot.signals {
let sig_id = sig.id; let sig_id = sig.id;
@@ -144,8 +171,24 @@ pub fn show_plot_list(
builder.close_dir(); builder.close_dir();
}); });
for action in actions { for action in actions {
if let Action::Move(dd) = action { match action {
events.borrow_mut().push(PlotListAction::Move(dd)); 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<PlotGroup>, dd: DragAndDrop<u64>) {
apply_plot_move(plots, source & !PLOT_FLAG, dd.target, &dd.position); apply_plot_move(plots, source & !PLOT_FLAG, dd.target, &dd.position);
} }
} }
dedupe_empty(plots);
} }
fn apply_signal_move( fn apply_signal_move(
+8 -3
View File
@@ -2,6 +2,8 @@ use can_dbc::Dbc;
use eframe::egui; use eframe::egui;
use egui::ScrollArea; use egui::ScrollArea;
use egui_ltreeview::{Action, TreeView, TreeViewBuilder}; use egui_ltreeview::{Action, TreeView, TreeViewBuilder};
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use libsvld::DecodeResult; use libsvld::DecodeResult;
pub struct SigNode { pub struct SigNode {
@@ -89,7 +91,8 @@ pub fn show_dbc_tree(
.hint_text("Filter messages/signals") .hint_text("Filter messages/signals")
.desired_width(f32::INFINITY), .desired_width(f32::INFINITY),
); );
let needle = filter.trim().to_lowercase(); let needle = filter.trim();
let matcher = SkimMatcherV2::default();
ScrollArea::both() ScrollArea::both()
.scroll([true, true]) .scroll([true, true])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded)
@@ -100,13 +103,15 @@ pub fn show_dbc_tree(
.show(ui, |builder: &mut TreeViewBuilder<'_, u64>| { .show(ui, |builder: &mut TreeViewBuilder<'_, u64>| {
for msg in tree { for msg in tree {
let msg_hit = needle.is_empty() 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 { let matching: Vec<&SigNode> = if msg_hit {
msg.signals.iter().collect() msg.signals.iter().collect()
} else { } else {
msg.signals msg.signals
.iter() .iter()
.filter(|s| s.name.to_lowercase().contains(&needle)) .filter(|s| {
matcher.fuzzy_match(&s.name, needle).is_some()
})
.collect() .collect()
}; };
if !msg_hit && matching.is_empty() { if !msg_hit && matching.is_empty() {