use crate::widget::high_level::PlotItemKind;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SelectableItem {
pub label: String,
pub kind: PlotItemKind,
pub selected: bool,
}
impl SelectableItem {
pub fn new(label: impl Into<String>, kind: PlotItemKind, selected: bool) -> Self {
Self {
label: label.into(),
kind,
selected,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SelectionMode {
#[default]
Multi,
Single,
}
const KIND_ORDER: [PlotItemKind; 8] = [
PlotItemKind::Curve,
PlotItemKind::Image,
PlotItemKind::Scatter,
PlotItemKind::Histogram,
PlotItemKind::Marker,
PlotItemKind::Mask,
PlotItemKind::Triangles,
PlotItemKind::Shape,
];
fn distinct_kinds(items: &[SelectableItem]) -> Vec<PlotItemKind> {
KIND_ORDER
.into_iter()
.filter(|k| items.iter().any(|it| it.kind == *k))
.collect()
}
pub struct ItemsSelectionDialog {
items: Vec<SelectableItem>,
shown_kinds: Vec<PlotItemKind>,
available_override: Option<Vec<PlotItemKind>>,
mode: SelectionMode,
}
fn in_kind_order(kinds: &[PlotItemKind]) -> Vec<PlotItemKind> {
KIND_ORDER
.into_iter()
.filter(|k| kinds.contains(k))
.collect()
}
impl ItemsSelectionDialog {
pub fn new(items: Vec<SelectableItem>) -> Self {
let shown_kinds = distinct_kinds(&items);
Self {
items,
shown_kinds,
available_override: None,
mode: SelectionMode::Multi,
}
}
pub fn set_items(&mut self, items: Vec<SelectableItem>) {
self.shown_kinds = distinct_kinds(&items);
self.available_override = None;
self.items = items;
self.enforce_single_selection();
}
fn enforce_single_selection(&mut self) {
if self.mode != SelectionMode::Single {
return;
}
let mut kept = false;
for it in &mut self.items {
if it.selected {
if kept {
it.selected = false;
} else {
kept = true;
}
}
}
}
pub fn items(&self) -> &[SelectableItem] {
&self.items
}
pub fn available_kinds(&self) -> Vec<PlotItemKind> {
match &self.available_override {
Some(kinds) => kinds.clone(),
None => distinct_kinds(&self.items),
}
}
pub fn set_available_kinds(&mut self, kinds: &[PlotItemKind]) {
let kinds = in_kind_order(kinds);
self.shown_kinds = kinds.clone();
self.available_override = Some(kinds);
}
pub fn selection_mode(&self) -> SelectionMode {
self.mode
}
pub fn set_selection_mode(&mut self, mode: SelectionMode) {
self.mode = mode;
self.enforce_single_selection();
}
pub fn is_kind_shown(&self, kind: PlotItemKind) -> bool {
self.shown_kinds.contains(&kind)
}
pub fn set_kind_shown(&mut self, kind: PlotItemKind, shown: bool) {
let present = self.shown_kinds.iter().position(|k| *k == kind);
match (shown, present) {
(true, None) => self.shown_kinds.push(kind),
(false, Some(i)) => {
self.shown_kinds.remove(i);
}
_ => {}
}
}
pub fn set_selected(&mut self, index: usize, selected: bool) {
if index >= self.items.len() {
return;
}
if selected && self.mode == SelectionMode::Single {
for item in &mut self.items {
item.selected = false;
}
}
self.items[index].selected = selected;
}
pub fn toggle_selected(&mut self, index: usize) {
if let Some(current) = self.items.get(index).map(|it| it.selected) {
self.set_selected(index, !current);
}
}
pub fn selected_items(&self) -> impl Iterator<Item = &SelectableItem> {
self.items
.iter()
.filter(|it| it.selected && self.shown_kinds.contains(&it.kind))
}
pub fn visible_groups(&self) -> Vec<(PlotItemKind, Vec<usize>)> {
let mut groups = Vec::new();
for kind in KIND_ORDER {
if !self.shown_kinds.contains(&kind) {
continue;
}
let indices: Vec<usize> = self
.items
.iter()
.enumerate()
.filter(|(_, it)| it.kind == kind)
.map(|(i, _)| i)
.collect();
if !indices.is_empty() {
groups.push((kind, indices));
}
}
groups
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label("Filter item kinds:");
ui.horizontal_wrapped(|ui| {
for kind in self.available_kinds() {
let mut shown = self.is_kind_shown(kind);
if ui.checkbox(&mut shown, kind.as_str()).changed() {
self.set_kind_shown(kind, shown);
}
}
});
ui.separator();
ui.label("Select items:");
let groups = self.visible_groups();
if groups.is_empty() {
ui.weak("No items");
return;
}
egui::ScrollArea::vertical().show(ui, |ui| {
for (kind, indices) in groups {
ui.strong(kind.as_str());
for index in indices {
let mut selected = self.items[index].selected;
let label = self.items[index].label.clone();
if ui.checkbox(&mut selected, label).changed() {
self.set_selected(index, selected);
}
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> ItemsSelectionDialog {
ItemsSelectionDialog::new(vec![
SelectableItem::new("curve A", PlotItemKind::Curve, true),
SelectableItem::new("curve B", PlotItemKind::Curve, false),
SelectableItem::new("image A", PlotItemKind::Image, true),
SelectableItem::new("scatter A", PlotItemKind::Scatter, false),
])
}
#[test]
fn all_kinds_shown_by_default() {
let d = sample();
assert!(d.is_kind_shown(PlotItemKind::Curve));
assert!(d.is_kind_shown(PlotItemKind::Image));
assert!(d.is_kind_shown(PlotItemKind::Scatter));
assert!(!d.is_kind_shown(PlotItemKind::Marker));
}
#[test]
fn available_kinds_are_distinct_in_kind_order() {
let d = sample();
assert_eq!(
d.available_kinds(),
vec![
PlotItemKind::Curve,
PlotItemKind::Image,
PlotItemKind::Scatter
]
);
}
#[test]
fn selected_items_returns_only_selected_and_shown() {
let d = sample();
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A", "image A"]);
}
#[test]
fn toggling_selection_changes_the_returned_subset() {
let mut d = sample();
d.toggle_selected(1);
d.set_selected(2, false);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A", "curve B"]);
}
#[test]
fn set_and_toggle_out_of_range_are_noops() {
let mut d = sample();
d.set_selected(99, true); d.toggle_selected(99);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A", "image A"]);
}
#[test]
fn hiding_a_kind_drops_its_items_from_the_subset_without_clearing_selection() {
let mut d = sample();
d.set_kind_shown(PlotItemKind::Curve, false);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["image A"]);
d.set_kind_shown(PlotItemKind::Curve, true);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A", "image A"]);
}
#[test]
fn visible_groups_filters_and_groups_in_kind_order() {
let mut d = sample();
let groups = d.visible_groups();
assert_eq!(
groups,
vec![
(PlotItemKind::Curve, vec![0, 1]),
(PlotItemKind::Image, vec![2]),
(PlotItemKind::Scatter, vec![3]),
]
);
d.set_kind_shown(PlotItemKind::Image, false);
let groups = d.visible_groups();
assert_eq!(
groups,
vec![
(PlotItemKind::Curve, vec![0, 1]),
(PlotItemKind::Scatter, vec![3]),
]
);
}
#[test]
fn single_selection_mode_keeps_at_most_one_selected() {
let mut d = sample();
d.set_selection_mode(SelectionMode::Single);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A"]);
d.set_selected(1, true);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve B"]);
d.toggle_selected(2);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["image A"]);
}
#[test]
fn set_available_kinds_restricts_offered_and_shown_kinds() {
let mut d = sample();
d.set_available_kinds(&[PlotItemKind::Histogram, PlotItemKind::Curve]);
assert_eq!(
d.available_kinds(),
vec![PlotItemKind::Curve, PlotItemKind::Histogram]
);
assert!(!d.is_kind_shown(PlotItemKind::Image));
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["curve A"]);
}
#[test]
fn fit_tool_config_picks_exactly_one_fittable_item() {
let mut d = ItemsSelectionDialog::new(vec![
SelectableItem::new("curve A", PlotItemKind::Curve, false),
SelectableItem::new("curve B", PlotItemKind::Curve, false),
SelectableItem::new("image A", PlotItemKind::Image, false),
]);
d.set_available_kinds(&[PlotItemKind::Curve, PlotItemKind::Histogram]);
d.set_selection_mode(SelectionMode::Single);
d.set_selected(1, true);
let chosen: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(chosen, vec!["curve B"]);
assert!(!d.available_kinds().contains(&PlotItemKind::Image));
}
#[test]
fn set_items_clears_available_override_and_keeps_mode() {
let mut d = sample();
d.set_available_kinds(&[PlotItemKind::Curve]);
d.set_selection_mode(SelectionMode::Single);
d.set_items(vec![
SelectableItem::new("img", PlotItemKind::Image, true),
SelectableItem::new("cur", PlotItemKind::Curve, true),
]);
assert_eq!(
d.available_kinds(),
vec![PlotItemKind::Curve, PlotItemKind::Image]
);
d.set_selected(0, true);
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["img"]);
}
#[test]
fn set_items_reshows_every_new_kind() {
let mut d = sample();
d.set_kind_shown(PlotItemKind::Curve, false);
d.set_items(vec![
SelectableItem::new("m", PlotItemKind::Marker, true),
SelectableItem::new("c", PlotItemKind::Curve, true),
]);
assert!(d.is_kind_shown(PlotItemKind::Marker));
assert!(d.is_kind_shown(PlotItemKind::Curve));
let labels: Vec<&str> = d.selected_items().map(|it| it.label.as_str()).collect();
assert_eq!(labels, vec!["m", "c"]);
}
}