From d5f1554949321f2ebfda89efe39b0477a6ff2e55 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:15:24 +0000 Subject: task-085: transactions view has transactions --- schist_desktop_gui/src/gui/components.rs | 2 + .../src/gui/components/account_navigation_entry.rs | 34 +++++++ .../src/gui/components/transactions_view.rs | 106 +++++++++------------ .../src/gui/screens/main_screen/main_screen.rs | 2 +- 4 files changed, 83 insertions(+), 61 deletions(-) create mode 100644 schist_desktop_gui/src/gui/components/account_navigation_entry.rs (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index bb5590f..606c418 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -1,3 +1,4 @@ +mod account_navigation_entry; pub mod balances_view; pub mod bucket_navigation_entry; pub mod buckets_view; @@ -7,6 +8,7 @@ pub mod panel; pub mod text; pub mod transactions_view; +pub use account_navigation_entry::AccountNavigationEntry; pub use balances_view::BalancesView; pub use bucket_navigation_entry::BucketNavigationEntry; pub use buckets_view::BucketsView; diff --git a/schist_desktop_gui/src/gui/components/account_navigation_entry.rs b/schist_desktop_gui/src/gui/components/account_navigation_entry.rs new file mode 100644 index 0000000..6a582a7 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/account_navigation_entry.rs @@ -0,0 +1,34 @@ +use iced::Element; +use schist_models::Account; + +use crate::{ + gui::components::{navigation, Text}, + impl_focusable, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct AccountNavigationEntry { + pub account: Account, + is_focused: bool, +} + +impl AccountNavigationEntry { + pub fn new(account: Account) -> Self { + Self { + account, + is_focused: false, + } + } +} + +impl<'a> From<&'a AccountNavigationEntry> + for Element<'a, navigation::Message> +{ + fn from(account_navigation_entry: &'a AccountNavigationEntry) -> Self { + Text::default(&account_navigation_entry.account.name) + .highlight_maybe(account_navigation_entry.is_focused) + .into() + } +} + +impl_focusable!(AccountNavigationEntry); diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 1cb569c..59d7ae3 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,26 +1,28 @@ +use std::collections::HashMap; + use iced::{ alignment, widget::{column, row, Container}, Element, Length, }; -use schist_models::Account; +use itertools::Itertools; +use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, Navigation, Text}, + gui::components::{navigation, AccountNavigationEntry, Navigation, Text}, style::SPACING_LG, traits::{Component, Viewable}, }; #[derive(Clone, Debug)] pub struct TransactionsView { - accounts: Vec, - greeting: String, - navigation: Navigation, + navigation: Navigation, + transactions_by_account_id: HashMap>, } #[derive(Clone, Debug)] pub enum Message { - NavigationMessage(navigation::Message), + NavigationMessage(navigation::Message), SelectNextTransaction, SelectPrevTransaction, SetAccounts(Vec), @@ -31,13 +33,20 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: Vec, greeting: &str) -> Self { - let account_names = view_account_names(accounts.clone()); - let navigation = Navigation::new(account_names.clone().first().cloned(), account_names); + pub fn new(accounts: Vec, transactions: Vec) -> Self { + let account_navigation_entries: Vec = accounts + .into_iter() + .map(AccountNavigationEntry::new) + .collect(); + let navigation = Navigation::new( + account_navigation_entries.first().cloned(), + account_navigation_entries, + ); Self { - accounts: accounts, - greeting: greeting.to_owned(), navigation: navigation, + transactions_by_account_id: transactions + .into_iter() + .into_group_map_by(|t| t.account_id), } } } @@ -45,7 +54,20 @@ impl TransactionsView { impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![Text::default(&self.greeting).as_element()]; + let main_content = column![ + Text::default("Hello, transactions!").as_element(), + Text::default(&format!( + "{} transactions in this account.", + self.navigation.active_option.clone().map_or( + 0, + |AccountNavigationEntry { account, .. }| self + .transactions_by_account_id + .get(&account.id) + .map_or(0, |transactions| transactions.len()) + ) + )) + .as_element(), + ]; row![ Container::new(navigation).width(Length::FillPortion(1)), Container::new(main_content).width(Length::FillPortion(3)), @@ -61,62 +83,26 @@ 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::ActivateOption(group_name) - | navigation::Action::SelectOption(group_name) => { - self.greeting = make_greeting(group_name); - Action::None - } - navigation::Action::None => Action::None, - }; + self.navigation.update(message); Action::None } Message::SetAccounts(accounts) => { - self.accounts = accounts.clone(); - match self - .navigation - .update(navigation::Message::SetOptions(view_account_names( - accounts, - ))) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + let account_navigation_entries: Vec = accounts + .into_iter() + .map(AccountNavigationEntry::new) + .collect(); + self.navigation + .update(navigation::Message::SetOptions(account_navigation_entries)); + Action::None } Message::SelectNextTransaction => { - match self.navigation.update(navigation::Message::SelectNext) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + self.navigation.update(navigation::Message::SelectNext); + Action::None } Message::SelectPrevTransaction => { - match self.navigation.update(navigation::Message::SelectPrev) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + self.navigation.update(navigation::Message::SelectPrev); + Action::None } } } } - -fn make_greeting(account_name: Text) -> String { - format!("Hello, {} account!", account_name.content) -} - -fn view_account_names(accounts: Vec) -> Vec { - accounts - .iter() - .map(|acc| Text::new(acc.name.as_str())) - .collect() -} 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 index 5ed4b30..e526dc5 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -197,7 +197,7 @@ impl MainScreen { balances_view: BalancesView::new(Vec::new()), buckets: Vec::new(), buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), - transactions_view: TransactionsView::new(Vec::new(), "Hello, transactions!"), + transactions_view: TransactionsView::new(Vec::new(), Vec::new()), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } -- cgit v1.2.3 From 5b592ca899ad5a9090c1acd5776221f79e44d121 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:16:13 +0000 Subject: task-085: rename SelectNextTransaction -> SelectNext Account as this was always a mis-nomer --- schist_desktop_gui/src/gui/components/transactions_view.rs | 8 ++++---- schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 59d7ae3..d28629d 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -23,8 +23,8 @@ pub struct TransactionsView { #[derive(Clone, Debug)] pub enum Message { NavigationMessage(navigation::Message), - SelectNextTransaction, - SelectPrevTransaction, + SelectNextAccount, + SelectPrevAccount, SetAccounts(Vec), } @@ -95,11 +95,11 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { .update(navigation::Message::SetOptions(account_navigation_entries)); Action::None } - Message::SelectNextTransaction => { + Message::SelectNextAccount => { self.navigation.update(navigation::Message::SelectNext); Action::None } - Message::SelectPrevTransaction => { + Message::SelectPrevAccount => { self.navigation.update(navigation::Message::SelectPrev); Action::None } 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 index e526dc5..0e8d084 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -130,7 +130,7 @@ impl<'a> Component<'a, Message, Action> for MainScreen { } View::Transactions => { self.transactions_view - .update(transactions_view::Message::SelectNextTransaction); + .update(transactions_view::Message::SelectNextAccount); Action::None } }, @@ -147,7 +147,7 @@ impl<'a> Component<'a, Message, Action> for MainScreen { } View::Transactions => { self.transactions_view - .update(transactions_view::Message::SelectPrevTransaction); + .update(transactions_view::Message::SelectPrevAccount); Action::None } }, -- cgit v1.2.3 From b9244f6049c685c2b983632359676521506f8772 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:22:50 +0000 Subject: task-085: fetches transactions from database --- schist_desktop_gui/src/gui/components/transactions_view.rs | 10 ++++++++++ schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index d28629d..6f2ad44 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -26,6 +26,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetTransactions(Vec), } pub enum Action { @@ -86,6 +87,7 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.navigation.update(message); Action::None } + Message::SetAccounts(accounts) => { let account_navigation_entries: Vec = accounts .into_iter() @@ -95,10 +97,18 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { .update(navigation::Message::SetOptions(account_navigation_entries)); Action::None } + + Message::SetTransactions(transactions) => { + self.transactions_by_account_id = + transactions.into_iter().into_group_map_by(|t| t.account_id); + Action::None + } + Message::SelectNextAccount => { self.navigation.update(navigation::Message::SelectNext); Action::None } + Message::SelectPrevAccount => { self.navigation.update(navigation::Message::SelectPrev); Action::None 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 index 0e8d084..0a2c355 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -4,7 +4,7 @@ use iced::{ widget::{row, Container}, Element, Length, }; -use schist_models::{Account, Bucket}; +use schist_models::{Account, Bucket, Transaction}; use crate::{ gui::components::{ @@ -36,6 +36,7 @@ pub enum Message { SelectView(View), SetAccounts(Vec), SetBuckets(Vec), + SetTransactions(Vec), ViewNavigationMessage(navigation::Message), } @@ -118,6 +119,12 @@ impl<'a> Component<'a, Message, Action> for MainScreen { Action::None } + Message::SetTransactions(transactions) => { + self.transactions_view + .update(transactions_view::Message::SetTransactions(transactions)); + Action::None + } + Message::NextItem => match self.active_view { View::Balances => { self.balances_view -- cgit v1.2.3 From 170c1a58ebd8b6ff1dceb5f46146f76b601c91ad Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:49:01 +0000 Subject: task-085: factor out Viewable impl for TransactionsView --- .../src/gui/components/transactions_view.rs | 40 +++------------------ .../transactions_view/view_transactions_view.rs | 41 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 6f2ad44..f39b522 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,17 +1,13 @@ +mod view_transactions_view; + use std::collections::HashMap; -use iced::{ - alignment, - widget::{column, row, Container}, - Element, Length, -}; use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation, Text}, - style::SPACING_LG, - traits::{Component, Viewable}, + gui::components::{navigation, AccountNavigationEntry, Navigation}, + traits::Component, }; #[derive(Clone, Debug)] @@ -52,34 +48,6 @@ impl TransactionsView { } } -impl<'a> Viewable<'a, Message> for TransactionsView { - fn view(&'a self) -> Element<'a, Message> { - let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![ - Text::default("Hello, transactions!").as_element(), - Text::default(&format!( - "{} transactions in this account.", - self.navigation.active_option.clone().map_or( - 0, - |AccountNavigationEntry { account, .. }| self - .transactions_by_account_id - .get(&account.id) - .map_or(0, |transactions| transactions.len()) - ) - )) - .as_element(), - ]; - 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 { diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs new file mode 100644 index 0000000..37b6655 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -0,0 +1,41 @@ +use iced::{ + alignment, + widget::{column, row, Container}, + Element, Length, +}; + +use crate::{ + gui::components::{AccountNavigationEntry, Text}, + style::SPACING_LG, + traits::Viewable, +}; + +use super::{Message, TransactionsView}; + +impl<'a> Viewable<'a, Message> for TransactionsView { + fn view(&'a self) -> Element<'a, Message> { + let navigation = self.navigation.view().map(Message::NavigationMessage); + let main_content = column![ + Text::default("Hello, transactions!").as_element(), + Text::default(&format!( + "{} transactions in this account.", + self.navigation.active_option.clone().map_or( + 0, + |AccountNavigationEntry { account, .. }| self + .transactions_by_account_id + .get(&account.id) + .map_or(0, |transactions| transactions.len()) + ) + )) + .as_element(), + ]; + 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() + } +} -- cgit v1.2.3 From 97b413aa5962bded37dedb380034fa7b97bdc06c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 20:26:30 +0000 Subject: task-085: display transaction data in tables --- schist_desktop_gui/src/gui/components.rs | 2 + schist_desktop_gui/src/gui/components/table.rs | 42 +++++++++++ .../src/gui/components/table/view_table.rs | 48 ++++++++++++ schist_desktop_gui/src/gui/components/text.rs | 31 ++++++++ .../src/gui/components/transactions_view.rs | 87 +++++++++++++++++----- .../transactions_view/view_transactions_view.rs | 27 ++----- .../src/gui/screens/main_screen/main_screen.rs | 2 +- 7 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 schist_desktop_gui/src/gui/components/table.rs create mode 100644 schist_desktop_gui/src/gui/components/table/view_table.rs (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index 606c418..ec91951 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -5,6 +5,7 @@ pub mod buckets_view; pub mod button; pub mod navigation; pub mod panel; +pub mod table; pub mod text; pub mod transactions_view; @@ -15,5 +16,6 @@ pub use buckets_view::BucketsView; pub use button::{button, panel_button}; pub use navigation::Navigation; pub use panel::Panel; +pub use table::Table; pub use text::Text; pub use transactions_view::TransactionsView; diff --git a/schist_desktop_gui/src/gui/components/table.rs b/schist_desktop_gui/src/gui/components/table.rs new file mode 100644 index 0000000..4c6d810 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/table.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +mod view_table; + +#[derive(Clone, Debug, Default)] +pub struct Table +where + Ix: Clone + std::fmt::Debug, +{ + pub cols: Vec>, + pub rows: Vec>, +} + +#[derive(Clone, Debug)] +pub enum Value { + String(String), + Currency(i32), +} + +#[derive(Clone, Debug)] +pub struct Column +where + Ix: Clone + std::fmt::Debug, +{ + pub index: Ix, + pub name: String, +} + +impl Column +where + Ix: Clone + std::fmt::Debug, +{ + pub fn new(index: Ix, name: &str) -> Self { + Self { + index, + name: name.to_string(), + } + } +} + +#[derive(Clone, Debug)] +pub enum Message {} diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs new file mode 100644 index 0000000..1d5f10b --- /dev/null +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -0,0 +1,48 @@ +use std::{fmt::Debug, hash::Hash}; + +use iced::widget::{column, container, row}; + +use crate::{gui::components::Text, style::SPACING_MD, traits::Viewable}; + +use super::{Message, Table, Value}; + +impl<'a, Ix> Viewable<'a, Message> for Table +where + Ix: Clone + Debug + Eq + Hash, +{ + fn view(&'a self) -> iced::Element<'a, Message> { + let headers = row(self.cols.iter().map(|col| { + container(Text::default(&col.name)) + .width(iced::Length::FillPortion(1)) + .into() + })) + .padding(SPACING_MD); + let rows = column(self.rows.iter().map(|row_data| { + row(self.cols.iter().map(|col| { + container( + row_data + .get(&col.index) + .map(|datum| match datum { + Value::String(text) => Text::default(text).weak(), + Value::Currency(amount) => format_currency(*amount), + }) + .unwrap_or_else(|| Text::default("")), + ) + .width(iced::Length::FillPortion(1)) + .into() + })) + .spacing(SPACING_MD) + .into() + })); + column![headers, rows].into() + } +} + +fn format_currency(amount: i32) -> Text { + let text = if amount >= 0 { + format!(" {} · {} ", amount / 100, amount % 100) + } else { + format!("({} · {})", -amount / 100, -amount % 100) + }; + Text::default(&text).align_right().width(iced::Length::Fill) +} diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index d171043..0d0c8c4 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -5,12 +5,20 @@ use crate::{impl_focusable, style::*}; #[derive(Clone, Debug, PartialEq)] pub struct Text { pub content: String, + align: Alignment, colour: Option, size: Option, strength: Option, + width: iced::Length, is_focused: bool, } +#[derive(Clone, Debug, PartialEq)] +enum Alignment { + Left, + Right, +} + #[derive(Clone, Debug, PartialEq)] enum Colour { Danger, @@ -33,20 +41,24 @@ enum Size { impl Text { pub fn default(content: &str) -> Self { Self { + align: Alignment::Left, content: String::from(content), colour: Some(Colour::Primary), size: Some(Size::Base), strength: Some(Strength::Base), is_focused: false, + width: iced::Length::Shrink, } } pub fn new(content: &str) -> Self { Self { + align: Alignment::Left, content: String::from(content), colour: None, size: None, strength: None, + width: iced::Length::Shrink, is_focused: false, } } @@ -86,6 +98,20 @@ impl Text { self.clone() } } + + pub fn align_right(&self) -> Self { + Self { + align: Alignment::Right, + ..self.clone() + } + } + + pub fn width(&self, width: iced::Length) -> Self { + Self { + width, + ..self.clone() + } + } } impl<'a, Message> From for iced::Element<'a, Message> { @@ -109,6 +135,11 @@ where Some(Size::Small) => TEXT_SIZE_SM, Some(Size::Base) | None => TEXT_SIZE_BASE, }) + .align_x(match &text.borrow().align { + Alignment::Left => iced::alignment::Horizontal::Left, + Alignment::Right => iced::alignment::Horizontal::Right, + }) + .width(text.borrow().width) .style(move |theme: &iced::Theme| iced::widget::text::Style { color: match ( &text.borrow().borrow().colour, diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index f39b522..8e076f1 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -6,13 +6,14 @@ use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation}, + gui::components::{navigation, table, AccountNavigationEntry, Navigation, Table}, traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { navigation: Navigation, + table: Table, transactions_by_account_id: HashMap>, } @@ -23,6 +24,7 @@ pub enum Message { SelectPrevAccount, SetAccounts(Vec), SetTransactions(Vec), + TableMessage(table::Message), } pub enum Action { @@ -30,57 +32,106 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: Vec, transactions: Vec) -> Self { + pub fn new(accounts: &[Account], transactions: &[Transaction]) -> Self { let account_navigation_entries: Vec = accounts - .into_iter() + .iter() + .cloned() .map(AccountNavigationEntry::new) .collect(); let navigation = Navigation::new( account_navigation_entries.first().cloned(), account_navigation_entries, ); - Self { + let mut transactions_view = Self { navigation: navigation, + table: Table::default(), transactions_by_account_id: transactions .into_iter() + .cloned() .into_group_map_by(|t| t.account_id), - } + }; + transactions_view.refresh_table(); + transactions_view + } + + fn refresh_table(&mut self) { + self.table = + Table { + cols: vec![ + table::Column::new(0, "Date"), + table::Column::new(1, "Bucket"), + table::Column::new(2, "Payee"), + table::Column::new(3, "Quantity"), + table::Column::new(4, "Balance"), + ], + rows: self.navigation.active_option.clone().map_or_else( + Vec::new, + |AccountNavigationEntry { account, .. }| { + self.transactions_by_account_id + .get(&account.id) + .cloned() + .unwrap_or_else(Vec::new) + .iter() + .map(|t| { + HashMap::from([ + (0, table::Value::String(t.date.to_string())), + ( + 1, + table::Value::String(t.bucket_id.map_or_else( + || String::from("None"), + |id| id.to_string(), + )), + ), + (2, table::Value::String(t.counterparty.clone())), + (3, table::Value::Currency(t.amount)), + (4, table::Value::String(String::from("TODO"))), + ]) + }) + .collect() + }, + ), + }; } } impl<'a> Component<'a, Message, Action> for TransactionsView { fn update(&mut self, message: Message) -> Action { match message { - Message::NavigationMessage(message) => { - self.navigation.update(message); - Action::None - } + Message::NavigationMessage(message) => self.update_navigation(message), Message::SetAccounts(accounts) => { let account_navigation_entries: Vec = accounts .into_iter() .map(AccountNavigationEntry::new) .collect(); - self.navigation - .update(navigation::Message::SetOptions(account_navigation_entries)); - Action::None + self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); + self.refresh_table(); Action::None } - Message::SelectNextAccount => { - self.navigation.update(navigation::Message::SelectNext); - Action::None - } + Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext), + + Message::SelectPrevAccount => self.update_navigation(navigation::Message::SelectPrev), + } + } +} - Message::SelectPrevAccount => { - self.navigation.update(navigation::Message::SelectPrev); +impl TransactionsView { + fn update_navigation( + &mut self, + message: navigation::Message, + ) -> Action { + match self.navigation.update(message) { + navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { + self.refresh_table(); Action::None } + navigation::Action::None => Action::None, } } } diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 37b6655..d1f6af2 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -1,37 +1,22 @@ use iced::{ alignment, - widget::{column, row, Container}, + widget::{row, Container}, Element, Length, }; -use crate::{ - gui::components::{AccountNavigationEntry, Text}, - style::SPACING_LG, - traits::Viewable, -}; +use crate::{style::SPACING_LG, traits::Viewable}; use super::{Message, TransactionsView}; impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![ - Text::default("Hello, transactions!").as_element(), - Text::default(&format!( - "{} transactions in this account.", - self.navigation.active_option.clone().map_or( - 0, - |AccountNavigationEntry { account, .. }| self - .transactions_by_account_id - .get(&account.id) - .map_or(0, |transactions| transactions.len()) - ) - )) - .as_element(), - ]; + + let table = self.table.view().map(Message::TableMessage); + row![ Container::new(navigation).width(Length::FillPortion(1)), - Container::new(main_content).width(Length::FillPortion(3)), + Container::new(table).width(Length::FillPortion(3)), ] .align_y(alignment::Vertical::Center) .height(Length::Fill) 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 index 0a2c355..9852b53 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -204,7 +204,7 @@ impl MainScreen { balances_view: BalancesView::new(Vec::new()), buckets: Vec::new(), buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), - transactions_view: TransactionsView::new(Vec::new(), Vec::new()), + transactions_view: TransactionsView::new(&[], &[]), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } -- cgit v1.2.3 From 921b52d1aa05fd6477bab56ba7dab1ab9d699b42 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 09:32:46 +0000 Subject: task-085: table is scrollable --- .../src/gui/components/table/view_table.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 1d5f10b..33fa0ef 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -1,6 +1,8 @@ use std::{fmt::Debug, hash::Hash}; -use iced::widget::{column, container, row}; +use iced::widget::{ + column, container, row, scrollable, scrollable::Direction as ScrollableDirection, +}; use crate::{gui::components::Text, style::SPACING_MD, traits::Viewable}; @@ -13,10 +15,12 @@ where fn view(&'a self) -> iced::Element<'a, Message> { let headers = row(self.cols.iter().map(|col| { container(Text::default(&col.name)) - .width(iced::Length::FillPortion(1)) + .width(iced::Length::Fixed(128.0)) .into() })) + .spacing(SPACING_MD) .padding(SPACING_MD); + let rows = column(self.rows.iter().map(|row_data| { row(self.cols.iter().map(|col| { container( @@ -28,13 +32,19 @@ where }) .unwrap_or_else(|| Text::default("")), ) - .width(iced::Length::FillPortion(1)) + .width(iced::Length::Fixed(128.0)) .into() })) .spacing(SPACING_MD) .into() })); - column![headers, rows].into() + + scrollable(column![headers, rows]) + .direction(ScrollableDirection::Both { + horizontal: Default::default(), + vertical: Default::default(), + }) + .into() } } -- cgit v1.2.3 From 8f57c4a25e6eb109259a082cd4f1e1007ad4044c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 10:10:27 +0000 Subject: task-085: each column has its own width --- schist_desktop_gui/src/gui/components/table.rs | 13 +++++++++++++ schist_desktop_gui/src/gui/components/table/view_table.rs | 9 ++++----- schist_desktop_gui/src/gui/components/transactions_view.rs | 6 +++--- 3 files changed, 20 insertions(+), 8 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/table.rs b/schist_desktop_gui/src/gui/components/table.rs index 4c6d810..68090c1 100644 --- a/schist_desktop_gui/src/gui/components/table.rs +++ b/schist_desktop_gui/src/gui/components/table.rs @@ -24,6 +24,7 @@ where { pub index: Ix, pub name: String, + pub width: f32, } impl Column @@ -34,8 +35,20 @@ where Self { index, name: name.to_string(), + width: 128.0, } } + + pub fn width(&self, width: f32) -> Self { + Self { + width, + ..self.clone() + } + } + + pub fn mul_width(&self, mul: f32) -> Self { + self.width(self.width * mul) + } } #[derive(Clone, Debug)] diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 33fa0ef..81755dc 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -15,11 +15,10 @@ where fn view(&'a self) -> iced::Element<'a, Message> { let headers = row(self.cols.iter().map(|col| { container(Text::default(&col.name)) - .width(iced::Length::Fixed(128.0)) + .width(iced::Length::Fixed(col.width)) .into() })) - .spacing(SPACING_MD) - .padding(SPACING_MD); + .spacing(SPACING_MD); let rows = column(self.rows.iter().map(|row_data| { row(self.cols.iter().map(|col| { @@ -32,14 +31,14 @@ where }) .unwrap_or_else(|| Text::default("")), ) - .width(iced::Length::Fixed(128.0)) + .width(iced::Length::Fixed(col.width)) .into() })) .spacing(SPACING_MD) .into() })); - scrollable(column![headers, rows]) + scrollable(column![headers, rows].spacing(SPACING_MD)) .direction(ScrollableDirection::Both { horizontal: Default::default(), vertical: Default::default(), diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 8e076f1..e78a142 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -60,9 +60,9 @@ impl TransactionsView { cols: vec![ table::Column::new(0, "Date"), table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee"), - table::Column::new(3, "Quantity"), - table::Column::new(4, "Balance"), + table::Column::new(2, "Payee").mul_width(1.5), + table::Column::new(3, "Quantity").mul_width(0.67), + table::Column::new(4, "Balance").mul_width(0.67), ], rows: self.navigation.active_option.clone().map_or_else( Vec::new, -- cgit v1.2.3 From 13f4d97a70cfaeb1e78bbc68ff73bd69de081cb2 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 10:13:03 +0000 Subject: task-085: factor out format_string() --- schist_desktop_gui/src/gui/components/table/view_table.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 81755dc..801bbb7 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -26,7 +26,7 @@ where row_data .get(&col.index) .map(|datum| match datum { - Value::String(text) => Text::default(text).weak(), + Value::String(text) => format_string(text), Value::Currency(amount) => format_currency(*amount), }) .unwrap_or_else(|| Text::default("")), @@ -47,11 +47,15 @@ where } } +fn format_string(text: &String) -> Text { + Text::default(text).weak() +} + fn format_currency(amount: i32) -> Text { let text = if amount >= 0 { format!(" {} · {} ", amount / 100, amount % 100) } else { format!("({} · {})", -amount / 100, -amount % 100) }; - Text::default(&text).align_right().width(iced::Length::Fill) + format_string(&text).align_right().width(iced::Length::Fill) } -- cgit v1.2.3 From b1988df5603a427994371d8176bce0c6c1f06fc2 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 10:14:40 +0000 Subject: task-085: rename small -> v_small --- schist_desktop_gui/src/gui/components/panel.rs | 4 ++-- schist_desktop_gui/src/gui/components/text.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/panel.rs b/schist_desktop_gui/src/gui/components/panel.rs index 364659b..85bfb97 100644 --- a/schist_desktop_gui/src/gui/components/panel.rs +++ b/schist_desktop_gui/src/gui/components/panel.rs @@ -39,7 +39,7 @@ where cols.push( Text::default(err) .highlight_maybe(value.is_focused) - .small() + .v_small() .danger() .into(), ); @@ -48,7 +48,7 @@ where cols.push( Text::default(small) .highlight_maybe(value.is_focused) - .small() + .v_small() .weak() .into(), ); diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 0d0c8c4..b67a7aa 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -34,7 +34,7 @@ enum Strength { #[derive(Clone, Debug, PartialEq)] enum Size { - Small, + VSmall, Base, } @@ -67,9 +67,9 @@ impl Text { >>::into(self) } - pub fn small(&self) -> Self { + pub fn v_small(&self) -> Self { Self { - size: Some(Size::Small), + size: Some(Size::VSmall), ..self.clone() } } @@ -132,7 +132,7 @@ where { iced::widget::text(text.borrow().content.clone()) .size(match text.borrow().size { - Some(Size::Small) => TEXT_SIZE_SM, + Some(Size::VSmall) => TEXT_SIZE_V_SM, Some(Size::Base) | None => TEXT_SIZE_BASE, }) .align_x(match &text.borrow().align { -- cgit v1.2.3 From 8659e9df22908fb10afb8d949a523d347634e637 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 10:18:42 +0000 Subject: task-085: table contents are small text size --- schist_desktop_gui/src/gui/components/table/view_table.rs | 2 +- schist_desktop_gui/src/gui/components/text.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 801bbb7..3d4da64 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -48,7 +48,7 @@ where } fn format_string(text: &String) -> Text { - Text::default(text).weak() + Text::default(text).weak().small() } fn format_currency(amount: i32) -> Text { diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index b67a7aa..36a6a13 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -35,6 +35,7 @@ enum Strength { #[derive(Clone, Debug, PartialEq)] enum Size { VSmall, + Small, Base, } @@ -74,6 +75,13 @@ impl Text { } } + pub fn small(&self) -> Self { + Self { + size: Some(Size::Small), + ..self.clone() + } + } + pub fn danger(&self) -> Self { Self { colour: Some(Colour::Danger), @@ -133,6 +141,7 @@ where iced::widget::text(text.borrow().content.clone()) .size(match text.borrow().size { Some(Size::VSmall) => TEXT_SIZE_V_SM, + Some(Size::Small) => TEXT_SIZE_SM, Some(Size::Base) | None => TEXT_SIZE_BASE, }) .align_x(match &text.borrow().align { -- cgit v1.2.3 From 3c8a428c195da414171bd6e9064f26c4a66ffe61 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 19:07:31 +0000 Subject: task-085: upgrade iced 0.13.1 -> 0.14.0 --- .../src/gui/components/balances_view/view_balances_view.rs | 2 +- schist_desktop_gui/src/gui/components/buckets_view.rs | 2 +- schist_desktop_gui/src/gui/components/table/view_table.rs | 6 +++--- .../src/gui/components/transactions_view/view_transactions_view.rs | 2 +- schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) (limited to 'schist_desktop_gui/src/gui') 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 index 22f22b6..013a6fb 100644 --- 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 @@ -24,7 +24,7 @@ impl<'a> Viewable<'a, Message> for BalancesView { ] .align_y(alignment::Vertical::Center) .height(Length::Fill) - .spacing(SPACING_LG) + .spacing(u32::from(SPACING_LG)) .into() } } diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs index 7f5b218..8f12019 100644 --- a/schist_desktop_gui/src/gui/components/buckets_view.rs +++ b/schist_desktop_gui/src/gui/components/buckets_view.rs @@ -115,7 +115,7 @@ impl<'a> Viewable<'a, Message> for BucketsView { ] .align_y(alignment::Vertical::Center) .height(Length::Fill) - .spacing(SPACING_LG) + .spacing(u32::from(SPACING_LG)) .into() } } diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 3d4da64..725c5ff 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -18,7 +18,7 @@ where .width(iced::Length::Fixed(col.width)) .into() })) - .spacing(SPACING_MD); + .spacing(u32::from(SPACING_MD)); let rows = column(self.rows.iter().map(|row_data| { row(self.cols.iter().map(|col| { @@ -34,11 +34,11 @@ where .width(iced::Length::Fixed(col.width)) .into() })) - .spacing(SPACING_MD) + .spacing(u32::from(SPACING_MD)) .into() })); - scrollable(column![headers, rows].spacing(SPACING_MD)) + scrollable(column![headers, rows].spacing(u32::from(SPACING_MD))) .direction(ScrollableDirection::Both { horizontal: Default::default(), vertical: Default::default(), diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index d1f6af2..8102314 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -20,7 +20,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ] .align_y(alignment::Vertical::Center) .height(Length::Fill) - .spacing(SPACING_LG) + .spacing(u32::from(SPACING_LG)) .into() } } 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 index 9852b53..86ce910 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -65,7 +65,7 @@ impl<'a> Viewable<'a, Message> for MainScreen { ] .align_y(alignment::Vertical::Center) .height(Length::Fill) - .spacing(SPACING_LG) + .spacing(u32::from(SPACING_LG)) .into() } } -- cgit v1.2.3 From b3f91aef7c9673c180690f6e22984d50e0dbeecf Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 19:11:22 +0000 Subject: task-085: clips overflowing table cells --- .../src/gui/components/table/view_table.rs | 2 +- schist_desktop_gui/src/gui/components/text.rs | 64 +++++++++++++++++----- .../src/gui/components/transactions_view.rs | 2 +- 3 files changed, 53 insertions(+), 15 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs index 725c5ff..f082b83 100644 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -48,7 +48,7 @@ where } fn format_string(text: &String) -> Text { - Text::default(text).weak().small() + Text::default(text).weak().small().clip() } fn format_currency(amount: i32) -> Text { diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 36a6a13..7a685c1 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -1,5 +1,7 @@ use std::borrow::Borrow; +use iced::widget::container; + use crate::{impl_focusable, style::*}; #[derive(Clone, Debug, PartialEq)] @@ -7,6 +9,7 @@ pub struct Text { pub content: String, align: Alignment, colour: Option, + overflow_strategy: OverflowStrategy, size: Option, strength: Option, width: iced::Length, @@ -26,6 +29,12 @@ enum Colour { Highlight, } +#[derive(Clone, Debug, PartialEq)] +enum OverflowStrategy { + Grow, + Clip, +} + #[derive(Clone, Debug, PartialEq)] enum Strength { Base, @@ -45,6 +54,7 @@ impl Text { align: Alignment::Left, content: String::from(content), colour: Some(Colour::Primary), + overflow_strategy: OverflowStrategy::Grow, size: Some(Size::Base), strength: Some(Strength::Base), is_focused: false, @@ -57,6 +67,7 @@ impl Text { align: Alignment::Left, content: String::from(content), colour: None, + overflow_strategy: OverflowStrategy::Grow, size: None, strength: None, width: iced::Length::Shrink, @@ -64,7 +75,10 @@ impl Text { } } - pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> { + pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> + where + Message: 'a, + { >>::into(self) } @@ -120,15 +134,28 @@ impl Text { ..self.clone() } } + + pub fn clip(&self) -> Self { + Self { + overflow_strategy: OverflowStrategy::Clip, + ..self.clone() + } + } } -impl<'a, Message> From for iced::Element<'a, Message> { +impl<'a, Message> From for iced::Element<'a, Message> +where + Message: 'a, +{ fn from(text: Text) -> Self { text_as_element(text) } } -impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> { +impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> +where + Message: 'a, +{ fn from(text: &'a Text) -> Self { text_as_element(text) } @@ -137,23 +164,27 @@ impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> { fn text_as_element<'a, T, Message>(text: T) -> iced::Element<'a, Message> where T: Borrow + 'a, + Message: 'a, { - iced::widget::text(text.borrow().content.clone()) + let overflow_strategy = &text.borrow().overflow_strategy.clone(); + + let text_elem: iced::widget::Text = iced::widget::text(text.borrow().content.clone()) .size(match text.borrow().size { - Some(Size::VSmall) => TEXT_SIZE_V_SM, - Some(Size::Small) => TEXT_SIZE_SM, - Some(Size::Base) | None => TEXT_SIZE_BASE, + Some(Size::VSmall) => u32::from(TEXT_SIZE_V_SM), + Some(Size::Small) => u32::from(TEXT_SIZE_SM), + Some(Size::Base) | None => u32::from(TEXT_SIZE_BASE), }) .align_x(match &text.borrow().align { Alignment::Left => iced::alignment::Horizontal::Left, Alignment::Right => iced::alignment::Horizontal::Right, }) + .wrapping(match overflow_strategy { + OverflowStrategy::Grow => iced::widget::text::Wrapping::default(), + OverflowStrategy::Clip => iced::widget::text::Wrapping::None, + }) .width(text.borrow().width) .style(move |theme: &iced::Theme| iced::widget::text::Style { - color: match ( - &text.borrow().borrow().colour, - &text.borrow().borrow().strength, - ) { + color: match (&text.borrow().colour, &text.borrow().strength) { (Some(Colour::Danger), Some(Strength::Base) | None) => { Some(theme.extended_palette().danger.base.text) } @@ -174,8 +205,15 @@ where } (None, _) => None, }, - }) - .into() + }); + + match overflow_strategy { + OverflowStrategy::Grow => text_elem.into(), + OverflowStrategy::Clip => container::<'a, Message, _, _>(text_elem) + .height(iced::Length::Shrink) + .clip(true) + .into(), + } } impl_focusable!(Text); diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index e78a142..51877cf 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -60,7 +60,7 @@ impl TransactionsView { cols: vec![ table::Column::new(0, "Date"), table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee").mul_width(1.5), + table::Column::new(2, "Payee").mul_width(2.0), table::Column::new(3, "Quantity").mul_width(0.67), table::Column::new(4, "Balance").mul_width(0.67), ], -- cgit v1.2.3 From 413afad1fdf1d15fefc1b709f2eba1332a110608 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 20:00:26 +0000 Subject: task-085: uses new iced::widget::Table instead of home-made widget --- schist_desktop_gui/src/gui/components.rs | 2 - schist_desktop_gui/src/gui/components/table.rs | 55 -------------- .../src/gui/components/table/view_table.rs | 61 ---------------- schist_desktop_gui/src/gui/components/text.rs | 9 +++ .../src/gui/components/transactions_view.rs | 52 +------------ .../transactions_view/view_transactions_view.rs | 85 +++++++++++++++++++++- 6 files changed, 93 insertions(+), 171 deletions(-) delete mode 100644 schist_desktop_gui/src/gui/components/table.rs delete mode 100644 schist_desktop_gui/src/gui/components/table/view_table.rs (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index ec91951..606c418 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -5,7 +5,6 @@ pub mod buckets_view; pub mod button; pub mod navigation; pub mod panel; -pub mod table; pub mod text; pub mod transactions_view; @@ -16,6 +15,5 @@ pub use buckets_view::BucketsView; pub use button::{button, panel_button}; pub use navigation::Navigation; pub use panel::Panel; -pub use table::Table; pub use text::Text; pub use transactions_view::TransactionsView; diff --git a/schist_desktop_gui/src/gui/components/table.rs b/schist_desktop_gui/src/gui/components/table.rs deleted file mode 100644 index 68090c1..0000000 --- a/schist_desktop_gui/src/gui/components/table.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::collections::HashMap; - -mod view_table; - -#[derive(Clone, Debug, Default)] -pub struct Table -where - Ix: Clone + std::fmt::Debug, -{ - pub cols: Vec>, - pub rows: Vec>, -} - -#[derive(Clone, Debug)] -pub enum Value { - String(String), - Currency(i32), -} - -#[derive(Clone, Debug)] -pub struct Column -where - Ix: Clone + std::fmt::Debug, -{ - pub index: Ix, - pub name: String, - pub width: f32, -} - -impl Column -where - Ix: Clone + std::fmt::Debug, -{ - pub fn new(index: Ix, name: &str) -> Self { - Self { - index, - name: name.to_string(), - width: 128.0, - } - } - - pub fn width(&self, width: f32) -> Self { - Self { - width, - ..self.clone() - } - } - - pub fn mul_width(&self, mul: f32) -> Self { - self.width(self.width * mul) - } -} - -#[derive(Clone, Debug)] -pub enum Message {} diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs deleted file mode 100644 index f082b83..0000000 --- a/schist_desktop_gui/src/gui/components/table/view_table.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::{fmt::Debug, hash::Hash}; - -use iced::widget::{ - column, container, row, scrollable, scrollable::Direction as ScrollableDirection, -}; - -use crate::{gui::components::Text, style::SPACING_MD, traits::Viewable}; - -use super::{Message, Table, Value}; - -impl<'a, Ix> Viewable<'a, Message> for Table -where - Ix: Clone + Debug + Eq + Hash, -{ - fn view(&'a self) -> iced::Element<'a, Message> { - let headers = row(self.cols.iter().map(|col| { - container(Text::default(&col.name)) - .width(iced::Length::Fixed(col.width)) - .into() - })) - .spacing(u32::from(SPACING_MD)); - - let rows = column(self.rows.iter().map(|row_data| { - row(self.cols.iter().map(|col| { - container( - row_data - .get(&col.index) - .map(|datum| match datum { - Value::String(text) => format_string(text), - Value::Currency(amount) => format_currency(*amount), - }) - .unwrap_or_else(|| Text::default("")), - ) - .width(iced::Length::Fixed(col.width)) - .into() - })) - .spacing(u32::from(SPACING_MD)) - .into() - })); - - scrollable(column![headers, rows].spacing(u32::from(SPACING_MD))) - .direction(ScrollableDirection::Both { - horizontal: Default::default(), - vertical: Default::default(), - }) - .into() - } -} - -fn format_string(text: &String) -> Text { - Text::default(text).weak().small().clip() -} - -fn format_currency(amount: i32) -> Text { - let text = if amount >= 0 { - format!(" {} · {} ", amount / 100, amount % 100) - } else { - format!("({} · {})", -amount / 100, -amount % 100) - }; - format_string(&text).align_right().width(iced::Length::Fill) -} diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 7a685c1..1760f4c 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -75,6 +75,15 @@ impl Text { } } + pub fn currency(amount: i32) -> Self { + let formatted_string = if amount < 0 { + format!("({} · {})", -amount / 100, -amount % 100) + } else { + format!(" {} · {} ", amount / 100, amount % 100) + }; + Self::default(&formatted_string) + } + pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> where Message: 'a, diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 51877cf..491049d 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -6,14 +6,13 @@ use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, table, AccountNavigationEntry, Navigation, Table}, + gui::components::{navigation, AccountNavigationEntry, Navigation}, traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { navigation: Navigation, - table: Table, transactions_by_account_id: HashMap>, } @@ -24,7 +23,6 @@ pub enum Message { SelectPrevAccount, SetAccounts(Vec), SetTransactions(Vec), - TableMessage(table::Message), } pub enum Action { @@ -42,55 +40,13 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - let mut transactions_view = Self { + Self { navigation: navigation, - table: Table::default(), transactions_by_account_id: transactions .into_iter() .cloned() .into_group_map_by(|t| t.account_id), - }; - transactions_view.refresh_table(); - transactions_view - } - - fn refresh_table(&mut self) { - self.table = - Table { - cols: vec![ - table::Column::new(0, "Date"), - table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee").mul_width(2.0), - table::Column::new(3, "Quantity").mul_width(0.67), - table::Column::new(4, "Balance").mul_width(0.67), - ], - rows: self.navigation.active_option.clone().map_or_else( - Vec::new, - |AccountNavigationEntry { account, .. }| { - self.transactions_by_account_id - .get(&account.id) - .cloned() - .unwrap_or_else(Vec::new) - .iter() - .map(|t| { - HashMap::from([ - (0, table::Value::String(t.date.to_string())), - ( - 1, - table::Value::String(t.bucket_id.map_or_else( - || String::from("None"), - |id| id.to_string(), - )), - ), - (2, table::Value::String(t.counterparty.clone())), - (3, table::Value::Currency(t.amount)), - (4, table::Value::String(String::from("TODO"))), - ]) - }) - .collect() - }, - ), - }; + } } } @@ -110,7 +66,6 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); - self.refresh_table(); Action::None } @@ -128,7 +83,6 @@ impl TransactionsView { ) -> Action { match self.navigation.update(message) { navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { - self.refresh_table(); Action::None } navigation::Action::None => Action::None, diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 8102314..72f4c8f 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -1,10 +1,17 @@ use iced::{ alignment, - widget::{row, Container}, + widget::{ + row, + scrollable::{self, Scrollbar}, + table, + table::column, + Container, Scrollable, + }, Element, Length, }; +use schist_models::Transaction; -use crate::{style::SPACING_LG, traits::Viewable}; +use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable}; use super::{Message, TransactionsView}; @@ -12,11 +19,81 @@ impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { let navigation = self.navigation.view().map(Message::NavigationMessage); - let table = self.table.view().map(Message::TableMessage); + let columns = [ + column( + Text::default("Date").width(iced::Length::Fixed(128.0)), + |t: Transaction| { + Text::default(t.date.to_string().as_str()) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() + }, + ), + column( + Text::default("Bucket").width(iced::Length::Fixed(128.0)), + |t: Transaction| { + Text::default( + t.bucket_id + .map(|id| id.to_string()) + .unwrap_or(String::from("None")) + .as_str(), + ) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() + }, + ), + column( + Text::default("Payee").width(iced::Length::Fixed(256.0)), + |t: Transaction| { + Text::default(&t.counterparty) + .width(iced::Length::Fixed(256.0)) + .small() + .clip() + }, + ), + column( + Text::default("Quantity").width(iced::Length::Fixed(96.0)), + |t: Transaction| { + Text::currency(t.amount) + .width(iced::Length::Fixed(96.0)) + .small() + .clip() + .align_right() + }, + ), + column( + Text::default("Balance").width(iced::Length::Fixed(96.0)), + |_t: Transaction| { + Text::default("TODO") + .width(iced::Length::Fixed(96.0)) + .small() + .clip() + }, + ), + ]; + + let rows = self + .navigation + .active_option + .clone() + .map_or_else(Vec::default, |option| { + self.transactions_by_account_id + .get(&option.account.id) + .cloned() + .unwrap_or_else(Vec::default) + }); + + let table = table(columns, rows); row![ Container::new(navigation).width(Length::FillPortion(1)), - Container::new(table).width(Length::FillPortion(3)), + Scrollable::new(table) + .direction(scrollable::Direction::Both { + horizontal: Scrollbar::default(), + vertical: Scrollbar::default(), + }) + .width(Length::FillPortion(3)), ] .align_y(alignment::Vertical::Center) .height(Length::Fill) -- cgit v1.2.3 From d119e7d7ba41ce7ee479336ce5de287ef62a2737 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sat, 21 Feb 2026 14:17:09 +0000 Subject: task-085: table is fixed width --- .../components/transactions_view/view_transactions_view.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 72f4c8f..d6ca180 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -84,16 +84,14 @@ impl<'a> Viewable<'a, Message> for TransactionsView { .unwrap_or_else(Vec::default) }); - let table = table(columns, rows); + let table = Scrollable::new(table(columns, rows)).direction(scrollable::Direction::Both { + horizontal: Scrollbar::default(), + vertical: Scrollbar::default(), + }); row![ Container::new(navigation).width(Length::FillPortion(1)), - Scrollable::new(table) - .direction(scrollable::Direction::Both { - horizontal: Scrollbar::default(), - vertical: Scrollbar::default(), - }) - .width(Length::FillPortion(3)), + Container::new(table).width(Length::FillPortion(3)), ] .align_y(alignment::Vertical::Center) .height(Length::Fill) -- cgit v1.2.3 From b3d80680c0ac19df7a810ade6f76cabf1005e914 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 08:15:36 +0000 Subject: task-085: format dates --- .../src/gui/components/transactions_view/view_transactions_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index d6ca180..eed55ad 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -23,7 +23,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { column( Text::default("Date").width(iced::Length::Fixed(128.0)), |t: Transaction| { - Text::default(t.date.to_string().as_str()) + Text::default(t.date.format("%e %b %Y").as_str()) .width(iced::Length::Fixed(128.0)) .small() .clip() -- cgit v1.2.3 From fe1d4bff956d957a6ad2dcd03b15533ec03bf9d6 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 08:36:01 +0000 Subject: task-085: factor out cell view functions --- .../transactions_view/view_transactions_view.rs | 82 ++++++++++++---------- 1 file changed, 46 insertions(+), 36 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index eed55ad..7ceadda 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -22,54 +22,23 @@ impl<'a> Viewable<'a, Message> for TransactionsView { let columns = [ column( Text::default("Date").width(iced::Length::Fixed(128.0)), - |t: Transaction| { - Text::default(t.date.format("%e %b %Y").as_str()) - .width(iced::Length::Fixed(128.0)) - .small() - .clip() - }, + view_date_cell, ), column( Text::default("Bucket").width(iced::Length::Fixed(128.0)), - |t: Transaction| { - Text::default( - t.bucket_id - .map(|id| id.to_string()) - .unwrap_or(String::from("None")) - .as_str(), - ) - .width(iced::Length::Fixed(128.0)) - .small() - .clip() - }, + |t: Transaction| view_bucket_cell(t), ), column( Text::default("Payee").width(iced::Length::Fixed(256.0)), - |t: Transaction| { - Text::default(&t.counterparty) - .width(iced::Length::Fixed(256.0)) - .small() - .clip() - }, + view_payee_cell, ), column( Text::default("Quantity").width(iced::Length::Fixed(96.0)), - |t: Transaction| { - Text::currency(t.amount) - .width(iced::Length::Fixed(96.0)) - .small() - .clip() - .align_right() - }, + view_quantity_cell, ), column( Text::default("Balance").width(iced::Length::Fixed(96.0)), - |_t: Transaction| { - Text::default("TODO") - .width(iced::Length::Fixed(96.0)) - .small() - .clip() - }, + |_t: Transaction| view_balance_cell(), ), ]; @@ -99,3 +68,44 @@ impl<'a> Viewable<'a, Message> for TransactionsView { .into() } } + +fn view_date_cell(transaction: Transaction) -> Text { + Text::default(transaction.date.format("%e %b %Y").as_str()) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() +} + +fn view_bucket_cell(t: Transaction) -> Text { + Text::default( + t.bucket_id + .map(|id| id.to_string()) + .unwrap_or(String::from("None")) + .as_str(), + ) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() +} + +fn view_payee_cell(t: Transaction) -> Text { + Text::default(&t.counterparty) + .width(iced::Length::Fixed(256.0)) + .small() + .clip() +} + +fn view_quantity_cell(t: Transaction) -> Text { + Text::currency(t.amount) + .width(iced::Length::Fixed(96.0)) + .small() + .clip() + .align_right() +} + +fn view_balance_cell() -> Text { + Text::default("TODO") + .width(iced::Length::Fixed(96.0)) + .small() + .clip() +} -- cgit v1.2.3 From 376ad725248c9c50596adb8bbebab9d6c5c9dea7 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 08:45:12 +0000 Subject: task-085: can make Text italic --- schist_desktop_gui/src/gui/components/text.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 1760f4c..0cf9698 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -1,6 +1,6 @@ use std::borrow::Borrow; -use iced::widget::container; +use iced::{font, widget::container, Font}; use crate::{impl_focusable, style::*}; @@ -9,6 +9,7 @@ pub struct Text { pub content: String, align: Alignment, colour: Option, + font_style: font::Style, overflow_strategy: OverflowStrategy, size: Option, strength: Option, @@ -54,6 +55,7 @@ impl Text { align: Alignment::Left, content: String::from(content), colour: Some(Colour::Primary), + font_style: font::Style::Normal, overflow_strategy: OverflowStrategy::Grow, size: Some(Size::Base), strength: Some(Strength::Base), @@ -67,6 +69,7 @@ impl Text { align: Alignment::Left, content: String::from(content), colour: None, + font_style: font::Style::Normal, overflow_strategy: OverflowStrategy::Grow, size: None, strength: None, @@ -150,6 +153,13 @@ impl Text { ..self.clone() } } + + pub fn italic(&self) -> Self { + Self { + font_style: font::Style::Italic, + ..self.clone() + } + } } impl<'a, Message> From for iced::Element<'a, Message> @@ -178,6 +188,10 @@ where let overflow_strategy = &text.borrow().overflow_strategy.clone(); let text_elem: iced::widget::Text = iced::widget::text(text.borrow().content.clone()) + .font(Font { + style: text.borrow().font_style, + ..Default::default() + }) .size(match text.borrow().size { Some(Size::VSmall) => u32::from(TEXT_SIZE_V_SM), Some(Size::Small) => u32::from(TEXT_SIZE_SM), -- cgit v1.2.3 From 6137e4bec5fd7f34c66162b32424343a30b3c5f2 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 08:49:25 +0000 Subject: task-085: formats bucket cells --- .../src/gui/components/transactions_view.rs | 12 ++++++++++-- .../transactions_view/view_transactions_view.rs | 21 ++++++++++++--------- .../src/gui/screens/main_screen/main_screen.rs | 6 ++++-- 3 files changed, 26 insertions(+), 13 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 491049d..11249a1 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -3,7 +3,7 @@ mod view_transactions_view; use std::collections::HashMap; use itertools::Itertools; -use schist_models::{Account, Transaction}; +use schist_models::{Account, Bucket, Transaction}; use crate::{ gui::components::{navigation, AccountNavigationEntry, Navigation}, @@ -12,6 +12,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { + buckets: Vec, navigation: Navigation, transactions_by_account_id: HashMap>, } @@ -22,6 +23,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetBuckets(Vec), SetTransactions(Vec), } @@ -30,7 +32,7 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: &[Account], transactions: &[Transaction]) -> Self { + pub fn new(accounts: &[Account], buckets: &[Bucket], transactions: &[Transaction]) -> Self { let account_navigation_entries: Vec = accounts .iter() .cloned() @@ -41,6 +43,7 @@ impl TransactionsView { account_navigation_entries, ); Self { + buckets: buckets.to_vec(), navigation: navigation, transactions_by_account_id: transactions .into_iter() @@ -63,6 +66,11 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } + Message::SetBuckets(buckets) => { + self.buckets = buckets; + Action::None + } + Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 7ceadda..f43ee48 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -9,7 +9,7 @@ use iced::{ }, Element, Length, }; -use schist_models::Transaction; +use schist_models::{Bucket, Transaction}; use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable}; @@ -26,7 +26,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Bucket").width(iced::Length::Fixed(128.0)), - |t: Transaction| view_bucket_cell(t), + |t: Transaction| view_bucket_cell(t, &self.buckets), ), column( Text::default("Payee").width(iced::Length::Fixed(256.0)), @@ -76,13 +76,16 @@ fn view_date_cell(transaction: Transaction) -> Text { .clip() } -fn view_bucket_cell(t: Transaction) -> Text { - Text::default( - t.bucket_id - .map(|id| id.to_string()) - .unwrap_or(String::from("None")) - .as_str(), - ) +fn view_bucket_cell(t: Transaction, buckets: &[Bucket]) -> Text { + (t.bucket_id.map_or_else( + || Text::default("None").italic(), + |id| { + buckets.iter().find(|b| b.id == id).map_or_else( + || Text::default(format!("<{}>", id).as_str()).italic(), + |b| Text::default(&b.name), + ) + }, + )) .width(iced::Length::Fixed(128.0)) .small() .clip() 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 index 86ce910..038d65f 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -109,7 +109,9 @@ impl<'a> Component<'a, Message, Action> for MainScreen { self.balances_view .update(balances_view::Message::SetBuckets(buckets.clone())); self.buckets_view - .update(buckets_view::Message::SetBuckets(buckets)); + .update(buckets_view::Message::SetBuckets(buckets.clone())); + self.transactions_view + .update(transactions_view::Message::SetBuckets(buckets)); Action::None } @@ -204,7 +206,7 @@ impl MainScreen { balances_view: BalancesView::new(Vec::new()), buckets: Vec::new(), buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), - transactions_view: TransactionsView::new(&[], &[]), + transactions_view: TransactionsView::new(&[], &[], &[]), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } -- cgit v1.2.3 From 96e1b8a6158e5ac3b56c03718de5650ff805d41b Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 09:32:21 +0000 Subject: task-085: displays running balances --- .../src/gui/components/transactions_view.rs | 33 ++++++++++++++++------ .../transactions_view/view_transactions_view.rs | 22 +++++++++++---- 2 files changed, 41 insertions(+), 14 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 11249a1..27e18cd 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,6 +1,6 @@ mod view_transactions_view; -use std::collections::HashMap; +use std::{collections::HashMap, mem::transmute}; use itertools::Itertools; use schist_models::{Account, Bucket, Transaction}; @@ -12,6 +12,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { + account_balances_by_transaction_id: HashMap, buckets: Vec, navigation: Navigation, transactions_by_account_id: HashMap>, @@ -42,13 +43,30 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - Self { + let mut transactions_view = Self { + account_balances_by_transaction_id: HashMap::with_capacity(transactions.len()), buckets: buckets.to_vec(), navigation: navigation, - transactions_by_account_id: transactions - .into_iter() - .cloned() - .into_group_map_by(|t| t.account_id), + transactions_by_account_id: HashMap::with_capacity(accounts.len()), + }; + transactions_view.set_transactions(transactions); + transactions_view + } + + fn set_transactions(&mut self, transactions: &[Transaction]) { + self.transactions_by_account_id = transactions + .into_iter() + .cloned() + .into_group_map_by(|t| t.account_id); + self.account_balances_by_transaction_id = HashMap::new(); + for entry in self.transactions_by_account_id.iter_mut() { + entry.1.sort_by(|t1, t2| t2.date.cmp(&t1.date)); + let mut running_balance = 0; + for transaction in entry.1.iter().rev() { + running_balance += transaction.amount; + self.account_balances_by_transaction_id + .insert(transaction.id, running_balance); + } } } } @@ -72,8 +90,7 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { } Message::SetTransactions(transactions) => { - self.transactions_by_account_id = - transactions.into_iter().into_group_map_by(|t| t.account_id); + self.set_transactions(&transactions); Action::None } diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index f43ee48..4f61493 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use iced::{ alignment, widget::{ @@ -38,7 +40,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Balance").width(iced::Length::Fixed(96.0)), - |_t: Transaction| view_balance_cell(), + |t: Transaction| view_balance_cell(t, &self.account_balances_by_transaction_id), ), ]; @@ -106,9 +108,17 @@ fn view_quantity_cell(t: Transaction) -> Text { .align_right() } -fn view_balance_cell() -> Text { - Text::default("TODO") - .width(iced::Length::Fixed(96.0)) - .small() - .clip() +fn view_balance_cell( + transaction: Transaction, + account_balances_by_transaction_id: &HashMap, +) -> Text { + (account_balances_by_transaction_id + .get(&transaction.id) + .map_or_else( + || Text::default("Error").danger(), + |&balance| Text::currency(balance), + )) + .width(iced::Length::Fixed(96.0)) + .small() + .clip() } -- cgit v1.2.3 From 4030a63ac6fe61df87dca07e4143a6b84a9e8475 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 21:34:24 +0000 Subject: task-085: include account transfers --- schist_desktop_gui/src/gui/components/text.rs | 7 + .../src/gui/components/transactions_view.rs | 235 ++++++++++++++++++--- .../transactions_view/view_transactions_view.rs | 61 ++---- .../src/gui/screens/main_screen/main_screen.rs | 13 +- 4 files changed, 247 insertions(+), 69 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 0cf9698..068e89a 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -160,6 +160,13 @@ impl Text { ..self.clone() } } + + pub fn style(&self, font_style: font::Style) -> Self { + Self { + font_style, + ..self.clone() + } + } } impl<'a, Message> From for iced::Element<'a, Message> diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 27e18cd..a52c419 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,9 +1,10 @@ mod view_transactions_view; -use std::{collections::HashMap, mem::transmute}; +use std::collections::{HashMap, VecDeque}; +use iced::font; use itertools::Itertools; -use schist_models::{Account, Bucket, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction}; use crate::{ gui::components::{navigation, AccountNavigationEntry, Navigation}, @@ -12,10 +13,12 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { - account_balances_by_transaction_id: HashMap, + accounts: Vec, + account_transfers: Vec, buckets: Vec, navigation: Navigation, - transactions_by_account_id: HashMap>, + transactions: Vec, + transaction_rows_by_account_id: HashMap>, } #[derive(Clone, Debug)] @@ -24,6 +27,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetAccountTransfers(Vec), SetBuckets(Vec), SetTransactions(Vec), } @@ -32,8 +36,134 @@ pub enum Action { None, } +#[derive(Debug, PartialEq, Eq)] +struct TransactionRowWithoutAggregations { + amount: i32, + bucket: String, + bucket_font_style: font::Style, + date: DateUtc, + payee: String, + payee_font_style: font::Style, +} + +impl TransactionRowWithoutAggregations { + fn from_transaction( + account_id: i32, + transaction: &Transaction, + buckets: &[Bucket], + ) -> Option { + if transaction.account_id == account_id { + let (bucket, bucket_font_style) = match transaction.bucket_id { + Some(bucket_id) => match buckets.iter().find(|b| b.id == bucket_id) { + Some(bucket) => (bucket.name.clone(), font::Style::Normal), + None => (format!("Bucket <{}>", bucket_id), font::Style::Italic), + }, + None => (String::from("None"), font::Style::Italic), + }; + Some(Self { + amount: transaction.amount, + bucket, + bucket_font_style, + date: transaction.date, + payee: transaction.counterparty.clone(), + payee_font_style: font::Style::Normal, + }) + } else { + None + } + } + + fn from_account_transfer( + account_id: i32, + account_transfer: &AccountTransfer, + accounts: &[Account], + ) -> Option { + if account_transfer.from_account_id == account_id { + let (payee, payee_font_style) = if let Some(account) = accounts + .iter() + .find(|a| a.id == account_transfer.to_account_id) + { + (account.name.clone(), font::Style::Normal) + } else { + (format!("<{}>", account_id), font::Style::Italic) + }; + Some(Self { + amount: -account_transfer.amount, + bucket: String::from("Account transfer"), + bucket_font_style: font::Style::Italic, + date: account_transfer.date, + payee, + payee_font_style, + }) + } else if account_transfer.to_account_id == account_id { + let (payee, payee_font_style) = if let Some(account) = accounts + .iter() + .find(|a| a.id == account_transfer.from_account_id) + { + (account.name.clone(), font::Style::Normal) + } else { + (format!("Account <{}>", account_id), font::Style::Italic) + }; + Some(Self { + amount: account_transfer.amount, + bucket: String::from("Account transfer"), + bucket_font_style: font::Style::Italic, + date: account_transfer.date, + payee, + payee_font_style, + }) + } else { + None + } + } + + fn with_aggregations(self, balance: i32) -> TransactionRow { + TransactionRow { + amount: self.amount, + balance, + bucket: self.bucket, + bucket_font_style: self.bucket_font_style, + date: self.date, + payee: self.payee, + payee_font_style: self.payee_font_style, + } + } + + fn compare_date_desc(&self, other: &Self) -> std::cmp::Ordering { + other.date.cmp(&self.date) + } +} + +impl std::cmp::PartialOrd for TransactionRowWithoutAggregations { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.compare_date_desc(other)) + } +} + +impl std::cmp::Ord for TransactionRowWithoutAggregations { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.compare_date_desc(other) + } +} + +#[derive(Clone, Debug, Hash)] +struct TransactionRow { + amount: i32, + balance: i32, + bucket: String, + bucket_font_style: font::Style, + date: DateUtc, + payee: String, + payee_font_style: font::Style, +} + impl TransactionsView { - pub fn new(accounts: &[Account], buckets: &[Bucket], transactions: &[Transaction]) -> Self { + pub fn new( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], + ) -> Self { let account_navigation_entries: Vec = accounts .iter() .cloned() @@ -43,30 +173,15 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - let mut transactions_view = Self { - account_balances_by_transaction_id: HashMap::with_capacity(transactions.len()), + let transaction_rows_by_account_id = + calculate_rows(accounts, account_transfers, buckets, transactions); + Self { + accounts: accounts.to_vec(), + account_transfers: account_transfers.to_vec(), buckets: buckets.to_vec(), navigation: navigation, - transactions_by_account_id: HashMap::with_capacity(accounts.len()), - }; - transactions_view.set_transactions(transactions); - transactions_view - } - - fn set_transactions(&mut self, transactions: &[Transaction]) { - self.transactions_by_account_id = transactions - .into_iter() - .cloned() - .into_group_map_by(|t| t.account_id); - self.account_balances_by_transaction_id = HashMap::new(); - for entry in self.transactions_by_account_id.iter_mut() { - entry.1.sort_by(|t1, t2| t2.date.cmp(&t1.date)); - let mut running_balance = 0; - for transaction in entry.1.iter().rev() { - running_balance += transaction.amount; - self.account_balances_by_transaction_id - .insert(transaction.id, running_balance); - } + transactions: transactions.to_vec(), + transaction_rows_by_account_id, } } } @@ -77,6 +192,13 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { Message::NavigationMessage(message) => self.update_navigation(message), Message::SetAccounts(accounts) => { + self.accounts = accounts.clone(); + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); let account_navigation_entries: Vec = accounts .into_iter() .map(AccountNavigationEntry::new) @@ -84,13 +206,36 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } + Message::SetAccountTransfers(account_transfers) => { + self.account_transfers = account_transfers; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); + Action::None + } + Message::SetBuckets(buckets) => { self.buckets = buckets; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); Action::None } Message::SetTransactions(transactions) => { - self.set_transactions(&transactions); + self.transactions = transactions; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); Action::None } @@ -114,3 +259,37 @@ impl TransactionsView { } } } + +fn calculate_rows( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], +) -> HashMap> { + HashMap::from_iter(accounts.iter().map(|account| { + let transaction_rows_without_aggs = transactions + .iter() + .filter_map(|t| { + TransactionRowWithoutAggregations::from_transaction(account.id, t, buckets) + }) + .chain(account_transfers.iter().filter_map(|at| { + TransactionRowWithoutAggregations::from_account_transfer(account.id, at, accounts) + })) + .sorted(); + + let mut running_balance: i32 = 0; + let mut transaction_rows = VecDeque::with_capacity(transaction_rows_without_aggs.len()); + for row in transaction_rows_without_aggs.rev() { + if running_balance.checked_add(row.amount).is_none() { + panic!( + "can't add {} + {}. row: {:?}", + running_balance, row.amount, row + ); + } + running_balance += row.amount; + transaction_rows.push_front(row.with_aggregations(running_balance)); + } + + (account.id, transaction_rows.into()) + })) +} diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 4f61493..1427830 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use iced::{ alignment, widget::{ @@ -11,11 +9,10 @@ use iced::{ }, Element, Length, }; -use schist_models::{Bucket, Transaction}; use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable}; -use super::{Message, TransactionsView}; +use super::{Message, TransactionRow, TransactionsView}; impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { @@ -28,7 +25,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Bucket").width(iced::Length::Fixed(128.0)), - |t: Transaction| view_bucket_cell(t, &self.buckets), + view_bucket_cell, ), column( Text::default("Payee").width(iced::Length::Fixed(256.0)), @@ -40,7 +37,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Balance").width(iced::Length::Fixed(96.0)), - |t: Transaction| view_balance_cell(t, &self.account_balances_by_transaction_id), + view_balance_cell, ), ]; @@ -49,7 +46,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { .active_option .clone() .map_or_else(Vec::default, |option| { - self.transactions_by_account_id + self.transaction_rows_by_account_id .get(&option.account.id) .cloned() .unwrap_or_else(Vec::default) @@ -71,54 +68,40 @@ impl<'a> Viewable<'a, Message> for TransactionsView { } } -fn view_date_cell(transaction: Transaction) -> Text { - Text::default(transaction.date.format("%e %b %Y").as_str()) +fn view_date_cell(row: TransactionRow) -> Text { + Text::default(row.date.format("%e %b %Y").as_str()) .width(iced::Length::Fixed(128.0)) .small() .clip() } -fn view_bucket_cell(t: Transaction, buckets: &[Bucket]) -> Text { - (t.bucket_id.map_or_else( - || Text::default("None").italic(), - |id| { - buckets.iter().find(|b| b.id == id).map_or_else( - || Text::default(format!("<{}>", id).as_str()).italic(), - |b| Text::default(&b.name), - ) - }, - )) - .width(iced::Length::Fixed(128.0)) - .small() - .clip() +fn view_bucket_cell(row: TransactionRow) -> Text { + Text::default(&row.bucket) + .style(row.bucket_font_style) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() } -fn view_payee_cell(t: Transaction) -> Text { - Text::default(&t.counterparty) +fn view_payee_cell(row: TransactionRow) -> Text { + Text::default(&row.payee) + .style(row.payee_font_style) .width(iced::Length::Fixed(256.0)) .small() .clip() } -fn view_quantity_cell(t: Transaction) -> Text { - Text::currency(t.amount) +fn view_quantity_cell(row: TransactionRow) -> Text { + Text::currency(row.amount) .width(iced::Length::Fixed(96.0)) .small() .clip() .align_right() } -fn view_balance_cell( - transaction: Transaction, - account_balances_by_transaction_id: &HashMap, -) -> Text { - (account_balances_by_transaction_id - .get(&transaction.id) - .map_or_else( - || Text::default("Error").danger(), - |&balance| Text::currency(balance), - )) - .width(iced::Length::Fixed(96.0)) - .small() - .clip() +fn view_balance_cell(row: TransactionRow) -> Text { + Text::currency(row.balance) + .width(iced::Length::Fixed(96.0)) + .small() + .clip() } 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 index 038d65f..6580758 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -4,7 +4,7 @@ use iced::{ widget::{row, Container}, Element, Length, }; -use schist_models::{Account, Bucket, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, Transaction}; use crate::{ gui::components::{ @@ -35,6 +35,7 @@ pub enum Message { TransactionsViewMessage(transactions_view::Message), SelectView(View), SetAccounts(Vec), + SetAccountTransfers(Vec), SetBuckets(Vec), SetTransactions(Vec), ViewNavigationMessage(navigation::Message), @@ -121,6 +122,14 @@ impl<'a> Component<'a, Message, Action> for MainScreen { Action::None } + Message::SetAccountTransfers(account_transfers) => { + self.transactions_view + .update(transactions_view::Message::SetAccountTransfers( + account_transfers, + )); + Action::None + } + Message::SetTransactions(transactions) => { self.transactions_view .update(transactions_view::Message::SetTransactions(transactions)); @@ -206,7 +215,7 @@ impl MainScreen { balances_view: BalancesView::new(Vec::new()), buckets: Vec::new(), buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), - transactions_view: TransactionsView::new(&[], &[], &[]), + transactions_view: TransactionsView::new(&[], &[], &[], &[]), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } -- cgit v1.2.3 From 1bf1819e9e28dec7a1983269377238258286b3c7 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 21:36:54 +0000 Subject: task-085: align balance right --- .../gui/components/transactions_view/view_transactions_view.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'schist_desktop_gui/src/gui') diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs index 1427830..268984b 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -32,11 +32,15 @@ impl<'a> Viewable<'a, Message> for TransactionsView { view_payee_cell, ), column( - Text::default("Quantity").width(iced::Length::Fixed(96.0)), + Text::default("Quantity") + .width(iced::Length::Fixed(96.0)) + .align_right(), view_quantity_cell, ), column( - Text::default("Balance").width(iced::Length::Fixed(96.0)), + Text::default("Balance") + .width(iced::Length::Fixed(96.0)) + .align_right(), view_balance_cell, ), ]; @@ -104,4 +108,5 @@ fn view_balance_cell(row: TransactionRow) -> Text { .width(iced::Length::Fixed(96.0)) .small() .clip() + .align_right() } -- cgit v1.2.3