From c6b8133286a4009cc1e72935e30af36d43705237 Mon Sep 17 00:00:00 2001 From: Aaron Carlucci Date: Thu, 11 Jul 2024 11:23:48 +0200 Subject: [PATCH 1/2] gui: add space separator to amount integer --- gui/ui/src/component/amount.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs index 172a1363..6eadd634 100644 --- a/gui/ui/src/component/amount.rs +++ b/gui/ui/src/component/amount.rs @@ -95,6 +95,22 @@ fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into (i, f), + None => (s.as_str(), "00 000 000"), + }; + let mut integer = integer + .chars() + .collect::>() + .rchunks(3) + .map(|c| c.iter().collect::()) + .collect::>(); + integer.reverse(); + let integer = integer.join(" "); + s = format!("{integer}.{fraction}"); + if bold { Row::new().push(text(s).bold().size(size)) } else { From 59035060e9b2b911809fdc225bfa862244f5f6d6 Mon Sep 17 00:00:00 2001 From: Aaron Carlucci Date: Tue, 16 Jul 2024 15:47:38 +0200 Subject: [PATCH 2/2] gui: refactor amount formatting --- gui/ui/src/component/amount.rs | 195 +++++++++++++++++---------------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs index 6eadd634..afe18af0 100644 --- a/gui/ui/src/component/amount.rs +++ b/gui/ui/src/component/amount.rs @@ -3,117 +3,124 @@ pub use bitcoin::Amount; use crate::{color, component::text::*, widget::*}; pub fn amount<'a, T: 'a>(a: &Amount) -> Row<'a, T> { - amount_with_size(a, P1_SIZE) + render_amount(amount_as_string(*a), P1_SIZE) } pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { - let spacing = if size > P1_SIZE { 10 } else { 5 }; - let sats = format!("{:.8}", a.to_btc()); - assert!(sats.len() >= 9); - let row = Row::new() - .spacing(spacing) - .push(split_digits(sats[0..sats.len() - 6].to_string(), size, true).into()) - .push(if a.to_sat() < 1_000_000 { - split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size, true).into() - } else { - Row::new() - .push( - text(sats[sats.len() - 6..sats.len() - 3].to_string()) - .bold() - .size(size), - ) - .into() - }) - .push(if a.to_sat() < 1000 { - split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, true).into() - } else { - Row::new() - .push( - text(sats[sats.len() - 3..sats.len()].to_string()) - .bold() - .size(size), - ) - .into() - }); - - Row::with_children(vec![ - row.into(), - text("BTC").size(size).style(color::GREY_3).into(), - ]) - .spacing(spacing) - .align_items(iced::Alignment::Center) + render_amount(amount_as_string(*a), size) } pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { - let spacing = if size > P1_SIZE { 10 } else { 5 }; - let sats = format!("{:.8}", a.to_btc()); - assert!(sats.len() >= 9); - let row = Row::new() - .spacing(spacing) - .push(split_digits(sats[0..sats.len() - 6].to_string(), size, false).into()) - .push(if a.to_sat() < 1_000_000 { - split_digits( - sats[sats.len() - 6..sats.len() - 3].to_string(), - size, - false, - ) - .into() - } else { - Row::new() - .push(text(sats[sats.len() - 6..sats.len() - 3].to_string()).size(size)) - .into() - }) - .push(if a.to_sat() < 1000 { - split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, false).into() - } else { - Row::new() - .push(text(sats[sats.len() - 3..sats.len()].to_string()).size(size)) - .into() - }); - - Row::with_children(vec![ - row.into(), - text("BTC").size(size).style(color::GREY_3).into(), - ]) - .spacing(spacing) - .align_items(iced::Alignment::Center) + render_unconfirmed_amount(amount_as_string(*a), size) } -fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into> { - let prefixes = vec!["0.00", "0.0", "0.", "000", "00", "0"]; - for prefix in prefixes { - if s.starts_with(prefix) { - let right = s.split_off(prefix.len()); - return Row::new() - .push(text(s).size(size).style(color::GREY_3)) - .push_maybe(if right.is_empty() { - None - } else if bold { - Some(text(right).bold().size(size)) - } else { - Some(text(right).size(size)) - }); - } - } +// +// Helpers +// + +// Format a BTC amount as a string for display. +fn amount_as_string(a: Amount) -> String { + let amount = a.to_btc().to_string(); // Reformat the integer portion of the amount with space separation. - let (integer, fraction) = match s.split_once('.') { + let (integer, fraction) = match amount.split_once('.') { Some((i, f)) => (i, f), - None => (s.as_str(), "00 000 000"), + None => (amount.as_str(), "00000000"), }; - let mut integer = integer + + let integer = format_amount_number_part(integer); + let fraction = format_amount_number_part(&format!("{:0<8}", fraction)); + + format!("{integer}.{fraction}") +} + +// Format a "part" of a number string with spaces to fit display requirements. +// Currently using French formatting rules so digits are space-separated in groups +// of three, starting from the right side. Incidentally, this works for both the +// integer portion of the number as well as the fraction part. +// Ex: +// 1000 => 1 000 +// 100000 => 100 000 +fn format_amount_number_part(s: &str) -> String { + let mut part = s .chars() .collect::>() .rchunks(3) .map(|c| c.iter().collect::()) .collect::>(); - integer.reverse(); - let integer = integer.join(" "); - s = format!("{integer}.{fraction}"); + part.reverse(); - if bold { - Row::new().push(text(s).bold().size(size)) - } else { - Row::new().push(text(s).size(size)) + part.join(" ") +} + +// Helper functions split a string at the first occurence of a non-zero integer (where +// the amount starts). +fn split_at_first_non_zero(s: String) -> Option<(String, String)> { + for (index, c) in s.char_indices() { + if c.is_ascii_digit() && c != '0' { + let (before, after) = s.split_at(index); + return Some((before.to_string(), after.to_string())); + } + } + None +} + +// Build the rendering elements for displaying a Bitcoin amount. +// The text should be bolded beginning where the BTC amount is non-zero. +fn render_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> { + let spacing = if size > P1_SIZE { 10 } else { 5 }; + + let (before, after) = match split_at_first_non_zero(amount) { + Some((b, a)) => (b, a), + None => (String::from(""), String::from("")), + }; + + let row = Row::new() + .spacing(spacing) + .push(text(before).size(size).style(color::GREY_3)) + .push(text(after).size(size).bold()); + + Row::with_children(vec![ + row.into(), + text("BTC").size(size).style(color::GREY_3).into(), + ]) + .spacing(spacing) + .align_items(iced::Alignment::Center) +} + +// Build the rendering elements for displaying a Bitcoin amount. +fn render_unconfirmed_amount<'a, T: 'a>(amount: String, size: u16) -> Row<'a, T> { + let spacing = if size > P1_SIZE { 10 } else { 5 }; + + let row = Row::new() + .spacing(spacing) + .push(text(amount).size(size).style(color::GREY_3)); + + Row::with_children(vec![ + row.into(), + text("BTC").size(size).style(color::GREY_3).into(), + ]) + .spacing(spacing) + .align_items(iced::Alignment::Center) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_amount_as_str() { + assert_eq!( + "0.00 799 800", + amount_as_string(bitcoin::Amount::from_btc(0.00799800).unwrap()) + ); + assert_eq!( + "1 000.00 799 800", + amount_as_string(bitcoin::Amount::from_btc(1000.00799800).unwrap()) + ); + assert_eq!( + "1 000.00 000 000", + amount_as_string(bitcoin::Amount::from_btc(1000.0).unwrap()) + ); } }