Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d34ca91
feat: amount input, first building version
lklimek Jul 24, 2025
dc665be
test(amount): fixed tests
lklimek Jul 24, 2025
0478847
chore: I think final
lklimek Jul 24, 2025
589cfb5
chore: my_tokens display correct amount
lklimek Jul 24, 2025
1585a93
chore: transfer tokens update
lklimek Jul 24, 2025
317add9
chore: hide unit on rewards estimate column
lklimek Jul 28, 2025
ed4f2af
chore: two new helper methods
lklimek Jul 28, 2025
f9cd9a9
chore: I think finals
lklimek Jul 28, 2025
3c36bc0
cargo fmt
lklimek Jul 28, 2025
212e3fa
feat: component trait
lklimek Jul 28, 2025
1b07901
impl Component for AmountInput
lklimek Jul 28, 2025
f2009cb
chore: updated component trait
lklimek Jul 28, 2025
8adb4b9
chore: update for egui enabled state mgmt
lklimek Jul 28, 2025
80cf70e
doc: component design pattern doc
lklimek Jul 28, 2025
4289d21
chore: component design pattern continued
lklimek Jul 28, 2025
ed47168
chore: amount improvements
lklimek Jul 28, 2025
4bdf017
chore: copilot review
lklimek Jul 28, 2025
5bc61f3
chore: amount improvements
lklimek Jul 28, 2025
39d329e
refactor: mint and burn token screens use AmountInput
lklimek Jul 28, 2025
7f6efe3
chore: use AmountInput on token creator screen
lklimek Jul 28, 2025
9ba74e2
fix: burn error handling
lklimek Jul 28, 2025
966843c
feat: errors displayed in the AmountInput component
lklimek Jul 29, 2025
879d6e3
fix: vertical align of amount input
lklimek Jul 29, 2025
51cd7a2
backport: amount component from
lklimek Jul 29, 2025
3636d41
chore: fix imports
lklimek Jul 29, 2025
e89ad0b
chore: refactor
lklimek Jul 29, 2025
c074249
chore: futher refactor
lklimek Jul 29, 2025
8710d44
chore: further refactor based on feedback
lklimek Jul 29, 2025
73c46b3
doc: simplified component design pattern description
lklimek Jul 29, 2025
463e82c
chore: peer review
lklimek Jul 30, 2025
d02c637
doc: update docs
lklimek Jul 30, 2025
a172f03
chore: amount input
lklimek Jul 30, 2025
5baf05a
Merge branch 'refactor/amount-input' into refactor/amount-in-mint-burn
lklimek Jul 31, 2025
616e1d1
Merge remote-tracking branch 'origin/v1.0-dev' into refactor/amount-i…
lklimek Jul 31, 2025
77c1dff
chore: fixes after merge
lklimek Jul 31, 2025
8f06a14
chore: self-review
lklimek Jul 31, 2025
aad042b
feat: amout set decimal places + rename label => with_label
lklimek Jul 31, 2025
dcb235b
refactor: amount input init on token screen
lklimek Jul 31, 2025
20974ed
chore: fix token creator layout
lklimek Jul 31, 2025
63b3e81
chore: format base amount with leading zeros in confirmation
lklimek Jul 31, 2025
571826e
chore: base supply 0 by default
lklimek Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend_task/system_task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl AppContext {
theme_mode: ThemeMode,
) -> Result<BackendTaskSuccessResult, String> {
let _guard = self.invalidate_settings_cache();

self.db
.update_theme_preference(theme_mode)
.map_err(|e| e.to_string())?;
Expand Down
192 changes: 126 additions & 66 deletions src/model/amount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,11 @@ impl PartialEq<TokenAmount> for &Amount {

impl Display for Amount {
/// Formats the TokenValue as a user-friendly string with optional unit name.
///
/// See [`Amount::to_string_opts()`] for more formatting options.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let amount_str = self.to_string_without_unit();

match &self.unit_name {
Some(unit) => write!(f, "{} {}", amount_str, unit),
None => write!(f, "{}", amount_str),
}
let amount_str = self.to_string_opts(true, true);
write!(f, "{}", amount_str)
}
}

Expand Down Expand Up @@ -88,8 +86,8 @@ impl Amount {
/// This extracts the decimal places and token alias from the token configuration
/// and creates an Amount with the specified value.
pub fn from_token(
value: TokenAmount,
token_info: &crate::ui::tokens::tokens_screen::IdentityTokenInfo,
value: TokenAmount,
) -> Self {
let decimal_places = token_info.token_config.conventions().decimals();
Self::new(value, decimal_places).with_unit_name(&token_info.token_alias)
Expand Down Expand Up @@ -221,7 +219,12 @@ impl Amount {

/// Sets the unit name.
pub fn with_unit_name(mut self, unit_name: &str) -> Self {
self.unit_name = Some(unit_name.to_string());
if unit_name.is_empty() {
self.unit_name = None;
} else {
self.unit_name = Some(unit_name.to_string());
}

self
}

Expand All @@ -232,25 +235,53 @@ impl Amount {
}

/// Returns the numeric string representation without the unit name.
/// Trailing zeroes are trimmed by default.
/// This is useful for text input fields where only the number should be shown.
///
/// ## See also
///
/// [`Amount::to_string_opts()`] for more formatting options.
pub fn to_string_without_unit(&self) -> String {
if self.decimal_places == 0 {
self.value.to_string()
} else {
let divisor = 10u64.pow(self.decimal_places as u32);
let whole = self.value / divisor;
let fraction = self.value % divisor;

if fraction == 0 {
whole.to_string()
} else {
// Format with the appropriate number of decimal places, removing trailing zeros
let fraction_str =
format!("{:0width$}", fraction, width = self.decimal_places as usize);
let trimmed = fraction_str.trim_end_matches('0');
format!("{}.{}", whole, trimmed)
self.to_string_opts(false, true)
}

/// Formats the Amount as a string with options for unit display and trailing zeroes.
pub fn to_string_opts(&self, show_unit: bool, trim_trailing_zeroes: bool) -> String {
let mut result = String::new();

let divisor = 10u64.pow(self.decimal_places as u32);
let whole = self.value / divisor;
let fraction = self.value % divisor;

// "123"
result.push_str(&whole.to_string());

if self.decimal_places != 0 {
// "123.0000"
result.push_str(&format!(
".{:0width$}",
fraction,
width = self.decimal_places as usize
));

if trim_trailing_zeroes {
// Remove trailing zeros
// "123."
result = result.trim_end_matches('0').to_string();
}
// "123"
result = result.trim_end_matches('.').to_string();
};

if show_unit
&& let Some(unit_name) = self.unit_name.as_ref()
&& !unit_name.is_empty()
{
result.push(' ');
result.push_str(unit_name);
}

result
}

/// Creates a new Amount with the specified value in TokenAmount.
Expand All @@ -259,29 +290,6 @@ impl Amount {
self
}

/// Updates the decimal places for this amount.
/// This adjusts the internal value to maintain the same displayed amount.
///
/// If new decimal places are equal to the current ones, it does nothing.
pub fn recalculate_decimal_places(mut self, new_decimal_places: u8) -> Self {
if self.decimal_places != new_decimal_places {
let current_decimals = self.decimal_places;

if new_decimal_places > current_decimals {
// More decimal places - multiply value
let factor = 10u64.pow((new_decimal_places - current_decimals) as u32);
self.value = self.value.saturating_mul(factor);
} else if new_decimal_places < current_decimals {
// Fewer decimal places - divide value
let factor = 10u64.pow((current_decimals - new_decimal_places) as u32);
self.value /= factor;
}

self.decimal_places = new_decimal_places;
}
self
}

/// Checks if the amount is for the same token as the other amount.
///
/// This is determined by comparing the unit names and decimal places.
Expand Down Expand Up @@ -668,25 +676,77 @@ mod tests {
}

#[test]
fn test_decimal_places_conversion() {
// Test converting from 2 decimal places to 8 decimal places
let amount = Amount::new(12345, 2); // 123.45
let converted = amount.recalculate_decimal_places(8);
assert_eq!(converted.value(), 12345000000); // 123.45 with 8 decimals
assert_eq!(converted.decimal_places(), 8);
assert_eq!(format!("{}", converted), "123.45");

// Test converting from 8 decimal places to 2 decimal places
let amount = Amount::new(12345000000, 8); // 123.45
let converted = amount.recalculate_decimal_places(2);
assert_eq!(converted.value(), 12345); // 123.45 with 2 decimals
assert_eq!(converted.decimal_places(), 2);
assert_eq!(format!("{}", converted), "123.45");

// Test no conversion (same decimal places)
let amount = Amount::new(12345, 2);
let same = amount.clone().recalculate_decimal_places(2);
assert_eq!(same.value(), 12345);
assert_eq!(same.decimal_places(), 2);
fn test_to_string_opts() {
// Test basic formatting options with 2 decimal places
let amount = Amount::new(12345, 2).with_unit_name("USD");

// Test all combinations of show_unit and trim_trailing_zeroes
assert_eq!(amount.to_string_opts(true, true), "123.45 USD"); // show unit, trim zeros
assert_eq!(amount.to_string_opts(false, true), "123.45"); // no unit, trim zeros
assert_eq!(amount.to_string_opts(true, false), "123.45 USD"); // show unit, no trim (same as above since no trailing zeros)
assert_eq!(amount.to_string_opts(false, false), "123.45"); // no unit, no trim (same as above since no trailing zeros)

// Test with trailing zeros
let amount_with_zeros = Amount::new(12300, 2).with_unit_name("USD");
assert_eq!(amount_with_zeros.to_string_opts(true, true), "123 USD"); // show unit, trim zeros
assert_eq!(amount_with_zeros.to_string_opts(false, true), "123"); // no unit, trim zeros
assert_eq!(amount_with_zeros.to_string_opts(true, false), "123.00 USD"); // show unit, no trim
assert_eq!(amount_with_zeros.to_string_opts(false, false), "123.00"); // no unit, no trim

// Test with partial trailing zeros
let amount_partial_zeros = Amount::new(12340, 2).with_unit_name("USD");
assert_eq!(amount_partial_zeros.to_string_opts(true, true), "123.4 USD"); // show unit, trim zeros
assert_eq!(amount_partial_zeros.to_string_opts(false, true), "123.4"); // no unit, trim zeros
assert_eq!(
amount_partial_zeros.to_string_opts(true, false),
"123.40 USD"
); // show unit, no trim
assert_eq!(amount_partial_zeros.to_string_opts(false, false), "123.40"); // no unit, no trim

// Test with 0 decimal places
let whole_amount = Amount::new(123, 0).with_unit_name("WHOLE");
assert_eq!(whole_amount.to_string_opts(true, true), "123 WHOLE");
assert_eq!(whole_amount.to_string_opts(false, true), "123");
assert_eq!(whole_amount.to_string_opts(true, false), "123 WHOLE");
assert_eq!(whole_amount.to_string_opts(false, false), "123");

// Test with high decimal places
let high_precision = Amount::new(123456789, 8).with_unit_name("BTC");
assert_eq!(high_precision.to_string_opts(true, true), "1.23456789 BTC"); // trim zeros
assert_eq!(high_precision.to_string_opts(false, true), "1.23456789"); // trim zeros
assert_eq!(high_precision.to_string_opts(true, false), "1.23456789 BTC"); // no trim (same as above since no trailing zeros)
assert_eq!(high_precision.to_string_opts(false, false), "1.23456789"); // no trim (same as above since no trailing zeros)

// Test with high decimal places and trailing zeros
let high_precision_zeros = Amount::new(100000000, 8).with_unit_name("BTC");
assert_eq!(high_precision_zeros.to_string_opts(true, true), "1 BTC"); // trim zeros
assert_eq!(high_precision_zeros.to_string_opts(false, true), "1"); // trim zeros
assert_eq!(
high_precision_zeros.to_string_opts(true, false),
"1.00000000 BTC"
); // no trim
assert_eq!(
high_precision_zeros.to_string_opts(false, false),
"1.00000000"
); // no trim

// Test zero amount
let zero_amount = Amount::new(0, 4).with_unit_name("TOKEN");
assert_eq!(zero_amount.to_string_opts(true, true), "0 TOKEN");
assert_eq!(zero_amount.to_string_opts(false, true), "0");
assert_eq!(zero_amount.to_string_opts(true, false), "0.0000 TOKEN");
assert_eq!(zero_amount.to_string_opts(false, false), "0.0000");

// Test amount without unit name
let no_unit = Amount::new(12345, 3);
assert_eq!(no_unit.to_string_opts(true, true), "12.345"); // show_unit=true but no unit name
assert_eq!(no_unit.to_string_opts(false, true), "12.345"); // show_unit=false
assert_eq!(no_unit.to_string_opts(true, false), "12.345"); // show_unit=true but no unit name, no trim
assert_eq!(no_unit.to_string_opts(false, false), "12.345"); // show_unit=false, no trim

// Test amount with empty unit name (should be treated as no unit)
let empty_unit = Amount::new(12345, 2).with_unit_name("");
assert_eq!(empty_unit.to_string_opts(true, true), "123.45"); // empty unit name should not show
assert_eq!(empty_unit.to_string_opts(false, true), "123.45");
}
}
56 changes: 44 additions & 12 deletions src/ui/components/amount_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ impl AmountInput {
show_max_button: false,
desired_width: None,
show_validation_errors: true, // Default to showing validation errors
changed: false,
changed: true, // Start as changed to force initial validation
}
}

Expand All @@ -135,13 +135,36 @@ impl AmountInput {
self.decimal_places
}

/// Update decimal places used to render values.
///
/// Value displayed in the input is not changed, but the actual [Amount]
/// will be multiplied or divided by 10^(difference of decimal places).
///
/// ## Example
///
/// The input contains `12.34` and decimal places is set to 3.
/// It will be interpreted as `12.340` when parsed (credits value `12_340`).
///
///
/// If you change the decimal places from 3 to 5:
///
/// * The input will still display `12.34` (unchanged)
/// * The next time the input is parsed, it will generate `12.34000`
/// (credits value `1_234_000`).
pub fn set_decimal_places(&mut self, decimal_places: u8) -> &mut Self {
self.decimal_places = decimal_places;
self.changed = true;

self
}

/// Gets the unit name this input is configured for.
pub fn unit_name(&self) -> Option<&str> {
self.unit_name.as_deref()
}

/// Sets the label for the input field.
pub fn label<T: Into<WidgetText>>(mut self, label: T) -> Self {
pub fn with_label<T: Into<WidgetText>>(mut self, label: T) -> Self {
self.label = Some(label.into());
self
}
Expand All @@ -154,7 +177,7 @@ impl AmountInput {
}

/// Sets the hint text for the input field.
pub fn hint_text<T: Into<WidgetText>>(mut self, hint_text: T) -> Self {
pub fn with_hint_text<T: Into<WidgetText>>(mut self, hint_text: T) -> Self {
self.hint_text = Some(hint_text.into());
self
}
Expand All @@ -167,7 +190,7 @@ impl AmountInput {

/// Sets the maximum amount allowed. If provided, a "Max" button will be shown
/// when `show_max_button` is true.
pub fn max_amount(mut self, max_amount: Option<Credits>) -> Self {
pub fn with_max_amount(mut self, max_amount: Option<Credits>) -> Self {
self.max_amount = max_amount;
self
}
Expand All @@ -181,7 +204,7 @@ impl AmountInput {

/// Sets the minimum amount allowed. Defaults to 1 (must be greater than zero).
/// Set to Some(0) to allow zero amounts, or None to disable minimum validation.
pub fn min_amount(mut self, min_amount: Option<Credits>) -> Self {
pub fn with_min_amount(mut self, min_amount: Option<Credits>) -> Self {
self.min_amount = min_amount;
self
}
Expand All @@ -193,7 +216,7 @@ impl AmountInput {
}

/// Whether to show a "Max" button that sets the amount to the maximum.
pub fn max_button(mut self, show: bool) -> Self {
pub fn with_max_button(mut self, show: bool) -> Self {
self.show_max_button = show;
self
}
Expand All @@ -205,7 +228,7 @@ impl AmountInput {
}

/// Sets the desired width of the input field.
pub fn desired_width(mut self, width: f32) -> Self {
pub fn with_desired_width(mut self, width: f32) -> Self {
self.desired_width = Some(width);
self
}
Expand Down Expand Up @@ -343,6 +366,15 @@ impl Component for AmountInput {
fn show(&mut self, ui: &mut Ui) -> InnerResponse<Self::Response> {
AmountInput::show_internal(self, ui)
}

fn current_value(&self) -> Option<Self::DomainType> {
// Validate the current amount string and return the parsed amount
match self.validate_amount() {
Ok(Some(amount)) => Some(amount),
Ok(None) => None, // Empty input
Err(_) => None, // Invalid input returns None
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -382,15 +414,15 @@ mod tests {
assert_eq!(input.min_amount, Some(1));

// Custom minimum
let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(1000));
let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(1000));
assert_eq!(input.min_amount, Some(1000));

// Allow zero
let input = AmountInput::new(Amount::new(0, 8)).min_amount(Some(0));
let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(Some(0));
assert_eq!(input.min_amount, Some(0));

// No minimum
let input = AmountInput::new(Amount::new(0, 8)).min_amount(None);
let input = AmountInput::new(Amount::new(0, 8)).with_min_amount(None);
assert_eq!(input.min_amount, None);
}

Expand Down Expand Up @@ -478,8 +510,8 @@ mod tests {
fn test_min_max_validation() {
let amount = Amount::new(0, 2);
let mut input = AmountInput::new(amount)
.min_amount(Some(100)) // Minimum 1.00
.max_amount(Some(10000)); // Maximum 100.00
.with_min_amount(Some(100)) // Minimum 1.00
.with_max_amount(Some(10000)); // Maximum 100.00

// Test amount below minimum
input.amount_str = "0.50".to_string(); // 50 (below min of 100)
Expand Down
8 changes: 8 additions & 0 deletions src/ui/components/component_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,12 @@ pub trait Component {
/// An [`InnerResponse`] containing the component's response data in [`InnerResponse::inner`] field.
/// [`InnerResponse::inner`] should implement [`ComponentResponse`] trait.
fn show(&mut self, ui: &mut Ui) -> InnerResponse<Self::Response>;

/// Returns the current value of the component.
///
/// Note that only valid values should be returned here.
/// If the component value is invalid, this should return `None`.
///
/// See [`ComponentResponse::current_value`] for more details.
fn current_value(&self) -> Option<Self::DomainType>;
}
Loading
Loading