gui: merge spend creation steps

This commit is contained in:
edouard 2023-04-24 15:27:43 +02:00
parent 4613da4ae4
commit 6b2c9a61e0
4 changed files with 401 additions and 417 deletions

View File

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

View File

@ -28,7 +28,6 @@ const DUST_OUTPUT_SATS: u64 = 5_000;
#[derive(Default, Clone)]
pub struct TransactionDraft {
inputs: Vec<Coin>,
outputs: HashMap<Address, u64>,
generated: Option<Psbt>,
}
@ -38,36 +37,77 @@ pub trait Step {
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
draft: &TransactionDraft,
message: Message,
) -> Command<Message>;
fn apply(&self, _draft: &mut TransactionDraft) {}
fn load(&mut self, _draft: &TransactionDraft) {}
}
pub struct ChooseRecipients {
pub struct DefineSpend {
balance_available: Amount,
recipients: Vec<Recipient>,
is_valid: bool,
is_duplicate: bool,
descriptor: LianaDescriptor,
timelock: u16,
coins: Vec<(Coin, bool)>,
amount_left_to_select: Option<Amount>,
feerate: form::Value<String>,
generated: Option<Psbt>,
warning: Option<Error>,
}
impl ChooseRecipients {
pub fn new(coins: &[Coin]) -> Self {
impl DefineSpend {
pub fn new(
descriptor: LianaDescriptor,
coins: Vec<Coin>,
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::<u64>() {
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 + <wit program>
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<dyn Daemon + Sync + Send>,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
_draft: &TransactionDraft,
message: Message,
) -> Command<Message> {
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::<u64>().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<OutPoint> = self
.coins
.iter()
.filter_map(
|(coin, selected)| if *selected { Some(coin.outpoint) } else { None },
)
.collect();
let mut outputs: HashMap<Address, u64> = 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::<u64>().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<Address, u64> = 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<Amount>,
feerate: form::Value<String>,
generated: Option<Psbt>,
warning: Option<Error>,
}
impl ChooseCoins {
pub fn new(
descriptor: LianaDescriptor,
coins: Vec<Coin>,
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::<u64>() {
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 + <wit program>
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<dyn Daemon + Sync + Send>,
_cache: &Cache,
draft: &TransactionDraft,
message: Message,
) -> Command<Message> {
match message {
Message::View(view::Message::CreateSpend(view::CreateSpendMessage::FeerateEdited(
s,
))) => {
if s.parse::<u64>().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<OutPoint> = 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::<u64>().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<Wallet>,
spend: Option<detail::SpendTxState>,
@ -460,7 +419,6 @@ impl Step for SaveSpend {
&mut self,
daemon: Arc<dyn Daemon + Sync + Send>,
cache: &Cache,
_draft: &TransactionDraft,
message: Message,
) -> Command<Message> {
if let Some(spend) = &mut self.spend {

View File

@ -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<Element<'a, Message>>,
total_amount: Amount,
is_valid: bool,
duplicate: bool,
timelock: u16,
coins: &[(Coin, bool)],
amount_left: Option<&Amount>,
feerate: &form::Value<String>,
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<String>,
amount: &form::Value<String>,
) -> 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<String>,
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()
}

View File

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