diff options
| author | Joe Carstairs <me@joeac.net> | 2026-02-01 09:52:07 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2026-02-01 09:52:07 +0000 |
| commit | 498f8fd62dc6ea017b9ff961b719ae86bcedbbd3 (patch) | |
| tree | 791a7a401031dc0aaebe30cf2ad92df987b3e0b0 /schist_desktop_gui/src/gui/components | |
| parent | e35e6d8ba4a7685af6a24b7cd3e77ff19e5f7b80 (diff) | |
Can replace navigation options
Diffstat (limited to 'schist_desktop_gui/src/gui/components')
3 files changed, 468 insertions, 0 deletions
diff --git a/schist_desktop_gui/src/gui/components/navigation.rs b/schist_desktop_gui/src/gui/components/navigation.rs index 4dc762a..d15e553 100644 --- a/schist_desktop_gui/src/gui/components/navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation.rs @@ -1,10 +1,14 @@ mod navigate_navigation; +mod navigation_replace_options; mod navigation_select_option; mod navigation_set_options; mod update_navigation; mod view_navigation; mod view_navigation_button; +#[cfg(test)] +mod dummy_navigation_option; + use std::fmt::Debug; #[derive(Clone, Debug)] diff --git a/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs b/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs new file mode 100644 index 0000000..ae5799f --- /dev/null +++ b/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use iced::Element; + +use crate::gui::components::{navigation, Text}; + +#[derive(Clone, Debug, PartialEq)] +pub struct DummyNavigationOption { + pub name: String, +} + +impl DummyNavigationOption { + pub fn new(name: &str) -> Self { + Self { + name: String::from(name), + } + } +} + +impl<'a> From<&'a DummyNavigationOption> + for Element<'a, navigation::Message<DummyNavigationOption>> +{ + fn from(option: &'a DummyNavigationOption) -> Self { + Text::new(option.name.as_str()).into() + } +} + +impl Display for DummyNavigationOption { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.name) + } +} diff --git a/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs b/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs new file mode 100644 index 0000000..11ef380 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs @@ -0,0 +1,432 @@ +use iced::Element; +use itertools::Itertools; + +use super::{Message, Navigation}; + +#[derive(PartialEq)] +pub enum ReplaceOptionResult { + ReplacedOption, + NoSuchOption, +} + +impl<'a, TOption> Navigation<TOption> +where + TOption: Clone + PartialEq + 'a, + Element<'a, Message<TOption>>: From<&'a TOption>, +{ + pub fn replace_first<P>(&'a mut self, pred: P, new: &'a Vec<TOption>) -> ReplaceOptionResult + where + P: Fn(&TOption) -> bool, + { + if self.replace_first_option_before_active(&pred, new) + == ReplaceOptionResult::ReplacedOption + { + ReplaceOptionResult::ReplacedOption + } else if self.replace_active_option_if(&pred, new) == ReplaceOptionResult::ReplacedOption { + ReplaceOptionResult::ReplacedOption + } else if self.replace_first_option_after_active(pred, new) + == ReplaceOptionResult::ReplacedOption + { + ReplaceOptionResult::ReplacedOption + } else { + ReplaceOptionResult::NoSuchOption + } + } + + fn replace_first_option_before_active<P>( + &mut self, + pred: P, + new: &'a Vec<TOption>, + ) -> ReplaceOptionResult + where + P: Fn(&TOption) -> bool, + { + if let Some((existing_option_index, _)) = self + .options_before_active + .iter() + .find_position(|option| pred(*option)) + { + self.options_before_active.splice( + existing_option_index..(existing_option_index + 1), + new.clone(), + ); + ReplaceOptionResult::ReplacedOption + } else { + ReplaceOptionResult::NoSuchOption + } + } + + fn replace_first_option_after_active<P>( + &mut self, + pred: P, + new: &'a Vec<TOption>, + ) -> ReplaceOptionResult + where + P: Fn(&TOption) -> bool, + { + if let Some((existing_option_index, _)) = self + .options_after_active + .iter() + .find_position(|option| pred(*option)) + { + self.options_after_active.splice( + existing_option_index..(existing_option_index + 1), + new.clone(), + ); + ReplaceOptionResult::ReplacedOption + } else { + ReplaceOptionResult::NoSuchOption + } + } + + fn replace_active_option_if<P>(&mut self, pred: P, new: &'a Vec<TOption>) -> ReplaceOptionResult + where + P: Fn(&TOption) -> bool, + { + if self + .active_option + .as_ref() + .is_none_or(|active_option| !pred(active_option)) + { + return ReplaceOptionResult::NoSuchOption; + } + + self.active_option = new.first().cloned(); + self.options_after_active + .splice(0..0, new.split_at(1).1.to_vec()); + ReplaceOptionResult::ReplacedOption + } +} + +#[cfg(test)] +mod test { + use itertools::Itertools; + + use crate::{ + gui::components::{ + navigation::{self, dummy_navigation_option::DummyNavigationOption}, + Navigation, + }, + traits::Component, + }; + + fn make_dummy_nav_with_options_a_to_g() -> Navigation<DummyNavigationOption> { + Navigation::new( + None, + vec![ + DummyNavigationOption::new("a"), + DummyNavigationOption::new("b"), + DummyNavigationOption::new("c"), + DummyNavigationOption::new("d"), + DummyNavigationOption::new("e"), + DummyNavigationOption::new("f"), + DummyNavigationOption::new("g"), + ], + ) + } + + fn select_dummy_nav_option(nav: &mut Navigation<DummyNavigationOption>, option: &str) { + nav.update(navigation::Message::SelectOption( + DummyNavigationOption::new(option), + )); + } + + #[test] + fn select_dummy_nav_option_selects_dummy_nav_option() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_first_option_before_active_with_one() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &["z", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_first_option_before_active_with_many() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &["x", "y", "z", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_last_option_before_active_with_one() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "c", &new); + assert_options_before_eq(&nav, &["a", "b", "z"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_last_option_before_active_with_many() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "c", &new); + assert_options_before_eq(&nav, &["a", "b", "x", "y", "z"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_active_option_with_one() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "d", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "z"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_active_option_with_one_when_active_option_is_first() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "a"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &[]); + assert_active_option_eq(&nav, "z"); + assert_options_after_eq(&nav, &["b", "c", "d", "e", "f", "g"]); + } + + #[test] + fn replace_active_option_with_one_when_active_option_is_last() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "g"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "g", &new); + assert_options_before_eq(&nav, &["a", "b", "c", "d", "e", "f"]); + assert_active_option_eq(&nav, "z"); + assert_options_after_eq(&nav, &[]); + } + + #[test] + fn replace_active_option_with_one_when_active_option_is_only() { + let active_option = DummyNavigationOption::new("a"); + let mut nav = Navigation::new(Some(active_option.clone()), vec![active_option]); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &[]); + assert_active_option_eq(&nav, "z"); + assert_options_after_eq(&nav, &[]); + } + + #[test] + fn replace_active_option_with_many() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "d", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "x"); + assert_options_after_eq(&nav, &["y", "z", "e", "f", "g"]); + } + + #[test] + fn replace_active_option_with_many_when_active_option_is_first() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "a"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &[]); + assert_active_option_eq(&nav, "x"); + assert_options_after_eq(&nav, &["y", "z", "b", "c", "d", "e", "f", "g"]); + } + + #[test] + fn replace_active_option_with_many_when_active_option_is_last() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "g"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "g", &new); + assert_options_before_eq(&nav, &["a", "b", "c", "d", "e", "f"]); + assert_active_option_eq(&nav, "x"); + assert_options_after_eq(&nav, &["y", "z"]); + } + #[test] + fn replace_active_option_with_many_when_active_option_is_only() { + let active_option = DummyNavigationOption::new("a"); + let mut nav = Navigation::new(Some(active_option.clone()), vec![active_option]); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &[]); + assert_active_option_eq(&nav, "x"); + assert_options_after_eq(&nav, &["y", "z"]); + } + + #[test] + fn replace_first_option_after_active_with_one() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "e", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["z", "f", "g"]); + } + + #[test] + fn replace_first_option_after_active_with_many() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "e", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["x", "y", "z", "f", "g"]); + } + + #[test] + fn replace_last_option_after_active_with_one() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "g", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "z"]); + } + + #[test] + fn replace_last_option_after_active_with_many() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + let new = vec![ + DummyNavigationOption::new("x"), + DummyNavigationOption::new("y"), + DummyNavigationOption::new("z"), + ]; + nav.replace_first(|o| o.name == "g", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "x", "y", "z"]); + } + + #[test] + fn replace_first_option_before_active_when_duplicates() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + nav.replace_first(|o| o.name == "c", &vec![DummyNavigationOption::new("a")]); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &["z", "b", "a"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_first_option_before_active_when_duplicate_in_active_option() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + nav.replace_first(|o| o.name == "d", &vec![DummyNavigationOption::new("a")]); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &["z", "b", "c"]); + assert_active_option_eq(&nav, "a"); + assert_options_after_eq(&nav, &["e", "f", "g"]); + } + + #[test] + fn replace_first_option_before_active_when_duplicate_in_options_after_active() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + nav.replace_first(|o| o.name == "f", &vec![DummyNavigationOption::new("a")]); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "a", &new); + assert_options_before_eq(&nav, &["z", "b", "c"]); + assert_active_option_eq(&nav, "d"); + assert_options_after_eq(&nav, &["e", "a", "g"]); + } + + #[test] + fn replace_active_option_when_duplicate_in_options_after_active() { + let mut nav = make_dummy_nav_with_options_a_to_g(); + select_dummy_nav_option(&mut nav, "d"); + nav.replace_first(|o| o.name == "f", &vec![DummyNavigationOption::new("d")]); + let new = vec![DummyNavigationOption::new("z")]; + nav.replace_first(|o| o.name == "d", &new); + assert_options_before_eq(&nav, &["a", "b", "c"]); + assert_active_option_eq(&nav, "z"); + assert_options_after_eq(&nav, &["e", "d", "g"]); + } + + fn assert_options_before_eq(nav: &Navigation<DummyNavigationOption>, options: &[&str]) { + let options_before_eq = options.len() == nav.options_before_active.len() + && (|| (0..options.len()).all(|i| options[i] == nav.options_before_active[i].name))(); + + if !options_before_eq { + panic!( + "navigation should have had options ({}) before active option, but were ({})", + options.iter().join(", "), + nav.options_before_active.iter().join(", "), + ); + } + } + + fn assert_active_option_eq(nav: &Navigation<DummyNavigationOption>, option: &str) { + if !nav.active_option.as_ref().is_some_and(|o| o.name == option) { + panic!( + "navigation active option should have been Some(\"{}\"), but was {:?}", + option, nav.active_option + ); + } + } + + fn assert_options_after_eq(nav: &Navigation<DummyNavigationOption>, options: &[&str]) { + let options_after_eq = options.len() == nav.options_after_active.len() + && (|| (0..options.len()).all(|i| options[i] == nav.options_after_active[i].name))(); + + if !options_after_eq { + panic!( + "navigation should have had options ({}) after active option, but were ({})", + options.iter().join(", "), + nav.options_after_active.iter().join(", "), + ); + } + } +} |
