Merge #720: gui: Trim text inputs

9895b493f1d79ae8fc5a1591ef455ccda2d81ccd gui: trim text input strings (jp1ac4)
d3f2931375ff95c201a60164bf1015f18c6e87ce gui: remove unused file (jp1ac4)

Pull request description:

  This is to resolve #323.

  Following suggestions in #323, I've added a new `TrimmedString` struct that takes a string and applies `trim()`. This type is then used as `form::Value<TrimmedString>` to ensure text inputs are trimmed.

  I'm creating this PR as draft to check if this is the right approach before applying the change to other inputs.

  I've also removed a view file that seems to have been created accidentally.

ACKs for top commit:
  edouardparis:
    ACK 9895b493f1d79ae8fc5a1591ef455ccda2d81ccd

Tree-SHA512: 76c7f28ed2d0b6b6b76658a9368b918b0b211e2fabd72ac5d19c13adcbb2d3645b78680a8849f235ee58620fbe8df440ef8354f2d50d39601879bb7727465335
This commit is contained in:
edouard 2023-10-06 10:22:47 +02:00
commit 19491d1d00
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F
8 changed files with 41 additions and 855 deletions

View File

@ -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<Fingerprint, String>,
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<Element<'a, Message>>>(
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<Fingerprint, String>,
) -> 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<Fingerprint, String>,
) -> 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::<Element<'a, Message>>::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<Fingerprint, String>,
) -> 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<Vec<usize>>,
receive_indexes: Option<Vec<usize>>,
) -> 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<Fingerprint>,
signer_alias: Option<&'a String>,
processing: bool,
chosen_hw: Option<usize>,
signed: &HashSet<Fingerprint>,
) -> 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<String>,
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()
}

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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(

View File

@ -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<String>) -> 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),
))

View File

@ -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<F>(placeholder: &str, value: &Value<String>, 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);