diff --git a/gui/src/app/state/spend/mod.rs b/gui/src/app/state/spend/mod.rs index cffb9865..fda7f111 100644 --- a/gui/src/app/state/spend/mod.rs +++ b/gui/src/app/state/spend/mod.rs @@ -26,8 +26,7 @@ impl CreateSpendPanel { draft: step::TransactionDraft::default(), current: 0, steps: vec![ - Box::new(step::ChooseRecipients::new(coins)), - Box::new(step::ChooseCoins::new( + Box::new(step::DefineSpend::new( descriptor, coins.to_vec(), timelock, @@ -72,7 +71,7 @@ impl State for CreateSpendPanel { } if let Some(step) = self.steps.get_mut(self.current) { - return step.update(daemon, cache, &self.draft, message); + return step.update(daemon, cache, message); } Command::none() diff --git a/gui/src/app/state/spend/step.rs b/gui/src/app/state/spend/step.rs index 33cd9434..a85a792d 100644 --- a/gui/src/app/state/spend/step.rs +++ b/gui/src/app/state/spend/step.rs @@ -28,7 +28,6 @@ const DUST_OUTPUT_SATS: u64 = 5_000; #[derive(Default, Clone)] pub struct TransactionDraft { inputs: Vec, - outputs: HashMap, generated: Option, } @@ -38,36 +37,77 @@ pub trait Step { &mut self, daemon: Arc, cache: &Cache, - draft: &TransactionDraft, message: Message, ) -> Command; fn apply(&self, _draft: &mut TransactionDraft) {} fn load(&mut self, _draft: &TransactionDraft) {} } -pub struct ChooseRecipients { +pub struct DefineSpend { balance_available: Amount, recipients: Vec, is_valid: bool, is_duplicate: bool, + + descriptor: LianaDescriptor, + timelock: u16, + coins: Vec<(Coin, bool)>, + amount_left_to_select: Option, + feerate: form::Value, + generated: Option, + warning: Option, } -impl ChooseRecipients { - pub fn new(coins: &[Coin]) -> Self { +impl DefineSpend { + pub fn new( + descriptor: LianaDescriptor, + coins: Vec, + timelock: u16, + blockheight: u32, + ) -> Self { + let balance_available = coins + .iter() + .filter_map(|coin| { + if coin.spend_info.is_none() { + Some(coin.amount) + } else { + None + } + }) + .sum(); + let mut coins: Vec<(Coin, bool)> = coins + .into_iter() + .filter_map(|c| { + if c.spend_info.is_none() { + Some((c, false)) + } else { + None + } + }) + .collect(); + coins.sort_by(|(a, _), (b, _)| { + if remaining_sequence(a, blockheight, timelock) + == remaining_sequence(b, blockheight, timelock) + { + // bigger amount first + b.amount.cmp(&a.amount) + } else { + // smallest blockheight (remaining_sequence) first + a.block_height.cmp(&b.block_height) + } + }); Self { - balance_available: coins - .iter() - .filter_map(|coin| { - if coin.spend_info.is_none() { - Some(coin.amount) - } else { - None - } - }) - .sum(), + balance_available, + descriptor, + timelock, + generated: None, + coins, recipients: vec![Recipient::default()], is_valid: false, is_duplicate: false, + feerate: form::Value::default(), + amount_left_to_select: None, + warning: None, } } fn check_valid(&mut self) { @@ -84,51 +124,170 @@ impl ChooseRecipients { } } } + fn amount_left_to_select(&mut self) { + // We need the feerate in order to compute the required amount of BTC to + // select. Return early if we don't to not do unnecessary computation. + let feerate = match self.feerate.value.parse::() { + Ok(f) => f, + Err(_) => { + self.amount_left_to_select = None; + return; + } + }; + + // The coins to be included in this transaction. + let selected_coins: Vec<_> = self + .coins + .iter() + .filter_map(|(c, selected)| if *selected { Some(c) } else { None }) + .collect(); + + // A dummy representation of the transaction that will be computed, for + // the purpose of computing its size in order to anticipate the fees needed. + // NOTE: we make the conservative estimation a change output will always be + // needed. + let tx_template = bitcoin::Transaction { + version: 2, + lock_time: bitcoin::PackedLockTime(0), + input: selected_coins + .iter() + .map(|_| bitcoin::TxIn::default()) + .collect(), + output: self + .recipients + .iter() + .filter_map(|recipient| { + if recipient.valid() { + Some(bitcoin::TxOut { + script_pubkey: Address::from_str(&recipient.address.value) + .unwrap() + .script_pubkey(), + value: recipient.amount().unwrap(), + }) + } else { + None + } + }) + .collect(), + }; + // nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + + const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32; + let satisfaction_vsize = self.descriptor.max_sat_weight() / 4; + let transaction_size = + tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE; + + // Now the calculation of the amount left to be selected by the user is a simple + // substraction between the value needed by the transaction to be created and the + // value that was selected already. + let selected_amount = selected_coins.iter().map(|c| c.amount.to_sat()).sum(); + let output_sum: u64 = tx_template.output.iter().map(|o| o.value).sum(); + let needed_amount: u64 = transaction_size as u64 * feerate + output_sum; + self.amount_left_to_select = Some(Amount::from_sat( + needed_amount.saturating_sub(selected_amount), + )); + } } -impl Step for ChooseRecipients { +impl Step for DefineSpend { fn update( &mut self, - _daemon: Arc, + daemon: Arc, cache: &Cache, - _draft: &TransactionDraft, message: Message, ) -> Command { if let Message::View(view::Message::CreateSpend(msg)) = message { - match &msg { + match msg { view::CreateSpendMessage::AddRecipient => { self.recipients.push(Recipient::default()); } view::CreateSpendMessage::DeleteRecipient(i) => { - self.recipients.remove(*i); + self.recipients.remove(i); } view::CreateSpendMessage::RecipientEdited(i, _, _) => { self.recipients - .get_mut(*i) + .get_mut(i) .unwrap() .update(cache.network, msg); } + + view::CreateSpendMessage::FeerateEdited(s) => { + if s.parse::().is_ok() { + self.feerate.value = s; + self.feerate.valid = true; + self.amount_left_to_select(); + } else if s.is_empty() { + self.feerate.value = "".to_string(); + self.feerate.valid = true; + self.amount_left_to_select = None; + } else { + self.feerate.valid = false; + self.amount_left_to_select = None; + } + self.warning = None; + } + view::CreateSpendMessage::Generate => { + let inputs: Vec = self + .coins + .iter() + .filter_map( + |(coin, selected)| if *selected { Some(coin.outpoint) } else { None }, + ) + .collect(); + let mut outputs: HashMap = HashMap::new(); + for recipient in &self.recipients { + outputs.insert( + Address::from_str(&recipient.address.value).expect("Checked before"), + recipient.amount().expect("Checked before"), + ); + } + let feerate_vb = self.feerate.value.parse::().unwrap_or(0); + self.warning = None; + return Command::perform( + async move { + daemon + .create_spend_tx(&inputs, &outputs, feerate_vb) + .map(|res| res.psbt) + .map_err(|e| e.into()) + }, + Message::Psbt, + ); + } + view::CreateSpendMessage::SelectCoin(i) => { + if let Some(coin) = self.coins.get_mut(i) { + coin.1 = !coin.1; + self.amount_left_to_select(); + } + } _ => {} } - self.check_valid(); + Command::none() + } else { + if let Message::Psbt(res) = message { + match res { + Ok(psbt) => { + self.generated = Some(psbt); + return Command::perform(async {}, |_| Message::View(view::Message::Next)); + } + Err(e) => self.warning = Some(e), + } + } + Command::none() } - Command::none() } fn apply(&self, draft: &mut TransactionDraft) { - let mut outputs: HashMap = HashMap::new(); - for recipient in &self.recipients { - outputs.insert( - Address::from_str(&recipient.address.value).expect("Checked before"), - recipient.amount().expect("Checked before"), - ); - } - draft.outputs = outputs; + draft.inputs = self + .coins + .iter() + .filter_map(|(coin, selected)| if *selected { Some(*coin) } else { None }) + .collect(); + draft.generated = self.generated.clone(); } - fn view<'a>(&'a self, _cache: &'a Cache) -> Element<'a, view::Message> { - view::spend::step::choose_recipients_view( + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + view::spend::step::create_spend_tx( + cache, &self.balance_available, self.recipients .iter() @@ -143,6 +302,11 @@ impl Step for ChooseRecipients { ), self.is_valid, self.is_duplicate, + self.timelock, + &self.coins, + self.amount_left_to_select.as_ref(), + &self.feerate, + self.warning.as_ref(), ) } } @@ -222,211 +386,6 @@ impl Recipient { } } -pub struct ChooseCoins { - descriptor: LianaDescriptor, - timelock: u16, - coins: Vec<(Coin, bool)>, - recipients: Vec<(Address, Amount)>, - - amount_left_to_select: Option, - feerate: form::Value, - generated: Option, - warning: Option, -} - -impl ChooseCoins { - pub fn new( - descriptor: LianaDescriptor, - coins: Vec, - timelock: u16, - blockheight: u32, - ) -> Self { - let mut coins: Vec<(Coin, bool)> = coins - .into_iter() - .filter_map(|c| { - if c.spend_info.is_none() { - Some((c, false)) - } else { - None - } - }) - .collect(); - coins.sort_by(|(a, _), (b, _)| { - if remaining_sequence(a, blockheight, timelock) - == remaining_sequence(b, blockheight, timelock) - { - // bigger amount first - b.amount.cmp(&a.amount) - } else { - // smallest blockheight (remaining_sequence) first - a.block_height.cmp(&b.block_height) - } - }); - Self { - descriptor, - timelock, - coins, - recipients: Vec::new(), - feerate: form::Value::default(), - generated: None, - warning: None, - amount_left_to_select: None, - } - } - - fn amount_left_to_select(&mut self) { - // We need the feerate in order to compute the required amount of BTC to - // select. Return early if we don't to not do unnecessary computation. - let feerate = match self.feerate.value.parse::() { - Ok(f) => f, - Err(_) => { - self.amount_left_to_select = None; - return; - } - }; - - // The coins to be included in this transaction. - let selected_coins: Vec<_> = self - .coins - .iter() - .filter_map(|(c, selected)| if *selected { Some(c) } else { None }) - .collect(); - - // A dummy representation of the transaction that will be computed, for - // the purpose of computing its size in order to anticipate the fees needed. - // NOTE: we make the conservative estimation a change output will always be - // needed. - let tx_template = bitcoin::Transaction { - version: 2, - lock_time: bitcoin::PackedLockTime(0), - input: selected_coins - .iter() - .map(|_| bitcoin::TxIn::default()) - .collect(), - output: self - .recipients - .iter() - .map(|(addr, amount)| bitcoin::TxOut { - script_pubkey: addr.script_pubkey(), - value: amount.to_sat(), - }) - .collect(), - }; - // nValue size + scriptPubKey CompactSize + OP_0 + PUSH32 + - const CHANGE_TXO_SIZE: usize = 8 + 1 + 1 + 1 + 32; - let satisfaction_vsize = self.descriptor.max_sat_weight() / 4; - let transaction_size = - tx_template.vsize() + satisfaction_vsize * tx_template.input.len() + CHANGE_TXO_SIZE; - - // Now the calculation of the amount left to be selected by the user is a simple - // substraction between the value needed by the transaction to be created and the - // value that was selected already. - let selected_amount = selected_coins.iter().map(|c| c.amount.to_sat()).sum(); - let output_sum: u64 = tx_template.output.iter().map(|o| o.value).sum(); - let needed_amount: u64 = transaction_size as u64 * feerate + output_sum; - self.amount_left_to_select = Some(Amount::from_sat( - needed_amount.saturating_sub(selected_amount), - )); - } -} - -impl Step for ChooseCoins { - fn load(&mut self, draft: &TransactionDraft) { - self.warning = None; - self.recipients = draft - .outputs - .iter() - .map(|(k, v)| (k.clone(), Amount::from_sat(*v))) - .collect(); - self.amount_left_to_select(); - } - - fn apply(&self, draft: &mut TransactionDraft) { - draft.inputs = self - .coins - .iter() - .filter_map(|(coin, selected)| if *selected { Some(*coin) } else { None }) - .collect(); - draft.generated = self.generated.clone(); - } - - fn update( - &mut self, - daemon: Arc, - _cache: &Cache, - draft: &TransactionDraft, - message: Message, - ) -> Command { - match message { - Message::View(view::Message::CreateSpend(view::CreateSpendMessage::FeerateEdited( - s, - ))) => { - if s.parse::().is_ok() { - self.feerate.value = s; - self.feerate.valid = true; - self.amount_left_to_select(); - } else if s.is_empty() { - self.feerate.value = "".to_string(); - self.feerate.valid = true; - self.amount_left_to_select = None; - } else { - self.feerate.valid = false; - self.amount_left_to_select = None; - } - self.warning = None; - } - Message::View(view::Message::CreateSpend(view::CreateSpendMessage::Generate)) => { - let inputs: Vec = self - .coins - .iter() - .filter_map( - |(coin, selected)| if *selected { Some(coin.outpoint) } else { None }, - ) - .collect(); - let outputs = draft.outputs.clone(); - let feerate_vb = self.feerate.value.parse::().unwrap_or(0); - self.warning = None; - return Command::perform( - async move { - daemon - .create_spend_tx(&inputs, &outputs, feerate_vb) - .map(|res| res.psbt) - .map_err(|e| e.into()) - }, - Message::Psbt, - ); - } - Message::Psbt(res) => match res { - Ok(psbt) => { - self.generated = Some(psbt); - return Command::perform(async {}, |_| Message::View(view::Message::Next)); - } - Err(e) => self.warning = Some(e), - }, - Message::View(view::Message::CreateSpend(view::CreateSpendMessage::SelectCoin(i))) => { - if let Some(coin) = self.coins.get_mut(i) { - coin.1 = !coin.1; - self.amount_left_to_select(); - } - } - _ => {} - } - - Command::none() - } - - fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { - view::spend::step::choose_coins_view( - cache, - self.timelock, - &self.coins, - self.amount_left_to_select.as_ref(), - &self.feerate, - self.warning.as_ref(), - ) - } -} - pub struct SaveSpend { wallet: Arc, spend: Option, @@ -460,7 +419,6 @@ impl Step for SaveSpend { &mut self, daemon: Arc, cache: &Cache, - _draft: &TransactionDraft, message: Message, ) -> Command { if let Some(spend) = &mut self.spend { diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index 8d87565c..9fc5d720 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -1,14 +1,14 @@ -use iced::{Alignment, Length}; +use iced::{ + alignment, + widget::{checkbox, scrollable, Space}, + Alignment, Length, +}; use liana::miniscript::bitcoin::Amount; use liana_ui::{ color, - component::{ - amount::*, - badge, button, form, - text::{text, Text}, - }, + component::{amount::*, badge, button, form, text::*}, icon, theme, util::Collection, widget::*, @@ -18,67 +18,139 @@ use crate::{ app::{ cache::Cache, error::Error, - view::{coins, message::*, modal}, + menu::Menu, + view::{coins, dashboard, message::*}, }, daemon::model::{remaining_sequence, Coin}, }; -pub fn choose_recipients_view<'a>( +#[allow(clippy::too_many_arguments)] +pub fn create_spend_tx<'a>( + cache: &'a Cache, balance_available: &'a Amount, recipients: Vec>, total_amount: Amount, is_valid: bool, duplicate: bool, + timelock: u16, + coins: &[(Coin, bool)], + amount_left: Option<&Amount>, + feerate: &form::Value, + error: Option<&Error>, ) -> Element<'a, Message> { - modal( - false, - None, + dashboard( + &Menu::CreateSpendTx, + cache, + error, Column::new() - .push(text("Choose recipients").bold().size(50)) + .push(h3("Send")) .push( Column::new() .push(Column::with_children(recipients).spacing(10)) .push( - button::transparent(Some(icon::plus_icon()), "Add recipient") - .on_press(Message::CreateSpend(CreateSpendMessage::AddRecipient)), + Row::new() + .push_maybe(if duplicate { + Some( + Container::new( + text("Two recipient addresses are the same") + .style(color::RED), + ) + .padding(10), + ) + } else { + None + }) + .push(Space::with_width(Length::Fill)) + .push( + button::secondary(Some(icon::plus_icon()), "Add recipient") + .on_press(Message::CreateSpend( + CreateSpendMessage::AddRecipient, + )), + ), ) - .padding(10) - .max_width(1000) - .spacing(10), + .spacing(20), ) - .spacing(20) - .align_items(Alignment::Center), - Some( - Container::new( + .push( + Row::new() + .push( + Row::new() + .push(Container::new(p1_bold("Fee rate")).padding(10)) + .spacing(10) + .push( + form::Form::new("Feerate (sat/vbyte)", feerate, move |msg| { + Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg)) + }) + .warning("Invalid feerate") + .size(20) + .padding(10), + ) + .width(Length::FillPortion(1)), + ) + .push(Space::with_width(Length::FillPortion(1))), + ) + .push( + Container::new( + Column::new() + .spacing(10) + .push( + Row::new() + .align_items(Alignment::Center) + .push(p1_bold("Coins selection").width(Length::Fill)) + .push(Container::new(if let Some(amount_left) = amount_left { + Row::new() + .spacing(5) + .push(amount_with_size(amount_left, P2_SIZE)) + .push(p2_regular("left to select").style(color::GREY_3)) + } else { + Row::new() + .push(text("Feerate needs to be set.").style(color::GREY_3)) + })) + .width(Length::Fill), + ) + .push( + Container::new(scrollable(coins.iter().enumerate().fold( + Column::new().spacing(10), + |col, (i, (coin, selected))| { + col.push(coin_list_view( + i, + coin, + timelock, + cache.blockheight as u32, + *selected, + )) + }, + ))) + .max_height(300), + ), + ) + .padding(20) + .style(theme::Card::Simple), + ) + .push( Row::new() .spacing(20) .align_items(Alignment::Center) + .push(Space::with_width(Length::Fill)) .push( - Container::new( - Row::new() - .align_items(Alignment::Center) - .spacing(5) - .push(text(format!("{}", total_amount)).bold()) - .push(text(format!("/ {}", balance_available))), - ) - .width(Length::Fill), + button::primary(None, "Clear") + .on_press(Message::Menu(Menu::CreateSpendTx)) + .width(Length::Units(100)), ) - .push_maybe(if duplicate { - Some(text("Two recipient addresses are the same").style(color::ORANGE)) - } else { - None - }) - .push(if is_valid && total_amount < *balance_available { - button::primary(None, "Next") - .on_press(Message::Next) - .width(Length::Units(100)) - } else { - button::primary(None, "Next").width(Length::Units(100)) - }), + .push( + if is_valid + && total_amount < *balance_available + && Some(&Amount::from_sat(0)) == amount_left + { + button::primary(None, "Next") + .on_press(Message::CreateSpend(CreateSpendMessage::Generate)) + .width(Length::Units(100)) + } else { + button::primary(None, "Next").width(Length::Units(100)) + }, + ), ) - .style(theme::Container::Foreground) - .padding(20), - ), + .push(Space::with_height(Length::Units(20))) + .spacing(20), ) } @@ -87,106 +159,60 @@ pub fn recipient_view<'a>( address: &form::Value, amount: &form::Value, ) -> Element<'a, CreateSpendMessage> { - Row::new() - .push( - form::Form::new("Address", address, move |msg| { - CreateSpendMessage::RecipientEdited(index, "address", msg) - }) - .warning("Invalid address (maybe it is for another network?)") - .size(20) - .padding(10), - ) - .push( - Container::new( - form::Form::new("Amount", amount, move |msg| { - CreateSpendMessage::RecipientEdited(index, "amount", msg) - }) - .warning("Invalid amount. Must be > 0.00005000 BTC.") - .size(20) - .padding(10), - ) - .width(Length::Units(300)), - ) - .spacing(5) - .push( - button::transparent(Some(icon::trash_icon()), "") - .on_press(CreateSpendMessage::DeleteRecipient(index)) - .width(Length::Shrink), - ) - .width(Length::Fill) - .into() -} - -pub fn choose_coins_view<'a>( - cache: &Cache, - timelock: u16, - coins: &[(Coin, bool)], - amount_left: Option<&Amount>, - feerate: &form::Value, - error: Option<&Error>, -) -> Element<'a, Message> { - modal( - true, - error, + Container::new( Column::new() - .push(text("Choose coins and feerate").bold().size(50)) + .spacing(10) .push( - Container::new( - form::Form::new("Feerate (sat/vbyte)", feerate, move |msg| { - Message::CreateSpend(CreateSpendMessage::FeerateEdited(msg)) - }) - .warning("Invalid feerate") - .size(20) - .padding(10), - ) - .width(Length::Units(250)), + Row::new().push(Space::with_width(Length::Fill)).push( + Button::new(icon::cross_icon()) + .style(theme::Button::Transparent) + .on_press(CreateSpendMessage::DeleteRecipient(index)) + .width(Length::Shrink), + ), ) .push( - Column::new() - .padding(10) - .spacing(10) - .push(coins.iter().enumerate().fold( - Column::new().spacing(10), - |col, (i, (coin, selected))| { - col.push(coin_list_view( - i, - coin, - timelock, - cache.blockheight as u32, - *selected, - )) - }, - )), - ) - .spacing(20) - .align_items(Alignment::Center), - Some( - Container::new( Row::new() - .align_items(Alignment::Center) + .align_items(Alignment::Start) + .spacing(10) .push( - Container::new(if let Some(amount_left) = amount_left { - Row::new() - .spacing(5) - .push(text("Amount left to select:")) - .push(text(amount_left.to_string()).bold()) - } else { - Row::new().push(text("Feerate needs to be set.")) - }) - .width(Length::Fill), + Container::new(p1_bold("Pay to")) + .align_x(alignment::Horizontal::Right) + .padding(10) + .width(Length::Units(80)), ) - .push(if Some(&Amount::from_sat(0)) == amount_left { - button::primary(None, "Next") - .on_press(Message::CreateSpend(CreateSpendMessage::Generate)) - .width(Length::Units(100)) - } else { - button::primary(None, "Next").width(Length::Units(100)) - }), + .push( + form::Form::new("Address", address, move |msg| { + CreateSpendMessage::RecipientEdited(index, "address", msg) + }) + .warning("Invalid address (maybe it is for another network?)") + .size(20) + .padding(10), + ), ) - .style(theme::Container::Foreground) - .padding(20), - ), + .push( + Row::new() + .align_items(Alignment::Start) + .spacing(10) + .push( + Container::new(p1_bold("Amount")) + .padding(10) + .align_x(alignment::Horizontal::Right) + .width(Length::Units(80)), + ) + .push( + form::Form::new("ex: 0.001", amount, move |msg| { + CreateSpendMessage::RecipientEdited(index, "amount", msg) + }) + .warning("Invalid amount. Must be > 0.00005000 BTC.") + .size(20) + .padding(10), + ) + .width(Length::Fill), + ), ) + .padding(20) + .style(theme::Card::Simple) + .into() } fn coin_list_view<'a>( @@ -196,37 +222,28 @@ fn coin_list_view<'a>( blockheight: u32, selected: bool, ) -> Element<'a, Message> { - Container::new( - Button::new( + Row::new() + .push( Row::new() - .push( - Row::new() - .push(if selected { - icon::square_check_icon() - } else { - icon::square_icon() - }) - .push(badge::coin()) - .push(if coin.spend_info.is_some() { - badge::spent() - } else if coin.block_height.is_none() { - badge::unconfirmed() - } else { - let seq = remaining_sequence(coin, blockheight, timelock); - coins::coin_sequence_label(seq, timelock as u32) - }) - .spacing(10) - .align_items(Alignment::Center) - .width(Length::Fill), - ) - .push(amount(&coin.amount)) + .push(checkbox("", selected, move |_| { + Message::CreateSpend(CreateSpendMessage::SelectCoin(i)) + })) + .push(if coin.spend_info.is_some() { + badge::spent() + } else if coin.block_height.is_none() { + badge::unconfirmed() + } else { + let seq = remaining_sequence(coin, blockheight, timelock); + coins::coin_sequence_label(seq, timelock as u32) + }) + .spacing(10) .align_items(Alignment::Center) - .spacing(20), + .width(Length::Fill), ) - .padding(10) - .on_press(Message::CreateSpend(CreateSpendMessage::SelectCoin(i))) - .style(theme::Button::TransparentBorder), - ) - .style(theme::Container::Card(theme::Card::Simple)) - .into() + .push(amount(&coin.amount)) + // give some space for the scroll bar without using padding + .push(Space::with_width(Length::Units(0))) + .align_items(Alignment::Center) + .spacing(20) + .into() } diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 3a853720..f42c72cc 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -418,20 +418,30 @@ pub struct CheckBox {} impl checkbox::StyleSheet for Theme { type Style = CheckBox; - fn active(&self, _style: &Self::Style, _is_selected: bool) -> checkbox::Appearance { - checkbox::Appearance { - background: iced::Color::TRANSPARENT.into(), - border_width: 1.0, - border_color: color::GREY_7, - checkmark_color: color::GREEN, - text_color: None, - border_radius: 0.0, + fn active(&self, _style: &Self::Style, is_selected: bool) -> checkbox::Appearance { + if is_selected { + checkbox::Appearance { + background: color::GREEN.into(), + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + checkmark_color: color::GREY_4, + text_color: None, + border_radius: 4.0, + } + } else { + checkbox::Appearance { + background: color::GREY_4.into(), + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + checkmark_color: color::GREEN, + text_color: None, + border_radius: 4.0, + } } } fn hovered(&self, style: &Self::Style, is_selected: bool) -> checkbox::Appearance { - let active = self.active(style, is_selected); - checkbox::Appearance { ..active } + self.active(style, is_selected) } }