diff --git a/core/resources/i18n/i18n.json b/core/resources/i18n/i18n.json index fa09d71..1fca341 100644 --- a/core/resources/i18n/i18n.json +++ b/core/resources/i18n/i18n.json @@ -3084,6 +3084,7 @@ "12 word mnemonic": "12 word mnemonic", "24 word mnemonic": "24 word mnemonic", "24h Change": "24h Change", + "3 hours or more": "3 hours or more", "A binary at another location is spawned a child process (experimental, for development purposes only).": "A binary at another location is spawned a child process (experimental, for development purposes only).", "A random node will be selected on startup": "A random node will be selected on startup", "A wallet is stored in a file on your computer.": "A wallet is stored in a file on your computer.", @@ -3177,6 +3178,7 @@ "Developer mode enables advanced and experimental features": "Developer mode enables advanced and experimental features", "Difficulty": "Difficulty", "Dimensions": "Dimensions", + "Disable Window Frame": "Disable Window Frame", "Disable password safety rules": "Disable password safety rules", "Disabled": "Disabled", "Disables node connectivity (Offline Mode).": "Disables node connectivity (Offline Mode).", @@ -3184,6 +3186,7 @@ "Donations": "Donations", "Double click on the graph to re-center...": "Double click on the graph to re-center...", "ECDSA": "ECDSA", + "Economic": "Economic", "Enable Market Monitor": "Enable Market Monitor", "Enable UPnP": "Enable UPnP", "Enable custom daemon arguments": "Enable custom daemon arguments", @@ -3261,6 +3264,7 @@ "Local": "Local", "Local p2p Node Configuration": "Local p2p Node Configuration", "Logs": "Logs", + "Low-priority": "Low-priority", "MT": "MT", "Main Kaspa network": "Main Kaspa network", "Mainnet": "Mainnet", @@ -3278,6 +3282,7 @@ "Mempool Size": "Mempool Size", "Metrics": "Metrics", "Metrics are not currently available": "Metrics are not currently available", + "Miner Fee": "Miner Fee", "Minimize the window": "Minimize the window", "Mnemonic Import": "Mnemonic Import", "NEXT": "NEXT", @@ -3297,12 +3302,14 @@ "Node Status": "Node Status", "Noise": "Noise", "None": "None", + "Normal": "Normal", "Not Connected": "Not Connected", "Not connected": "Not connected", "Notifications": "Notifications", "Open Data Folder": "Open Data Folder", "Opening wallet:": "Opening wallet:", "Optional": "Optional", + "Options": "Options", "Other operations": "Other operations", "Outbound": "Outbound", "Overview": "Overview", @@ -3403,6 +3410,7 @@ "Storage": "Storage", "Storage Read": "Storage Read", "Storage Read/s": "Storage Read/s", + "Storage Size": "Storage Size", "Storage Write": "Storage Write", "Storage Write/s": "Storage Write/s", "Strong": "Strong", @@ -3520,7 +3528,9 @@ "wRPC JSON Tx": "wRPC JSON Tx", "wRPC JSON Tx/s": "wRPC JSON Tx/s", "wRPC URL:": "wRPC URL:", - "words": "words" + "words": "words", + "~2 hours": "~2 hours", + "~30 minutes": "~30 minutes" }, "es": { "'Phishing hint' is a secret word or a phrase that is displayed when you open your wallet. If you do not see the hint when opening your wallet, you may be accessing a fake wallet designed to steal your funds. If this occurs, stop using the wallet immediately, check the browser URL domain name and seek help on social networks (Kaspa Discord or Telegram).": "'Phishing hint' is a secret word or a phrase that is displayed when you open your wallet. If you do not see the hint when opening your wallet, you may be accessing a fake wallet designed to steal your funds. If this occurs, stop using the wallet immediately, check the browser URL domain name and seek help on social networks (Kaspa Discord or Telegram).", diff --git a/core/src/egui/mod.rs b/core/src/egui/mod.rs index f96234f..557a64f 100644 --- a/core/src/egui/mod.rs +++ b/core/src/egui/mod.rs @@ -9,6 +9,7 @@ mod network; mod pagination; mod panel; mod popup; +mod selection_panels; mod theme; pub use collapsable::*; @@ -22,4 +23,5 @@ pub use network::NetworkInterfaceEditor; pub use pagination::*; pub use panel::Panel; pub use popup::PopupPanel; +pub use selection_panels::*; pub use theme::*; diff --git a/core/src/egui/selection_panels.rs b/core/src/egui/selection_panels.rs new file mode 100644 index 0000000..56c597d --- /dev/null +++ b/core/src/egui/selection_panels.rs @@ -0,0 +1,351 @@ +use egui::*; +use std::hash::Hash; + +trait UILayoutExt { + //fn layout_with_max_rect(&mut self, max_rect:Rect, layout:Layout, add_contents: impl FnOnce(&mut Ui) -> R)->InnerResponse; + fn indent_with_size<'c, R>( + &mut self, + id_source: impl Hash, + indent: f32, + add_contents: Box R + 'c>, + ) -> InnerResponse; +} + +impl UILayoutExt for Ui { + // fn layout_with_max_rect(&mut self, max_rect:Rect, layout:Layout, add_contents: impl FnOnce(&mut Ui) -> R)->InnerResponse { + // let mut child_ui = self.child_ui(max_rect, layout); + // let inner = add_contents(&mut child_ui); + // let rect = child_ui.min_rect(); + // let id = self.advance_cursor_after_rect(rect); + + // InnerResponse::new(inner, self.interact(rect, id, Sense::hover())) + + // } + + fn indent_with_size<'c, R>( + &mut self, + id_source: impl Hash, + indent: f32, + add_contents: Box R + 'c>, + ) -> InnerResponse { + assert!( + self.layout().is_vertical(), + "You can only indent vertical layouts, found {:?}", + self.layout() + ); + + let mut child_rect = self.available_rect_before_wrap(); + child_rect.min.x += indent; + + let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source); + let ret = add_contents(&mut child_ui); + + // let left_vline = self.visuals().indent_has_left_vline; + // let end_with_horizontal_line = self.spacing().indent_ends_with_horizontal_line; + + // if left_vline || end_with_horizontal_line { + // if end_with_horizontal_line { + // child_ui.add_space(4.0); + // } + + // let stroke = self.visuals().widgets.noninteractive.bg_stroke; + // let left_top = child_rect.min - 0.5 * indent * Vec2::X; + // let left_top = self.painter().round_pos_to_pixels(left_top); + // let left_bottom = pos2(left_top.x, child_ui.min_rect().bottom() - 2.0); + // let left_bottom = self.painter().round_pos_to_pixels(left_bottom); + + // if left_vline { + // // draw a faint line on the left to mark the indented section + // self.painter.line_segment([left_top, left_bottom], stroke); + // } + + // if end_with_horizontal_line { + // let fudge = 2.0; // looks nicer with button rounding in collapsing headers + // let right_bottom = pos2(child_ui.min_rect().right() - fudge, left_bottom.y); + // self.painter + // .line_segment([left_bottom, right_bottom], stroke); + // } + // } + + let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); + InnerResponse::new(ret, response) + } +} + +type UiBuilderFn = Box; +type FooterUiBuilderFn = Box; + +pub struct SelectionPanel { + pub title: WidgetText, + pub sub: WidgetText, + pub value: V, + pub build_heading: Option, + pub build_footer: Option, +} + +impl SelectionPanel { + pub fn new(value: Value, title: impl Into, sub: impl Into) -> Self { + Self { + title: title.into(), + sub: sub.into(), + value, + build_heading: None, + build_footer: None, + } + } + pub fn heading(mut self, build_heading: impl FnOnce(&mut Ui) + 'static) -> Self { + self.build_heading = Some(Box::new(build_heading)); + self + } + pub fn footer(mut self, build_footer: impl FnOnce(&mut Ui) + 'static) -> Self { + self.build_footer = Some(Box::new(build_footer)); + self + } + + pub fn render( + self, + ui: &mut Ui, + bg_color: Color32, + width: f32, + min_height: &mut f32, + current_value: &mut Value, + ) -> Response { + let selected = *current_value == self.value; + let visuals = ui.visuals(); + let selected_bg = visuals.selection.bg_fill; + let hover_stroke = Stroke::new(1.0, visuals.text_color()); //visuals.window_stroke; + let frame = Frame::none() + .stroke(Stroke::new(1.0, Color32::TRANSPARENT)) + .fill(if selected { selected_bg } else { bg_color }); + let mut prepared = frame.begin(ui); + + let add_contents = |ui: &mut Ui| { + ui.allocate_ui_with_layout( + egui::vec2(width, ui.available_height()), + Layout::top_down(Align::Center), + |ui| { + ui.label(" "); + ui.label(self.title.strong().heading()); + ui.label(self.sub); + if let Some(build) = self.build_heading { + (build)(ui); + } + let icon = if selected { + egui_phosphor::bold::CHECK + } else { + egui_phosphor::bold::DOT_OUTLINE + }; + ui.label(RichText::new(icon).heading()); + if let Some(build) = self.build_footer { + //ui.visuals_mut().override_text_color = Some(Color32::WHITE); + (build)(ui); + } + ui.label(" "); + }, + ) + .response + }; + + let _res = add_contents(&mut prepared.content_ui); + *min_height = min_height.max(prepared.content_ui.min_rect().height()); + prepared.content_ui.set_min_height(*min_height); + let rect = prepared + .frame + .inner_margin + .expand_rect(prepared.content_ui.min_rect()); + if !selected && ui.allocate_rect(rect, Sense::hover()).hovered() { + //prepared.frame = prepared.frame.stroke(hover_stroke); + prepared.frame = prepared.frame.fill(selected_bg).stroke(hover_stroke); + } + + let res = prepared.end(ui); + + let mut response = res.interact(Sense::click()); + if response.clicked() && *current_value != self.value { + *current_value = self.value; + response.mark_changed(); + } + response + } +} + +pub struct SelectionPanels { + pub title: WidgetText, + pub panel_min_width: f32, + pub panel_max_width: f32, + pub panels: Vec>, + pub build_footer: FooterUiBuilderFn, + pub panel_min_height: f32, + pub vertical: bool, + pub sep_ratio: f32, +} + +impl SelectionPanels { + pub fn new( + panel_min_width: f32, + panel_max_width: f32, + title: impl Into, + build_footer: impl FnOnce(&mut Ui, &mut Value) + 'static, + ) -> Self { + Self { + title: title.into(), + panel_min_width, + panel_max_width, + build_footer: Box::new(build_footer), + panels: vec![], + panel_min_height: 0., + vertical: false, + sep_ratio: 1.0, + } + } + pub fn add( + mut self, + value: Value, + title: impl Into, + sub: impl Into, + ) -> Self { + self.panels.push(SelectionPanel::new(value, title, sub)); + self + } + pub fn add_with_footer( + mut self, + value: Value, + title: impl Into, + sub: impl Into, + build_footer: impl FnOnce(&mut Ui) + 'static, + ) -> Self { + self.panels + .push(SelectionPanel::new(value, title, sub).footer(build_footer)); + self + } + pub fn panel_min_height(mut self, min_height: f32) -> Self { + self.panel_min_height = min_height; + self + } + + pub fn vertical(mut self, vertical: bool) -> Self { + self.vertical = vertical; + self + } + pub fn sep_ratio(mut self, sep_ratio: f32) -> Self { + self.sep_ratio = sep_ratio; + self + } + + pub fn render(self, ui: &mut Ui, current_value: &mut Value) -> Response { + let visuals = ui.visuals(); + let sep_ratio = self.sep_ratio; + let text_color = visuals.text_color(); + let bg_color = visuals.code_bg_color; + let before_wrap_width = ui.available_rect_before_wrap().width(); + let mut panel_width = self.panel_min_width.max( + self.panel_max_width + .min(before_wrap_width / self.panels.len() as f32), + ); + let vertical = self.vertical || (before_wrap_width < (panel_width + 2.0) * 3.0); + let panels_width = if vertical { + panel_width = self + .panel_min_width + .max(self.panel_max_width.min(before_wrap_width - 10.0)); + panel_width + } else { + let mut width = 0.0; + let mut available_width = ui.available_rect_before_wrap().width(); + for _ in 0..self.panels.len() { + if (available_width - 2.0) < panel_width { + break; + } + available_width -= panel_width; + width += panel_width; + } + width + }; + + let indent = (before_wrap_width - panels_width) / 2.0; + + let add_contents = |ui: &mut Ui| { + let mut responce = ui.label(" "); + //ui.visuals_mut().override_text_color = Some(Color32::WHITE); + { + let available_width = ui.available_width() - indent; + let title = + self.title + .into_galley(ui, Some(true), available_width, TextStyle::Heading); + let text_indent = (available_width - title.size().x) / 2.0; + let rect = ui.cursor().translate(Vec2::new(text_indent, 10.0)); + ui.allocate_exact_size( + title.size() + Vec2::new(text_indent, 10.0), + Sense::focusable_noninteractive(), + ); + title.paint_with_fallback_color(ui.painter(), rect.min, text_color); + } + + // ui.label(format!("before_wrap_width: {before_wrap_width}")); + // ui.label(format!("panel_width: {panel_width}")); + let _panels_res = ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = Vec2::ZERO; + let mut min_height = self.panel_min_height; + let mut first_row = true; + for (index, panel) in self.panels.into_iter().enumerate() { + let rect = ui.available_rect_before_wrap(); + let mut row_first_item = index == 0; + if (index > 0 && vertical) || rect.width() - 2.0 < panel_width { + ui.end_row(); + row_first_item = true; + first_row = false; + } + // left separator + if !row_first_item { + let Pos2 { x, y } = ui.cursor().min; + let height = min_height * sep_ratio; + let m = (min_height - height) / 2.0; + ui.painter().vline( + x, + (y + m)..=(y + m + height), + Stroke::new(1.0, text_color), + ); + } + + // top seperator + if !first_row { + let Pos2 { x, y } = ui.cursor().min; + let width = panel_width * sep_ratio; + let m = (panel_width - width) / 2.0; + ui.painter().hline( + (x + m)..=(x + m + width), + y, + Stroke::new(1.0, text_color), + ); + } + responce |= + panel.render(ui, bg_color, panel_width, &mut min_height, current_value); + } + }); + + // let total_width = panels_res.response.rect.width(); + // ui.allocate_ui_with_layout( + // egui::vec2(total_width, ui.available_height()), + // Layout::top_down(Align::Center), + // |ui| { + // ui.set_width(total_width); + // (self.build_footer)(ui, current_value) + // } + // ); + + // ui.label(format!("bottom width {}", b.response.rect.width())); + // ui.label(format!("ui.min_rect().width() {}", ui.min_rect().width())); + responce + }; + + let mut response = ui + .indent_with_size("selection-panels", indent, Box::new(add_contents)) + .response; + response |= ui + .vertical_centered(|ui| (self.build_footer)(ui, current_value)) + .response; + ui.label(" "); + // ui.label(format!(" vertical: {vertical}")); + // ui.label(format!("panels_width {}", panels_width)); + response + } +} diff --git a/core/src/modules/account_manager/estimator.rs b/core/src/modules/account_manager/estimator.rs index fda8113..fbd7a4a 100644 --- a/core/src/modules/account_manager/estimator.rs +++ b/core/src/modules/account_manager/estimator.rs @@ -69,6 +69,54 @@ impl<'context> Estimator<'context> { } ui.add_space(8.); + + if core.settings.developer.enable{ + //let child_ui = ui.child_ui(max_rect, Layout::top_down(Align::Center)); + let fee_selection = SelectionPanels::new( + 120.0, + 150.0, + i18n("Miner Fee"), + |ui, value|{ + ui.label("1 in / 2 outputs, ~1.2 Kg"); + ui.label(format!("Fee Mode: {:?}", value)); + }) + //.panel_min_height(300.) + //.vertical(true) + //.add(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more")) + .add_with_footer(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more"), |ui|{ + ui.label("12.88716 µKAS"); + ui.label(RichText::new("~0.00000215 USD").strong()); + ui.label("9 SOMPI/G"); + }) + .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + ui.label("15.83525 µKAS"); + ui.label(RichText::new("~0.00000264 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + ui.label("20.78334 µKAS"); + ui.label(RichText::new("~0.00000347 USD").strong()); + ui.label("10 SOMPI/G"); + }); + // .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + // ui.label("13.83525 µKAS"); + // ui.label(RichText::new("~608.83 USD").strong()); + // ui.label("10 SOMPI/G"); + // }) + // .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + // ui.label("14.78334 µKAS"); + // ui.label(RichText::new("~650.56 USD").strong()); + // ui.label("10 SOMPI/G"); + // }); + + if fee_selection.render(ui, &mut self.context.fee_mode).clicked(){ + log_info!("clicked: self.fee_mode: {:?}", self.context.fee_mode); + runtime().toast(UserNotification::success(format!("selection: {:?}", self.context.fee_mode)).short()) + } + ui.add_space(8.); + } + + if ui .checkbox(&mut self.context.enable_priority_fees,i18n("Include QoS Priority Fees")) // .on_hover_text_at_pointer(i18n("Add priority fees to ensure faster confirmation.\nUseful only if the network is congested.")) diff --git a/core/src/modules/account_manager/mod.rs b/core/src/modules/account_manager/mod.rs index ce63242..952a6db 100644 --- a/core/src/modules/account_manager/mod.rs +++ b/core/src/modules/account_manager/mod.rs @@ -111,6 +111,15 @@ enum AddressStatus { Invalid(String), } +#[derive(PartialEq, Debug, Default)] +pub enum FeeMode{ + #[default] + None, + LowPriority, + Economic, + Normal, +} + #[derive(Default)] pub struct ManagerContext { transfer_to_account : Option, @@ -129,6 +138,7 @@ pub struct ManagerContext { wallet_secret : String, payment_secret : String, loading : bool, + fee_mode : FeeMode } impl ManagerContext { diff --git a/core/src/modules/testing.rs b/core/src/modules/testing.rs index 4501b48..1ffdad1 100644 --- a/core/src/modules/testing.rs +++ b/core/src/modules/testing.rs @@ -1,5 +1,4 @@ use kaspa_bip32::{Mnemonic,WordCount}; - use crate::imports::*; // use egui_plot::PlotPoint; @@ -12,9 +11,18 @@ pub enum State { Unlocking, } +#[derive(PartialEq, Debug)] +pub enum FeeMode{ + None, + LowPriority, + Economic, + Normal, +} + pub struct Testing { #[allow(dead_code)] runtime: Runtime, + fee_mode: FeeMode, // pub state: State, // pub message: Option, @@ -44,6 +52,7 @@ impl Testing { Self { runtime, + fee_mode: FeeMode::None, // state: State::Select, // message: None, // graph_data, @@ -95,6 +104,112 @@ impl ModuleT for Testing { if ui.large_button("notify info").clicked() { runtime().notify(UserNotification::info("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, ").short()); } + + + let fee_selection = SelectionPanels::new( + 100.0, + 130.0, + i18n("Miner Fee"), + |ui, value|{ + ui.label("1 in / 2 outputs, ~1.2 Kg"); + ui.label(format!("Fee Mode: {:?}", value)); + }) + //.panel_min_height(300.) + //.vertical(true) + //.add(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more")) + .add_with_footer(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more"), |ui|{ + ui.label("12.88716 µKAS"); + ui.label(RichText::new("~0.00000215 USD").strong()); + ui.label("9 SOMPI/G"); + }) + .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + ui.label("15.83525 µKAS"); + ui.label(RichText::new("~0.00000264 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + ui.label("20.78334 µKAS"); + ui.label(RichText::new("~0.00000347 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more"), |ui|{ + ui.label("12.88716 µKAS"); + ui.label(RichText::new("~0.00000215 USD").strong()); + ui.label("9 SOMPI/G"); + }) + .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + ui.label("15.83525 µKAS"); + ui.label(RichText::new("~0.00000264 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + ui.label("20.78334 µKAS"); + ui.label(RichText::new("~0.00000347 USD").strong()); + ui.label("10 SOMPI/G"); + }); + // .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + // ui.label("13.83525 µKAS"); + // ui.label(RichText::new("~608.83 USD").strong()); + // ui.label("10 SOMPI/G"); + // }) + // .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + // ui.label("14.78334 µKAS"); + // ui.label(RichText::new("~650.56 USD").strong()); + // ui.label("10 SOMPI/G"); + // }); + + + if fee_selection.render(ui, &mut self.fee_mode).clicked(){ + log_info!("clicked: self.fee_mode: {:?}", self.fee_mode); + runtime().toast(UserNotification::success(format!("selection: {:?}", self.fee_mode)).short()) + } + + let fee_selection = SelectionPanels::new( + 100.0, + 150.0, + i18n("Miner Fee"), + |ui, value|{ + ui.label("1 in / 2 outputs, ~1.2 Kg"); + ui.label(format!("Fee Mode: {:?}", value)); + }) + //.panel_min_height(300.) + //.vertical(true) + //.add(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more")) + .add_with_footer(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more"), |ui|{ + ui.label("12.88716 µKAS"); + ui.label(RichText::new("~0.00000215 USD").strong()); + ui.label("9 SOMPI/G"); + }) + .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + ui.label("15.83525 µKAS"); + ui.label(RichText::new("~0.00000264 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + ui.label("20.78334 µKAS"); + ui.label(RichText::new("~0.00000347 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::LowPriority, i18n("Low-priority"), i18n("3 hours or more"), |ui|{ + ui.label("12.88716 µKAS"); + ui.label(RichText::new("~0.00000215 USD").strong()); + ui.label("9 SOMPI/G"); + }) + .add_with_footer(FeeMode::Economic, i18n("Economic"), i18n("~2 hours"), |ui|{ + ui.label("15.83525 µKAS"); + ui.label(RichText::new("~0.00000264 USD").strong()); + ui.label("10 SOMPI/G"); + }) + .add_with_footer(FeeMode::Normal, i18n("Normal"), i18n("~30 minutes"), |ui|{ + ui.label("20.78334 µKAS"); + ui.label(RichText::new("~0.00000347 USD").strong()); + ui.label("10 SOMPI/G"); + }); + + if fee_selection.sep_ratio(0.7).render(ui, &mut self.fee_mode).clicked(){ + log_info!("clicked: self.fee_mode: {:?}", self.fee_mode); + runtime().toast(UserNotification::success(format!("selection: {:?}", self.fee_mode)).short()) + } } }