summaryrefslogtreecommitdiff
path: root/schist_desktop_gui/src/gui/components
diff options
context:
space:
mode:
Diffstat (limited to 'schist_desktop_gui/src/gui/components')
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view.rs63
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/make_balances_view_greeting.rs9
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs121
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs22
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs143
-rw-r--r--schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs24
-rw-r--r--schist_desktop_gui/src/gui/components/buckets_view.rs105
-rw-r--r--schist_desktop_gui/src/gui/components/button.rs24
-rw-r--r--schist_desktop_gui/src/gui/components/navigation.rs68
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/new_navigation.rs72
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/update_navigation.rs128
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/view_navigation.rs26
-rw-r--r--schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs27
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs22
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs118
15 files changed, 972 insertions, 0 deletions
diff --git a/schist_desktop_gui/src/gui/components/balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view.rs
new file mode 100644
index 0000000..2b72cd3
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/balances_view.rs
@@ -0,0 +1,63 @@
+mod make_balances_view_greeting;
+mod update_balances_view;
+mod view_balances_view;
+mod view_group_names;
+
+use self::{
+ update_balances_view::update_balances_view, view_balances_view::view_balances_view,
+ view_group_names::view_group_names,
+};
+
+use schist_models::bucket::Bucket;
+
+use crate::{
+ gui::components::{
+ balances_view::make_balances_view_greeting::make_balances_view_greeting, navigation,
+ Navigation, Text,
+ },
+ traits::{Component, Viewable},
+};
+
+#[derive(Clone, Debug)]
+pub struct BalancesView {
+ buckets: Vec<Bucket>,
+ greeting: String,
+ navigation: Navigation<Text>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ NavigationMessage(navigation::Message<Text>),
+ SelectNextBucket,
+ SelectPrevBucket,
+ SetBuckets(Vec<Bucket>),
+}
+
+pub enum Action {
+ None,
+}
+
+impl BalancesView {
+ pub fn new(buckets: Vec<Bucket>) -> Self {
+ let groups = view_group_names(buckets.clone());
+ let active_group = groups.clone().first().cloned();
+ let navigation = Navigation::new(active_group.clone(), groups);
+ Self {
+ buckets: buckets.clone(),
+ greeting: make_balances_view_greeting(active_group),
+ navigation: navigation,
+ }
+ }
+}
+
+impl<'a> Viewable<'a, Message> for BalancesView {
+ fn view(&'a self) -> iced::Element<'a, Message> {
+ view_balances_view(self)
+ }
+}
+
+impl<'a> Component<'a, Message, Action> for BalancesView {
+ fn update(&mut self, message: Message) -> Action {
+ update_balances_view(self, message)
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/balances_view/make_balances_view_greeting.rs b/schist_desktop_gui/src/gui/components/balances_view/make_balances_view_greeting.rs
new file mode 100644
index 0000000..4926046
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/balances_view/make_balances_view_greeting.rs
@@ -0,0 +1,9 @@
+use crate::gui::components::text::Text;
+
+pub fn make_balances_view_greeting(group_name: Option<Text>) -> String {
+ if let Some(group_name) = group_name {
+ format!("Hello, {} balances!", group_name.content)
+ } else {
+ String::from("Hello, balances!")
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs
new file mode 100644
index 0000000..100cbc9
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs
@@ -0,0 +1,121 @@
+use super::{
+ make_balances_view_greeting::make_balances_view_greeting, view_group_names::view_group_names,
+ Action, BalancesView, Message,
+};
+use crate::{
+ gui::components::{navigation, Text},
+ traits::Component,
+};
+
+pub fn update_balances_view(balances_view: &mut BalancesView, message: Message) -> Action {
+ match message {
+ Message::NavigationMessage(message) => update_navigation(balances_view, message),
+ Message::SetBuckets(buckets) => set_buckets(balances_view, buckets),
+ Message::SelectNextBucket => select_next_bucket(balances_view),
+ Message::SelectPrevBucket => select_prev_bucket(balances_view),
+ }
+}
+
+fn select_prev_bucket(balances_view: &mut BalancesView) -> Action {
+ match balances_view
+ .navigation
+ .update(navigation::Message::SelectPrev)
+ {
+ navigation::Action::SelectOption(option) => {
+ balances_view.greeting = make_balances_view_greeting(Some(option));
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+}
+
+fn select_next_bucket(balances_view: &mut BalancesView) -> Action {
+ match balances_view
+ .navigation
+ .update(navigation::Message::SelectNext)
+ {
+ navigation::Action::SelectOption(option) => {
+ balances_view.greeting = make_balances_view_greeting(Some(option));
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+}
+
+fn set_buckets(
+ balances_view: &mut BalancesView,
+ buckets: Vec<schist_models::bucket::Bucket>,
+) -> Action {
+ balances_view.buckets = buckets.clone();
+ match balances_view
+ .navigation
+ .update(navigation::Message::SetOptions(view_group_names(buckets)))
+ {
+ navigation::Action::SelectOption(option) => {
+ balances_view.greeting = make_balances_view_greeting(Some(option));
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+}
+
+fn update_navigation(
+ balances_view: &mut BalancesView,
+ message: navigation::Message<Text>,
+) -> Action {
+ match balances_view.navigation.update(message) {
+ navigation::Action::SelectOption(group_name) => {
+ balances_view.greeting = make_balances_view_greeting(Some(group_name));
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use schist_fakes::bucket::make_fake_bucket_builder;
+
+ use crate::{
+ gui::components::{
+ balances_view::{BalancesView, Message},
+ navigation,
+ },
+ traits::Component,
+ };
+
+ #[test]
+ fn update_navigation_updates_greeting() {
+ let groceries = make_fake_bucket_builder(0)
+ .group(String::from("normal expenses"))
+ .build()
+ .unwrap();
+ let fragrance = make_fake_bucket_builder(1)
+ .group(String::from("luxuries"))
+ .build()
+ .unwrap();
+ let maps = make_fake_bucket_builder(2)
+ .group(String::from("outdoors"))
+ .build()
+ .unwrap();
+ let buckets = vec![groceries, fragrance.clone(), maps];
+
+ let mut balances_view = BalancesView::new(buckets);
+ assert_eq!("Hello, normal expenses balances!", balances_view.greeting);
+
+ balances_view.update(Message::NavigationMessage(navigation::Message::SelectPrev));
+ assert_eq!("Hello, normal expenses balances!", balances_view.greeting);
+
+ balances_view.update(Message::NavigationMessage(navigation::Message::SelectPrev));
+ assert_eq!("Hello, normal expenses balances!", balances_view.greeting);
+
+ balances_view.update(Message::NavigationMessage(navigation::Message::SelectNext));
+ assert_eq!("Hello, luxuries balances!", balances_view.greeting);
+
+ balances_view.update(Message::NavigationMessage(navigation::Message::SelectNext));
+ assert_eq!("Hello, outdoors balances!", balances_view.greeting);
+
+ balances_view.update(Message::NavigationMessage(navigation::Message::SelectNext));
+ assert_eq!("Hello, outdoors balances!", balances_view.greeting);
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
new file mode 100644
index 0000000..e8a1e46
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
@@ -0,0 +1,22 @@
+use iced::widget::{column, row, Container};
+use iced::{alignment, Element, Length};
+
+use crate::gui::components::{balances_view::Message, BalancesView};
+use crate::style::SPACING_LG;
+use crate::traits::Viewable;
+
+pub fn view_balances_view<'a>(balances_view: &'a BalancesView) -> Element<'a, Message> {
+ let navigation = balances_view
+ .navigation
+ .view()
+ .map(Message::NavigationMessage);
+ let main_content = column![iced::widget::text(balances_view.greeting.clone())];
+ row![
+ Container::new(navigation).width(Length::FillPortion(1)),
+ Container::new(main_content).width(Length::FillPortion(3)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(SPACING_LG)
+ .into()
+}
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
new file mode 100644
index 0000000..02900e9
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
@@ -0,0 +1,143 @@
+use itertools::Itertools;
+use schist_models::bucket::Bucket;
+
+use crate::gui::components::Text;
+
+pub fn view_group_names(buckets: Vec<Bucket>) -> Vec<Text> {
+ buckets
+ .iter()
+ .map(|b| b.group.clone())
+ .unique()
+ .map(Text::new)
+ .collect()
+}
+
+#[cfg(test)]
+mod test {
+ use schist_fakes::bucket::make_fake_bucket_builder;
+
+ use crate::gui::components::{balances_view::view_group_names::view_group_names, text::Text};
+
+ #[test]
+ fn preserves_order() {
+ let buckets = vec![
+ make_fake_bucket_builder(0)
+ .group(String::from("hydrogen"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(1)
+ .group(String::from("helium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(2)
+ .group(String::from("lithium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(3)
+ .group(String::from("beryllium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(4)
+ .group(String::from("boron"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(5)
+ .group(String::from("carbon"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(6)
+ .group(String::from("nitrogen"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(7)
+ .group(String::from("oxygen"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("flourine"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("flourine"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("neon"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("magnesium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("aluminium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("silicon"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("phosphorus"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("sulphur"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("chlorine"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(8)
+ .group(String::from("argon"))
+ .build()
+ .unwrap(),
+ ];
+ let group_names = view_group_names(buckets);
+ assert_eq!(17, group_names.len());
+ assert_eq!(group_names[0], Text::new(String::from("hydrogen")));
+ assert_eq!(group_names[1], Text::new(String::from("helium")));
+ assert_eq!(group_names[2], Text::new(String::from("lithium")));
+ assert_eq!(group_names[3], Text::new(String::from("beryllium")));
+ assert_eq!(group_names[4], Text::new(String::from("boron")));
+ assert_eq!(group_names[5], Text::new(String::from("carbon")));
+ assert_eq!(group_names[6], Text::new(String::from("nitrogen")));
+ assert_eq!(group_names[7], Text::new(String::from("oxygen")));
+ assert_eq!(group_names[8], Text::new(String::from("flourine")));
+ assert_eq!(group_names[9], Text::new(String::from("neon")));
+ assert_eq!(group_names[10], Text::new(String::from("magnesium")));
+ assert_eq!(group_names[11], Text::new(String::from("aluminium")));
+ assert_eq!(group_names[12], Text::new(String::from("silicon")));
+ assert_eq!(group_names[13], Text::new(String::from("phosphorus")));
+ assert_eq!(group_names[14], Text::new(String::from("sulphur")));
+ assert_eq!(group_names[15], Text::new(String::from("chlorine")));
+ assert_eq!(group_names[16], Text::new(String::from("argon")));
+ }
+
+ #[test]
+ fn removes_duplicates() {
+ let buckets = vec![
+ make_fake_bucket_builder(0)
+ .group(String::from("hydrogen"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(1)
+ .group(String::from("helium"))
+ .build()
+ .unwrap(),
+ make_fake_bucket_builder(2)
+ .group(String::from("hydrogen"))
+ .build()
+ .unwrap(),
+ ];
+ let group_names = view_group_names(buckets);
+ assert_eq!(
+ vec![
+ Text::new(String::from("hydrogen")),
+ Text::new(String::from("helium")),
+ ],
+ group_names
+ );
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
new file mode 100644
index 0000000..fa2dcac
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
@@ -0,0 +1,24 @@
+use iced::{
+ widget::{row, text},
+ Element,
+};
+use schist_models::bucket::Bucket;
+
+use crate::gui::components::navigation;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct BucketNameAndBalance {
+ pub bucket: Bucket,
+}
+
+impl BucketNameAndBalance {
+ pub fn new(bucket: Bucket) -> Self {
+ Self { bucket }
+ }
+}
+
+impl<'a> Into<Element<'a, navigation::Message<BucketNameAndBalance>>> for BucketNameAndBalance {
+ fn into(self) -> Element<'a, navigation::Message<BucketNameAndBalance>> {
+ row![text(self.bucket.name.clone()), text(self.bucket.balance)].into()
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs
new file mode 100644
index 0000000..d27b7ff
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/buckets_view.rs
@@ -0,0 +1,105 @@
+use iced::{
+ alignment,
+ widget::{column, row, Container},
+ Length,
+};
+use schist_models::bucket::Bucket;
+
+use crate::{
+ gui::components::{navigation, BucketNameAndBalance, Navigation},
+ style::SPACING_LG,
+ traits::{Component, Viewable},
+};
+
+#[derive(Clone, Debug)]
+pub struct BucketsView {
+ buckets: Vec<Bucket>,
+ greeting: String,
+ navigation: Navigation<BucketNameAndBalance>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ NavigationMessage(navigation::Message<BucketNameAndBalance>),
+ SelectNextBucket,
+ SelectPrevBucket,
+ SetBuckets(Vec<Bucket>),
+}
+
+pub enum Action {
+ None,
+}
+
+impl BucketsView {
+ pub fn new(buckets: Vec<Bucket>, greeting: &str) -> Self {
+ Self {
+ buckets: buckets.clone(),
+ greeting: greeting.to_owned(),
+ navigation: Navigation::new(
+ buckets.first().cloned().map(BucketNameAndBalance::new),
+ buckets.into_iter().map(BucketNameAndBalance::new).collect(),
+ ),
+ }
+ }
+
+ fn update_greeting(&mut self, active_bucket: &Bucket) {
+ self.greeting = format!("Hello, {} bucket!", active_bucket.name);
+ }
+}
+
+impl<'a> Viewable<'a, Message> for BucketsView {
+ fn view(&'a self) -> iced::Element<'a, Message>
+ where
+ Message: 'a,
+ {
+ let navigation = self.navigation.view().map(Message::NavigationMessage);
+ let main_content = column![iced::widget::text(self.greeting.clone())];
+ row![
+ Container::new(navigation).width(Length::FillPortion(1)),
+ Container::new(main_content).width(Length::FillPortion(3)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(SPACING_LG)
+ .into()
+ }
+}
+
+impl<'a> Component<'a, Message, Action> for BucketsView {
+ fn update(&mut self, message: Message) -> Action {
+ match message {
+ Message::SetBuckets(buckets) => {
+ self.buckets = buckets.clone();
+ self.navigation.update(navigation::Message::SetOptions(
+ buckets.into_iter().map(BucketNameAndBalance::new).collect(),
+ ));
+ Action::None
+ }
+ Message::NavigationMessage(message) => match self.navigation.update(message) {
+ navigation::Action::SelectOption(bucket) => {
+ self.update_greeting(&bucket.bucket);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ },
+ Message::SelectNextBucket => {
+ match self.navigation.update(navigation::Message::SelectNext) {
+ navigation::Action::SelectOption(bucket) => {
+ self.update_greeting(&bucket.bucket);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
+ Message::SelectPrevBucket => {
+ match self.navigation.update(navigation::Message::SelectPrev) {
+ navigation::Action::SelectOption(bucket) => {
+ self.update_greeting(&bucket.bucket);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
+ }
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs
new file mode 100644
index 0000000..bc9e5f9
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/button.rs
@@ -0,0 +1,24 @@
+use iced::{widget::Button, Element};
+
+pub fn active_button<'a, Content: Into<Element<'a, Message>>, Message>(
+ content: Content,
+) -> Button<'a, Message> {
+ iced::widget::button(content)
+ .style(|theme: &iced::Theme, _status| iced::widget::button::Style {
+ background: Some(iced::Background::Color(
+ theme.extended_palette().primary.base.text,
+ )),
+ text_color: theme.extended_palette().primary.base.color,
+ ..Default::default()
+ })
+ .width(iced::Length::Fill)
+}
+
+pub fn inactive_button<'a, Content: Into<Element<'a, Message>>, Message>(
+ content: Content,
+ on_press: Message,
+) -> Button<'a, Message> {
+ iced::widget::button(content)
+ .on_press(on_press)
+ .width(iced::Length::Fill)
+}
diff --git a/schist_desktop_gui/src/gui/components/navigation.rs b/schist_desktop_gui/src/gui/components/navigation.rs
new file mode 100644
index 0000000..bed3221
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/navigation.rs
@@ -0,0 +1,68 @@
+mod new_navigation;
+mod update_navigation;
+mod view_navigation;
+mod view_navigation_button;
+
+use new_navigation::new_navigation;
+use update_navigation::update_navigation;
+use view_navigation::view_navigation;
+use view_navigation_button::view_navigation_button;
+
+use std::fmt::Debug;
+
+use iced::Element;
+
+use crate::traits::{Component, Viewable};
+
+#[derive(Clone, Debug)]
+pub struct Navigation<TOption> {
+ options_before_active: Vec<TOption>,
+ active_option: Option<TOption>,
+ options_after_active: Vec<TOption>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message<TOption> {
+ SelectNext,
+ SelectOption(TOption),
+ SelectPrev,
+ SetOptions(Vec<TOption>),
+}
+
+pub enum Action<TOption> {
+ SelectOption(TOption),
+ None,
+}
+
+impl<TOption: Clone + Debug + PartialEq> Navigation<TOption> {
+ pub fn new(active_option: Option<TOption>, options: Vec<TOption>) -> Self {
+ new_navigation(active_option, options)
+ }
+}
+
+impl<'a, TOption> Navigation<TOption>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ pub fn button(&'a self, option: &'a TOption) -> Element<'a, Message<TOption>> {
+ view_navigation_button(self, option)
+ }
+}
+
+impl<'a, TOption> Viewable<'a, Message<TOption>> for Navigation<TOption>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ fn view(&'a self) -> iced::Element<'a, Message<TOption>> {
+ view_navigation(self)
+ }
+}
+
+impl<'a, TOption> Component<'a, Message<TOption>, Action<TOption>> for Navigation<TOption>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ fn update(&mut self, message: Message<TOption>) -> Action<TOption> {
+ update_navigation(self, message)
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs
new file mode 100644
index 0000000..b5c0c19
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/navigation/new_navigation.rs
@@ -0,0 +1,72 @@
+use super::Navigation;
+
+pub fn new_navigation<TOption>(
+ active_option: Option<TOption>,
+ options: Vec<TOption>,
+) -> Navigation<TOption>
+where
+ TOption: Clone + PartialEq,
+{
+ let active_option_index = active_option.clone().map_or(Option::None, |active_option| {
+ options.iter().position(|option| option.eq(&active_option))
+ });
+ if let Some(active_option_index) = active_option_index {
+ let split = options.split_at(active_option_index);
+ Navigation {
+ options_before_active: split.0.to_vec(),
+ active_option,
+ options_after_active: split.1.split_at(1).1.to_vec(),
+ }
+ } else {
+ Navigation {
+ options_before_active: options,
+ active_option,
+ options_after_active: vec![],
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::Navigation;
+
+ #[test]
+ fn when_active_option_at_start_of_options_then_finds_active_option() {
+ let navigation = Navigation::new(Some(1), vec![1, 2, 3, 4, 5]);
+ assert_eq!(Vec::<i32>::new(), navigation.options_before_active);
+ assert_eq!(Some(1), navigation.active_option);
+ assert_eq!(vec![2, 3, 4, 5], navigation.options_after_active);
+ }
+
+ #[test]
+ fn when_active_option_in_middle_of_options_then_finds_active_option() {
+ let navigation = Navigation::new(Some(3), vec![1, 2, 3, 4, 5]);
+ assert_eq!(vec![1, 2], navigation.options_before_active);
+ assert_eq!(Some(3), navigation.active_option);
+ assert_eq!(vec![4, 5], navigation.options_after_active);
+ }
+
+ #[test]
+ fn when_active_option_at_end_of_options_then_finds_active_option() {
+ let navigation = Navigation::new(Some(5), vec![1, 2, 3, 4, 5]);
+ assert_eq!(vec![1, 2, 3, 4], navigation.options_before_active);
+ assert_eq!(Some(5), navigation.active_option);
+ assert_eq!(Vec::<i32>::new(), navigation.options_after_active);
+ }
+
+ #[test]
+ fn when_active_option_not_in_options_then_appends_active_option() {
+ let navigation = Navigation::new(Some(6), vec![1, 2, 3, 4, 5]);
+ assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active);
+ assert_eq!(Some(6), navigation.active_option);
+ assert_eq!(Vec::<i32>::new(), navigation.options_after_active);
+ }
+
+ #[test]
+ fn when_no_active_option_then_all_options_are_before_active_option() {
+ let navigation = Navigation::new(None, vec![1, 2, 3, 4, 5]);
+ assert_eq!(vec![1, 2, 3, 4, 5], navigation.options_before_active);
+ assert_eq!(None, navigation.active_option);
+ assert_eq!(Vec::<i32>::new(), navigation.options_after_active);
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs
new file mode 100644
index 0000000..f86867f
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs
@@ -0,0 +1,128 @@
+use iced::Element;
+
+use super::{Action, Message, Navigation};
+
+pub fn update_navigation<'a, TOption>(
+ navigation: &mut Navigation<TOption>,
+ message: Message<TOption>,
+) -> Action<TOption>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ match message {
+ Message::SelectOption(option) => {
+ if navigation
+ .active_option
+ .as_ref()
+ .is_some_and(|active_option| active_option.clone() == option)
+ {
+ Action::None
+ } else if let Some(index) = navigation
+ .options_before_active
+ .iter()
+ .position(|o| o.clone() == option)
+ {
+ let split = navigation.options_before_active.split_at(index);
+ navigation.options_after_active = vec![
+ navigation
+ .active_option
+ .clone()
+ .map_or(vec![], |active_option| vec![active_option]),
+ split.1.split_at(1).1.to_vec(),
+ navigation.options_after_active.clone(),
+ ]
+ .concat();
+ navigation.options_before_active = split.0.to_vec();
+ navigation.active_option = Some(option.clone());
+ Action::SelectOption(option)
+ } else if let Some(index) = navigation
+ .options_after_active
+ .iter()
+ .position(|o| o.clone() == option)
+ {
+ let split = navigation.options_after_active.split_at(index);
+ navigation.options_before_active = vec![
+ navigation.options_before_active.clone(),
+ split.0.to_vec(),
+ navigation
+ .active_option
+ .clone()
+ .map_or(vec![], |active_option| vec![active_option]),
+ ]
+ .concat();
+ navigation.active_option = Some(option.clone());
+ navigation.options_after_active = split.1.split_at(1).1.to_vec();
+ Action::SelectOption(option)
+ } else {
+ if let Some(active_option) = &navigation.active_option {
+ navigation.options_before_active.push(active_option.clone());
+ }
+ navigation
+ .options_before_active
+ .append(&mut navigation.options_after_active);
+ navigation.active_option = Some(option.clone());
+ navigation.options_after_active = vec![];
+ Action::SelectOption(option)
+ }
+ }
+ Message::SetOptions(options) => {
+ let active_option_index =
+ navigation
+ .active_option
+ .clone()
+ .map_or(Option::None, |active_option| {
+ options
+ .iter()
+ .position(|option| active_option == option.clone())
+ });
+ if let Some(active_option_index) = active_option_index {
+ let split = options.split_at(active_option_index);
+ navigation.options_before_active = split.0.to_vec();
+ navigation.options_after_active = split.1.split_at(1).1.to_vec();
+ Action::None
+ } else {
+ navigation.options_before_active = options
+ .split_last()
+ .map_or_else(Vec::new, |(_last, rest)| rest.to_vec());
+ navigation.active_option = options.last().cloned();
+ options
+ .last()
+ .cloned()
+ .map_or(Action::None, Action::SelectOption)
+ }
+ }
+ Message::SelectNext => {
+ if let Some(next) = navigation.options_after_active.clone().first() {
+ navigation.options_before_active = vec![
+ navigation.options_before_active.clone(),
+ navigation.active_option.clone().map_or(vec![], |o| vec![o]),
+ ]
+ .concat();
+ navigation.active_option = Some(next.clone());
+ navigation.options_after_active =
+ navigation.options_after_active.split_at(1).1.to_vec();
+ Action::SelectOption(next.clone())
+ } else {
+ Action::None
+ }
+ }
+ Message::SelectPrev => {
+ if let Some(prev) = navigation.options_before_active.clone().last() {
+ navigation.options_after_active = vec![
+ navigation.active_option.clone().map_or(vec![], |o| vec![o]),
+ navigation.options_after_active.clone(),
+ ]
+ .concat();
+ navigation.active_option = Some(prev.clone());
+ navigation.options_before_active = navigation
+ .options_before_active
+ .split_last()
+ .map(|split| split.1.to_vec())
+ .unwrap_or_else(Vec::new);
+ Action::SelectOption(prev.clone())
+ } else {
+ Action::None
+ }
+ }
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs
new file mode 100644
index 0000000..beec818
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/navigation/view_navigation.rs
@@ -0,0 +1,26 @@
+use iced::{widget::column, Element};
+
+use super::{Message, Navigation};
+
+pub fn view_navigation<'a, TOption>(
+ navigation: &'a Navigation<TOption>,
+) -> Element<'a, Message<TOption>>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ let mut options = Vec::new();
+ navigation
+ .options_before_active
+ .iter()
+ .map(|option| navigation.button(&option))
+ .for_each(|option| options.push(option));
+ if let Some(active_option) = navigation.active_option.as_ref() {
+ options.push(navigation.button(&active_option));
+ }
+ navigation
+ .options_after_active
+ .iter()
+ .map(|option| navigation.button(&option))
+ .for_each(|option| options.push(option));
+ column(options).into()
+}
diff --git a/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs
new file mode 100644
index 0000000..3132bf1
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs
@@ -0,0 +1,27 @@
+use iced::Element;
+
+use crate::gui::components::{active_button, inactive_button};
+
+use super::{Message, Navigation};
+
+pub fn view_navigation_button<'a, TOption>(
+ navigation: &'a Navigation<TOption>,
+ option: &'a TOption,
+) -> Element<'a, Message<TOption>>
+where
+ TOption: Clone + PartialEq + Into<Element<'a, Message<TOption>>>,
+{
+ if navigation
+ .active_option
+ .as_ref()
+ .is_some_and(|o| *o == *option)
+ {
+ active_button(Into::<Element<'a, Message<TOption>>>::into(option.clone())).into()
+ } else {
+ inactive_button(
+ Into::<Element<'a, Message<TOption>>>::into(option.clone()),
+ Message::SelectOption(option.clone()),
+ )
+ .into()
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs
new file mode 100644
index 0000000..d334de3
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/text.rs
@@ -0,0 +1,22 @@
+use iced::Element;
+
+use crate::gui::components::navigation;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Text {
+ pub content: String,
+}
+
+impl Text {
+ pub fn new(group_name: String) -> Self {
+ Self {
+ content: group_name.clone(),
+ }
+ }
+}
+
+impl<'a> Into<Element<'a, navigation::Message<Text>>> for Text {
+ fn into(self) -> Element<'a, navigation::Message<Text>> {
+ iced::widget::text(self.content.clone()).into()
+ }
+}
diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs
new file mode 100644
index 0000000..6181a14
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/transactions_view.rs
@@ -0,0 +1,118 @@
+use iced::{
+ alignment,
+ widget::{column, row, Container},
+ Element, Length,
+};
+use schist_models::account::Account;
+
+use crate::{
+ gui::components::{navigation, Navigation, Text},
+ style::SPACING_LG,
+ traits::{Component, Viewable},
+};
+
+#[derive(Clone, Debug)]
+pub struct TransactionsView {
+ accounts: Vec<Account>,
+ greeting: String,
+ navigation: Navigation<Text>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ NavigationMessage(navigation::Message<Text>),
+ SelectNextTransaction,
+ SelectPrevTransaction,
+ SetAccounts(Vec<Account>),
+}
+
+pub enum Action {
+ None,
+}
+
+impl TransactionsView {
+ pub fn new(accounts: Vec<Account>, greeting: &str) -> Self {
+ let account_names = view_account_names(accounts.clone());
+ let navigation = Navigation::new(account_names.clone().first().cloned(), account_names);
+ Self {
+ accounts: accounts,
+ greeting: greeting.to_owned(),
+ navigation: navigation,
+ }
+ }
+}
+
+impl<'a> Viewable<'a, Message> for TransactionsView {
+ fn view(&self) -> Element<Message> {
+ let navigation = self.navigation.view().map(Message::NavigationMessage);
+ let main_content = column![iced::widget::text(self.greeting.clone())];
+ row![
+ Container::new(navigation).width(Length::FillPortion(1)),
+ Container::new(main_content).width(Length::FillPortion(3)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(SPACING_LG)
+ .into()
+ }
+}
+
+impl<'a> Component<'a, Message, Action> for TransactionsView {
+ fn update(&mut self, message: Message) -> Action {
+ match message {
+ Message::NavigationMessage(message) => {
+ match self.navigation.update(message) {
+ navigation::Action::SelectOption(group_name) => {
+ self.greeting = make_greeting(group_name);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ };
+ Action::None
+ }
+ Message::SetAccounts(accounts) => {
+ self.accounts = accounts.clone();
+ match self
+ .navigation
+ .update(navigation::Message::SetOptions(view_account_names(
+ accounts,
+ ))) {
+ navigation::Action::SelectOption(option) => {
+ self.greeting = make_greeting(option);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
+ Message::SelectNextTransaction => {
+ match self.navigation.update(navigation::Message::SelectNext) {
+ navigation::Action::SelectOption(option) => {
+ self.greeting = make_greeting(option);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
+ Message::SelectPrevTransaction => {
+ match self.navigation.update(navigation::Message::SelectPrev) {
+ navigation::Action::SelectOption(option) => {
+ self.greeting = make_greeting(option);
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
+ }
+ }
+}
+
+fn make_greeting(account_name: Text) -> String {
+ format!("Hello, {} account!", account_name.content)
+}
+
+fn view_account_names(accounts: Vec<Account>) -> Vec<Text> {
+ accounts
+ .iter()
+ .map(|acc| Text::new(acc.name.clone()))
+ .collect()
+}