diff options
| author | Joe Carstairs <me@joeac.net> | 2025-08-25 16:35:45 +0000 |
|---|---|---|
| committer | joeac <me@joeac.net> | 2025-08-25 16:35:45 +0000 |
| commit | fe85652e4d3e8906824baddaafd05d19105579b8 (patch) | |
| tree | 31b56e31cbf289a34ec1d6707becf3c60933b56f /schist_desktop_gui/src/gui | |
| parent | d082a30de6a0cbd1fbe2ab5a4316db0891004129 (diff) | |
task-072
Co-authored-by: Joe Carstairs <jcarstairs@scottlogic.com>
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/3
Co-authored-by: Joe Carstairs <me@joeac.net>
Co-committed-by: Joe Carstairs <me@joeac.net>
Diffstat (limited to 'schist_desktop_gui/src/gui')
14 files changed, 416 insertions, 49 deletions
diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index fc3c838..a242b0b 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -9,7 +9,7 @@ pub mod transactions_view; pub use balances_view::BalancesView; pub use bucket_name_and_balance::BucketNameAndBalance; pub use buckets_view::BucketsView; -pub use button::{active_button, inactive_button}; +pub use button::{active_button, inactive_button, panel_button}; pub use navigation::Navigation; pub use text::Text; pub use transactions_view::TransactionsView; diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs index e8a1e46..6a64508 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 @@ -1,6 +1,7 @@ use iced::widget::{column, row, Container}; use iced::{alignment, Element, Length}; +use crate::gui::components::Text; use crate::gui::components::{balances_view::Message, BalancesView}; use crate::style::SPACING_LG; use crate::traits::Viewable; @@ -10,7 +11,7 @@ pub fn view_balances_view<'a>(balances_view: &'a BalancesView) -> Element<'a, Me .navigation .view() .map(Message::NavigationMessage); - let main_content = column![iced::widget::text(balances_view.greeting.clone())]; + let main_content = column![Text::default(&balances_view.greeting).as_element()]; row![ Container::new(navigation).width(Length::FillPortion(1)), Container::new(main_content).width(Length::FillPortion(3)), diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs index 271e76c..18ddf0b 100644 --- a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs +++ b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs @@ -6,7 +6,7 @@ use crate::gui::components::Text; pub fn view_group_names(buckets: Vec<Bucket>) -> Vec<Text> { buckets .iter() - .map(|b| b.group.clone()) + .map(|b| b.group.as_str()) .unique() .map(Text::new) .collect() @@ -96,23 +96,23 @@ mod test { ]; let group_names = view_group_names(buckets); assert_eq!(17, group_names.len()); - assert_eq!(group_names[0], Text::new(String::from("hydrogen"))); - assert_eq!(group_names[1], Text::new(String::from("helium"))); - assert_eq!(group_names[2], Text::new(String::from("lithium"))); - assert_eq!(group_names[3], Text::new(String::from("beryllium"))); - assert_eq!(group_names[4], Text::new(String::from("boron"))); - assert_eq!(group_names[5], Text::new(String::from("carbon"))); - assert_eq!(group_names[6], Text::new(String::from("nitrogen"))); - assert_eq!(group_names[7], Text::new(String::from("oxygen"))); - assert_eq!(group_names[8], Text::new(String::from("flourine"))); - assert_eq!(group_names[9], Text::new(String::from("neon"))); - assert_eq!(group_names[10], Text::new(String::from("magnesium"))); - assert_eq!(group_names[11], Text::new(String::from("aluminium"))); - assert_eq!(group_names[12], Text::new(String::from("silicon"))); - assert_eq!(group_names[13], Text::new(String::from("phosphorus"))); - assert_eq!(group_names[14], Text::new(String::from("sulphur"))); - assert_eq!(group_names[15], Text::new(String::from("chlorine"))); - assert_eq!(group_names[16], Text::new(String::from("argon"))); + assert_eq!(group_names[0], Text::new("hydrogen")); + assert_eq!(group_names[1], Text::new("helium")); + assert_eq!(group_names[2], Text::new("lithium")); + assert_eq!(group_names[3], Text::new("beryllium")); + assert_eq!(group_names[4], Text::new("boron")); + assert_eq!(group_names[5], Text::new("carbon")); + assert_eq!(group_names[6], Text::new("nitrogen")); + assert_eq!(group_names[7], Text::new("oxygen")); + assert_eq!(group_names[8], Text::new("flourine")); + assert_eq!(group_names[9], Text::new("neon")); + assert_eq!(group_names[10], Text::new("magnesium")); + assert_eq!(group_names[11], Text::new("aluminium")); + assert_eq!(group_names[12], Text::new("silicon")); + assert_eq!(group_names[13], Text::new("phosphorus")); + assert_eq!(group_names[14], Text::new("sulphur")); + assert_eq!(group_names[15], Text::new("chlorine")); + assert_eq!(group_names[16], Text::new("argon")); } #[test] @@ -134,8 +134,8 @@ mod test { let group_names = view_group_names(buckets); assert_eq!( vec![ - Text::new(String::from("hydrogen")), - Text::new(String::from("helium")), + Text::new("hydrogen"), + Text::new("helium"), ], group_names ); diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs index db3fc2e..24a3952 100644 --- a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs +++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs @@ -1,10 +1,7 @@ -use iced::{ - widget::{row, text}, - Element, -}; +use iced::{widget::row, Element}; use schist_models::Bucket; -use crate::gui::components::navigation; +use crate::gui::components::{navigation, Text}; #[derive(Clone, Debug, PartialEq)] pub struct BucketNameAndBalance { @@ -20,8 +17,8 @@ impl BucketNameAndBalance { impl<'a> Into<Element<'a, navigation::Message<BucketNameAndBalance>>> for BucketNameAndBalance { fn into(self) -> Element<'a, navigation::Message<BucketNameAndBalance>> { row![ - text(self.bucket.name.clone()), - text(self.bucket.balance.unwrap_or(0)) + Text::new(&self.bucket.name).as_element(), + Text::new(&self.bucket.balance.unwrap_or(0).to_string()).as_element(), ] .into() } diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs index bb4dbb4..e513a35 100644 --- a/schist_desktop_gui/src/gui/components/buckets_view.rs +++ b/schist_desktop_gui/src/gui/components/buckets_view.rs @@ -6,7 +6,7 @@ use iced::{ use schist_models::Bucket; use crate::{ - gui::components::{navigation, BucketNameAndBalance, Navigation}, + gui::components::{navigation, BucketNameAndBalance, Navigation, Text}, style::SPACING_LG, traits::{Component, Viewable}, }; @@ -53,7 +53,7 @@ impl<'a> Viewable<'a, Message> for BucketsView { Message: 'a, { let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![iced::widget::text(self.greeting.clone())]; + 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)), diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs index bc9e5f9..e071f33 100644 --- a/schist_desktop_gui/src/gui/components/button.rs +++ b/schist_desktop_gui/src/gui/components/button.rs @@ -1,5 +1,7 @@ use iced::{widget::Button, Element}; +use crate::style::*; + pub fn active_button<'a, Content: Into<Element<'a, Message>>, Message>( content: Content, ) -> Button<'a, Message> { @@ -22,3 +24,22 @@ pub fn inactive_button<'a, Content: Into<Element<'a, Message>>, Message>( .on_press(on_press) .width(iced::Length::Fill) } + +pub fn panel_button<'a, Content: Into<Element<'a, Message>>, Message>( + content: Content, + on_press: Message, +) -> Button<'a, Message> { + iced::widget::button(content) + .on_press(on_press) + .style(|theme: &iced::Theme, _status| iced::widget::button::Style { + background: None, + border: iced::Border { + color: theme.extended_palette().primary.base.text, + radius: iced::border::Radius::new(0), + width: 2.0, + }, + ..Default::default() + }) + .width(iced::Length::Fill) + .padding(iced::Padding::new(SPACING_MD.into())) +} diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index d334de3..90cd7bc 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -1,22 +1,100 @@ -use iced::Element; - -use crate::gui::components::navigation; +use crate::style::*; #[derive(Clone, Debug, PartialEq)] pub struct Text { pub content: String, + colour: Option<Colour>, + size: Option<Size>, + strength: Option<Strength>, +} + +#[derive(Clone, Debug, PartialEq)] +enum Colour { + Danger, + Primary, +} + +#[derive(Clone, Debug, PartialEq)] +enum Strength { + Base, + Weak, +} + +#[derive(Clone, Debug, PartialEq)] +enum Size { + Small, + Base, } impl Text { - pub fn new(group_name: String) -> Self { + pub fn default(content: &str) -> Self { + Self { + content: String::from(content), + colour: Some(Colour::Primary), + size: Some(Size::Base), + strength: Some(Strength::Base), + } + } + + pub fn new(content: &str) -> Self { + Self { + content: String::from(content), + colour: None, + size: None, + strength: None, + } + } + + pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> { + <Self as Into<iced::Element<'a, Message>>>::into(self) + } + + pub fn small(&self) -> Self { + Self { + size: Some(Size::Small), + ..self.clone() + } + } + + pub fn danger(&self) -> Self { + Self { + colour: Some(Colour::Danger), + ..self.clone() + } + } + + pub fn weak(&self) -> Self { Self { - content: group_name.clone(), + strength: Some(Strength::Weak), + ..self.clone() } } } -impl<'a> Into<Element<'a, navigation::Message<Text>>> for Text { - fn into(self) -> Element<'a, navigation::Message<Text>> { - iced::widget::text(self.content.clone()).into() +impl<'a, Message> Into<iced::Element<'a, Message>> for Text { + fn into(self) -> iced::Element<'a, Message> { + iced::widget::text(self.content.clone()) + .size(match self.size { + Some(Size::Small) => TEXT_SIZE_SM, + Some(Size::Base) | None => TEXT_SIZE_BASE, + }) + .style(move |theme: &iced::Theme| iced::widget::text::Style { + color: match (&self.colour, &self.strength) { + (Some(Colour::Danger), Some(Strength::Base) | None) => { + Some(theme.extended_palette().danger.base.text) + } + (Some(Colour::Danger), Some(Strength::Weak)) => { + Some(theme.extended_palette().danger.weak.text) + } + (Some(Colour::Primary), Some(Strength::Base) | None) => { + Some(theme.extended_palette().primary.base.text) + } + (Some(Colour::Primary), Some(Strength::Weak)) => { + Some(theme.extended_palette().primary.weak.text) + } + (None, _) => None, + }, + }) + .into() } } diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 88532a9..1956d38 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -45,7 +45,7 @@ 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![iced::widget::text(self.greeting.clone())]; + 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)), @@ -113,6 +113,6 @@ fn make_greeting(account_name: Text) -> String { fn view_account_names(accounts: Vec<Account>) -> Vec<Text> { accounts .iter() - .map(|acc| Text::new(acc.name.clone())) + .map(|acc| Text::new(acc.name.as_str())) .collect() } diff --git a/schist_desktop_gui/src/gui/screens.rs b/schist_desktop_gui/src/gui/screens.rs index 3ae97dd..abca2b6 100644 --- a/schist_desktop_gui/src/gui/screens.rs +++ b/schist_desktop_gui/src/gui/screens.rs @@ -1,9 +1,12 @@ +pub mod files_screen; pub mod main_screen; +pub use files_screen::FilesScreen; pub use main_screen::MainScreen; #[derive(Default)] pub enum Screen { #[default] - Main, + FilesScreen, + MainScreen, } diff --git a/schist_desktop_gui/src/gui/screens/files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen.rs new file mode 100644 index 0000000..68a9556 --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen.rs @@ -0,0 +1,36 @@ +mod update_files_screen; +mod view_files_screen; + +use std::{fmt::Debug, path::PathBuf}; + +use crate::DatabaseConnection; + +pub struct FilesScreen { + new_file_err: Option<(PathBuf, String)>, + file_paths: Vec<PathBuf>, + open_file_err: Option<(PathBuf, String)>, +} + +#[derive(Clone, Debug)] +pub enum Message { + ImportFromActualbudget, + NewFile, + OpenFile(std::path::PathBuf), + PickFile, +} + +#[derive(Debug)] +pub enum Action { + None, + OpenedFile(DatabaseConnection), +} + +impl<'a> FilesScreen { + pub fn new(file_paths: &'a [PathBuf]) -> Self { + Self { + file_paths: file_paths.to_vec(), + new_file_err: None, + open_file_err: None, + } + } +} diff --git a/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs new file mode 100644 index 0000000..9c45d6e --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs @@ -0,0 +1,125 @@ +use actualbudget_to_schist_transformer::schist_state::{export_schist_state, SchistState}; +use itertools::Itertools; + +use crate::{ + database_connection::DatabaseConnectionResult, + dialog::{import_from_actualbudget_dialog, new_file_dialog, pick_file_dialog}, + paths::get_schist_database_directory, + traits::Component, + DatabaseConnection, +}; + +use super::{Action, FilesScreen, Message}; + +impl<'a> Component<'a, Message, Action> for FilesScreen { + fn update(&mut self, message: Message) -> Action { + match message { + Message::NewFile => match self.new_file() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | None => Action::None, + }, + + Message::OpenFile(file_path) => self + .open_file(file_path) + .map_or(Action::None, Action::OpenedFile), + + Message::PickFile => match self.pick_file() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | None => Action::None, + }, + + Message::ImportFromActualbudget => match self.import_from_actualbudget() { + Some(Ok(file)) => Action::OpenedFile(file), + Some(Err(_)) | None => Action::None, + }, + } + } +} + +impl FilesScreen { + fn pick_file( + &mut self, + ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> { + pick_file_dialog(get_schist_database_directory().ok()).map(|db_connection_result| { + db_connection_result.ok().inspect_err(|(path, err)| { + self.open_file_err = Some((path.clone(), err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + }) + } + + fn open_file( + &mut self, + file_path: std::path::PathBuf, + ) -> Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)> { + DatabaseConnection::establish(file_path.clone()) + .ok() + .inspect_err(|(_path, err)| { + self.open_file_err = Some((file_path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + } + + fn new_file( + &mut self, + ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> { + new_file_dialog(get_schist_database_directory().ok()).map(|result| { + result.ok().inspect_err(|(path, err)| { + self.new_file_err = Some((path.clone(), err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + }) + }) + } + + fn import_from_actualbudget(&mut self) -> Option<anyhow::Result<DatabaseConnection>> { + match import_from_actualbudget_dialog() { + Some(Ok(state)) => self.new_file_with_state(&state), + Some(Err((path, err))) => Some({ + self.open_file_err = Some((path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + Err(err) + }), + None => None, + } + } + + fn new_file_with_state( + &mut self, + state: &SchistState, + ) -> Option<anyhow::Result<DatabaseConnection>> { + match new_file_dialog(get_schist_database_directory().ok()) + .map(DatabaseConnectionResult::ok) + { + Some(Ok(mut file)) => Some({ + export_schist_state(state, &mut file.connection).ok()?; + Ok(file) + }), + Some(Err((path, err))) => Some({ + self.new_file_err = Some((path, err.to_string())); + rfd::MessageDialog::new() + .set_title("Failed to import from Actualbudget") + .set_level(rfd::MessageLevel::Error) + .set_description(format!("{}", err.chain().join("\n | "))) + .show(); + Err(err) + }), + None => None, + } + } +} diff --git a/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs new file mode 100644 index 0000000..3cbd939 --- /dev/null +++ b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs @@ -0,0 +1,106 @@ +use std::path::PathBuf; + +use iced::widget::column; + +use crate::{ + gui::components::{panel_button, Text}, + style::*, + traits::Viewable, +}; + +use super::{FilesScreen, Message}; + +impl<'a> Viewable<'a, Message> for FilesScreen { + fn view(&'a self) -> iced::Element<'a, Message> { + let mut cols = Vec::new(); + if let Some(col) = self.view_new_file_err() { + cols.push(col); + }; + if let Some(col) = self.view_open_file_err() { + cols.push(col); + }; + cols.push(self.view_new_file_button()); + cols.push(self.view_open_file_button()); + cols.push(self.view_import_from_actualbudget_button()); + cols.append(&mut self.view_files()); + column(cols).padding(SPACING_LG).spacing(SPACING_MD).into() + } +} + +impl<'a> FilesScreen { + fn view_new_file_err(&'a self) -> Option<iced::Element<'a, Message>> { + self.new_file_err.as_ref().map(|(path, err)| { + let msg = format!("Failed to open new file {:#?}: {:#?}", path, err); + Text::new(&msg).danger().into() + }) + } + + fn view_open_file_err(&'a self) -> Option<iced::Element<'a, Message>> { + self.open_file_err + .as_ref() + .filter(|(file_path, _err)| !self.file_paths.iter().any(|fp| *fp == *file_path)) + .map(|(file_path, err)| { + let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err); + Text::new(&msg).danger().into() + }) + } + + fn view_files(&'a self) -> Vec<iced::Element<'a, Message>> { + self.file_paths + .iter() + .map(|f| { + view_file( + f, + self.open_file_err + .clone() + .filter(|(file, _err)| *file == *f) + .map(|(_file, err)| err), + ) + }) + .collect() + } + + fn view_new_file_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>(Text::default("New file").into(), Message::NewFile).into() + } + + fn view_open_file_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>(Text::default("Open file").into(), Message::PickFile).into() + } + + fn view_import_from_actualbudget_button(&'a self) -> iced::Element<'a, Message> { + panel_button::<Text, Message>( + Text::default("Import from Actualbudget").into(), + Message::ImportFromActualbudget, + ) + .into() + } +} + +fn view_file<'a>(file_path: &'a PathBuf, err: Option<String>) -> iced::Element<'a, Message> { + let mut cols: Vec<iced::Element<'a, Message>> = err + .map(|err| { + let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err); + vec![Text::new(&msg).danger().small().into()] + }) + .unwrap_or_else(Vec::new); + + cols.push( + Text::default(file_path.to_str().unwrap_or("")) + .small() + .weak() + .into(), + ); + + cols.push( + Text::default( + file_path + .file_name() + .map(|os_str| os_str.to_str().unwrap_or("[invalid Unicode]")) + .unwrap_or("[invalid filename]"), + ) + .into(), + ); + + panel_button(column(cols), Message::OpenFile(file_path.clone())).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 0af3c28..00f4ec3 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 @@ -147,12 +147,12 @@ impl<'a> Component<'a, Message, Action> for MainScreen { } impl MainScreen { - pub fn new(buckets: Vec<Bucket>, accounts: Vec<Account>) -> Self { + pub fn new() -> Self { Self { - balances_view: BalancesView::new(buckets.clone()), - buckets: buckets.clone(), - buckets_view: BucketsView::new(buckets, "Hello, buckets!"), - transactions_view: TransactionsView::new(accounts, "Hello, transactions!"), + 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!"), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } diff --git a/schist_desktop_gui/src/gui/screens/main_screen/view.rs b/schist_desktop_gui/src/gui/screens/main_screen/view.rs index eb9c88e..2be427b 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/view.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/view.rs @@ -1,4 +1,4 @@ -use iced::Element; +use iced::{widget::Text, Element}; use crate::gui::components::navigation; @@ -43,6 +43,6 @@ impl View { impl<'a> Into<Element<'a, navigation::Message<View>>> for View { fn into(self) -> Element<'a, navigation::Message<View>> { - iced::widget::text(self.name()).into() + Text::new(self.name()).into() } } |
