Merge #1198: GUI: Refactor amount formatting

59035060e9b2b911809fdc225bfa862244f5f6d6 gui: refactor amount formatting (Aaron Carlucci)
c6b8133286a4009cc1e72935e30af36d43705237 gui: add space separator to amount integer (Aaron Carlucci)

Pull request description:

  This PR is based on #1182 and aims to actually refactor the amount component while adding a space separator to the integer portion of BTC amount displays as per #962.

  The code organization is reworked to try and separate the conversion of `bitcoin::Amount` type to a `iced` renderable `Row` elements in steps via smaller functions by:

  1. Converting the `Amount` type to a string with the integer and fraction portions formatted in space-separated three digit chunks.
  2. Detecting where in that string the non-zero BTC amount occurs
  3. Converting the string into parts and rendering the preceding zeros and spaces with normal styling while applying a bold `Row` render element for the significant amount.

  To me, this approach makes the code easier to read and is a small step toward separating the string formatting functionality from the render element building. If we find more bugs with this approach, or the team simply doesn't like the reorganization, it's fine to scrap it, as it was a decent learning exercise anyway. Looking forward to feedback.

ACKs for top commit:
  edouardparis:
    ACK 59035060e9b2b911809fdc225bfa862244f5f6d6

Tree-SHA512: d71b94cf261cf799891239da3795dae49d2b22ebc888b2d3400fbf754f84df6620fcdeb6b6594ef99e4de8a67089181ac8598bdc7a4aba3e71c16d89ccb63fae
This commit is contained in:
edouardparis 2024-07-29 18:45:22 +02:00
commit 0de58cf3b7
No known key found for this signature in database
GPG Key ID: E65F7A089C20DC8F

View File

@ -3,73 +3,82 @@ 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> {
render_unconfirmed_amount(amount_as_string(*a), 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 amount.split_once('.') {
Some((i, f)) => (i, f),
None => (amount.as_str(), "00000000"),
};
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::<Vec<_>>()
.rchunks(3)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<_>>();
part.reverse();
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 sats = format!("{:.8}", a.to_btc());
assert!(sats.len() >= 9);
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(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()
});
.push(text(before).size(size).style(color::GREY_3))
.push(text(after).size(size).bold());
Row::with_children(vec![
row.into(),
@ -79,25 +88,39 @@ pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a,
.align_items(iced::Alignment::Center)
}
fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into<Element<'a, T>> {
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))
});
}
}
if bold {
Row::new().push(text(s).bold().size(size))
} else {
Row::new().push(text(s).size(size))
// 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())
);
}
}