summaryrefslogtreecommitdiff
path: root/schist_desktop_gui/src/gui/components
diff options
context:
space:
mode:
authorjoeac <me@joeac.net>2026-02-28 08:50:46 +0000
committerjoeac <me@joeac.net>2026-02-28 08:50:46 +0000
commit342dadaf91093c1ce9c3ad248a2952ed3cde4de6 (patch)
tree0546aa27e558c498cbc730a6f1854f11e9fa844b /schist_desktop_gui/src/gui/components
parent65596363d43995bf2002d4aa8405d84484f2542b (diff)
parentf3659bd8e9d5e20528aaa1e0e7c1b206fb0eb31c (diff)
Merge pull request 'task-085: view transactions in desktop GUI' (#5) from task-085 into main
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/5
Diffstat (limited to 'schist_desktop_gui/src/gui/components')
-rw-r--r--schist_desktop_gui/src/gui/components/account_navigation_entry.rs34
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/buckets_view.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/panel.rs4
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs132
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs337
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs112
7 files changed, 525 insertions, 98 deletions
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<AccountNavigationEntry>>
+{
+ 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/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/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 d171043..068e89a 100644
--- a/schist_desktop_gui/src/gui/components/text.rs
+++ b/schist_desktop_gui/src/gui/components/text.rs
@@ -1,17 +1,29 @@
use std::borrow::Borrow;
+use iced::{font, widget::container, Font};
+
use crate::{impl_focusable, style::*};
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
pub content: String,
+ align: Alignment,
colour: Option<Colour>,
+ font_style: font::Style,
+ overflow_strategy: OverflowStrategy,
size: Option<Size>,
strength: Option<Strength>,
+ width: iced::Length,
is_focused: bool,
}
#[derive(Clone, Debug, PartialEq)]
+enum Alignment {
+ Left,
+ Right,
+}
+
+#[derive(Clone, Debug, PartialEq)]
enum Colour {
Danger,
Primary,
@@ -19,6 +31,12 @@ enum Colour {
}
#[derive(Clone, Debug, PartialEq)]
+enum OverflowStrategy {
+ Grow,
+ Clip,
+}
+
+#[derive(Clone, Debug, PartialEq)]
enum Strength {
Base,
Weak,
@@ -26,6 +44,7 @@ enum Strength {
#[derive(Clone, Debug, PartialEq)]
enum Size {
+ VSmall,
Small,
Base,
}
@@ -33,28 +52,55 @@ enum Size {
impl Text {
pub fn default(content: &str) -> Self {
Self {
+ 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),
is_focused: false,
+ width: iced::Length::Shrink,
}
}
pub fn new(content: &str) -> Self {
Self {
+ align: Alignment::Left,
content: String::from(content),
colour: None,
+ font_style: font::Style::Normal,
+ overflow_strategy: OverflowStrategy::Grow,
size: None,
strength: None,
+ width: iced::Length::Shrink,
is_focused: false,
}
}
- pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> {
+ 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,
+ {
<Self as Into<iced::Element<'a, Message>>>::into(self)
}
+ pub fn v_small(&self) -> Self {
+ Self {
+ size: Some(Size::VSmall),
+ ..self.clone()
+ }
+ }
+
pub fn small(&self) -> Self {
Self {
size: Some(Size::Small),
@@ -86,15 +132,56 @@ 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()
+ }
+ }
+
+ pub fn clip(&self) -> Self {
+ Self {
+ overflow_strategy: OverflowStrategy::Clip,
+ ..self.clone()
+ }
+ }
+
+ pub fn italic(&self) -> Self {
+ Self {
+ font_style: font::Style::Italic,
+ ..self.clone()
+ }
+ }
+
+ pub fn style(&self, font_style: font::Style) -> Self {
+ Self {
+ font_style,
+ ..self.clone()
+ }
+ }
}
-impl<'a, Message> From<Text> for iced::Element<'a, Message> {
+impl<'a, Message> From<Text> 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)
}
@@ -103,17 +190,31 @@ 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<Text> + '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())
+ .font(Font {
+ style: text.borrow().font_style,
+ ..Default::default()
+ })
.size(match text.borrow().size {
- 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)
}
@@ -134,8 +235,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 1cb569c..a52c419 100644
--- a/schist_desktop_gui/src/gui/components/transactions_view.rs
+++ b/schist_desktop_gui/src/gui/components/transactions_view.rs
@@ -1,122 +1,295 @@
-use iced::{
- alignment,
- widget::{column, row, Container},
- Element, Length,
-};
-use schist_models::Account;
+mod view_transactions_view;
+
+use std::collections::{HashMap, VecDeque};
+
+use iced::font;
+use itertools::Itertools;
+use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction};
use crate::{
- gui::components::{navigation, Navigation, Text},
- style::SPACING_LG,
- traits::{Component, Viewable},
+ gui::components::{navigation, AccountNavigationEntry, Navigation},
+ traits::Component,
};
#[derive(Clone, Debug)]
pub struct TransactionsView {
accounts: Vec<Account>,
- greeting: String,
- navigation: Navigation<Text>,
+ account_transfers: Vec<AccountTransfer>,
+ buckets: Vec<Bucket>,
+ navigation: Navigation<AccountNavigationEntry>,
+ transactions: Vec<Transaction>,
+ transaction_rows_by_account_id: HashMap<i32, Vec<TransactionRow>>,
}
#[derive(Clone, Debug)]
pub enum Message {
- NavigationMessage(navigation::Message<Text>),
- SelectNextTransaction,
- SelectPrevTransaction,
+ NavigationMessage(navigation::Message<AccountNavigationEntry>),
+ SelectNextAccount,
+ SelectPrevAccount,
SetAccounts(Vec<Account>),
+ SetAccountTransfers(Vec<AccountTransfer>),
+ SetBuckets(Vec<Bucket>),
+ SetTransactions(Vec<Transaction>),
}
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,
+#[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<Self> {
+ 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<Self> {
+ 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<std::cmp::Ordering> {
+ Some(self.compare_date_desc(other))
+ }
}
-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()];
- 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 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],
+ account_transfers: &[AccountTransfer],
+ buckets: &[Bucket],
+ transactions: &[Transaction],
+ ) -> Self {
+ let account_navigation_entries: Vec<AccountNavigationEntry> = accounts
+ .iter()
+ .cloned()
+ .map(AccountNavigationEntry::new)
+ .collect();
+ let navigation = Navigation::new(
+ account_navigation_entries.first().cloned(),
+ account_navigation_entries,
+ );
+ 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: transactions.to_vec(),
+ transaction_rows_by_account_id,
+ }
}
}
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,
- };
- Action::None
- }
+ Message::NavigationMessage(message) => self.update_navigation(message),
+
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,
- }
+ self.transaction_rows_by_account_id = calculate_rows(
+ &self.accounts,
+ &self.account_transfers,
+ &self.buckets,
+ &self.transactions,
+ );
+ let account_navigation_entries: Vec<AccountNavigationEntry> = accounts
+ .into_iter()
+ .map(AccountNavigationEntry::new)
+ .collect();
+ self.update_navigation(navigation::Message::SetOptions(account_navigation_entries))
}
- 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,
- }
+
+ 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::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,
- }
+
+ Message::SetTransactions(transactions) => {
+ self.transactions = transactions;
+ self.transaction_rows_by_account_id = calculate_rows(
+ &self.accounts,
+ &self.account_transfers,
+ &self.buckets,
+ &self.transactions,
+ );
+ Action::None
}
+
+ Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext),
+
+ Message::SelectPrevAccount => self.update_navigation(navigation::Message::SelectPrev),
}
}
}
-fn make_greeting(account_name: Text) -> String {
- format!("Hello, {} account!", account_name.content)
+impl TransactionsView {
+ fn update_navigation(
+ &mut self,
+ message: navigation::Message<AccountNavigationEntry>,
+ ) -> Action {
+ match self.navigation.update(message) {
+ navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => {
+ Action::None
+ }
+ navigation::Action::None => Action::None,
+ }
+ }
}
-fn view_account_names(accounts: Vec<Account>) -> Vec<Text> {
- accounts
- .iter()
- .map(|acc| Text::new(acc.name.as_str()))
- .collect()
+fn calculate_rows(
+ accounts: &[Account],
+ account_transfers: &[AccountTransfer],
+ buckets: &[Bucket],
+ transactions: &[Transaction],
+) -> HashMap<i32, Vec<TransactionRow>> {
+ 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
new file mode 100644
index 0000000..268984b
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs
@@ -0,0 +1,112 @@
+use iced::{
+ alignment,
+ widget::{
+ row,
+ scrollable::{self, Scrollbar},
+ table,
+ table::column,
+ Container, Scrollable,
+ },
+ Element, Length,
+};
+
+use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable};
+
+use super::{Message, TransactionRow, TransactionsView};
+
+impl<'a> Viewable<'a, Message> for TransactionsView {
+ fn view(&'a self) -> Element<'a, Message> {
+ let navigation = self.navigation.view().map(Message::NavigationMessage);
+
+ let columns = [
+ column(
+ Text::default("Date").width(iced::Length::Fixed(128.0)),
+ view_date_cell,
+ ),
+ column(
+ Text::default("Bucket").width(iced::Length::Fixed(128.0)),
+ view_bucket_cell,
+ ),
+ column(
+ Text::default("Payee").width(iced::Length::Fixed(256.0)),
+ view_payee_cell,
+ ),
+ column(
+ Text::default("Quantity")
+ .width(iced::Length::Fixed(96.0))
+ .align_right(),
+ view_quantity_cell,
+ ),
+ column(
+ Text::default("Balance")
+ .width(iced::Length::Fixed(96.0))
+ .align_right(),
+ view_balance_cell,
+ ),
+ ];
+
+ let rows = self
+ .navigation
+ .active_option
+ .clone()
+ .map_or_else(Vec::default, |option| {
+ self.transaction_rows_by_account_id
+ .get(&option.account.id)
+ .cloned()
+ .unwrap_or_else(Vec::default)
+ });
+
+ 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)),
+ Container::new(table).width(Length::FillPortion(3)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(u32::from(SPACING_LG))
+ .into()
+ }
+}
+
+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(row: TransactionRow) -> Text {
+ Text::default(&row.bucket)
+ .style(row.bucket_font_style)
+ .width(iced::Length::Fixed(128.0))
+ .small()
+ .clip()
+}
+
+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(row: TransactionRow) -> Text {
+ Text::currency(row.amount)
+ .width(iced::Length::Fixed(96.0))
+ .small()
+ .clip()
+ .align_right()
+}
+
+fn view_balance_cell(row: TransactionRow) -> Text {
+ Text::currency(row.balance)
+ .width(iced::Length::Fixed(96.0))
+ .small()
+ .clip()
+ .align_right()
+}