summaryrefslogtreecommitdiff
path: root/schist_desktop_gui
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2026-02-01 09:52:07 +0000
committerJoe Carstairs <me@joeac.net>2026-02-01 09:52:07 +0000
commit498f8fd62dc6ea017b9ff961b719ae86bcedbbd3 (patch)
tree791a7a401031dc0aaebe30cf2ad92df987b3e0b0 /schist_desktop_gui
parente35e6d8ba4a7685af6a24b7cd3e77ff19e5f7b80 (diff)
Can replace navigation options
Diffstat (limited to 'schist_desktop_gui')
-rw-r--r--schist_desktop_gui/src/gui/components/navigation.rs4
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs32
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs432
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(", "),
+ );
+ }
+ }
+}