feat: calculate fee and maximum amount on send

Reviewed-on: https://code.gri.mw/GUI/grim/pulls/32
This commit is contained in:
ardocrat
2026-02-07 13:11:23 +00:00
parent 9bb5f1d66a
commit b54fd3251f
20 changed files with 576 additions and 270 deletions
+57 -8
View File
@@ -12,22 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use egui::{Layout, TextBuffer, TextStyle, Widget, Align, ViewportCommand};
use egui::text_edit::TextEditState;
use egui::{Align, Layout, TextBuffer, TextStyle, ViewportCommand, Widget};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::sync::Arc;
use crate::gui::Colors;
use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::input::keyboard::KeyboardContent;
use crate::gui::views::{KeyboardEvent, View};
use crate::gui::Colors;
/// Text input content.
pub struct TextEdit {
/// View identifier.
id: egui::Id,
/// Check if input is enabled or disabled.
enabled: bool,
/// Horizontal text centering is needed.
h_center: bool,
/// Focus is needed.
@@ -59,6 +61,7 @@ impl TextEdit {
pub fn new(id: egui::Id) -> Self {
Self {
id,
enabled: true,
h_center: false,
focus: true,
focus_request: false,
@@ -73,8 +76,26 @@ impl TextEdit {
}
}
/// Draw text input content.
/// Draw text input.
pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) {
self.input_ui(ui, input, |_| {}, cb);
}
/// Draw text input with additional buttons (right to left order).
pub fn custom_buttons_ui(&mut self,
ui: &mut egui::Ui,
input: &mut String,
cb: &dyn PlatformCallbacks,
buttons_content: impl FnOnce(&mut egui::Ui)) {
self.input_ui(ui, input, buttons_content, cb);
}
/// Draw text input content.
fn input_ui(&mut self,
ui: &mut egui::Ui,
input: &mut String,
buttons_content: impl FnOnce(&mut egui::Ui),
cb: &dyn PlatformCallbacks) {
let mut layout_rect = ui.available_rect_before_wrap();
layout_rect.set_height(Self::TEXT_EDIT_HEIGHT);
ui.allocate_ui_with_layout(layout_rect.size(), Layout::right_to_left(Align::Max), |ui| {
@@ -95,6 +116,9 @@ impl TextEdit {
ui.add_space(8.0);
}
// Extra buttons content.
(buttons_content)(ui);
// Setup copy button.
if self.copy {
let copy_icon = COPY.to_string();
@@ -137,6 +161,12 @@ impl TextEdit {
// Show text edit.
let text_edit_resp = egui::TextEdit::singleline(input)
.text_color(if self.enabled {
Colors::text(false)
} else {
Colors::inactive_text()
})
.interactive(self.enabled)
.id(self.id)
.font(TextStyle::Heading)
.min_size(edit_rect.size())
@@ -181,10 +211,8 @@ impl TextEdit {
}
});
});
// Repaint on Android to handle input from Java code without delays.
if is_android() {
ui.ctx().request_repaint();
}
// Immediate repaint when input is open.
ui.ctx().request_repaint();
}
/// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed.
@@ -283,6 +311,27 @@ impl TextEdit {
false
}
/// Set cursor to the end of text.
pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) {
let mut state = TextEditState::load(ui.ctx(), self.id).unwrap();
match state.cursor.char_range() {
None => {}
Some(range) => {
let mut r = range.clone();
r.primary.index = text_len;
r.secondary.index = text_len;
state.cursor.set_char_range(Some(r));
TextEditState::store(state, ui.ctx(), self.id);
}
}
}
/// Disable input.
pub fn disable(mut self) -> Self {
self.enabled = false;
self
}
/// Center text horizontally.
pub fn h_center(mut self) -> Self {
self.h_center = true;
+7 -2
View File
@@ -460,12 +460,17 @@ impl View {
});
}
/// Draw loading spinner.
pub fn loading_spinner(ui: &mut egui::Ui, size: f32) {
Spinner::new().size(size).color(Colors::gold()).ui(ui);
}
/// Size of big loading spinner.
pub const BIG_SPINNER_SIZE: f32 = 104.0;
/// Draw big gold loading spinner.
pub fn big_loading_spinner(ui: &mut egui::Ui) {
Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui);
View::loading_spinner(ui, View::BIG_SPINNER_SIZE);
}
/// Size of big loading spinner.
@@ -473,7 +478,7 @@ impl View {
/// Draw small gold loading spinner.
pub fn small_loading_spinner(ui: &mut egui::Ui) {
Spinner::new().size(30.0).color(Colors::gold()).ui(ui);
View::loading_spinner(ui, View::SMALL_SPINNER_SIZE);
}
/// Draw the button that looks like checkbox with callback on check.
+9 -4
View File
@@ -130,11 +130,16 @@ impl ContentContainer for WalletsContent {
}
fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) {
ui.ctx().request_repaint_after(if OperatingSystem::from_target_os() == OperatingSystem::Android {
Duration::from_millis(100)
let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android;
let account_list_showing = self.wallet_content.account_content.list_content.is_some();
// Small repaint delay is needed for Android back navigation and account list opening.
ui.ctx().request_repaint_after(Duration::from_millis(if account_list_showing {
10
} else if is_android {
100
} else {
Duration::from_millis(1000)
});
1000
}));
if let Some(data) = crate::consume_incoming_data() {
if !data.is_empty() {
+82 -32
View File
@@ -19,13 +19,11 @@ use grin_chain::SyncStatus;
use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::account::AccountContent;
use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent};
use crate::gui::views::wallets::wallet::transport::WalletTransportContent;
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::gui::views::wallets::wallet::WalletSettings;
use crate::gui::views::wallets::WalletTransactions;
use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent};
use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View};
use crate::gui::Colors;
use crate::node::Node;
@@ -35,8 +33,11 @@ use crate::AppConfig;
/// Wallet content.
pub struct WalletContent {
/// Current tab content to show.
current_tab: Box<dyn WalletTab>,
/// Transactions content.
pub txs_content: Option<WalletTransactionsContent>,
/// Settings content.
pub settings_content: Option<WalletSettingsContent>,
/// Account panel content.
pub account_content: AccountContent,
@@ -44,9 +45,9 @@ pub struct WalletContent {
pub transport_content: WalletTransportContent,
/// Invoice request creation [`Modal`] content.
invoice_request_content: Option<InvoiceRequestContent>,
invoice_content: Option<InvoiceRequestContent>,
/// Send request creation [`Modal`] content.
send_request_content: Option<SendRequestContent>,
send_content: Option<SendRequestContent>,
/// Tab button to pick file for parsing.
file_pick_tab_button: FilePickContent,
@@ -68,12 +69,12 @@ impl WalletContentContainer for WalletContent {
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
match m.id {
INVOICE_MODAL_ID => {
if let Some(c) = self.invoice_request_content.as_mut() {
if let Some(c) = self.invoice_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
SEND_MODAL_ID => {
if let Some(c) = self.send_request_content.as_mut() {
if let Some(c) = self.send_content.as_mut() {
c.modal_ui(ui, w, m, cb);
}
}
@@ -91,7 +92,7 @@ impl WalletContentContainer for WalletContent {
// Show wallet account panel not on settings tab when navigation is not blocked and QR code
// scanner is not showing and wallet data is not empty.
let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !block_nav
let mut show_account = self.settings_content.is_none() && !block_nav
&& !wallet.sync_error() && data.is_some();
if wallet.get_current_connection() == ConnectionMethod::Integrated &&
!Node::is_running() {
@@ -222,7 +223,7 @@ impl WalletContentContainer for WalletContent {
top: 0.0 as i8,
bottom: 4.0 as i8,
},
fill: if self.current_tab.get_type() == WalletTabType::Settings {
fill: if self.settings_content.is_some() {
Colors::fill_lite()
} else {
Colors::TRANSPARENT
@@ -231,25 +232,69 @@ impl WalletContentContainer for WalletContent {
})
.show_inside(ui, |ui| {
let rect = ui.available_rect_before_wrap();
let tab_type = self.current_tab.get_type();
let show_sync = (tab_type != WalletTabType::Settings || block_nav) &&
let show_settings = self.settings_content.is_some();
let show_txs = self.txs_content.is_some();
let show_sync = (!show_settings || block_nav) &&
sync_ui(ui, &wallet);
if !show_sync {
if tab_type != WalletTabType::Txs {
if show_settings {
ui.add_space(3.0);
ScrollArea::vertical()
.id_salt(Id::from("wallet_tab_content_scroll")
.with(tab_type.name())
.with(wallet_id))
.id_salt(Id::from("wallet_tab_content_scroll").with(wallet_id))
.auto_shrink([false; 2])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| {
self.current_tab.ui(ui, &wallet, cb);
self.settings_content
.as_mut()
.unwrap()
.ui(ui, &wallet, cb);
});
});
} else {
self.current_tab.ui(ui, &wallet, cb);
} else if show_txs {
self.txs_content
.as_mut()
.unwrap()
.ui(ui, &wallet, cb);
}
// Handle wallet task result.
if let Some((id, t)) = wallet.consume_task_result() {
match Modal::opened() {
None => {
// Show transaction modal on wallet task result.
if let Some(id) = id {
let tx = wallet.get_data().unwrap().tx_by_slate_id(id);
if tx.is_some() {
self.txs_content = Some(WalletTransactionsContent::new(tx));
self.settings_content = None;
}
}
}
Some(modal_id) => {
match modal_id {
SEND_MODAL_ID => {
match t {
WalletTask::CalculateFee(_, f) => {
// Setup calculated tx fee at modal.
if let Some(m) = self.send_content.as_mut() {
if m.max_calculating {
let data = wallet.get_data().unwrap();
let a = data.info.amount_currently_spendable;
let max = a - f;
m.on_max_amount_calculated(max, f);
} else {
m.on_fee_calculated(f);
}
}
}
_ => {}
}
}
_ => {}
}
}
}
}
}
let rect = {
@@ -276,11 +321,12 @@ impl WalletContentContainer for WalletContent {
impl Default for WalletContent {
fn default() -> Self {
Self {
current_tab: Box::new(WalletTransactions::new(None)),
txs_content: Some(WalletTransactionsContent::new(None)),
settings_content: None,
account_content: AccountContent::default(),
transport_content: WalletTransportContent::default(),
invoice_request_content: None,
send_request_content: None,
invoice_content: None,
send_content: None,
file_pick_tab_button: FilePickContent::new(FilePickContentType::Tab),
}
}
@@ -297,8 +343,10 @@ impl WalletContent {
t!("wallets.transport")
} else if self.transport_content.qr_address_content.is_some() {
t!("network_mining.address")
} else if self.settings_content.is_some() {
t!("wallets.settings")
} else {
self.current_tab.get_type().name()
t!("wallets.txs")
}
}
@@ -342,12 +390,13 @@ impl WalletContent {
let can_send = has_wallet_data &&
wallet.get_data().unwrap().info.amount_currently_spendable > 0;
let current_type = self.current_tab.get_type();
let tabs_amount = if can_send { 5 } else { 4 };
ui.columns(tabs_amount, |columns| {
columns[0].vertical_centered_justified(|ui| {
View::tab_button(ui, STACK, None, Some(current_type == WalletTabType::Txs), |_| {
self.current_tab = Box::new(WalletTransactions::new(None));
let active = self.settings_content.is_none() && self.txs_content.is_some();
View::tab_button(ui, STACK, None, Some(active), |_| {
self.txs_content = Some(WalletTransactionsContent::new(None));
self.settings_content = None;
});
});
let active = if has_wallet_data { Some(false) } else { None };
@@ -358,7 +407,7 @@ impl WalletContent {
} else {
let (icon, color) = (FILE_ARROW_DOWN, Some(Colors::green()));
View::tab_button(ui, icon, color, active, |_| {
self.invoice_request_content = Some(InvoiceRequestContent::default());
self.invoice_content = Some(InvoiceRequestContent::default());
Modal::new(INVOICE_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.receive"))
@@ -385,7 +434,7 @@ impl WalletContent {
} else {
let (icon, color) = (FILE_ARROW_UP, Some(Colors::red()));
View::tab_button(ui, icon, color, Some(false), |_| {
self.send_request_content = Some(SendRequestContent::new(None));
self.send_content = Some(SendRequestContent::new(None));
Modal::new(SEND_MODAL_ID)
.position(ModalPosition::CenterTop)
.title(t!("wallets.send"))
@@ -395,10 +444,11 @@ impl WalletContent {
});
}
columns[tabs_amount - 1].vertical_centered_justified(|ui| {
let active = Some(current_type == WalletTabType::Settings);
View::tab_button(ui, GEAR_FINE, None, active, |ui| {
let active = self.settings_content.is_some();
View::tab_button(ui, GEAR_FINE, None, Some(active), |ui| {
ExternalConnection::check(None, ui.ctx());
self.current_tab = Box::new(WalletSettings::default());
self.txs_content = None;
self.settings_content = Some(WalletSettingsContent::default());
});
});
});
+12 -12
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use egui::{Id, RichText};
use grin_core::core::amount_from_hr_string;
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, TextEdit, View};
@@ -79,21 +79,21 @@ impl InvoiceRequestContent {
if !self.amount_edit.is_empty() {
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
Ok(amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
} else {
// Check input after ".".
let parts = self.amount_edit
.split(".")
.collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && (parts[1].len() > 9 ||
(amount == 0 && parts[1].len() > 8)) {
self.amount_edit = amount_edit_before;
return;
}
}
}
+100 -20
View File
@@ -14,8 +14,8 @@
use egui::{Id, RichText};
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
use grin_core::global::get_accept_fee_base;
use grin_wallet_libwallet::SlatepackAddress;
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
use crate::gui::Colors;
@@ -24,8 +24,14 @@ use crate::wallet::Wallet;
/// Content to create a request to send funds.
pub struct SendRequestContent {
/// Amount to send or receive.
/// Amount to send.
amount_edit: String,
/// Flag to check if maximum amount is calculating.
pub max_calculating: bool,
/// Fee amount.
fee_edit: String,
/// Receiver address.
address_edit: String,
/// Flag to check if entered address is incorrect.
@@ -40,12 +46,26 @@ impl SendRequestContent {
pub fn new(addr: Option<String>) -> Self {
Self {
amount_edit: "".to_string(),
max_calculating: false,
fee_edit: "".to_string(),
address_edit: addr.unwrap_or("".to_string()),
address_error: false,
address_scan_content: None,
}
}
/// Setup fee amount.
pub fn on_fee_calculated(&mut self, fee: u64) {
self.fee_edit = amount_to_hr_string(fee, true);
}
/// Setup maximum amount to send and fee.
pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) {
self.max_calculating = false;
self.amount_edit = amount_to_hr_string(amount, true);
self.fee_edit = amount_to_hr_string(fee, true);
}
/// Draw [`Modal`] content.
pub fn modal_ui(&mut self,
ui: &mut egui::Ui,
@@ -109,45 +129,107 @@ impl SendRequestContent {
.h_center()
.numeric()
.focus(Modal::first_draw());
if self.max_calculating {
amount_edit = amount_edit.disable();
}
let amount_edit_before = self.amount_edit.clone();
amount_edit.ui(ui, &mut self.amount_edit, cb);
// Draw button to calculate maximum amount to send.
let mut calculate_max = false;
amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| {
if self.max_calculating {
ui.add_space(12.0);
View::loading_spinner(ui, 40.0);
ui.add_space(12.0);
} else {
View::button(ui, t!("max_short"), Colors::white_or_black(false), || {
calculate_max = true;
});
ui.add_space(8.0);
}
});
if calculate_max {
self.max_calculating = true;
let max = wallet.get_data().unwrap().info.amount_currently_spendable;
self.amount_edit = amount_to_hr_string(max, true);
}
ui.add_space(8.0);
// Check value if input was changed.
if amount_edit_before != self.amount_edit {
if !self.amount_edit.is_empty() {
// Trim text, replace "," by "." and parse amount.
// Trim text, replace `,` by `.` and parse amount.
self.amount_edit = self.amount_edit.trim().replace(",", ".");
match amount_from_hr_string(self.amount_edit.as_str()) {
Ok(a) => {
Ok(mut amount) => {
if !self.amount_edit.contains(".") {
// To avoid input of several "0".
if a == 0 {
self.amount_edit = "0".to_string();
return;
// To avoid input of several `0` before `.` and put `.` after first `0`.
if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") {
let amount_text = amount_to_hr_string(amount, true);
let amount_parts = amount_text.split(".").collect::<Vec<&str>>();
self.amount_edit = format!("0.{}", amount_parts[0]);
amount = amount_from_hr_string(self.amount_edit.as_str())
.unwrap_or_else(|_| amount);
amount_edit.cursor_to_end(self.amount_edit.len(), ui);
}
// Reset fee amount on `0`.
if amount == 0 {
self.fee_edit = "".to_string();
}
} else {
// Check input after `.`.
let parts = self.amount_edit.split(".").collect::<Vec<&str>>();
if parts.len() == 2 && parts[1].len() > 9 {
self.amount_edit = amount_edit_before;
return;
if parts.len() == 2 && (parts[1].len() > 9 ||
(amount == 0 && parts[1].len() > 8)) {
self.amount_edit = amount_edit_before.clone();
}
}
// Do not input amount more than balance in sending.
let b = wallet.get_data().unwrap().info.amount_currently_spendable;
if b < a {
self.amount_edit = amount_edit_before;
// Do not input amount more than balance.
if amount != 0 && self.amount_edit != amount_edit_before {
let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0);
let max = wallet.get_data().unwrap().info.amount_currently_spendable;
if amount > max || amount + fee > max {
self.max_calculating = true;
wallet.task(WalletTask::CalculateFee(max, 0));
} else {
wallet.task(WalletTask::CalculateFee(amount, 0));
}
}
}
Err(_) => {
self.amount_edit = amount_edit_before;
}
}
} else {
self.fee_edit = "".to_string();
}
}
// Show fee value.
ui.vertical_centered(|ui| {
let fee_label = t!(
"wallets.fee_base_desc",
"value" => format!(": {}", get_accept_fee_base())
);
ui.label(RichText::new(fee_label)
.size(17.0)
.color(Colors::gray()));
});
ui.add_space(6.0);
let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id);
let mut fee_edit = TextEdit::new(fee_edit_id)
.focus(false)
.h_center()
.disable();
let mut loading_label = format!("{}...", t!("wallets.loading"));
fee_edit.ui(ui, if wallet.fee_calculating() {
&mut loading_label
} else {
&mut self.fee_edit
}, cb);
ui.add_space(8.0);
// Show address error or input description.
ui.vertical_centered(|ui| {
if self.address_error {
@@ -179,12 +261,10 @@ impl SendRequestContent {
self.address_scan_content = Some(CameraContent::default());
}
ui.add_space(12.0);
// Check value if input was changed.
if addr_edit_before != self.address_edit {
self.address_error = false;
}
// Continue on Enter press.
if address_edit.enter_pressed {
self.on_continue(wallet);
@@ -211,7 +291,7 @@ impl SendRequestContent {
/// Callback when Continue button was pressed.
fn on_continue(&mut self, wallet: &Wallet) {
if self.amount_edit.is_empty() {
if self.amount_edit.is_empty() || self.max_calculating || wallet.fee_calculating() {
return;
}
// Check address to send over Tor if enabled.
@@ -15,12 +15,11 @@
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::ContentContainer;
use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings};
use crate::gui::views::wallets::types::{WalletTab, WalletTabType};
use crate::gui::views::wallets::wallet::types::WalletContentContainer;
use crate::wallet::Wallet;
/// Wallet settings tab content.
pub struct WalletSettings {
pub struct WalletSettingsContent {
/// Common setup content.
common_setup: CommonSettings,
/// Connection setup content.
@@ -29,7 +28,7 @@ pub struct WalletSettings {
recovery_setup: RecoverySettings
}
impl Default for WalletSettings {
impl Default for WalletSettingsContent {
fn default() -> Self {
Self {
common_setup: CommonSettings::default(),
@@ -39,12 +38,8 @@ impl Default for WalletSettings {
}
}
impl WalletTab for WalletSettings {
fn get_type(&self) -> WalletTabType {
WalletTabType::Settings
}
fn ui(&mut self,
impl WalletSettingsContent {
pub fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
+20 -43
View File
@@ -24,8 +24,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use crate::gui::icons::{ARCHIVE_BOX, ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::types::{LinePosition, ModalPosition};
use crate::gui::views::wallets::types::WalletTab;
use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN};
use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN};
use crate::gui::views::wallets::wallet::WalletTransactionContent;
use crate::gui::views::{Content, Modal, PullToRefresh, View};
use crate::gui::Colors;
@@ -33,7 +32,7 @@ use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTran
use crate::wallet::Wallet;
/// Wallet transactions tab content.
pub struct WalletTransactions {
pub struct WalletTransactionsContent {
/// Transaction information [`Modal`] content.
tx_info_content: Option<WalletTransactionContent>,
@@ -44,22 +43,26 @@ pub struct WalletTransactions {
manual_sync: Option<u128>
}
impl WalletTab for WalletTransactions {
fn get_type(&self) -> WalletTabType {
WalletTabType::Txs
impl WalletContentContainer for WalletTransactionsContent {
fn modal_ids(&self) -> Vec<&'static str> {
vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL]
}
fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
if Modal::opened().is_none() {
// Show transaction modal on task result.
if let Some(id) = wallet.consume_tx_task_result() {
let tx = wallet.get_data().unwrap().tx_by_slate_id(id);
if let Some(tx) = tx {
self.show_tx_info_modal(tx.data.id);
fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) {
match m.id {
TX_INFO_MODAL => {
if let Some(content) = self.tx_info_content.as_mut() {
content.ui(ui, w, m, cb);
}
}
CANCEL_TX_CONFIRMATION_MODAL => {
self.cancel_confirmation_modal(ui, w);
}
_ => {}
}
self.modal_content_ui(ui, wallet, cb);
}
fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) {
self.txs_ui(ui, wallet);
}
}
@@ -69,7 +72,7 @@ const TX_INFO_MODAL: &'static str = "tx_info_modal";
/// Identifier for transaction cancellation confirmation [`Modal`].
const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal";
impl WalletTransactions {
impl WalletTransactionsContent {
/// Height of transaction list item.
pub const TX_ITEM_HEIGHT: f32 = 75.0;
@@ -122,6 +125,7 @@ impl WalletTransactions {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
let refresh = self.manual_sync.unwrap_or(0) + 1600 > now;
let refresh_resp = PullToRefresh::new(refresh)
.id(Id::from("refresh_tx_list").with(config.id))
.can_refresh(!refresh && !wallet.syncing())
.min_refresh_distance(70.0)
.scroll_area_ui(ui, |ui| {
@@ -241,33 +245,6 @@ impl WalletTransactions {
ui.painter().set(bg_idx, bg);
}
/// Draw [`Modal`] content for this ui container.
fn modal_content_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
match Modal::opened() {
None => {}
Some(id) => {
match id {
TX_INFO_MODAL => {
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
if let Some(content) = self.tx_info_content.as_mut() {
content.ui(ui, wallet, modal, cb);
}
});
}
CANCEL_TX_CONFIRMATION_MODAL => {
Modal::ui(ui.ctx(), cb, |ui, _, _| {
self.cancel_confirmation_modal(ui, wallet);
});
}
_ => {}
}
}
}
}
/// Draw transaction item.
pub fn tx_item_ui(ui: &mut egui::Ui,
tx: &WalletTransaction,
@@ -417,7 +394,7 @@ impl WalletTransactions {
TxLogEntryType::TxSent | TxLogEntryType::TxReceived => {
let min_conf = data.info.minimum_confirmations;
if tx.height.is_none() || (tx.height.unwrap() != 0 &&
height - tx.height.unwrap() > min_conf - 1) {
height - tx.height.unwrap() >= min_conf - 1) {
let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent {
(ARROW_CIRCLE_UP, t!("wallets.tx_sent"))
} else {
+4 -4
View File
@@ -19,7 +19,7 @@ use grin_wallet_libwallet::TxLogEntryType;
use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::wallets::wallet::txs::WalletTransactions;
use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent;
use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View};
use crate::gui::Colors;
use crate::wallet::types::{WalletTask, WalletTransaction};
@@ -220,7 +220,7 @@ impl WalletTransactionContent {
ui.add_space(6.0);
let mut rect = ui.available_rect_before_wrap();
rect.set_height(WalletTransactions::TX_ITEM_HEIGHT);
rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT);
// Draw tx item background.
let p = ui.painter();
@@ -229,7 +229,7 @@ impl WalletTransactionContent {
// Show transaction amount status and time.
let data = wallet.get_data().unwrap();
WalletTransactions::tx_item_ui(ui, tx, rect, &data, |ui| {
WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| {
// Show block height or buttons.
if let Some(h) = tx.height {
if h != 0 {
@@ -281,7 +281,7 @@ impl WalletTransactionContent {
} else {
View::item_rounding(0, 2, true)
};
WalletTransactions::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast);
WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast);
}
}
});
-26
View File
@@ -42,32 +42,6 @@ pub trait WalletContentContainer {
}
}
/// Wallet tab content interface.
pub trait WalletTab {
fn get_type(&self) -> WalletTabType;
fn ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks);
}
/// Type of [`WalletTab`] content.
#[derive(PartialEq)]
pub enum WalletTabType {
Txs,
Settings
}
impl WalletTabType {
/// Name of wallet tab to show at ui.
pub fn name(&self) -> String {
match *self {
WalletTabType::Txs => t!("wallets.txs"),
WalletTabType::Settings => t!("wallets.settings")
}
}
}
/// Get wallet status text.
pub fn wallet_status_text(wallet: &Wallet) -> String {
if wallet.is_open() {