diff --git a/gui/src/app/view/pbst.rs b/gui/src/app/view/pbst.rs deleted file mode 100644 index 32850458..00000000 --- a/gui/src/app/view/pbst.rs +++ /dev/null @@ -1,836 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use iced::{ - widget::{scrollable, tooltip, Space}, - Alignment, Length, -}; - -use liana::{ - descriptors::{LianaPolicy, PathInfo, PathSpendInfo}, - miniscript::bitcoin::{ - util::bip32::{DerivationPath, Fingerprint}, - Address, Amount, Network, Transaction, - }, -}; - -use liana_ui::{ - color, - component::{ - amount::*, - badge, button, card, - collapse::Collapse, - form, hw, separation, - text::{text, Text}, - }, - icon, theme, - util::Collection, - widget::*, -}; - -use crate::{ - app::{ - cache::Cache, - error::Error, - menu::Menu, - view::{dashboard, hw::hw_list_view, message::*, warning::warn}, - }, - daemon::model::{Coin, SpendStatus, SpendTx}, - hw::HardwareWallet, -}; - -pub fn psbt_view<'a>( - cache: &'a Cache, - tx: &'a SpendTx, - desc_info: &'a LianaPolicy, - key_aliases: &'a HashMap, - network: Network, -) -> Element<'a, Message> { - dashboard( - &Menu::PSBTs, - &cache, - None, - Column::new() - .align_items(Alignment::Center) - .spacing(20) - .push(spend_header(tx)) - .push(spend_overview_view(tx, desc_info, key_aliases)) - .push(inputs_and_outputs_view( - &tx.coins, - &tx.psbt.unsigned_tx, - network, - Some(tx.change_indexes.clone()), - None, - )), - ) -} - -pub fn save_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> { - if saved { - card::simple(text("Transaction is saved")) - .width(Length::Units(400)) - .align_x(iced::alignment::Horizontal::Center) - .into() - } else { - card::simple( - Column::new() - .spacing(10) - .push_maybe(warning.map(|w| warn(Some(w)))) - .push(text("Save the transaction as draft")) - .push( - Row::new() - .push(Column::new().width(Length::Fill)) - .push(button::alert(None, "Ignore").on_press(Message::Close)) - .push( - button::primary(None, "Save") - .on_press(Message::Spend(SpendTxMessage::Confirm)), - ), - ), - ) - .width(Length::Units(400)) - .into() - } -} - -pub fn broadcast_action<'a>(warning: Option<&Error>, saved: bool) -> Element<'a, Message> { - if saved { - card::simple(text("Transaction is broadcast")) - .width(Length::Units(400)) - .align_x(iced::alignment::Horizontal::Center) - .into() - } else { - card::simple( - Column::new() - .spacing(10) - .push_maybe(warning.map(|w| warn(Some(w)))) - .push(text("Broadcast the transaction")) - .push( - Row::new().push(Column::new().width(Length::Fill)).push( - button::primary(None, "Broadcast") - .on_press(Message::Spend(SpendTxMessage::Confirm)), - ), - ), - ) - .width(Length::Units(400)) - .into() - } -} - -pub fn delete_action<'a>(warning: Option<&Error>, deleted: bool) -> Element<'a, Message> { - if deleted { - card::simple( - Column::new() - .spacing(20) - .align_items(Alignment::Center) - .push(text("Transaction is deleted")) - .push(button::primary(None, "Go back to drafts").on_press(Message::Close)), - ) - .align_x(iced::alignment::Horizontal::Center) - .width(Length::Units(400)) - .into() - } else { - card::simple( - Column::new() - .spacing(10) - .push_maybe(warning.map(|w| warn(Some(w)))) - .push(text("Delete the transaction draft")) - .push( - Row::new() - .push(Column::new().width(Length::Fill)) - .push( - button::transparent(None, "Cancel") - .on_press(Message::Spend(SpendTxMessage::Cancel)), - ) - .push( - button::alert(None, "Delete") - .on_press(Message::Spend(SpendTxMessage::Confirm)), - ), - ), - ) - .width(Length::Units(400)) - .into() - } -} - -pub fn spend_modal<'a, T: Into>>( - saved: bool, - warning: Option<&Error>, - content: T, -) -> Element<'a, Message> { - Column::new() - .push(warn(warning)) - .push( - Container::new( - Row::new() - .push(if saved { - Column::new() - .push( - button::alert(Some(icon::trash_icon()), "Delete") - .on_press(Message::Spend(SpendTxMessage::Delete)), - ) - .width(Length::Fill) - } else { - Column::new() - .push( - button::transparent(None, "< Previous").on_press(Message::Previous), - ) - .width(Length::Fill) - }) - .align_items(iced::Alignment::Center) - .push(if saved { - button::primary(Some(icon::cross_icon()), "Close").on_press(Message::Close) - } else { - button::primary(Some(icon::cross_icon()), "Close") - .on_press(Message::Spend(SpendTxMessage::Save)) - }), - ) - .padding(10) - .style(theme::Container::Background), - ) - .push( - Container::new(scrollable( - Container::new(Container::new(content).max_width(800)) - .width(Length::Fill) - .center_x(), - )) - .height(Length::Fill) - .style(theme::Container::Background), - ) - .width(Length::Fill) - .height(Length::Fill) - .into() -} - -pub fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> { - Column::new() - .spacing(20) - .align_items(Alignment::Center) - .push( - Row::new() - .push(badge::Badge::new(icon::send_icon()).style(theme::Badge::Standard)) - .push(if !tx.sigs.recovery_paths().is_empty() { - text("Recovery").bold() - } else if tx.spend_amount == Amount::from_sat(0) { - text("Self send").bold() - } else { - text("Spend").bold() - }) - .spacing(5) - .align_items(Alignment::Center), - ) - .push_maybe(match tx.status { - SpendStatus::Deprecated => Some(badge::deprecated()), - SpendStatus::Broadcast => Some(badge::unconfirmed()), - SpendStatus::Spent => Some(badge::spent()), - _ => None, - }) - .push( - Column::new() - .align_items(Alignment::Center) - .push(amount_with_size(&tx.spend_amount, 50)) - .push( - Row::new() - .push(text("Miner fee: ")) - .push(amount(&tx.fee_amount)), - ), - ) - .into() -} - -pub fn spend_overview_view<'a>( - tx: &'a SpendTx, - desc_info: &'a LianaPolicy, - key_aliases: &'a HashMap, -) -> Element<'a, Message> { - Container::new( - Column::new() - .push( - Column::new() - .padding(15) - .spacing(10) - .push( - Row::new() - .align_items(Alignment::Center) - .push(text("PSBT:").bold().width(Length::Fill)) - .push( - Row::new() - .spacing(5) - .push( - button::secondary(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clipboard(tx.psbt.to_string())), - ) - .push( - button::secondary(Some(icon::import_icon()), "Update") - .on_press(Message::Spend(SpendTxMessage::EditPsbt)), - ), - ) - .align_items(Alignment::Center), - ) - .push( - Row::new() - .push(text("Tx ID:").bold().width(Length::Fill)) - .push(text(tx.psbt.unsigned_tx.txid().to_string()).small()) - .push( - Button::new(icon::clipboard_icon()) - .on_press(Message::Clipboard( - tx.psbt.unsigned_tx.txid().to_string(), - )) - .style(theme::Button::TransparentBorder), - ) - .align_items(Alignment::Center), - ), - ) - .push(signatures(tx, desc_info, key_aliases)), - ) - .style(theme::Container::Card(theme::Card::Simple)) - .into() -} - -pub fn signatures<'a>( - tx: &'a SpendTx, - desc_info: &'a LianaPolicy, - keys_aliases: &'a HashMap, -) -> Element<'a, Message> { - Column::new() - .push( - if let Some(sigs) = tx.path_ready() { - Container::new( - scrollable( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(icon::circle_check_icon().style(color::GREEN)) - .push(text("Ready").bold().style(color::GREEN)) - .push(text(", signed by")) - .push( - sigs.signed_pubkeys - .keys() - .fold(Row::new().spacing(5), |row, value| { - row.push(if let Some(alias) = keys_aliases.get(&value.0) { - Container::new( - tooltip::Tooltip::new( - Container::new(text(alias)) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)), - value.0.to_string(), - tooltip::Position::Bottom, - ) - .style(theme::Container::Card(theme::Card::Simple)), - ) - } else { - Container::new(text(value.0.to_string())) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)) - }) - }), - ) - ).horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2)) - ).padding(15) - } else{ - Container::new( - Collapse::new( - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push(Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(icon::circle_cross_icon()) - .push(text("Not ready").bold()) - .width(Length::Fill) - ) - .push(icon::collapse_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push( - Row::new() - .spacing(5) - .align_items(Alignment::Center) - .push(icon::circle_cross_icon()) - .push(text("Not ready").bold()) - .width(Length::Fill) - ) - .push(icon::collapsed_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Into::>::into( - Column::new().push(separation().width(Length::Fill)).push( - Column::new() - .padding(15) - .spacing(10) - .push(text(if !tx.sigs.recovery_paths().is_empty() { - "Multiple spending paths available. Finalizing this transaction requires either:" - } else { - "1 spending path available. Finalizing this transaction requires:" - })) - .push(path_view( - desc_info.primary_path(), - tx.sigs.primary_path(), - keys_aliases, - )) - .push(tx.sigs.recovery_paths().iter().fold(Column::new().spacing(10), |col, (seq, path)| { - let keys = &desc_info.recovery_paths()[seq]; - col.push(path_view(keys, path, keys_aliases)) - })), - ), - ) - }, - ))}) - .push_maybe(if tx.status == SpendStatus::Pending { - Some( - Column::new().push(separation().width(Length::Fill)).push( - Container::new( - Row::new() - .push(Space::with_width(Length::Fill)) - .push_maybe(if tx.path_ready().is_none() { - Some( - button::primary(None, "Sign") - .on_press(Message::Spend(SpendTxMessage::Sign)) - .width(Length::Units(150)), - ) - } else { - Some( - button::primary(None, "Broadcast") - .on_press(Message::Spend(SpendTxMessage::Broadcast)) - .width(Length::Units(150)), - ) - }) - .align_items(Alignment::Center) - .spacing(20), - ) - .padding(15), - ), - ) - } else { - None - }) - .into() -} - -pub fn path_view<'a>( - path: &'a PathInfo, - sigs: &'a PathSpendInfo, - key_aliases: &'a HashMap, -) -> Element<'a, Message> { - let mut keys: Vec<(Fingerprint, DerivationPath)> = - path.thresh_origins().1.into_iter().collect(); - let missing_signatures = if sigs.sigs_count >= sigs.threshold { - 0 - } else { - sigs.threshold - sigs.sigs_count - }; - keys.sort(); - scrollable( - Row::new() - .align_items(Alignment::Center) - .push(if sigs.sigs_count >= sigs.threshold { - icon::circle_check_icon().style(color::GREEN) - } else { - icon::circle_cross_icon() - }) - .push(text(format!(" {}", missing_signatures)).bold()) - .push(text(format!( - " more signature{}", - if missing_signatures > 1 { - "s from " - } else if missing_signatures == 0 { - "" - } else { - " from " - } - ))) - .push_maybe(if keys.is_empty() { - None - } else { - Some(keys.iter().fold(Row::new().spacing(5), |row, value| { - row.push_maybe(if !sigs.signed_pubkeys.contains_key(value) { - Some(if let Some(alias) = key_aliases.get(&value.0) { - Container::new( - tooltip::Tooltip::new( - Container::new(text(alias)) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)), - value.0.to_string(), - tooltip::Position::Bottom, - ) - .style(theme::Container::Card(theme::Card::Simple)), - ) - } else { - Container::new(text(value.0.to_string())) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)) - }) - } else { - None - }) - })) - }) - .push_maybe(if sigs.signed_pubkeys.is_empty() { - None - } else { - Some(text(", already signed by ")) - }) - .push( - sigs.signed_pubkeys - .keys() - .fold(Row::new().spacing(5), |row, value| { - row.push(if let Some(alias) = key_aliases.get(&value.0) { - Container::new( - tooltip::Tooltip::new( - Container::new(text(alias)) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)), - value.0.to_string(), - tooltip::Position::Bottom, - ) - .style(theme::Container::Card(theme::Card::Simple)), - ) - } else { - Container::new(text(value.0.to_string())) - .padding(3) - .style(theme::Container::Pill(theme::Pill::Simple)) - }) - }), - ), - ) - .horizontal_scroll(scrollable::Properties::new().width(2).scroller_width(2)) - .into() -} - -pub fn inputs_and_outputs_view<'a>( - coins: &'a [Coin], - tx: &'a Transaction, - network: Network, - change_indexes: Option>, - receive_indexes: Option>, -) -> Element<'a, Message> { - Column::new() - .push( - Column::new() - .spacing(10) - .push_maybe(if !coins.is_empty() { - Some( - Container::new(Collapse::new( - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push( - text(format!( - "{} spent coin{}", - coins.len(), - if coins.len() == 1 { "" } else { "s" } - )) - .bold() - .width(Length::Fill), - ) - .push(icon::collapse_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push( - text(format!( - "{} spent coin{}", - coins.len(), - if coins.len() == 1 { "" } else { "s" } - )) - .bold() - .width(Length::Fill), - ) - .push(icon::collapsed_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - coins - .iter() - .fold(Column::new(), |col: Column<'a, Message>, coin| { - col.push( - Row::new() - .padding(15) - .align_items(Alignment::Center) - .width(Length::Fill) - .push( - Row::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push( - text(coin.outpoint.to_string()) - .small() - ) - .push( - Button::new(icon::clipboard_icon()) - .on_press(Message::Clipboard( - coin.outpoint.to_string(), - )) - .style( - theme::Button::TransparentBorder, - ), - ), - ) - .push(amount(&coin.amount)), - ) - }) - .into() - }, - )) - .style(theme::Container::Card(theme::Card::Simple)), - ) - } else { - None - }) - .push( - Container::new(Collapse::new( - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push( - text(format!( - "{} recipient{}", - tx.output.len(), - if tx.output.len() == 1 { "" } else { "s" } - )) - .bold() - .width(Length::Fill), - ) - .push(icon::collapse_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - Button::new( - Row::new() - .align_items(Alignment::Center) - .push( - text(format!( - "{} recipient{}", - tx.output.len(), - if tx.output.len() == 1 { "" } else { "s" } - )) - .bold() - .width(Length::Fill), - ) - .push(icon::collapsed_icon()), - ) - .padding(15) - .width(Length::Fill) - .style(theme::Button::TransparentBorder) - }, - move || { - tx.output - .iter() - .enumerate() - .fold(Column::new(), |col: Column<'a, Message>, (i, output)| { - let addr = Address::from_script(&output.script_pubkey, network).unwrap(); - col.push( - Column::new() - .padding(15) - .width(Length::Fill) - .spacing(10) - .push( - Row::new() - .width(Length::Fill) - .push( - Row::new() - .align_items(Alignment::Center) - .width(Length::Fill) - .push(text(addr.to_string()).small()) - .push( - Button::new(icon::clipboard_icon()) - .on_press(Message::Clipboard( - addr.to_string(), - )) - .style( - theme::Button::TransparentBorder, - ), - ), - ) - .push( - amount(&Amount::from_sat(output.value)) - ), - ) - .push_maybe( - if let Some(indexes) = change_indexes.as_ref() { - if indexes.contains(&i) { - Some( - Container::new(text("Change")) - .padding(5) - .style(theme::Container::Pill(theme::Pill::Success)), - ) - } else { - None - } - } else { - None - }, - ) - .push_maybe( - if let Some(indexes) = receive_indexes.as_ref() { - if indexes.contains(&i) { - Some( - Container::new(text("Deposit")) - .padding(5) - .style(theme::Container::Pill(theme::Pill::Success)), - ) - } else { - None - } - } else { - None - }, - ), - ) - }) - .into() - }, - )) - .style(theme::Container::Card(theme::Card::Simple)), - ), - ) - .into() -} - -pub fn sign_action<'a>( - warning: Option<&Error>, - hws: &'a [HardwareWallet], - signer: Option, - signer_alias: Option<&'a String>, - processing: bool, - chosen_hw: Option, - signed: &HashSet, -) -> Element<'a, Message> { - Column::new() - .push_maybe(warning.map(|w| warn(Some(w)))) - .push(card::simple( - Column::new() - .push( - Column::new() - .push( - Row::new() - .push( - text("Select signing device to sign with:") - .bold() - .width(Length::Fill), - ) - .push(button::secondary(None, "Refresh").on_press(Message::Reload)) - .align_items(Alignment::Center), - ) - .spacing(10) - .push(hws.iter().enumerate().fold( - Column::new().spacing(10), - |col, (i, hw)| { - col.push(hw_list_view( - i, - hw, - Some(i) == chosen_hw, - processing, - hw.fingerprint() - .map(|f| signed.contains(&f)) - .unwrap_or(false), - )) - }, - )) - .push_maybe(signer.map(|fingerprint| { - Button::new(if signed.contains(&fingerprint) { - hw::sign_success_hot_signer(fingerprint, signer_alias) - } else { - hw::hot_signer(fingerprint, signer_alias) - }) - .on_press(Message::Spend(SpendTxMessage::SelectHotSigner)) - .padding(10) - .style(theme::Button::Border) - .width(Length::Fill) - })) - .width(Length::Fill), - ) - .spacing(20) - .width(Length::Fill) - .align_items(Alignment::Center), - )) - .width(Length::Units(500)) - .into() -} - -pub fn update_spend_view<'a>( - psbt: String, - updated: &form::Value, - error: Option<&Error>, - processing: bool, -) -> Element<'a, Message> { - Column::new() - .push(warn(error)) - .push(card::simple( - Column::new() - .spacing(20) - .push( - Row::new() - .push(text("PSBT:").bold().width(Length::Fill)) - .push( - button::border(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clipboard(psbt)), - ) - .align_items(Alignment::Center), - ) - .push(separation().width(Length::Fill)) - .push( - Column::new() - .spacing(10) - .push(text("Insert updated PSBT:").bold()) - .push( - form::Form::new("PSBT", updated, move |msg| { - Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) - }) - .warning("Please enter the correct base64 encoded PSBT") - .size(20) - .padding(10), - ) - .push(Row::new().push(Space::with_width(Length::Fill)).push( - if updated.valid && !updated.value.is_empty() && !processing { - button::primary(None, "Update") - .on_press(Message::ImportSpend(ImportSpendMessage::Confirm)) - } else if processing { - button::primary(None, "Processing...") - } else { - button::primary(None, "Update") - }, - )), - ), - )) - .max_width(400) - .into() -} - -pub fn update_spend_success_view<'a>() -> Element<'a, Message> { - Column::new() - .push( - card::simple(Container::new( - text("Spend transaction is updated").style(color::GREEN), - )) - .padding(50), - ) - .width(Length::Units(400)) - .align_items(Alignment::Center) - .into() -} diff --git a/gui/src/app/view/psbt.rs b/gui/src/app/view/psbt.rs index 3f591176..f314ab36 100644 --- a/gui/src/app/view/psbt.rs +++ b/gui/src/app/view/psbt.rs @@ -770,7 +770,7 @@ pub fn update_spend_view<'a>( .spacing(10) .push(text("Insert updated PSBT:").bold()) .push( - form::Form::new("PSBT", updated, move |msg| { + form::Form::new_trimmed("PSBT", updated, move |msg| { Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) }) .warning("Please enter the correct base64 encoded PSBT") diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index e7d08f35..cf49d488 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -27,7 +27,7 @@ pub fn import_psbt_view<'a>( .spacing(10) .push(text("Insert PSBT:").bold()) .push( - form::Form::new("PSBT", imported, move |msg| { + form::Form::new_trimmed("PSBT", imported, move |msg| { Message::ImportSpend(ImportSpendMessage::PsbtEdited(msg)) }) .warning("Please enter a base64 encoded PSBT") diff --git a/gui/src/app/view/recovery.rs b/gui/src/app/view/recovery.rs index 12f46030..b21f94d3 100644 --- a/gui/src/app/view/recovery.rs +++ b/gui/src/app/view/recovery.rs @@ -66,7 +66,7 @@ pub fn recovery<'a>( .push(text("Destination").bold()) .push( Container::new( - form::Form::new("Address", address, move |msg| { + form::Form::new_trimmed("Address", address, move |msg| { Message::CreateSpend(CreateSpendMessage::RecipientEdited( 0, "address", msg, )) @@ -81,7 +81,7 @@ pub fn recovery<'a>( .push(text("Feerate").bold()) .push( Container::new( - form::Form::new("42 (sats/vbyte)", feerate, move |msg| { + form::Form::new_trimmed("42 (sats/vbyte)", feerate, move |msg| { Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg)) }) .warning("Invalid feerate") diff --git a/gui/src/app/view/settings.rs b/gui/src/app/view/settings.rs index 05f192c4..c8e12dfa 100644 --- a/gui/src/app/view/settings.rs +++ b/gui/src/app/view/settings.rs @@ -247,7 +247,7 @@ pub fn bitcoind_edit<'a>( Column::new() .push(text("Cookie file path:").bold().small()) .push( - form::Form::new("Cookie file path", cookie_path, |value| { + form::Form::new_trimmed("Cookie file path", cookie_path, |value| { SettingsEditMessage::FieldEdited("cookie_file_path", value) }) .warning("Please enter a valid filesystem path") @@ -260,7 +260,7 @@ pub fn bitcoind_edit<'a>( Column::new() .push(text("Socket address:").bold().small()) .push( - form::Form::new("Socket address:", addr, |value| { + form::Form::new_trimmed("Socket address:", addr, |value| { SettingsEditMessage::FieldEdited("socket_address", value) }) .warning("Please enter a valid address") @@ -456,7 +456,7 @@ pub fn rescan<'a>( Row::new() .push(text("Year:").bold().small()) .push( - form::Form::new("2022", year, |value| { + form::Form::new_trimmed("2022", year, |value| { SettingsEditMessage::FieldEdited("rescan_year", value) }) .size(20) @@ -464,7 +464,7 @@ pub fn rescan<'a>( ) .push(text("Month:").bold().small()) .push( - form::Form::new("12", month, |value| { + form::Form::new_trimmed("12", month, |value| { SettingsEditMessage::FieldEdited("rescan_month", value) }) .size(20) @@ -472,7 +472,7 @@ pub fn rescan<'a>( ) .push(text("Day:").bold().small()) .push( - form::Form::new("31", day, |value| { + form::Form::new_trimmed("31", day, |value| { SettingsEditMessage::FieldEdited("rescan_day", value) }) .size(20) diff --git a/gui/src/app/view/spend/mod.rs b/gui/src/app/view/spend/mod.rs index 3098e0e1..1a4cdee8 100644 --- a/gui/src/app/view/spend/mod.rs +++ b/gui/src/app/view/spend/mod.rs @@ -141,9 +141,13 @@ pub fn create_spend_tx<'a>( .push(Container::new(p1_bold("Feerate")).padding(10)) .spacing(10) .push( - form::Form::new("42 (in sats/vbyte)", feerate, move |msg| { - Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg)) - }) + form::Form::new_trimmed( + "42 (in sats/vbyte)", + feerate, + move |msg| { + Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg)) + }, + ) .warning("Invalid feerate") .size(20) .padding(10), @@ -265,7 +269,7 @@ pub fn recipient_view<'a>( .width(Length::Fixed(80.0)), ) .push( - form::Form::new("Address", address, move |msg| { + form::Form::new_trimmed("Address", address, move |msg| { CreateSpendMessage::RecipientEdited(index, "address", msg) }) .warning("Invalid address (maybe it is for another network?)") @@ -284,7 +288,7 @@ pub fn recipient_view<'a>( .width(Length::Fixed(80.0)), ) .push( - form::Form::new("0.001 (in BTC)", amount, move |msg| { + form::Form::new_trimmed("0.001 (in BTC)", amount, move |msg| { CreateSpendMessage::RecipientEdited(index, "amount", msg) }) .warning( diff --git a/gui/src/installer/view.rs b/gui/src/installer/view.rs index 11548575..d04709df 100644 --- a/gui/src/installer/view.rs +++ b/gui/src/installer/view.rs @@ -388,7 +388,7 @@ pub fn import_descriptor<'a>( let col_descriptor = Column::new() .push(text("Descriptor:").bold()) .push( - form::Form::new("Descriptor", imported_descriptor, |msg| { + form::Form::new_trimmed("Descriptor", imported_descriptor, |msg| { Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg)) }) .warning("Incompatible descriptor.") @@ -807,7 +807,7 @@ pub fn define_bitcoin<'a>( let col_address = Column::new() .push(text("Address:").bold()) .push( - form::Form::new("Address", address, |msg| { + form::Form::new_trimmed("Address", address, |msg| { Message::DefineBitcoind(message::DefineBitcoind::AddressEdited(msg)) }) .warning("Please enter correct address") @@ -819,7 +819,7 @@ pub fn define_bitcoin<'a>( let col_cookie = Column::new() .push(text("Cookie path:").bold()) .push( - form::Form::new("Cookie path", cookie_path, |msg| { + form::Form::new_trimmed("Cookie path", cookie_path, |msg| { Message::DefineBitcoind(message::DefineBitcoind::CookiePathEdited(msg)) }) .warning("Please enter correct path") @@ -1342,7 +1342,7 @@ pub fn edit_key_modal<'a>( .push( Row::new() .push( - form::Form::new("Extended public key", form_xpub, |msg| { + form::Form::new_trimmed("Extended public key", form_xpub, |msg| { Message::DefineDescriptor( message::DefineDescriptor::KeyModal( message::ImportKeyModal::XPubEdited(msg), @@ -1456,7 +1456,7 @@ pub fn edit_sequence_modal<'a>(sequence: &form::Value) -> Element<'a, Me Row::new() .push( Container::new( - form::Form::new("ex: 1000", sequence, |v| { + form::Form::new_trimmed("ex: 1000", sequence, |v| { Message::DefineDescriptor(message::DefineDescriptor::SequenceModal( message::SequenceModal::SequenceEdited(v), )) diff --git a/gui/ui/src/component/form.rs b/gui/ui/src/component/form.rs index d268d2f6..ea2fb214 100644 --- a/gui/ui/src/component/form.rs +++ b/gui/ui/src/component/form.rs @@ -44,6 +44,24 @@ where } } + /// Creates a new [`Form`] that trims input values before applying the `on_change` function. + /// + /// It expects: + /// - a placeholder + /// - the current value + /// - a function that produces a message when the [`Form`] changes + pub fn new_trimmed(placeholder: &str, value: &Value, on_change: F) -> Self + where + F: 'static + Fn(String) -> Message, + { + Self { + input: text_input::TextInput::new(placeholder, &value.value) + .on_input(move |s| on_change(s.trim().to_string())), + warning: None, + valid: value.valid, + } + } + /// Sets the [`Form`] with a warning message pub fn warning(mut self, warning: &'a str) -> Self { self.warning = Some(warning);