diff options
| author | Joe Carstairs <me@joeac.net> | 2026-02-07 11:52:08 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2026-02-07 12:02:27 +0000 |
| commit | 1664e741ad35ce52a01788c34029f0b2ea477e32 (patch) | |
| tree | dd6bd7c83a7e834c75815a786d67b5188cdd1f48 /schist_desktop_gui/src/gui/components | |
| parent | 179878d12ba10ac242da2044f4c6dfe4d4829e59 (diff) | |
task-001: can navigate file screen options with keyboard
Diffstat (limited to 'schist_desktop_gui/src/gui/components')
18 files changed, 351 insertions, 102 deletions
diff --git a/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs index 1dd5c6f..5661954 100644 --- a/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs +++ b/schist_desktop_gui/src/gui/components/balances_view/update_balances_view.rs @@ -38,7 +38,7 @@ fn select_prev_bucket(balances_view: &mut BalancesView) -> () { .navigation .update(navigation::Message::SelectPrev) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) | navigation::Action::SelectOption(option) => { balances_view.greeting = make_balances_view_greeting(Some(option)); } navigation::Action::None => {} @@ -50,7 +50,7 @@ fn select_next_bucket(balances_view: &mut BalancesView) -> () { .navigation .update(navigation::Message::SelectNext) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) | navigation::Action::SelectOption(option) => { balances_view.greeting = make_balances_view_greeting(Some(option)); } navigation::Action::None => {} @@ -63,7 +63,7 @@ fn set_buckets(balances_view: &mut BalancesView, buckets: Vec<schist_models::Buc .navigation .update(navigation::Message::SetOptions(view_group_names(buckets))) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) | navigation::Action::SelectOption(option) => { balances_view.greeting = make_balances_view_greeting(Some(option)); } navigation::Action::None => {} @@ -72,7 +72,8 @@ fn set_buckets(balances_view: &mut BalancesView, buckets: Vec<schist_models::Buc fn update_navigation(balances_view: &mut BalancesView, message: navigation::Message<Text>) -> () { match balances_view.navigation.update(message) { - navigation::Action::SelectOption(group_name) => { + navigation::Action::ActivateOption(group_name) + | navigation::Action::SelectOption(group_name) => { balances_view.greeting = make_balances_view_greeting(Some(group_name)); } navigation::Action::None => {} diff --git a/schist_desktop_gui/src/gui/components/bucket_navigation_entry.rs b/schist_desktop_gui/src/gui/components/bucket_navigation_entry.rs index be11178..69665a3 100644 --- a/schist_desktop_gui/src/gui/components/bucket_navigation_entry.rs +++ b/schist_desktop_gui/src/gui/components/bucket_navigation_entry.rs @@ -4,7 +4,7 @@ use iced::Element; use itertools::Either; use schist_models::Bucket; -use crate::gui::components::navigation; +use crate::{gui::components::navigation, traits::Focusable}; mod bucket_group_name; mod bucket_name_and_balance; @@ -66,3 +66,19 @@ impl<'a> From<&'a BucketNavigationEntry> bucket_navigation_entry.0.as_ref().either_into() } } + +impl Focusable for BucketNavigationEntry { + fn focus(&mut self) { + match self.0.as_mut() { + Either::Left(group) => group.focus(), + Either::Right(bucket) => bucket.focus(), + }; + } + + fn unfocus(&mut self) { + match self.0.as_mut() { + Either::Left(group) => group.unfocus(), + Either::Right(bucket) => bucket.unfocus(), + }; + } +} diff --git a/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_group_name.rs b/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_group_name.rs index cbc9521..98902ee 100644 --- a/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_group_name.rs +++ b/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_group_name.rs @@ -3,12 +3,16 @@ use iced::{ Element, Length, }; -use crate::gui::components::{navigation, BucketNavigationEntry}; +use crate::{ + gui::components::{navigation, BucketNavigationEntry}, + impl_focusable, +}; #[derive(Clone, Debug, PartialEq)] pub struct BucketGroupName { pub group: String, pub is_open: bool, + pub is_focused: bool, } impl BucketGroupName { @@ -16,6 +20,7 @@ impl BucketGroupName { Self { group: group.to_string(), is_open, + is_focused: false, } } } @@ -34,3 +39,5 @@ impl<'a> From<&'a BucketGroupName> for Element<'a, navigation::Message<BucketNav row![text(text_content).width(Length::Fill),].into() } } + +impl_focusable!(BucketGroupName); diff --git a/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_name_and_balance.rs index 7abca57..874540f 100644 --- a/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_name_and_balance.rs +++ b/schist_desktop_gui/src/gui/components/bucket_navigation_entry/bucket_name_and_balance.rs @@ -4,17 +4,22 @@ use iced::{ }; use schist_models::Bucket; -use crate::gui::components::{navigation, BucketNavigationEntry}; +use crate::{ + gui::components::{navigation, BucketNavigationEntry}, + impl_focusable, +}; #[derive(Clone, Debug, PartialEq)] pub struct BucketNameAndBalance { pub bucket: Bucket, + is_focused: bool, } impl BucketNameAndBalance { pub fn new(bucket: &Bucket) -> Self { Self { bucket: bucket.clone(), + is_focused: false, } } } @@ -40,3 +45,5 @@ impl<'a> From<&'a BucketNameAndBalance> .into() } } + +impl_focusable!(BucketNameAndBalance); diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs index 2b3258a..7f5b218 100644 --- a/schist_desktop_gui/src/gui/components/buckets_view.rs +++ b/schist_desktop_gui/src/gui/components/buckets_view.rs @@ -63,6 +63,43 @@ impl BucketsView { fn update_greeting(&mut self, active_entry: &str) { self.greeting = format!("Hello, {}!", active_entry); } + + fn expand_group(&mut self, group: String, buckets: Vec<Bucket>) -> Action { + match self.navigation.insert_after_first( + |bucket_navigation_entry| { + bucket_navigation_entry.is_group_and(|bucket_group_name| { + !bucket_group_name.is_open && bucket_group_name.group.eq(group.as_str()) + }) + }, + &buckets + .iter() + .map(BucketNavigationEntry::from_bucket) + .collect(), + ) { + InsertOptionResult::InsertedOptions => { + self.navigation.replace_first( + |bucket_navigation_entry| { + bucket_navigation_entry.is_group_and(|g| g.group.eq(group.as_str())) + }, + &vec![BucketNavigationEntry::from_group_open(&group)], + ); + Action::None + } + InsertOptionResult::NoSuchOption => Action::None, + } + } + + fn collapse_group(&mut self, group: String) -> Action { + self.navigation + .remove_options_where(|bucket_navigation_entry| { + bucket_navigation_entry.is_bucket_and(|bucket| bucket.group.eq(&group)) + }); + self.navigation.replace_first( + |bucket_navigation_entry| bucket_navigation_entry.is_group_and(|g| g.group.eq(&group)), + &vec![BucketNavigationEntry::from_group_closed(&group)], + ); + Action::None + } } impl<'a> Viewable<'a, Message> for BucketsView { @@ -97,7 +134,8 @@ impl<'a> Component<'a, Message, Action> for BucketsView { .navigation .update(navigation::Message::SetOptions(groups)) { - navigation::Action::SelectOption(bucket_navigation_entry) => { + navigation::Action::ActivateOption(bucket_navigation_entry) + | navigation::Action::SelectOption(bucket_navigation_entry) => { self.update_greeting(&bucket_navigation_entry.to_string()); Action::None } @@ -106,7 +144,8 @@ impl<'a> Component<'a, Message, Action> for BucketsView { } Message::NavigationMessage(message) => match self.navigation.update(message) { - navigation::Action::SelectOption(bucket_navigation_entry) => { + navigation::Action::ActivateOption(bucket_navigation_entry) + | navigation::Action::SelectOption(bucket_navigation_entry) => { self.update_greeting(&bucket_navigation_entry.to_string()); Action::None } @@ -114,7 +153,8 @@ impl<'a> Component<'a, Message, Action> for BucketsView { }, Message::SelectNext => match self.navigation.update(navigation::Message::SelectNext) { - navigation::Action::SelectOption(bucket_navigation_entry) => { + navigation::Action::ActivateOption(bucket_navigation_entry) + | navigation::Action::SelectOption(bucket_navigation_entry) => { self.update_greeting(&bucket_navigation_entry.to_string()); Action::None } @@ -122,51 +162,17 @@ impl<'a> Component<'a, Message, Action> for BucketsView { }, Message::SelectPrev => match self.navigation.update(navigation::Message::SelectPrev) { - navigation::Action::SelectOption(bucket_navigation_entry) => { + navigation::Action::ActivateOption(bucket_navigation_entry) + | navigation::Action::SelectOption(bucket_navigation_entry) => { self.update_greeting(&bucket_navigation_entry.to_string()); Action::None } navigation::Action::None => Action::None, }, - Message::ExpandGroup(group, buckets) => { - match self.navigation.insert_after_first( - |bucket_navigation_entry| { - bucket_navigation_entry.is_group_and(|bucket_group_name| { - !bucket_group_name.is_open && bucket_group_name.group.eq(group.as_str()) - }) - }, - &buckets - .iter() - .map(BucketNavigationEntry::from_bucket) - .collect(), - ) { - InsertOptionResult::InsertedOptions => { - self.navigation.replace_first( - |bucket_navigation_entry| { - bucket_navigation_entry.is_group_and(|g| g.group.eq(group.as_str())) - }, - &vec![BucketNavigationEntry::from_group_open(&group)], - ); - Action::None - } - InsertOptionResult::NoSuchOption => Action::None, - } - } + Message::ExpandGroup(group, buckets) => self.expand_group(group, buckets), - Message::CollapseGroup(group) => { - self.navigation - .remove_options_where(|bucket_navigation_entry| { - bucket_navigation_entry.is_bucket_and(|bucket| bucket.group.eq(&group)) - }); - self.navigation.replace_first( - |bucket_navigation_entry| { - bucket_navigation_entry.is_group_and(|g| g.group.eq(&group)) - }, - &vec![BucketNavigationEntry::from_group_closed(&group)], - ); - Action::None - } + Message::CollapseGroup(group) => self.collapse_group(group), } } } diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs index 6882273..8a33e39 100644 --- a/schist_desktop_gui/src/gui/components/button.rs +++ b/schist_desktop_gui/src/gui/components/button.rs @@ -1,29 +1,58 @@ -use iced::{widget::Button, Element}; +use iced::{widget::Button, Element, Theme}; use crate::style::*; -pub fn active_button<'a, Content, Message>(content: &'a Content) -> Button<'a, Message> +pub fn button<'a, Content, Message>( + content: &'a Content, + message: Message, + focus: bool, +) -> Button<'a, Message> where Element<'a, Message>: From<&'a Content>, { iced::widget::button(content) - .style(|theme: &iced::Theme, _status| iced::widget::button::Style { - background: Some(iced::Background::Color( - theme.extended_palette().primary.base.text, - )), - text_color: theme.extended_palette().primary.base.color, - ..Default::default() + .on_press(message) + .style(move |theme, status| match (status, focus) { + (iced::widget::button::Status::Hovered, _) => hovered_button_style(theme), + (iced::widget::button::Status::Pressed, _) | (_, true) => focused_button_style(theme), + (iced::widget::button::Status::Active, false) => base_button_style(theme), + (iced::widget::button::Status::Disabled, false) => todo!(), }) .width(iced::Length::Fill) } -pub fn inactive_button<'a, Content: Into<Element<'a, Message>>, Message>( - content: Content, - on_press: Message, -) -> Button<'a, Message> { - iced::widget::button(content) - .on_press(on_press) - .width(iced::Length::Fill) +fn hovered_button_style(theme: &Theme) -> iced::widget::button::Style { + iced::widget::button::Style { + background: Some(iced::Background::Color( + theme.extended_palette().background.weak.color, + )), + text_color: theme.extended_palette().primary.base.text, + ..base_button_style(theme) + } +} + +fn focused_button_style(theme: &Theme) -> iced::widget::button::Style { + iced::widget::button::Style { + background: Some(iced::Background::Color( + theme.extended_palette().primary.base.text, + )), + text_color: theme.extended_palette().primary.base.color, + ..base_button_style(theme) + } +} + +fn base_button_style(theme: &Theme) -> iced::widget::button::Style { + iced::widget::button::Style { + background: Some(iced::Background::Color( + theme.extended_palette().primary.base.color, + )), + border: iced::Border { + color: theme.extended_palette().primary.base.text, + ..Default::default() + }, + text_color: theme.extended_palette().primary.base.text, + ..Default::default() + } } pub fn panel_button<'a, Content: Into<Element<'a, Message>>, Message>( diff --git a/schist_desktop_gui/src/gui/components/navigation.rs b/schist_desktop_gui/src/gui/components/navigation.rs index 6fe100d..1780f1a 100644 --- a/schist_desktop_gui/src/gui/components/navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation.rs @@ -1,4 +1,5 @@ mod navigate_navigation; +mod navigation_find; mod navigation_insert_options; mod navigation_remove_options; mod navigation_replace_options; @@ -22,8 +23,9 @@ pub struct Navigation<TOption> { options_after_active: Vec<TOption>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Message<TOption> { + ActivateOption(TOption), SelectNext, SelectOption(TOption), SelectPrev, @@ -31,6 +33,7 @@ pub enum Message<TOption> { } pub enum Action<TOption> { + ActivateOption(TOption), SelectOption(TOption), None, } diff --git a/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs b/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs index ae5799f..6875305 100644 --- a/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs +++ b/schist_desktop_gui/src/gui/components/navigation/dummy_navigation_option.rs @@ -2,17 +2,22 @@ use std::fmt::Display; use iced::Element; -use crate::gui::components::{navigation, Text}; +use crate::{ + gui::components::{navigation, Text}, + impl_focusable, +}; #[derive(Clone, Debug, PartialEq)] pub struct DummyNavigationOption { pub name: String, + is_focused: bool, } impl DummyNavigationOption { pub fn new(name: &str) -> Self { Self { name: String::from(name), + is_focused: false, } } } @@ -30,3 +35,5 @@ impl Display for DummyNavigationOption { f.write_str(&self.name) } } + +impl_focusable!(DummyNavigationOption); diff --git a/schist_desktop_gui/src/gui/components/navigation/navigate_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/navigate_navigation.rs index f399cd0..72bc503 100644 --- a/schist_desktop_gui/src/gui/components/navigation/navigate_navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation/navigate_navigation.rs @@ -1,24 +1,39 @@ use iced::Element; -use crate::traits::{Navigable, NavigationResult}; +use crate::traits::{Focusable, Navigable, NavigationResult}; use super::{Message, Navigation}; impl<'a, TOption> Navigable<TOption> for Navigation<TOption> where - TOption: Clone + PartialEq + 'a, + TOption: Clone + Focusable + PartialEq + 'a, Element<'a, Message<TOption>>: From<&'a TOption>, { fn move_next(&mut self) -> NavigationResult<TOption> { - if let Some(next) = self.options_after_active.clone().first() { + if let Some(next) = self.options_after_active.clone().first_mut() { self.options_before_active = vec![ self.options_before_active.clone(), - self.active_option.clone().map_or(vec![], |o| vec![o]), + self.active_option.clone().map_or_else(Vec::new, |mut o| { + o.unfocus(); + vec![o] + }), ] .concat(); + next.focus(); self.active_option = Some(next.clone()); - self.options_after_active = self.options_after_active.split_at(1).1.to_vec(); + self.options_after_active.remove(0); NavigationResult::Moved(next.clone()) + } else if let Some(first) = self + .options_before_active + .clone() + .first_mut() + .filter(|_| self.active_option.is_none()) + { + self.options_after_active = self.options_before_active.split_at(1).1.to_vec(); + self.options_before_active = Vec::new(); + first.focus(); + self.active_option = Some(first.clone()); + NavigationResult::Moved(first.clone()) } else { NavigationResult::LastOption } @@ -27,11 +42,15 @@ where fn move_prev(&mut self) -> NavigationResult<TOption> { if let Some(prev) = self.options_before_active.clone().last() { self.options_after_active = vec![ - self.active_option.clone().map_or(vec![], |o| vec![o]), + self.active_option.clone().map_or(vec![], |mut o| { + o.unfocus(); + vec![o] + }), self.options_after_active.clone(), ] .concat(); self.active_option = Some(prev.clone()); + self.active_option.iter_mut().for_each(TOption::focus); self.options_before_active = self .options_before_active .split_last() diff --git a/schist_desktop_gui/src/gui/components/navigation/navigation_find.rs b/schist_desktop_gui/src/gui/components/navigation/navigation_find.rs new file mode 100644 index 0000000..79c8ab0 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/navigation/navigation_find.rs @@ -0,0 +1,16 @@ +impl<'a, TOption> super::Navigation<TOption> +where + TOption: Clone + PartialEq + 'a, +{ + pub fn find<P>(&self, pred: P) -> Option<TOption> + where + P: Fn(&TOption) -> bool, + { + self.options_before_active + .iter() + .find(|o| pred(o)) + .cloned() + .or_else(|| self.active_option.clone().filter(|o| pred(o))) + .or_else(|| self.options_after_active.iter().find(|o| pred(o)).cloned()) + } +} diff --git a/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs b/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs index e24059d..496250d 100644 --- a/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs +++ b/schist_desktop_gui/src/gui/components/navigation/navigation_replace_options.rs @@ -1,6 +1,8 @@ use iced::Element; use itertools::Itertools; +use crate::traits::Focusable; + use super::{Message, Navigation}; #[derive(PartialEq)] @@ -11,7 +13,7 @@ pub enum ReplaceOptionResult { impl<'a, TOption> Navigation<TOption> where - TOption: Clone + PartialEq + 'a, + TOption: Clone + Focusable + PartialEq + 'a, Element<'a, Message<TOption>>: From<&'a TOption>, { pub fn replace_first<P>(&'a mut self, pred: P, new: &'a Vec<TOption>) -> ReplaceOptionResult @@ -92,6 +94,7 @@ where } self.active_option = new.first().cloned(); + self.active_option.iter_mut().for_each(|o| o.focus()); self.options_after_active .splice(0..0, new.split_at(1).1.to_vec()); ReplaceOptionResult::ReplacedOption diff --git a/schist_desktop_gui/src/gui/components/navigation/navigation_select_option.rs b/schist_desktop_gui/src/gui/components/navigation/navigation_select_option.rs index 4192059..5aa96cd 100644 --- a/schist_desktop_gui/src/gui/components/navigation/navigation_select_option.rs +++ b/schist_desktop_gui/src/gui/components/navigation/navigation_select_option.rs @@ -1,5 +1,7 @@ use iced::Element; +use crate::traits::Focusable; + use super::{Message, Navigation}; pub enum SelectOptionResult<TOption> { @@ -10,7 +12,7 @@ pub enum SelectOptionResult<TOption> { impl<'a, TOption> Navigation<TOption> where - TOption: Clone + PartialEq + 'a, + TOption: Clone + Focusable + PartialEq + 'a, Element<'a, Message<TOption>>: From<&'a TOption>, { pub fn select_option(&mut self, option: TOption) -> SelectOptionResult<TOption> @@ -30,15 +32,17 @@ where { let split = self.options_before_active.split_at(index); self.options_after_active = vec![ - self.active_option - .clone() - .map_or(vec![], |active_option| vec![active_option]), + self.active_option.as_mut().map_or(vec![], |active_option| { + active_option.unfocus(); + vec![active_option.clone()] + }), split.1.split_at(1).1.to_vec(), self.options_after_active.clone(), ] .concat(); self.options_before_active = split.0.to_vec(); self.active_option = Some(option.clone()); + self.active_option.iter_mut().for_each(TOption::focus); SelectOptionResult::SelectedExistingOption(option) } else if let Some(index) = self .options_after_active @@ -49,21 +53,25 @@ where self.options_before_active = vec![ self.options_before_active.clone(), split.0.to_vec(), - self.active_option - .clone() - .map_or(vec![], |active_option| vec![active_option]), + self.active_option.as_mut().map_or(vec![], |active_option| { + active_option.unfocus(); + vec![active_option.clone()] + }), ] .concat(); self.active_option = Some(option.clone()); + self.active_option.iter_mut().for_each(TOption::focus); self.options_after_active = split.1.split_at(1).1.to_vec(); SelectOptionResult::SelectedExistingOption(option) } else { - if let Some(active_option) = &self.active_option { + if let Some(active_option) = &mut self.active_option { + active_option.unfocus(); self.options_before_active.push(active_option.clone()); } self.options_before_active .append(&mut self.options_after_active); self.active_option = Some(option.clone()); + self.active_option.iter_mut().for_each(TOption::focus); self.options_after_active = vec![]; SelectOptionResult::SelectedNewOption(option) } diff --git a/schist_desktop_gui/src/gui/components/navigation/navigation_set_options.rs b/schist_desktop_gui/src/gui/components/navigation/navigation_set_options.rs index c2c9952..3eb455c 100644 --- a/schist_desktop_gui/src/gui/components/navigation/navigation_set_options.rs +++ b/schist_desktop_gui/src/gui/components/navigation/navigation_set_options.rs @@ -1,5 +1,7 @@ use iced::Element; +use crate::traits::Focusable; + use super::{Message, Navigation}; pub enum SetOptionsResult<TOption> { @@ -9,10 +11,12 @@ pub enum SetOptionsResult<TOption> { impl<'a, TOption> Navigation<TOption> where - TOption: Clone + PartialEq + 'a, + TOption: Clone + Focusable + PartialEq + 'a, Element<'a, Message<TOption>>: From<&'a TOption>, { - pub fn set_options(&mut self, options: Vec<TOption>) -> SetOptionsResult<TOption> { + pub fn set_options(&mut self, options: &mut Vec<TOption>) -> SetOptionsResult<TOption> { + options.iter_mut().for_each(TOption::unfocus); + let active_option_index = self.active_option .clone() @@ -31,6 +35,7 @@ where .split_last() .map_or_else(Vec::new, |(_last, rest)| rest.to_vec()); self.active_option = options.last().cloned(); + self.active_option.iter_mut().for_each(TOption::focus); options.last().cloned().map_or( SetOptionsResult::SetOptionsAndKeptSelection, SetOptionsResult::SetOptionsAndSetSelectedOption, diff --git a/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs index 1d5a734..7bcef98 100644 --- a/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs +++ b/schist_desktop_gui/src/gui/components/navigation/update_navigation.rs @@ -4,14 +4,14 @@ use crate::{ gui::components::navigation::{ navigation_select_option::SelectOptionResult, navigation_set_options::SetOptionsResult, }, - traits::{Component, Navigable, NavigationResult}, + traits::{Component, Focusable, Navigable, NavigationResult}, }; use super::{Action, Message, Navigation}; impl<'a, TOption> Component<'a, Message<TOption>, Action<TOption>> for Navigation<TOption> where - TOption: Clone + PartialEq + 'a, + TOption: Clone + Focusable + PartialEq + 'a, Element<'a, Message<TOption>>: From<&'a TOption>, { fn update(&mut self, message: Message<TOption>) -> Action<TOption> { @@ -21,23 +21,22 @@ where | SelectOptionResult::SelectedNewOption(option) => Action::SelectOption(option), SelectOptionResult::AlreadySelected => Action::None, }, - - Message::SetOptions(options) => match self.set_options(options) { + Message::SetOptions(mut options) => match self.set_options(&mut options) { SetOptionsResult::SetOptionsAndKeptSelection => Action::None, SetOptionsResult::SetOptionsAndSetSelectedOption(option) => { Action::SelectOption(option) } }, - Message::SelectNext => match self.move_next() { NavigationResult::Moved(option) => Action::SelectOption(option), NavigationResult::LastOption => Action::None, }, - Message::SelectPrev => match self.move_prev() { NavigationResult::Moved(option) => Action::SelectOption(option), NavigationResult::LastOption => Action::None, }, + + Message::ActivateOption(option) => Action::ActivateOption(option), } } } diff --git a/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs index d65328c..80d6d59 100644 --- a/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs +++ b/schist_desktop_gui/src/gui/components/navigation/view_navigation_button.rs @@ -1,6 +1,6 @@ use iced::Element; -use crate::gui::components::{active_button, inactive_button}; +use crate::gui::components::button; use super::{Message, Navigation}; @@ -13,15 +13,10 @@ where &'a self, option: &'a TOption, ) -> Element<'a, Message<TOption>> { - if self + let focus = self .active_option .as_ref() - .is_some_and(|o| TOption::eq(o, option)) - { - active_button(option) - } else { - inactive_button(option, Message::SelectOption(option.clone())) - } - .into() + .is_some_and(|o| TOption::eq(o, option)); + button(option, Message::ActivateOption(option.clone()), focus).into() } } diff --git a/schist_desktop_gui/src/gui/components/panel.rs b/schist_desktop_gui/src/gui/components/panel.rs new file mode 100644 index 0000000..364659b --- /dev/null +++ b/schist_desktop_gui/src/gui/components/panel.rs @@ -0,0 +1,101 @@ +use iced::{ + widget::{button::Status, column}, + Theme, +}; + +use crate::{ + gui::components::{navigation, panel_button, Text}, + traits::Focusable, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct Panel<Id> { + pub err: Option<String>, + pub id: Id, + pub small: Option<String>, + pub name: String, + pub is_focused: bool, +} + +impl<Id> Panel<Id> { + pub fn new(id: Id, text: &str, small: Option<String>, err: Option<String>) -> Self { + Self { + err: err.clone(), + id, + small: small.clone(), + name: text.to_string(), + is_focused: false, + } + } +} + +impl<'a, Id> From<&'a Panel<Id>> for iced::Element<'a, navigation::Message<Panel<Id>>> +where + Id: Clone, +{ + fn from(value: &'a Panel<Id>) -> Self { + let mut cols: Vec<iced::Element<'a, navigation::Message<Panel<Id>>>> = Vec::new(); + if let Some(err) = &value.err { + cols.push( + Text::default(err) + .highlight_maybe(value.is_focused) + .small() + .danger() + .into(), + ); + } + if let Some(small) = &value.small { + cols.push( + Text::default(small) + .highlight_maybe(value.is_focused) + .small() + .weak() + .into(), + ); + } + cols.push( + Text::default(&value.name) + .highlight_maybe(value.is_focused) + .into(), + ); + panel_button( + column(cols), + navigation::Message::ActivateOption(value.clone()), + ) + .style(|theme, status| match (status, value.is_focused) { + (Status::Hovered, false) => hovered_style(theme), + (_, true) | (Status::Pressed, _) => focused_style(theme), + (Status::Active, false) | (Status::Disabled, false) => Default::default(), + }) + .into() + } +} + +fn hovered_style(theme: &Theme) -> iced::widget::button::Style { + iced::widget::button::Style { + background: Some(iced::Background::Color( + theme.extended_palette().background.weak.color, + )), + text_color: theme.extended_palette().primary.base.text, + ..Default::default() + } +} + +fn focused_style(theme: &Theme) -> iced::widget::button::Style { + iced::widget::button::Style { + background: Some(iced::Background::Color( + theme.extended_palette().primary.base.text, + )), + text_color: theme.extended_palette().primary.base.color, + ..Default::default() + } +} + +impl<Id> Focusable for Panel<Id> { + fn focus(&mut self) { + self.is_focused = true; + } + fn unfocus(&mut self) { + self.is_focused = false; + } +} diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index a3093ac..d171043 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 crate::style::*; +use crate::{impl_focusable, style::*}; #[derive(Clone, Debug, PartialEq)] pub struct Text { @@ -8,12 +8,14 @@ pub struct Text { colour: Option<Colour>, size: Option<Size>, strength: Option<Strength>, + is_focused: bool, } #[derive(Clone, Debug, PartialEq)] enum Colour { Danger, Primary, + Highlight, } #[derive(Clone, Debug, PartialEq)] @@ -35,6 +37,7 @@ impl Text { colour: Some(Colour::Primary), size: Some(Size::Base), strength: Some(Strength::Base), + is_focused: false, } } @@ -44,6 +47,7 @@ impl Text { colour: None, size: None, strength: None, + is_focused: false, } } @@ -71,6 +75,17 @@ impl Text { ..self.clone() } } + + pub fn highlight_maybe(&self, do_highlight: bool) -> Self { + if do_highlight { + Self { + colour: Some(Colour::Highlight), + ..self.clone() + } + } else { + self.clone() + } + } } impl<'a, Message> From<Text> for iced::Element<'a, Message> { @@ -111,8 +126,16 @@ where (Some(Colour::Primary), Some(Strength::Weak)) => { Some(theme.extended_palette().primary.weak.text) } + (Some(Colour::Highlight), Some(Strength::Base) | None) => { + Some(theme.extended_palette().primary.base.color) + } + (Some(Colour::Highlight), Some(Strength::Weak)) => { + Some(theme.extended_palette().primary.weak.color) + } (None, _) => None, }, }) .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 1956d38..1cb569c 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -62,7 +62,8 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { match message { Message::NavigationMessage(message) => { match self.navigation.update(message) { - navigation::Action::SelectOption(group_name) => { + navigation::Action::ActivateOption(group_name) + | navigation::Action::SelectOption(group_name) => { self.greeting = make_greeting(group_name); Action::None } @@ -77,7 +78,8 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { .update(navigation::Message::SetOptions(view_account_names( accounts, ))) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) + | navigation::Action::SelectOption(option) => { self.greeting = make_greeting(option); Action::None } @@ -86,7 +88,8 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { } Message::SelectNextTransaction => { match self.navigation.update(navigation::Message::SelectNext) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) + | navigation::Action::SelectOption(option) => { self.greeting = make_greeting(option); Action::None } @@ -95,7 +98,8 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { } Message::SelectPrevTransaction => { match self.navigation.update(navigation::Message::SelectPrev) { - navigation::Action::SelectOption(option) => { + navigation::Action::ActivateOption(option) + | navigation::Action::SelectOption(option) => { self.greeting = make_greeting(option); Action::None } |
