From 4576615f5282eaeeb0f02eb5d2650763c05e9865 Mon Sep 17 00:00:00 2001 From: Ron Nakamoto <14256602+ronnakamoto@users.noreply.github.com> Date: Sat, 17 May 2025 20:18:54 +0530 Subject: [PATCH 1/2] feat: updated transfer ui with a cleaner look --- app/gui/layout_style.rs | 72 +++++++ app/gui/mod.rs | 1 + app/gui/parent_chain/transfer.rs | 345 ++++++++++++++++++------------- 3 files changed, 276 insertions(+), 142 deletions(-) create mode 100644 app/gui/layout_style.rs diff --git a/app/gui/layout_style.rs b/app/gui/layout_style.rs new file mode 100644 index 0000000..f47da21 --- /dev/null +++ b/app/gui/layout_style.rs @@ -0,0 +1,72 @@ +//! Layout styling for the Thunder UI + +use eframe::egui::{self, Color32, Response, Ui}; + +/// Layout dimensions +pub struct LayoutDimensions; + +impl LayoutDimensions { + pub const CONTAINER_PADDING: f32 = 16.0; + pub const SECTION_SPACING: f32 = 24.0; + pub const ELEMENT_SPACING: f32 = 8.0; + pub const SMALL_SPACING: f32 = 4.0; +} + +/// Layout colors +pub struct LayoutColors; + +impl LayoutColors { + pub const BACKGROUND: Color32 = Color32::from_rgb(0xF5, 0xF5, 0xF7); + pub const CARD_BACKGROUND: Color32 = Color32::from_rgb(0xFF, 0xFF, 0xFF); + pub const BORDER: Color32 = Color32::from_rgb(0xD2, 0xD2, 0xD7); +} + +/// Layout UI extensions +pub trait LayoutUiExt { + /// Create a card container with proper spacing and borders + fn layout_card(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> R; + + /// Add a section heading + fn layout_heading(&mut self, text: &str) -> Response; +} + +impl LayoutUiExt for Ui { + fn layout_card(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + let frame = egui::Frame::none() + .fill(LayoutColors::CARD_BACKGROUND) + .stroke(egui::Stroke::new(1.0, LayoutColors::BORDER)) + .inner_margin(LayoutDimensions::CONTAINER_PADDING); + + frame.show(self, add_contents).inner + } + + fn layout_heading(&mut self, text: &str) -> Response { + self.add_space(LayoutDimensions::ELEMENT_SPACING); + let response = self.heading(text); + self.add_space(LayoutDimensions::ELEMENT_SPACING); + response + } +} + +/// Helper functions for layout +pub struct LayoutHelpers; + +impl LayoutHelpers { + /// Create a BTC input field with label + pub fn btc_input_field(ui: &mut Ui, value: &mut String, label: &str, hint: &str) -> Response { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.label(label); + ui.add_space(LayoutDimensions::SMALL_SPACING); + + ui.horizontal(|ui| { + let text_edit = egui::TextEdit::singleline(value) + .hint_text(hint) + .desired_width(ui.available_width() - 40.0); + + let response = ui.add(text_edit); + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.label("BTC"); + response + }).inner + } +} diff --git a/app/gui/mod.rs b/app/gui/mod.rs index df3dedb..66d7399 100644 --- a/app/gui/mod.rs +++ b/app/gui/mod.rs @@ -11,6 +11,7 @@ mod block_explorer; mod coins; mod console_logs; mod fonts; +mod layout_style; mod mempool_explorer; mod miner; mod parent_chain; diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs index f64ee66..2185742 100644 --- a/app/gui/parent_chain/transfer.rs +++ b/app/gui/parent_chain/transfer.rs @@ -1,6 +1,9 @@ -use eframe::egui::{self, Button}; +use eframe::egui; -use crate::app::App; +use crate::{ + app::App, + gui::layout_style::{LayoutColors, LayoutDimensions, LayoutHelpers, LayoutUiExt}, +}; #[derive(Debug, Default)] pub struct Deposit { @@ -10,53 +13,60 @@ pub struct Deposit { impl Deposit { pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - ui.add_sized((110., 10.), |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - let amount_edit = egui::TextEdit::singleline(&mut self.amount) - .hint_text("amount") - .desired_width(80.); - ui.add(amount_edit); - ui.label("BTC"); - }) - .response - }); - ui.add_sized((110., 10.), |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - let fee_edit = egui::TextEdit::singleline(&mut self.fee) - .hint_text("fee") - .desired_width(80.); - ui.add(fee_edit); - ui.label("BTC"); - }) - .response - }); - let amount = bitcoin::Amount::from_str_in( - &self.amount, - bitcoin::Denomination::Bitcoin, - ); - let fee = bitcoin::Amount::from_str_in( - &self.fee, - bitcoin::Denomination::Bitcoin, - ); - - if ui - .add_enabled( - app.is_some() && amount.is_ok() && fee.is_ok(), - egui::Button::new("deposit"), - ) - .clicked() - { - let app = app.unwrap(); - if let Err(err) = app.deposit( - app.wallet.get_new_address().expect("should not happen"), - amount.expect("should not happen"), - fee.expect("should not happen"), - ) { - tracing::error!("{err}"); - } else { - *self = Self::default(); + ui.layout_card(|ui| { + // Amount field + LayoutHelpers::btc_input_field(ui, &mut self.amount, "Amount", "Enter amount"); + + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + + // Fee field + LayoutHelpers::btc_input_field(ui, &mut self.fee, "Fee", "Enter fee"); + + // Parse values + let amount = bitcoin::Amount::from_str_in( + &self.amount, + bitcoin::Denomination::Bitcoin, + ); + + // Show validation feedback for amount + if !self.amount.is_empty() && amount.is_err() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid amount format" + ); } - } + + let fee = bitcoin::Amount::from_str_in( + &self.fee, + bitcoin::Denomination::Bitcoin, + ); + + // Show validation feedback for fee + if !self.fee.is_empty() && fee.is_err() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid fee format" + ); + } + + // Deposit button + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + let enabled = app.is_some() && amount.is_ok() && fee.is_ok(); + if ui.add_enabled(enabled, egui::Button::new("Deposit")).clicked() { + let app = app.unwrap(); + if let Err(err) = app.deposit( + app.wallet.get_new_address().expect("should not happen"), + amount.expect("should not happen"), + fee.expect("should not happen"), + ) { + tracing::error!("{err}"); + } else { + *self = Self::default(); + } + } + }); } } @@ -89,17 +99,28 @@ fn create_withdrawal( impl Withdrawal { pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - ui.add_sized((250., 10.), |ui: &mut egui::Ui| { + ui.layout_card(|ui| { + // Mainchain address field with generate button + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.label("Mainchain Address"); + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.horizontal(|ui| { - let mainchain_address_edit = - egui::TextEdit::singleline(&mut self.mainchain_address) - .hint_text("mainchain address") - .desired_width(150.); - ui.add(mainchain_address_edit); - if ui - .add_enabled(app.is_some(), Button::new("generate")) - .clicked() - { + // Set a maximum length of 62 characters for Bitcoin addresses + // This covers all Bitcoin address formats (P2PKH, P2SH, Bech32, etc.) + if self.mainchain_address.len() > 62 { + self.mainchain_address.truncate(62); + } + + let text_edit = egui::TextEdit::singleline(&mut self.mainchain_address) + .hint_text("Enter mainchain address") + .desired_width(ui.available_width() - 100.0) + .char_limit(62); // Enforce the 62 character limit + + ui.add(text_edit); + ui.add_space(LayoutDimensions::SMALL_SPACING); + + if ui.add_enabled(app.is_some(), egui::Button::new("Generate")).clicked() { match app.unwrap().get_new_main_address() { Ok(main_address) => { self.mainchain_address = main_address.to_string(); @@ -110,79 +131,101 @@ impl Withdrawal { } }; } - }) - .response - }); - ui.add_sized((110., 10.), |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - let amount_edit = egui::TextEdit::singleline(&mut self.amount) - .hint_text("amount") - .desired_width(80.); - ui.add(amount_edit); - ui.label("BTC"); - }) - .response - }); - ui.add_sized((110., 10.), |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - let fee_edit = egui::TextEdit::singleline(&mut self.fee) - .hint_text("fee") - .desired_width(80.); - ui.add(fee_edit); - ui.label("BTC"); - }) - .response - }); - ui.add_sized((110., 10.), |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - let fee_edit = - egui::TextEdit::singleline(&mut self.mainchain_fee) - .hint_text("mainchain fee") - .desired_width(80.); - ui.add(fee_edit); - ui.label("BTC"); - }) - .response - }); - let mainchain_address: Option< - bitcoin::Address, - > = self.mainchain_address.parse().ok(); - let amount = bitcoin::Amount::from_str_in( - &self.amount, - bitcoin::Denomination::Bitcoin, - ); - let fee = bitcoin::Amount::from_str_in( - &self.fee, - bitcoin::Denomination::Bitcoin, - ); - let mainchain_fee = bitcoin::Amount::from_str_in( - &self.mainchain_fee, - bitcoin::Denomination::Bitcoin, - ); - - if ui - .add_enabled( - app.is_some() - && mainchain_address.is_some() - && amount.is_ok() - && fee.is_ok() - && mainchain_fee.is_ok(), - egui::Button::new("withdraw"), - ) - .clicked() - { - if let Err(err) = create_withdrawal( - app.unwrap(), - mainchain_address.expect("should not happen"), - amount.expect("should not happen"), - fee.expect("should not happen"), - mainchain_fee.expect("should not happen"), - ) { - tracing::error!("{err:#}"); - } else { - *self = Self::default(); + }); + + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + + // Amount field + LayoutHelpers::btc_input_field(ui, &mut self.amount, "Amount", "Enter amount"); + + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + + // Fee field + LayoutHelpers::btc_input_field(ui, &mut self.fee, "Fee", "Enter fee"); + + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + + // Mainchain fee field + LayoutHelpers::btc_input_field(ui, &mut self.mainchain_fee, "Mainchain Fee", "Enter mainchain fee"); + + // Parse values + let mainchain_address: Option< + bitcoin::Address, + > = self.mainchain_address.parse().ok(); + + // Show validation feedback for the mainchain address + if !self.mainchain_address.is_empty() && mainchain_address.is_none() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid Bitcoin address format" + ); + } + + let amount = bitcoin::Amount::from_str_in( + &self.amount, + bitcoin::Denomination::Bitcoin, + ); + + // Show validation feedback for amount + if !self.amount.is_empty() && amount.is_err() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid amount format" + ); + } + + let fee = bitcoin::Amount::from_str_in( + &self.fee, + bitcoin::Denomination::Bitcoin, + ); + + // Show validation feedback for fee + if !self.fee.is_empty() && fee.is_err() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid fee format" + ); + } + + let mainchain_fee = bitcoin::Amount::from_str_in( + &self.mainchain_fee, + bitcoin::Denomination::Bitcoin, + ); + + // Show validation feedback for mainchain fee + if !self.mainchain_fee.is_empty() && mainchain_fee.is_err() { + ui.add_space(LayoutDimensions::SMALL_SPACING); + ui.colored_label( + egui::Color32::from_rgb(255, 0, 0), + "Invalid mainchain fee format" + ); } - } + + // Withdraw button + ui.add_space(LayoutDimensions::ELEMENT_SPACING); + let enabled = app.is_some() + && mainchain_address.is_some() + && amount.is_ok() + && fee.is_ok() + && mainchain_fee.is_ok(); + + if ui.add_enabled(enabled, egui::Button::new("Withdraw")).clicked() { + if let Err(err) = create_withdrawal( + app.unwrap(), + mainchain_address.expect("should not happen"), + amount.expect("should not happen"), + fee.expect("should not happen"), + mainchain_fee.expect("should not happen"), + ) { + tracing::error!("{err:#}"); + } else { + *self = Self::default(); + } + } + }); } } @@ -194,20 +237,38 @@ pub(super) struct Transfer { impl Transfer { pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - egui::SidePanel::left("deposit") - .exact_width(ui.available_width() / 2.) - .resizable(false) - .show_inside(ui, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Deposit"); + // Set background color for the entire panel + let frame = egui::Frame::none() + .fill(LayoutColors::BACKGROUND) + .inner_margin(LayoutDimensions::CONTAINER_PADDING); + + frame.show(ui, |ui| { + // Add heading for the entire screen + ui.layout_heading("Transfer"); + + // Create a horizontal layout with spacing between panels + ui.horizontal(|ui| { + // Left panel (Deposit) + ui.vertical(|ui| { + let available_width = ui.available_width() / 2.0 - LayoutDimensions::ELEMENT_SPACING; + ui.set_width(available_width); + + ui.layout_heading("Deposit"); self.deposit.show(app, ui); - }) + }); + + // Add spacing between panels + ui.add_space(LayoutDimensions::SECTION_SPACING); + + // Right panel (Withdrawal) + ui.vertical(|ui| { + let available_width = ui.available_width(); + ui.set_width(available_width); + + ui.layout_heading("Withdrawal"); + self.withdrawal.show(app, ui); + }); }); - egui::CentralPanel::default().show_inside(ui, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Withdrawal"); - self.withdrawal.show(app, ui); - }) }); } } From 7e88a40aea6ca67fbce13970eb1e764dea137596 Mon Sep 17 00:00:00 2001 From: Ron Nakamoto <14256602+ronnakamoto@users.noreply.github.com> Date: Sat, 17 May 2025 20:46:18 +0530 Subject: [PATCH 2/2] chore: removed the transfer heading --- app/gui/parent_chain/transfer.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs index 2185742..4a808e5 100644 --- a/app/gui/parent_chain/transfer.rs +++ b/app/gui/parent_chain/transfer.rs @@ -243,9 +243,6 @@ impl Transfer { .inner_margin(LayoutDimensions::CONTAINER_PADDING); frame.show(ui, |ui| { - // Add heading for the entire screen - ui.layout_heading("Transfer"); - // Create a horizontal layout with spacing between panels ui.horizontal(|ui| { // Left panel (Deposit)