summaryrefslogtreecommitdiff
path: root/schist_desktop_gui
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2025-08-13 07:50:21 +0000
committerjoeac <me@joeac.net>2025-08-13 07:50:21 +0000
commitee3cd780e21260257b401c3f5cbababd9e27a15a (patch)
treeb0571649a1a1a7644dbc50183cc4c3a29966a963 /schist_desktop_gui
parent49342e8b553d5c92f1c043b3b0ce18bdf1c2cd44 (diff)
task-073
Co-authored-by: Joe Carstairs <jcarstairs@scottlogic.com> Reviewed-on: https://git.joeac.net/joeac/schist/pulls/1 Co-authored-by: Joe Carstairs <me@joeac.net> Co-committed-by: Joe Carstairs <me@joeac.net>
Diffstat (limited to 'schist_desktop_gui')
-rw-r--r--schist_desktop_gui/Cargo.toml5
-rw-r--r--schist_desktop_gui/src/config.rs17
-rw-r--r--schist_desktop_gui/src/config/keys.rs45
-rw-r--r--schist_desktop_gui/src/dummy_data.rs70
-rw-r--r--schist_desktop_gui/src/gui.rs132
-rw-r--r--schist_desktop_gui/src/gui/components.rs15
-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
-rw-r--r--schist_desktop_gui/src/gui/screens.rs9
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen.rs7
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs160
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/view.rs48
-rw-r--r--schist_desktop_gui/src/main.rs22
-rw-r--r--schist_desktop_gui/src/message.rs3
-rw-r--r--schist_desktop_gui/src/shortcut.rs339
-rw-r--r--schist_desktop_gui/src/state.rs18
-rw-r--r--schist_desktop_gui/src/style.rs1
-rw-r--r--schist_desktop_gui/src/theme.rs6
-rw-r--r--schist_desktop_gui/src/traits.rs5
-rw-r--r--schist_desktop_gui/src/traits/component.rs5
-rw-r--r--schist_desktop_gui/src/traits/viewable.rs3
-rw-r--r--schist_desktop_gui/src/view.rs7
-rw-r--r--schist_desktop_gui/src/window_settings.rs2
36 files changed, 1847 insertions, 44 deletions
diff --git a/schist_desktop_gui/Cargo.toml b/schist_desktop_gui/Cargo.toml
index 75072e7..b7b80da 100644
--- a/schist_desktop_gui/Cargo.toml
+++ b/schist_desktop_gui/Cargo.toml
@@ -1,6 +1,11 @@
[package]
name = "schist_desktop_gui"
+version = "0.1.0"
edition = "2021"
[dependencies]
+anyhow = { workspace = true }
iced = { workspace = true }
+itertools = { workspace = true }
+schist_fakes = { workspace = true }
+schist_models = { workspace = true }
diff --git a/schist_desktop_gui/src/config.rs b/schist_desktop_gui/src/config.rs
index cab9eba..d45c460 100644
--- a/schist_desktop_gui/src/config.rs
+++ b/schist_desktop_gui/src/config.rs
@@ -1,8 +1,17 @@
+mod keys;
+
+use crate::config::keys::Keyboard;
+
#[derive(Clone)]
-pub struct Config {}
+pub struct Config {
+ pub keyboard: Keyboard,
+}
impl Default for Config {
- fn default() -> Self {
- Self {}
- }
+ fn default() -> Self {
+ let keyboard = Keyboard::default();
+ Self {
+ keyboard: keyboard,
+ }
+ }
}
diff --git a/schist_desktop_gui/src/config/keys.rs b/schist_desktop_gui/src/config/keys.rs
new file mode 100644
index 0000000..ff28757
--- /dev/null
+++ b/schist_desktop_gui/src/config/keys.rs
@@ -0,0 +1,45 @@
+// Borrowed from https://github.com/squidowl/halloy/tree/main/data/src/config/keys.rs
+
+use crate::{
+ gui::screens::main_screen::View,
+ shortcut::{shortcut, Command, KeyBind, Shortcut},
+};
+
+#[derive(Debug, Clone)]
+pub struct Keyboard {
+ pub goto_next_item: KeyBind,
+ pub goto_prev_item: KeyBind,
+ pub goto_next_view: KeyBind,
+ pub goto_prev_view: KeyBind,
+ pub goto_view_1: KeyBind,
+ pub goto_view_2: KeyBind,
+ pub goto_view_3: KeyBind,
+}
+
+impl Default for Keyboard {
+ fn default() -> Self {
+ Self {
+ goto_next_item: KeyBind::goto_next_item(),
+ goto_prev_item: KeyBind::goto_prev_item(),
+ goto_next_view: KeyBind::goto_next_view(),
+ goto_prev_view: KeyBind::goto_prev_view(),
+ goto_view_1: KeyBind::goto_view_1(),
+ goto_view_2: KeyBind::goto_view_2(),
+ goto_view_3: KeyBind::goto_view_3(),
+ }
+ }
+}
+
+impl Keyboard {
+ pub fn shortcuts(&self) -> Vec<Shortcut> {
+ vec![
+ shortcut(self.goto_next_item.clone(), Command::GotoNextItem),
+ shortcut(self.goto_prev_item.clone(), Command::GotoPrevItem),
+ shortcut(self.goto_next_view.clone(), Command::GotoNextView),
+ shortcut(self.goto_prev_view.clone(), Command::GotoPrevView),
+ shortcut(self.goto_view_1.clone(), Command::GotoView(View::VIEW_1)),
+ shortcut(self.goto_view_2.clone(), Command::GotoView(View::VIEW_2)),
+ shortcut(self.goto_view_3.clone(), Command::GotoView(View::VIEW_3)),
+ ]
+ }
+}
diff --git a/schist_desktop_gui/src/dummy_data.rs b/schist_desktop_gui/src/dummy_data.rs
new file mode 100644
index 0000000..079b687
--- /dev/null
+++ b/schist_desktop_gui/src/dummy_data.rs
@@ -0,0 +1,70 @@
+use schist_models::{
+ account::Account, bucket::Bucket, budget_period_unit::BudgetPeriodUnit, date_utc::DateUtc,
+};
+
+#[derive(Clone, Debug)]
+pub struct Error {}
+
+impl From<anyhow::Error> for Error {
+ fn from(_value: anyhow::Error) -> Self {
+ Error {}
+ }
+}
+
+pub async fn fetch_accounts() -> Result<Vec<Account>, Error> {
+ Ok(vec![
+ Account {
+ id: 0,
+ name: String::from("Nationwide current account"),
+ opening_balance: 0,
+ opening_date: DateUtc::from_ymd(2024, 06, 12)?,
+ },
+ Account {
+ id: 1,
+ name: String::from("cash account"),
+ opening_balance: 0,
+ opening_date: DateUtc::from_ymd(2024, 06, 12)?,
+ },
+ Account {
+ id: 2,
+ name: String::from("YBS savings account"),
+ opening_balance: 0,
+ opening_date: DateUtc::from_ymd(2024, 06, 12)?,
+ },
+ ])
+}
+
+pub async fn fetch_buckets() -> Result<Vec<Bucket>, Error> {
+ Ok(vec![
+ Bucket {
+ id: 0,
+ name: String::from("groceries"),
+ balance: 123,
+ balance_date: DateUtc::from_ymd(2025, 8, 10)?,
+ budget_period: 1,
+ budget_period_unit: BudgetPeriodUnit::Month,
+ budget_quantity: 300,
+ group: String::from("normal expenses"),
+ },
+ Bucket {
+ id: 1,
+ name: String::from("outdoor trips"),
+ balance: -11,
+ balance_date: DateUtc::from_ymd(2025, 8, 10)?,
+ budget_period: 1,
+ budget_period_unit: BudgetPeriodUnit::Month,
+ budget_quantity: 110,
+ group: String::from("outdoors"),
+ },
+ Bucket {
+ id: 2,
+ name: String::from("salary"),
+ balance: 1018,
+ balance_date: DateUtc::from_ymd(2025, 8, 10)?,
+ budget_period: 1,
+ budget_period_unit: BudgetPeriodUnit::Month,
+ budget_quantity: -2196,
+ group: String::from("income"),
+ },
+ ])
+}
diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs
new file mode 100644
index 0000000..0f84f41
--- /dev/null
+++ b/schist_desktop_gui/src/gui.rs
@@ -0,0 +1,132 @@
+pub mod components;
+pub mod screens;
+
+use iced::Element;
+use schist_models::{account::Account, bucket::Bucket};
+
+use crate::{
+ config::Config,
+ dummy_data::{self, fetch_accounts, fetch_buckets},
+ gui::screens::{main_screen, MainScreen, Screen},
+ shortcut::KeyBind,
+ traits::{Component, Viewable},
+};
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ Event(iced::Event),
+ MainScreenMessage(main_screen::Message),
+ RefetchedAccounts(Result<Vec<Account>, dummy_data::Error>),
+ RefetchedBuckets(Result<Vec<Bucket>, dummy_data::Error>),
+}
+
+pub struct Gui {
+ active_screen: Screen,
+ buckets: Vec<Bucket>,
+ main_screen: MainScreen,
+ config: Config,
+}
+
+impl Gui {
+ pub fn new() -> (Self, iced::Task<Message>) {
+ let tasks = vec![
+ iced::Task::perform(fetch_buckets(), Message::RefetchedBuckets),
+ iced::Task::perform(fetch_accounts(), Message::RefetchedAccounts),
+ ];
+ let buckets = Vec::new();
+ let accounts = Vec::new();
+ let gui = Self {
+ active_screen: Screen::Main,
+ buckets: buckets.clone(),
+ main_screen: MainScreen::new(buckets, accounts),
+ config: Config::default(),
+ };
+ (gui, iced::Task::batch(tasks))
+ }
+}
+
+impl Gui {
+ pub fn view(&self) -> Element<Message> {
+ match self.active_screen {
+ Screen::Main => self.main_screen.view().map(Message::MainScreenMessage),
+ }
+ }
+
+ pub fn update(&mut self, message: Message) -> iced::Task<Message> {
+ match message {
+ Message::Event(iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
+ key,
+ modifiers,
+ ..
+ })) => {
+ let key_bind = KeyBind::from((key.clone(), modifiers));
+ if let Some(command) = self
+ .config
+ .keyboard
+ .shortcuts()
+ .iter()
+ .find_map(|shortcut| shortcut.execute(&key_bind))
+ {
+ match command {
+ crate::shortcut::Command::GotoNextView => {
+ self.main_screen.update(main_screen::Message::SelectView(
+ self.main_screen.active_view.next(),
+ ));
+ iced::Task::none()
+ }
+ crate::shortcut::Command::GotoPrevView => {
+ self.main_screen.update(main_screen::Message::SelectView(
+ self.main_screen.active_view.prev(),
+ ));
+ iced::Task::none()
+ }
+ crate::shortcut::Command::GotoView(view) => {
+ self.main_screen
+ .update(main_screen::Message::SelectView(view));
+ iced::Task::none()
+ }
+ crate::shortcut::Command::GotoNextItem => {
+ self.main_screen.update(main_screen::Message::NextItem);
+ iced::Task::none()
+ }
+ crate::shortcut::Command::GotoPrevItem => {
+ self.main_screen.update(main_screen::Message::PrevItem);
+ iced::Task::none()
+ }
+ }
+ } else {
+ iced::Task::none()
+ }
+ }
+ Message::Event(_) => iced::Task::none(),
+ Message::MainScreenMessage(message) => match self.main_screen.update(message) {
+ main_screen::Action::None => iced::Task::none(),
+ },
+ Message::RefetchedBuckets(buckets) => {
+ match buckets {
+ Ok(buckets) => {
+ self.buckets = buckets.clone();
+ self.main_screen
+ .update(main_screen::Message::SetBuckets(buckets));
+ }
+ Err(err) => println!("Failed to get buckets: {:?}", err),
+ };
+ iced::Task::none()
+ }
+ Message::RefetchedAccounts(accounts) => {
+ match accounts {
+ Ok(accounts) => {
+ self.main_screen
+ .update(main_screen::Message::SetAccounts(accounts));
+ }
+ Err(err) => println!("Failed to get accounts: {:?}", err),
+ }
+ iced::Task::none()
+ }
+ }
+ }
+}
+
+pub fn subscription(_state: &Gui) -> iced::Subscription<Message> {
+ iced::event::listen().map(Message::Event)
+}
diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs
new file mode 100644
index 0000000..fc3c838
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components.rs
@@ -0,0 +1,15 @@
+pub mod balances_view;
+pub mod bucket_name_and_balance;
+pub mod buckets_view;
+pub mod button;
+pub mod navigation;
+pub mod text;
+pub mod transactions_view;
+
+pub use balances_view::BalancesView;
+pub use bucket_name_and_balance::BucketNameAndBalance;
+pub use buckets_view::BucketsView;
+pub use button::{active_button, inactive_button};
+pub use navigation::Navigation;
+pub use text::Text;
+pub use transactions_view::TransactionsView;
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()
+}
diff --git a/schist_desktop_gui/src/gui/screens.rs b/schist_desktop_gui/src/gui/screens.rs
new file mode 100644
index 0000000..3ae97dd
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens.rs
@@ -0,0 +1,9 @@
+pub mod main_screen;
+
+pub use main_screen::MainScreen;
+
+#[derive(Default)]
+pub enum Screen {
+ #[default]
+ Main,
+}
diff --git a/schist_desktop_gui/src/gui/screens/main_screen.rs b/schist_desktop_gui/src/gui/screens/main_screen.rs
new file mode 100644
index 0000000..21ef44a
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/main_screen.rs
@@ -0,0 +1,7 @@
+mod main_screen;
+mod view;
+
+pub use main_screen::Action;
+pub use main_screen::MainScreen;
+pub use main_screen::Message;
+pub use view::View;
diff --git a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs
new file mode 100644
index 0000000..bad3dee
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs
@@ -0,0 +1,160 @@
+use super::View;
+use iced::{
+ alignment,
+ widget::{row, Container},
+ Element, Length,
+};
+use schist_models::{account::Account, bucket::Bucket};
+
+use crate::{
+ gui::components::{
+ balances_view, buckets_view, navigation, transactions_view, BalancesView, BucketsView,
+ Navigation, TransactionsView,
+ },
+ style::SPACING_LG,
+ traits::{Component, Viewable},
+};
+
+pub struct MainScreen {
+ pub active_view: View,
+ pub buckets: Vec<Bucket>,
+ pub balances_view: BalancesView,
+ pub buckets_view: BucketsView,
+ pub transactions_view: TransactionsView,
+ pub view_navigation: Navigation<View>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ BalancesViewMessage(balances_view::Message),
+ BucketsViewMessage(buckets_view::Message),
+ NextItem,
+ PrevItem,
+ TransactionsViewMessage(transactions_view::Message),
+ SelectView(View),
+ SetAccounts(Vec<Account>),
+ SetBuckets(Vec<Bucket>),
+ ViewNavigationMessage(navigation::Message<View>),
+}
+
+pub enum Action {
+ None,
+}
+
+impl<'a> Viewable<'a, Message> for MainScreen {
+ fn view(&'a self) -> Element<'a, Message> {
+ let view_navigation = self
+ .view_navigation
+ .view()
+ .map(Message::ViewNavigationMessage);
+ let view = match &self.active_view {
+ View::Balances => self.balances_view.view().map(Message::BalancesViewMessage),
+ View::Buckets => self.buckets_view.view().map(Message::BucketsViewMessage),
+ View::Transactions => self
+ .transactions_view
+ .view()
+ .map(Message::TransactionsViewMessage),
+ };
+
+ row![
+ Container::new(view_navigation).width(Length::FillPortion(1)),
+ Container::new(view).width(Length::FillPortion(4)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(SPACING_LG)
+ .into()
+ }
+}
+
+impl<'a> Component<'a, Message, Action> for MainScreen {
+ fn update(&mut self, message: Message) -> Action {
+ match message {
+ Message::SelectView(view) => {
+ self.active_view = view;
+ self.view_navigation
+ .update(navigation::Message::SelectOption(view));
+ Action::None
+ }
+ Message::BalancesViewMessage(message) => match self.balances_view.update(message) {
+ balances_view::Action::None => Action::None,
+ },
+ Message::BucketsViewMessage(message) => {
+ self.buckets_view.update(message);
+ Action::None
+ }
+ Message::TransactionsViewMessage(message) => {
+ match self.transactions_view.update(message) {
+ transactions_view::Action::None => Action::None,
+ }
+ }
+ Message::ViewNavigationMessage(message) => match self.view_navigation.update(message) {
+ navigation::Action::SelectOption(view) => {
+ self.active_view = view;
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ },
+ Message::SetBuckets(buckets) => {
+ self.buckets = buckets.clone();
+ self.balances_view
+ .update(balances_view::Message::SetBuckets(buckets.clone()));
+ self.buckets_view
+ .update(buckets_view::Message::SetBuckets(buckets));
+ Action::None
+ }
+ Message::SetAccounts(accounts) => {
+ self.transactions_view
+ .update(transactions_view::Message::SetAccounts(accounts));
+ Action::None
+ }
+ Message::NextItem => match self.active_view {
+ View::Balances => {
+ self.balances_view
+ .update(balances_view::Message::SelectNextBucket);
+ Action::None
+ }
+ View::Buckets => {
+ self.buckets_view
+ .update(buckets_view::Message::SelectNextBucket);
+ Action::None
+ }
+ View::Transactions => {
+ self.transactions_view
+ .update(transactions_view::Message::SelectNextTransaction);
+ Action::None
+ }
+ },
+ Message::PrevItem => match self.active_view {
+ View::Balances => {
+ self.balances_view
+ .update(balances_view::Message::SelectPrevBucket);
+ Action::None
+ }
+ View::Buckets => {
+ self.buckets_view
+ .update(buckets_view::Message::SelectPrevBucket);
+ Action::None
+ }
+ View::Transactions => {
+ self.transactions_view
+ .update(transactions_view::Message::SelectPrevTransaction);
+ Action::None
+ }
+ },
+ }
+ }
+}
+
+impl MainScreen {
+ pub fn new(buckets: Vec<Bucket>, accounts: Vec<Account>) -> Self {
+ Self {
+ balances_view: BalancesView::new(buckets.clone()),
+ buckets: buckets.clone(),
+ buckets_view: BucketsView::new(buckets, "Hello, buckets!"),
+ transactions_view: TransactionsView::new(accounts, "Hello, transactions!"),
+ active_view: View::Balances,
+ view_navigation: Navigation::new(Some(View::Balances), View::views()),
+ }
+ }
+}
diff --git a/schist_desktop_gui/src/gui/screens/main_screen/view.rs b/schist_desktop_gui/src/gui/screens/main_screen/view.rs
new file mode 100644
index 0000000..eb9c88e
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/main_screen/view.rs
@@ -0,0 +1,48 @@
+use iced::Element;
+
+use crate::gui::components::navigation;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum View {
+ Balances,
+ Buckets,
+ Transactions,
+}
+
+impl View {
+ pub const VIEW_1: View = View::Buckets;
+ pub const VIEW_2: View = View::Balances;
+ pub const VIEW_3: View = View::Transactions;
+
+ pub fn views() -> Vec<View> {
+ vec![Self::VIEW_1, Self::VIEW_2, Self::VIEW_3]
+ }
+
+ pub const fn name(self) -> &'static str {
+ match self {
+ View::Balances => "balances",
+ View::Buckets => "buckets",
+ View::Transactions => "transactions",
+ }
+ }
+
+ pub fn next(&self) -> View {
+ match *self {
+ Self::VIEW_1 => Self::VIEW_2,
+ Self::VIEW_2 | Self::VIEW_3 => Self::VIEW_3,
+ }
+ }
+
+ pub fn prev(&self) -> View {
+ match *self {
+ Self::VIEW_1 | Self::VIEW_2 => Self::VIEW_1,
+ Self::VIEW_3 => Self::VIEW_2,
+ }
+ }
+}
+
+impl<'a> Into<Element<'a, navigation::Message<View>>> for View {
+ fn into(self) -> Element<'a, navigation::Message<View>> {
+ iced::widget::text(self.name()).into()
+ }
+}
diff --git a/schist_desktop_gui/src/main.rs b/schist_desktop_gui/src/main.rs
index b836453..7e54ddb 100644
--- a/schist_desktop_gui/src/main.rs
+++ b/schist_desktop_gui/src/main.rs
@@ -1,18 +1,24 @@
mod config;
-mod message;
+mod dummy_data;
+mod gui;
mod settings;
-mod state;
+mod shortcut;
+mod style;
mod theme;
+mod traits;
mod window_settings;
-mod view;
-use crate::{config::Config, settings::make_settings, state::State, theme::make_theme, window_settings::make_window_settings, view::view};
+use crate::{
+ config::Config, gui::Gui, settings::make_settings, theme::make_theme,
+ window_settings::make_window_settings,
+};
fn main() -> iced::Result {
let config = Config::default();
- iced::application("Schist", State::update, view)
+ iced::application("Schist", Gui::update, Gui::view)
.settings(make_settings(&config.clone()))
- .theme(|state: &State| make_theme(state))
+ .subscription(gui::subscription)
+ .theme(make_theme)
.window(make_window_settings(&config.clone()))
- .run()
-} \ No newline at end of file
+ .run_with(Gui::new)
+}
diff --git a/schist_desktop_gui/src/message.rs b/schist_desktop_gui/src/message.rs
deleted file mode 100644
index 10f734b..0000000
--- a/schist_desktop_gui/src/message.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-#[derive(Debug)]
-pub enum Message {
-}
diff --git a/schist_desktop_gui/src/shortcut.rs b/schist_desktop_gui/src/shortcut.rs
new file mode 100644
index 0000000..f979f4e
--- /dev/null
+++ b/schist_desktop_gui/src/shortcut.rs
@@ -0,0 +1,339 @@
+// borrowed from https://github.com/squidowl/halloy/tree/main/src/shortcut.rs#L230
+
+use iced::keyboard::{self, key};
+use std::hash::Hash;
+use std::str::FromStr;
+use std::{fmt, ops};
+
+use crate::gui::screens::main_screen::View;
+
+pub fn shortcut(key_bind: KeyBind, command: Command) -> Shortcut {
+ Shortcut { key_bind, command }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Shortcut {
+ key_bind: KeyBind,
+ command: Command,
+}
+
+impl Shortcut {
+ pub fn execute(&self, key_bind: &KeyBind) -> Option<Command> {
+ (self.key_bind == *key_bind).then_some(self.command)
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Command {
+ GotoNextItem,
+ GotoPrevItem,
+ GotoNextView,
+ GotoPrevView,
+ GotoView(View),
+}
+
+macro_rules! default {
+ ($name:ident, $k:tt) => {
+ pub fn $name() -> KeyBind {
+ KeyBind {
+ key_code: KeyCode(iced::keyboard::Key::Named(iced::keyboard::key::Named::$k)),
+ modifiers: Modifiers::default(),
+ }
+ }
+ };
+ ($name:ident, $k:literal, $m:expr) => {
+ pub fn $name() -> KeyBind {
+ KeyBind {
+ key_code: KeyCode(iced::keyboard::Key::Character($k.into())),
+ modifiers: $m,
+ }
+ }
+ };
+ ($name:ident, $k:tt, $m:expr) => {
+ pub fn $name() -> KeyBind {
+ KeyBind {
+ key_code: KeyCode(iced::keyboard::Key::Named(iced::keyboard::key::Named::$k)),
+ modifiers: $m,
+ }
+ }
+ };
+}
+
+#[derive(Debug, Clone, Eq, Ord, PartialOrd)]
+pub struct KeyBind {
+ key_code: KeyCode,
+ modifiers: Modifiers,
+}
+
+impl fmt::Display for KeyBind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} {}", self.modifiers, self.key_code)
+ }
+}
+
+impl PartialEq for KeyBind {
+ fn eq(&self, other: &Self) -> bool {
+ if self.modifiers != other.modifiers {
+ return false;
+ }
+
+ match (&self.key_code.0, &other.key_code.0) {
+ // SHIFT modifier effects if this comes across as `a` or `A`, but
+ // we explicitly define / check modifiers so it doesn't matter if
+ // user defined it as `a` or `A` in their keymap
+ (keyboard::Key::Character(a), keyboard::Key::Character(b)) => {
+ a.to_lowercase() == b.to_lowercase()
+ }
+ (a, b) => a == b,
+ }
+ }
+}
+
+impl Hash for KeyBind {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.key_code.hash(state);
+ self.modifiers.hash(state);
+ }
+}
+
+// For defaults check the platform specific defaults:
+// macOS: https://support.apple.com/en-us/102650
+// Windows: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec
+// Linux FreeDesktop (not ready yet): https://wiki.freedesktop.org/www/Specifications/default-keys-spec/
+// Linux - KDE: https://docs.kde.org/stable5/en/khelpcenter/fundamentals/kbd.html
+// Linux - Gnome: https://help.gnome.org/users/gnome-help/stable/keyboard-nav.html.en
+
+impl KeyBind {
+ default!(goto_next_item, "j", Modifiers::EMPTY);
+ default!(goto_prev_item, "k", Modifiers::EMPTY);
+ default!(goto_next_view, "j", Modifiers::CTRL);
+ default!(goto_prev_view, "k", Modifiers::CTRL);
+ default!(goto_view_1, "1", Modifiers::CTRL);
+ default!(goto_view_2, "2", Modifiers::CTRL);
+ default!(goto_view_3, "3", Modifiers::CTRL);
+}
+
+impl From<(keyboard::Key, keyboard::Modifiers)> for KeyBind {
+ fn from((key_code, modifiers): (keyboard::Key, keyboard::Modifiers)) -> Self {
+ Self {
+ key_code: KeyCode(key_code),
+ modifiers: Modifiers(modifiers),
+ }
+ }
+}
+
+#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone)]
+pub struct KeyCode(keyboard::Key);
+
+#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy, Default)]
+pub struct Modifiers(keyboard::Modifiers);
+
+impl Modifiers {
+ const CTRL: Modifiers = Modifiers(keyboard::Modifiers::CTRL);
+ const EMPTY: Modifiers = Modifiers(keyboard::Modifiers::empty());
+}
+
+impl From<keyboard::Modifiers> for Modifiers {
+ fn from(modifiers: keyboard::Modifiers) -> Self {
+ Self(modifiers)
+ }
+}
+
+impl ops::BitOr for Modifiers {
+ type Output = Self;
+
+ fn bitor(self, rhs: Self) -> Self::Output {
+ Self(self.0 | rhs.0)
+ }
+}
+
+impl fmt::Display for Modifiers {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut mods = vec![];
+ let inner = self.0;
+
+ if inner.contains(keyboard::Modifiers::SHIFT) {
+ mods.push("Shift");
+ }
+ if inner.contains(keyboard::Modifiers::CTRL) {
+ if cfg!(target_os = "macos") {
+ // macOS: ⌃
+ mods.push("\u{2303}");
+ } else {
+ mods.push("Ctrl");
+ }
+ }
+ if inner.contains(keyboard::Modifiers::ALT) {
+ if cfg!(target_os = "macos") {
+ // macOS: ⌥
+ mods.push("\u{2325}");
+ } else {
+ mods.push("Alt");
+ }
+ }
+ if inner.contains(keyboard::Modifiers::LOGO) {
+ // macOS: ⌘
+ mods.push("\u{2318}");
+ }
+
+ if mods.is_empty() {
+ write!(f, "")
+ } else {
+ write!(f, "{}", mods.join(" "))
+ }
+ }
+}
+
+impl fmt::Display for KeyCode {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let key = match self.0.clone() {
+ key::Key::Named(name) => {
+ let named = match name {
+ key::Named::F1 => "F1",
+ key::Named::F2 => "F2",
+ key::Named::F3 => "F3",
+ key::Named::F4 => "F4",
+ key::Named::F5 => "F5",
+ key::Named::F6 => "F6",
+ key::Named::F7 => "F7",
+ key::Named::F8 => "F8",
+ key::Named::F9 => "F9",
+ key::Named::F10 => "F10",
+ key::Named::F11 => "F11",
+ key::Named::F12 => "F12",
+ key::Named::F13 => "F13",
+ key::Named::F14 => "F14",
+ key::Named::F15 => "F15",
+ key::Named::F16 => "F16",
+ key::Named::F17 => "F17",
+ key::Named::F18 => "F18",
+ key::Named::F19 => "F19",
+ key::Named::F20 => "F20",
+ key::Named::F21 => "F21",
+ key::Named::F22 => "F22",
+ key::Named::F23 => "F23",
+ key::Named::F24 => "F24",
+ key::Named::Home => "Home",
+ key::Named::Delete => "Delete",
+ key::Named::End => "End",
+ key::Named::PageDown => "PageDown",
+ key::Named::PageUp => "PageUp",
+ key::Named::ArrowLeft => "←",
+ key::Named::ArrowUp => "↑",
+ key::Named::ArrowRight => "→",
+ key::Named::ArrowDown => "↓",
+ key::Named::Backspace => "Backspace",
+ key::Named::Enter => "Enter",
+ key::Named::Space => "Space",
+ key::Named::NumLock => "NumLock",
+ key::Named::Alt => "Alt",
+ key::Named::Tab => "Tab",
+ key::Named::Pause => "Pause",
+ key::Named::Insert => "Insert",
+ key::Named::Cut => "Cut",
+ key::Named::Paste => "Paste",
+ key::Named::Copy => "Copy",
+ key::Named::AudioVolumeDown => "VolumeDown",
+ key::Named::AudioVolumeUp => "VolumeUp",
+ key::Named::Shift => "Shift",
+ key::Named::Control => "Control",
+ key::Named::AudioVolumeMute => "Mute",
+ key::Named::MediaStop => "MediaStop",
+ key::Named::MediaPause => "MediaPause",
+ key::Named::MediaTrackNext => "MediaTrackNext",
+ key::Named::MediaTrackPrevious => "MediaTrackPrev",
+ _ => "",
+ };
+
+ named.to_string()
+ }
+ key::Key::Character(c) => c.to_uppercase(),
+ key::Key::Unidentified => String::new(),
+ };
+
+ write!(f, "{key}")
+ }
+}
+
+impl FromStr for KeyCode {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Ok(Self(match s.to_ascii_lowercase().as_str() {
+ "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "a" | "b" | "c" | "d"
+ | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r"
+ | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "`" | "-" | "=" | "[" | "]"
+ | "\\" | ";" | "'" | "," | "." | "/" => keyboard::Key::Character(s.into()),
+ "escape" | "esc" => keyboard::Key::Named(key::Named::Escape),
+ "f1" => keyboard::Key::Named(key::Named::F1),
+ "f2" => keyboard::Key::Named(key::Named::F2),
+ "f3" => keyboard::Key::Named(key::Named::F3),
+ "f4" => keyboard::Key::Named(key::Named::F4),
+ "f5" => keyboard::Key::Named(key::Named::F5),
+ "f6" => keyboard::Key::Named(key::Named::F6),
+ "f7" => keyboard::Key::Named(key::Named::F7),
+ "f8" => keyboard::Key::Named(key::Named::F8),
+ "f9" => keyboard::Key::Named(key::Named::F9),
+ "f10" => keyboard::Key::Named(key::Named::F10),
+ "f11" => keyboard::Key::Named(key::Named::F11),
+ "f12" => keyboard::Key::Named(key::Named::F12),
+ "f13" => keyboard::Key::Named(key::Named::F13),
+ "f14" => keyboard::Key::Named(key::Named::F14),
+ "f15" => keyboard::Key::Named(key::Named::F15),
+ "f16" => keyboard::Key::Named(key::Named::F16),
+ "f17" => keyboard::Key::Named(key::Named::F17),
+ "f18" => keyboard::Key::Named(key::Named::F18),
+ "f19" => keyboard::Key::Named(key::Named::F19),
+ "f20" => keyboard::Key::Named(key::Named::F20),
+ "f21" => keyboard::Key::Named(key::Named::F21),
+ "f22" => keyboard::Key::Named(key::Named::F22),
+ "f23" => keyboard::Key::Named(key::Named::F23),
+ "f24" => keyboard::Key::Named(key::Named::F24),
+ "home" => keyboard::Key::Named(key::Named::Home),
+ "delete" => keyboard::Key::Named(key::Named::Delete),
+ "end" => keyboard::Key::Named(key::Named::End),
+ "pagedown" => keyboard::Key::Named(key::Named::PageDown),
+ "pageup" => keyboard::Key::Named(key::Named::PageUp),
+ "left" => keyboard::Key::Named(key::Named::ArrowLeft),
+ "up" => keyboard::Key::Named(key::Named::ArrowUp),
+ "right" => keyboard::Key::Named(key::Named::ArrowRight),
+ "down" => keyboard::Key::Named(key::Named::ArrowDown),
+ "backspace" => keyboard::Key::Named(key::Named::Backspace),
+ "enter" => keyboard::Key::Named(key::Named::Enter),
+ "space" => keyboard::Key::Named(key::Named::Space),
+ "numlock" => keyboard::Key::Named(key::Named::NumLock),
+ "alt" => keyboard::Key::Named(key::Named::Alt),
+ "tab" => keyboard::Key::Named(key::Named::Tab),
+ "pause" => keyboard::Key::Named(key::Named::Pause),
+ "insert" => keyboard::Key::Named(key::Named::Insert),
+ "cut" => keyboard::Key::Named(key::Named::Cut),
+ "paste" => keyboard::Key::Named(key::Named::Paste),
+ "copy" => keyboard::Key::Named(key::Named::Copy),
+ "volumedown" => keyboard::Key::Named(key::Named::AudioVolumeDown),
+ "volumeup" => keyboard::Key::Named(key::Named::AudioVolumeUp),
+ "shift" => keyboard::Key::Named(key::Named::Shift),
+ "control" => keyboard::Key::Named(key::Named::Control),
+ "mute" => keyboard::Key::Named(key::Named::AudioVolumeMute),
+ "mediastop" => keyboard::Key::Named(key::Named::MediaStop),
+ "mediapause" => keyboard::Key::Named(key::Named::MediaPause),
+ "mediatracknext" => keyboard::Key::Named(key::Named::MediaTrackNext),
+ "mediatrackprev" => keyboard::Key::Named(key::Named::MediaTrackPrevious),
+ _ => return Err(anyhow::format_err!("Invalid key code: {}", s)),
+ }))
+ }
+}
+
+impl FromStr for Modifiers {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> anyhow::Result<Self> {
+ Ok(Self(match s.to_lowercase().as_str() {
+ "shift" => keyboard::Modifiers::SHIFT,
+ "ctrl" => keyboard::Modifiers::CTRL,
+ "alt" | "option" | "opt" => keyboard::Modifiers::ALT,
+ "cmd" | "command" => keyboard::Modifiers::COMMAND,
+ "logo" | "super" | "windows" => keyboard::Modifiers::LOGO,
+ _ => return Err(anyhow::format_err!("Invalid error: {}", s)),
+ }))
+ }
+}
diff --git a/schist_desktop_gui/src/state.rs b/schist_desktop_gui/src/state.rs
deleted file mode 100644
index 0183104..0000000
--- a/schist_desktop_gui/src/state.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use crate::message::Message;
-
-pub struct State {
- pub greeting: String,
-}
-
-impl State {
- pub fn update(&mut self, _message: Message) {
- }
-}
-
-impl Default for State {
- fn default() -> Self {
- Self {
- greeting: String::from("Hello, world!"),
- }
- }
-}
diff --git a/schist_desktop_gui/src/style.rs b/schist_desktop_gui/src/style.rs
new file mode 100644
index 0000000..01ec2ad
--- /dev/null
+++ b/schist_desktop_gui/src/style.rs
@@ -0,0 +1 @@
+pub const SPACING_LG: u16 = 32;
diff --git a/schist_desktop_gui/src/theme.rs b/schist_desktop_gui/src/theme.rs
index 695ff8a..61d736b 100644
--- a/schist_desktop_gui/src/theme.rs
+++ b/schist_desktop_gui/src/theme.rs
@@ -1,7 +1,7 @@
use iced::Theme;
-use crate::state::State;
+use crate::gui::Gui;
-pub fn make_theme(_state: &State) -> Theme {
+pub fn make_theme(_state: &Gui) -> Theme {
Theme::GruvboxDark
-} \ No newline at end of file
+}
diff --git a/schist_desktop_gui/src/traits.rs b/schist_desktop_gui/src/traits.rs
new file mode 100644
index 0000000..a36ef19
--- /dev/null
+++ b/schist_desktop_gui/src/traits.rs
@@ -0,0 +1,5 @@
+mod component;
+mod viewable;
+
+pub use component::Component;
+pub use viewable::Viewable;
diff --git a/schist_desktop_gui/src/traits/component.rs b/schist_desktop_gui/src/traits/component.rs
new file mode 100644
index 0000000..cece636
--- /dev/null
+++ b/schist_desktop_gui/src/traits/component.rs
@@ -0,0 +1,5 @@
+use crate::traits::Viewable;
+
+pub trait Component<'a, Message, Action>: Viewable<'a, Message> {
+ fn update(&mut self, message: Message) -> Action;
+}
diff --git a/schist_desktop_gui/src/traits/viewable.rs b/schist_desktop_gui/src/traits/viewable.rs
new file mode 100644
index 0000000..e0f0eb7
--- /dev/null
+++ b/schist_desktop_gui/src/traits/viewable.rs
@@ -0,0 +1,3 @@
+pub trait Viewable<'a, Message> {
+ fn view(&'a self) -> iced::Element<'a, Message>;
+}
diff --git a/schist_desktop_gui/src/view.rs b/schist_desktop_gui/src/view.rs
deleted file mode 100644
index 7d169b5..0000000
--- a/schist_desktop_gui/src/view.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use iced::widget::{column, Column};
-
-use crate::{message::Message, state::State};
-
-pub fn view(state: &State) -> Column<Message> {
- column![iced::widget::text(state.greeting.clone())]
-}
diff --git a/schist_desktop_gui/src/window_settings.rs b/schist_desktop_gui/src/window_settings.rs
index cd71d09..8533f97 100644
--- a/schist_desktop_gui/src/window_settings.rs
+++ b/schist_desktop_gui/src/window_settings.rs
@@ -4,4 +4,4 @@ use crate::config::Config;
pub fn make_window_settings(_config: &Config) -> Settings {
Settings::default()
-} \ No newline at end of file
+}