298 lines
9.9 KiB
Rust
298 lines
9.9 KiB
Rust
use std::cell::RefCell;
|
|
|
|
use eframe::egui;
|
|
use egui::ScrollArea;
|
|
use egui_ltreeview::{
|
|
Action, DirPosition, DragAndDrop, NodeBuilder, TreeView, TreeViewBuilder,
|
|
};
|
|
|
|
use crate::signal::PlottedSignal;
|
|
use crate::tree::{
|
|
decode_plot_node_id, plot_node_id, PLOT_FLAG, ROOT_NODE_ID, SIGNAL_FLAG,
|
|
};
|
|
|
|
/// Auto-placement cap. The user can manually drag more signals into a plot.
|
|
pub const MAX_SIGNALS_PER_PLOT: usize = 5;
|
|
|
|
pub struct PlotGroup {
|
|
pub id: u64,
|
|
pub signals: Vec<PlottedSignal>,
|
|
}
|
|
|
|
pub enum PlotListAction {
|
|
NewPlot,
|
|
Move(DragAndDrop<u64>),
|
|
RemovePlot(u64),
|
|
RemoveSignal(u64),
|
|
}
|
|
|
|
pub fn remove_plot(plots: &mut Vec<PlotGroup>, 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<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`
|
|
/// when the plot is empty.
|
|
pub fn majority_unit(signals: &[PlottedSignal]) -> Option<&str> {
|
|
let mut counts: Vec<(&str, usize)> = Vec::new();
|
|
for sig in signals {
|
|
if let Some(c) = counts.iter_mut().find(|(u, _)| *u == sig.unit.as_str()) {
|
|
c.1 += 1;
|
|
} else {
|
|
counts.push((sig.unit.as_str(), 1));
|
|
}
|
|
}
|
|
counts.into_iter().max_by_key(|(_, c)| *c).map(|(u, _)| u)
|
|
}
|
|
|
|
pub fn contains_signal(plots: &[PlotGroup], signal_id: u64) -> bool {
|
|
plots
|
|
.iter()
|
|
.any(|p| p.signals.iter().any(|s| s.id == signal_id))
|
|
}
|
|
|
|
pub fn remove_signal(plots: &mut Vec<PlotGroup>, 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);
|
|
break;
|
|
}
|
|
}
|
|
dedupe_empty(plots);
|
|
}
|
|
|
|
/// Place `sig` into the first plot whose majority unit matches and that
|
|
/// is under the cap; otherwise append a new plot at the end.
|
|
pub fn add_signal_auto(
|
|
plots: &mut Vec<PlotGroup>,
|
|
next_plot_id: &mut u64,
|
|
sig: PlottedSignal,
|
|
) {
|
|
let unit = sig.unit.as_str();
|
|
// 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,
|
|
signals: vec![sig],
|
|
});
|
|
*next_plot_id += 1;
|
|
}
|
|
}
|
|
|
|
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 {
|
|
id: *next_plot_id,
|
|
signals: Vec::new(),
|
|
});
|
|
*next_plot_id += 1;
|
|
}
|
|
|
|
pub fn show_plot_list(
|
|
ui: &mut egui::Ui,
|
|
plots: &[PlotGroup],
|
|
) -> Vec<PlotListAction> {
|
|
let events: RefCell<Vec<PlotListAction>> = RefCell::new(Vec::new());
|
|
if ui.button("+ New plot").clicked() {
|
|
events.borrow_mut().push(PlotListAction::NewPlot);
|
|
}
|
|
ScrollArea::both()
|
|
.scroll([true, true])
|
|
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded)
|
|
.show(ui, |ui| {
|
|
let (_resp, actions) =
|
|
TreeView::<u64>::new(ui.make_persistent_id("PlotsTree"))
|
|
.show(ui, |builder: &mut TreeViewBuilder<'_, u64>| {
|
|
// Implicit root so the library can produce
|
|
// Before/After positions for top-level plots.
|
|
builder.dir(ROOT_NODE_ID, "Plots");
|
|
for (i, plot) in plots.iter().enumerate() {
|
|
let unit = majority_unit(&plot.signals).unwrap_or("empty");
|
|
let label = format!("Plot {} [{}]", i + 1, unit);
|
|
let plot_id = plot.id;
|
|
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()
|
|
.push(PlotListAction::RemovePlot(plot_id));
|
|
ui.close();
|
|
}
|
|
});
|
|
builder.node(node);
|
|
for sig in &plot.signals {
|
|
let sig_id = sig.id;
|
|
let node = NodeBuilder::leaf(sig.id)
|
|
.label(sig.label.as_str())
|
|
.context_menu(|ui| {
|
|
if ui.button("Remove signal").clicked() {
|
|
events.borrow_mut().push(
|
|
PlotListAction::RemoveSignal(sig_id),
|
|
);
|
|
ui.close();
|
|
}
|
|
});
|
|
builder.node(node);
|
|
}
|
|
builder.close_dir();
|
|
}
|
|
builder.close_dir();
|
|
});
|
|
for action in actions {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
events.into_inner()
|
|
}
|
|
|
|
/// Apply a drag-and-drop move to the plot list.
|
|
///
|
|
/// Sources are either signal leaves (move signal between/within plots) or
|
|
/// plot dirs (reorder plots). Mixed sources are handled item-by-item.
|
|
pub fn apply_move(plots: &mut Vec<PlotGroup>, dd: DragAndDrop<u64>) {
|
|
for source in dd.source {
|
|
if source & SIGNAL_FLAG != 0 {
|
|
apply_signal_move(plots, source, dd.target, &dd.position);
|
|
} else if source & PLOT_FLAG != 0 {
|
|
apply_plot_move(plots, source & !PLOT_FLAG, dd.target, &dd.position);
|
|
}
|
|
}
|
|
dedupe_empty(plots);
|
|
}
|
|
|
|
fn apply_signal_move(
|
|
plots: &mut Vec<PlotGroup>,
|
|
signal_id: u64,
|
|
target: u64,
|
|
position: &DirPosition<u64>,
|
|
) {
|
|
// Resolve the target plot. If dropped on the root or somewhere we
|
|
// can't interpret, leave the signal where it is.
|
|
let target_plot_id = if let Some(pid) = decode_plot_node_id(target) {
|
|
pid
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
// Detach from its current plot.
|
|
let detached = plots.iter_mut().find_map(|p| {
|
|
p.signals
|
|
.iter()
|
|
.position(|s| s.id == signal_id)
|
|
.map(|pos| p.signals.remove(pos))
|
|
});
|
|
let Some(sig) = detached else {
|
|
return;
|
|
};
|
|
|
|
let Some(target_plot) = plots.iter_mut().find(|p| p.id == target_plot_id) else {
|
|
return;
|
|
};
|
|
let insert_at = match position {
|
|
DirPosition::First => 0,
|
|
DirPosition::Last => target_plot.signals.len(),
|
|
DirPosition::Before(sibling) => target_plot
|
|
.signals
|
|
.iter()
|
|
.position(|s| s.id == *sibling)
|
|
.unwrap_or(target_plot.signals.len()),
|
|
DirPosition::After(sibling) => target_plot
|
|
.signals
|
|
.iter()
|
|
.position(|s| s.id == *sibling)
|
|
.map(|i| i + 1)
|
|
.unwrap_or(target_plot.signals.len()),
|
|
};
|
|
target_plot.signals.insert(insert_at, sig);
|
|
}
|
|
|
|
fn apply_plot_move(
|
|
plots: &mut Vec<PlotGroup>,
|
|
plot_id: u64,
|
|
target: u64,
|
|
position: &DirPosition<u64>,
|
|
) {
|
|
// Only reordering at the root level is supported. Dropping a plot
|
|
// onto another plot is treated as "place after that plot".
|
|
let from = match plots.iter().position(|p| p.id == plot_id) {
|
|
Some(i) => i,
|
|
None => return,
|
|
};
|
|
let plot = plots.remove(from);
|
|
|
|
let to = if target == ROOT_NODE_ID {
|
|
match position {
|
|
DirPosition::First => 0,
|
|
DirPosition::Last => plots.len(),
|
|
DirPosition::Before(sibling) => decode_plot_node_id(*sibling)
|
|
.and_then(|pid| plots.iter().position(|p| p.id == pid))
|
|
.unwrap_or(plots.len()),
|
|
DirPosition::After(sibling) => decode_plot_node_id(*sibling)
|
|
.and_then(|pid| plots.iter().position(|p| p.id == pid))
|
|
.map(|i| i + 1)
|
|
.unwrap_or(plots.len()),
|
|
}
|
|
} else if let Some(target_plot_id) = decode_plot_node_id(target) {
|
|
plots
|
|
.iter()
|
|
.position(|p| p.id == target_plot_id)
|
|
.map(|i| i + 1)
|
|
.unwrap_or(plots.len())
|
|
} else {
|
|
plots.len()
|
|
};
|
|
|
|
let to = to.min(plots.len());
|
|
plots.insert(to, plot);
|
|
}
|