ui: wallet list content

This commit is contained in:
ardocrat 2023-07-30 18:57:12 +03:00
parent a31d38dc1c
commit 18b05fffe9
7 changed files with 199 additions and 103 deletions

View file

@ -25,6 +25,8 @@ wallets:
invalid_url: Entered URL is invalid invalid_url: Entered URL is invalid
open: Open the wallet open: Open the wallet
wrong_pass: Entered password is wrong wrong_pass: Entered password is wrong
locked: Locked
unlocked: Unlocked
network: network:
self: Network self: Network
node: Integrated node node: Integrated node

View file

@ -25,6 +25,8 @@ wallets:
invalid_url: Введенный URL-адрес недействителен invalid_url: Введенный URL-адрес недействителен
open: Открыть кошелёк open: Открыть кошелёк
wrong_pass: Введён неправильный пароль wrong_pass: Введён неправильный пароль
locked: Заблокирован
unlocked: Разблокирован
network: network:
self: Сеть self: Сеть
node: Встроенный узел node: Встроенный узел

View file

@ -19,6 +19,7 @@ pub struct Colors;
impl Colors { impl Colors {
pub const WHITE: Color32 = Color32::from_gray(253); pub const WHITE: Color32 = Color32::from_gray(253);
pub const BLACK: Color32 = Color32::from_gray(2); pub const BLACK: Color32 = Color32::from_gray(2);
pub const DARK_TEXT: Color32 = Color32::from_gray(30);
pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0); pub const TRANSPARENT: Color32 = Color32::from_rgba_premultiplied(0, 0, 0, 0);
pub const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100); pub const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100);
pub const YELLOW: Color32 = Color32::from_rgb(254, 241, 2); pub const YELLOW: Color32 = Color32::from_rgb(254, 241, 2);
@ -36,5 +37,6 @@ impl Colors {
pub const STROKE: Color32 = Color32::from_gray(190); pub const STROKE: Color32 = Color32::from_gray(190);
pub const INACTIVE_TEXT: Color32 = Color32::from_gray(150); pub const INACTIVE_TEXT: Color32 = Color32::from_gray(150);
pub const ITEM_STROKE: Color32 = Color32::from_gray(220); pub const ITEM_STROKE: Color32 = Color32::from_gray(220);
pub const ITEM_HOVER_STROKE: Color32 = Color32::from_gray(205); pub const ITEM_HOVER: Color32 = Color32::from_gray(205);
pub const ITEM_CURRENT: Color32 = Color32::from_gray(227);
} }

View file

@ -28,10 +28,10 @@ pub struct View;
impl View { impl View {
/// Default stroke around views. /// Default stroke around views.
pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Colors::STROKE }; pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Colors::STROKE };
/// Stroke around list items. /// Stroke for items.
pub const ITEM_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_STROKE }; pub const ITEM_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_STROKE };
/// Stroke around list items. /// Stroke for hovered items and buttons.
pub const ITEM_HOVER_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_HOVER_STROKE }; pub const ITEM_HOVER_STROKE: Stroke = Stroke { width: 1.0, color: Colors::ITEM_HOVER };
/// Callback on Enter key press event. /// Callback on Enter key press event.
pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) { pub fn on_enter_key(ui: &mut egui::Ui, cb: impl FnOnce()) {
@ -161,45 +161,6 @@ impl View {
} }
} }
/// Draw round [`Button`] with icon.
pub fn round_button(ui: &mut egui::Ui, icon: &'static str, action: impl FnOnce()) {
ui.scope(|ui| {
// Setup colors.
ui.visuals_mut().widgets.inactive.bg_fill = Colors::BUTTON;
ui.visuals_mut().widgets.hovered.bg_fill = Colors::FILL;
ui.visuals_mut().widgets.active.bg_fill = Colors::FILL_DARK;
// Setup radius.
let mut r = 44.0 * 0.5;
let size = egui::Vec2::splat(2.0 * r + 5.0);
let (rect, br) = ui.allocate_at_least(size, Sense::click_and_drag());
let mut icon_color = Colors::GRAY;
// Increase radius and change icon size and color on-hover.
if br.hovered() {
r = r * 1.05;
icon_color = Colors::TEXT_BUTTON;
}
let visuals = ui.style().interact(&br);
ui.painter().add(CircleShape {
center: rect.center(),
radius: r,
fill: visuals.bg_fill,
stroke: Self::DEFAULT_STROKE,
});
ui.allocate_ui_at_rect(rect, |ui| {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(icon).color(icon_color).size(25.0));
});
});
if Self::touched(ui, br) {
(action)();
}
});
}
/// Draw list item [`Button`] with given vertical padding and rounding on left and right sides. /// Draw list item [`Button`] with given vertical padding and rounding on left and right sides.
pub fn item_button(ui: &mut egui::Ui, r: [bool; 2], icon: &'static str, action: impl FnOnce()) { pub fn item_button(ui: &mut egui::Ui, r: [bool; 2], icon: &'static str, action: impl FnOnce()) {
let rounding = Self::get_rounding([r[0], r[1], r[1], r[0]]); let rounding = Self::get_rounding([r[0], r[1], r[1], r[0]]);
@ -235,6 +196,47 @@ impl View {
}); });
} }
/// Draw circle [`Button`] with icon.
pub fn circle_button(ui: &mut egui::Ui, icon: &'static str, action: impl FnOnce()) {
ui.scope(|ui| {
// Setup colors.
ui.visuals_mut().widgets.inactive.bg_fill = Colors::BUTTON;
ui.visuals_mut().widgets.hovered.bg_fill = Colors::FILL;
ui.visuals_mut().widgets.active.bg_fill = Colors::FILL_DARK;
// Setup radius.
let mut r = 44.0 * 0.5;
let size = egui::Vec2::splat(2.0 * r + 5.0);
let (rect, br) = ui.allocate_at_least(size, Sense::click_and_drag());
let mut icon_color = Colors::GRAY;
let mut stroke = Self::DEFAULT_STROKE;
// Increase radius and change icon size and color on-hover.
if br.hovered() {
r = r * 1.04;
icon_color = Colors::TEXT_BUTTON;
stroke = Self::ITEM_HOVER_STROKE;
}
let visuals = ui.style().interact(&br);
ui.painter().add(CircleShape {
center: rect.center(),
radius: r,
fill: visuals.bg_fill,
stroke,
});
ui.allocate_ui_at_rect(rect, |ui| {
ui.centered_and_justified(|ui| {
ui.label(RichText::new(icon).color(icon_color).size(25.0));
});
});
if Self::touched(ui, br) {
(action)();
}
});
}
/// Get rounding for provided corners clockwise. /// Get rounding for provided corners clockwise.
fn get_rounding(corners: [bool; 4]) -> Rounding { fn get_rounding(corners: [bool; 4]) -> Rounding {
Rounding { Rounding {
@ -276,7 +278,7 @@ impl View {
se: if r[3] { 8.0 } else { 0.0 }, se: if r[3] { 8.0 } else { 0.0 },
}, },
fill: Colors::WHITE, fill: Colors::WHITE,
stroke: Stroke { width: 1.0, color: Colors::ITEM_STROKE }, stroke: Self::ITEM_STROKE,
}; };
let bg_idx = ui.painter().add(bg_shape); let bg_idx = ui.painter().add(bg_shape);

View file

@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::cmp::max; use std::cmp::{max, min};
use egui::{Align2, Margin, RichText, TextStyle, Widget}; use egui::{Align, Align2, Layout, Margin, RichText, Rounding, TextStyle, Widget};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use crate::gui::Colors; use crate::gui::Colors;
use crate::gui::icons::{ARROW_LEFT, EYE, EYE_SLASH, FOLDER_PLUS, GEAR, GLOBE, PLUS}; use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, EYE, EYE_SLASH, FOLDER, FOLDER_LOCK, FOLDER_OPEN, GEAR, GLOBE, GLOBE_SIMPLE, PLUS};
use crate::gui::platform::PlatformCallbacks; use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::{Modal, ModalContainer, ModalPosition, Root, TitlePanel, TitleType, View}; use crate::gui::views::{Modal, ModalContainer, ModalPosition, Root, TitlePanel, TitleType, View};
use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation}; use crate::gui::views::wallets::creation::{MnemonicSetup, WalletCreation};
@ -97,79 +97,83 @@ impl WalletsContent {
// Get wallets. // Get wallets.
let wallets = Wallets::list(); let wallets = Wallets::list();
let is_list_empty = wallets.is_empty(); let empty_list = wallets.is_empty();
let selected = wallets.iter().find(|x| Some(x.config.id) == Wallets::selected_id());
// Show title panel.
self.title_ui(ui, frame);
// Setup wallet content flags. // Setup wallet content flags.
let is_wallet_creating = self.creation_content.can_go_back(); let create_wallet = self.creation_content.can_go_back();
let is_wallet_showing = if let Some(id) = Wallets::selected_id() { let show_wallet = if let Some(id) = Wallets::selected_id() {
Wallets::is_open(id) Wallets::is_open(id)
} else { } else {
false false
}; };
// Setup panels parameters. // Setup panels parameters.
let is_dual_panel = Self::is_dual_panel_mode(ui, frame); let dual_panel = Self::is_dual_panel_mode(ui, frame);
let is_wallet_panel_open let open_wallet_panel = dual_panel || show_wallet || create_wallet || empty_list;
= is_dual_panel || is_wallet_showing || is_wallet_creating || is_list_empty; let wallet_panel_width = self.wallet_panel_width(ui, empty_list, dual_panel, show_wallet);
let wallet_panel_width
= self.wallet_panel_width(ui, is_list_empty, is_dual_panel, is_wallet_showing); // Show title panel.
self.title_ui(ui, frame, dual_panel);
// Show wallet panel content. // Show wallet panel content.
egui::SidePanel::right("wallet_panel") egui::SidePanel::right("wallet_panel")
.resizable(false) .resizable(false)
.exact_width(wallet_panel_width) .exact_width(wallet_panel_width)
.frame(egui::Frame { .frame(egui::Frame {
fill: if is_list_empty && !is_wallet_creating { fill: if empty_list && !create_wallet {
Colors::FILL_DARK Colors::FILL_DARK
} else { } else {
Colors::WHITE Colors::WHITE
}, },
..Default::default() ..Default::default()
}) })
.show_animated_inside(ui, is_wallet_panel_open, |ui| { .show_animated_inside(ui, open_wallet_panel, |ui| {
if is_wallet_showing { if create_wallet || !show_wallet {
self.wallet_content.ui(ui, frame, selected.unwrap(), cb); // Show wallet creation content
} else {
self.creation_content.ui(ui, cb); self.creation_content.ui(ui, cb);
} else {
for w in wallets.iter() {
// Show content for selected wallet.
if Some(w.config.id) == Wallets::selected_id() {
self.wallet_content.ui(ui, frame, &w, cb);
break;
}
}
} }
}); });
// Show wallets list. // Show non-empty list if wallet is not creating and not open at single panel mode.
if !is_list_empty && (!is_wallet_panel_open || is_dual_panel) { if !empty_list && !create_wallet && (!open_wallet_panel || dual_panel) {
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame { .frame(egui::Frame {
stroke: View::DEFAULT_STROKE, stroke: View::DEFAULT_STROKE,
fill: Colors::FILL_DARK, fill: Colors::FILL_DARK,
inner_margin: Margin { inner_margin: Margin {
left: View::far_left_inset_margin(ui) + 4.0, left: View::far_left_inset_margin(ui) + 6.0,
right: View::far_right_inset_margin(ui, frame) + 4.0, right: View::far_right_inset_margin(ui, frame) + 6.0,
top: 3.0, top: 5.0,
bottom: 4.0, bottom: View::get_bottom_inset(),
}, },
..Default::default() ..Default::default()
}) })
.show_inside(ui, |ui| { .show_inside(ui, |ui| {
self.list_ui(ui, &wallets, cb); self.wallet_list_ui(ui, dual_panel, &wallets, cb);
}); });
// Do not show creation button if wallets panel is not showing at root // Do not show creation button if wallets panel is not showing at root
// or if wallet is not showing at dual panel mode. // or if wallet creation is showing at dual panel mode.
let root_dual_panel = Root::is_dual_panel_mode(frame); let root_dual_panel = Root::is_dual_panel_mode(frame);
let wallets_panel_not_open = !root_dual_panel && Root::is_network_panel_open(); let wallets_panel_not_open = !root_dual_panel && Root::is_network_panel_open();
if wallets_panel_not_open || (is_wallet_panel_open && !is_wallet_showing) { if wallets_panel_not_open || (open_wallet_panel && !show_wallet) {
return; return;
} else { } else {
self.create_wallet_btn_ui(ui, if is_dual_panel { wallet_panel_width } else { 0.0 }); self.create_wallet_btn_ui(ui, if dual_panel { wallet_panel_width } else { 0.0 });
} }
} }
} }
/// Draw [`TitlePanel`] content. /// Draw [`TitlePanel`] content.
fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn title_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, dual_panel: bool) {
// Setup title text. // Setup title text.
let title_text = if self.creation_content.can_go_back() { let title_text = if self.creation_content.can_go_back() {
t!("wallets.add") t!("wallets.add")
@ -180,12 +184,10 @@ impl WalletsContent {
// Draw title panel. // Draw title panel.
TitlePanel::ui(title_content, |ui, frame| { TitlePanel::ui(title_content, |ui, frame| {
if Wallets::selected_id().is_some() { if Wallets::selected_id().is_some() && !dual_panel {
if !Self::is_dual_panel_mode(ui, frame) { View::title_button(ui, ARROW_LEFT, || {
View::title_button(ui, ARROW_LEFT, || { Wallets::select(None);
Wallets::select(None); });
});
}
} else if self.creation_content.can_go_back() { } else if self.creation_content.can_go_back() {
View::title_button(ui, ARROW_LEFT, || { View::title_button(ui, ARROW_LEFT, || {
self.creation_content.back(); self.creation_content.back();
@ -203,29 +205,114 @@ impl WalletsContent {
} }
/// Draw list of wallets. /// Draw list of wallets.
fn list_ui(&mut self, ui: &mut egui::Ui, wallets: &Vec<Wallet>, cb: &dyn PlatformCallbacks) { fn wallet_list_ui(&mut self,
for w in wallets { ui: &mut egui::Ui,
ui.label(&w.config.name); dual_panel: bool,
wallets: &Vec<Wallet>,
cb: &dyn PlatformCallbacks) {
let available_width = ui.available_width();
// Calculate list width.
let width = if dual_panel {
available_width
} else {
min(available_width as i64, (Root::SIDE_PANEL_MIN_WIDTH * 1.3) as i64) as f32
};
// Draw list of wallets.
ui.vertical_centered(|ui| {
let mut rect = ui.available_rect_before_wrap();
rect.set_width(width);
ui.allocate_ui(rect.size(), |ui| {
for (index, w) in wallets.iter().enumerate() {
// Draw wallet list item.
self.wallet_item_ui(ui, w, cb);
// Add space after last item.
if index == wallets.len() - 1 {
ui.add_space(36.0);
}
}
});
});
}
/// Draw wallet list item.
fn wallet_item_ui(&mut self,
ui: &mut egui::Ui,
wallet: &Wallet,
cb: &dyn PlatformCallbacks) {
let id = wallet.config.id;
let is_selected = Some(id) == Wallets::selected_id();
let is_open = Wallets::is_open(id);
let is_current = is_open && is_selected;
// Draw round background.
let mut rect = ui.available_rect_before_wrap();
rect.set_height(76.0);
let rounding = View::item_rounding(0, 1);
let bg_color = if is_current { Colors::ITEM_CURRENT } else { Colors::FILL };
let stroke = if is_current { View::ITEM_HOVER_STROKE } else { View::ITEM_HOVER_STROKE };
ui.painter().rect(rect, rounding, bg_color, stroke);
ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| {
// Setup padding for item buttons.
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 0.0);
// Setup rounding for item buttons.
ui.style_mut().visuals.widgets.inactive.rounding = Rounding::same(8.0);
ui.style_mut().visuals.widgets.hovered.rounding = Rounding::same(8.0);
ui.style_mut().visuals.widgets.active.rounding = Rounding::same(8.0);
/// Show open/close button
let id = w.config.id;
let is_selected = Some(id) == Wallets::selected_id();
let is_open = Wallets::is_open(id);
if !is_open { if !is_open {
View::button(ui, "open me".to_string(), Colors::GOLD, || { // Show button to open closed wallet.
View::item_button(ui, [false, true], FOLDER_OPEN, || {
Wallets::select(Some(id)); Wallets::select(Some(id));
self.show_open_wallet_modal(cb); self.show_open_wallet_modal(cb);
}); });
} else if !is_selected { } else if !is_selected {
View::button(ui, "select me".to_string(), Colors::GOLD, || { // Show button to select opened wallet.
View::item_button(ui, [false, true], CARET_RIGHT, || {
Wallets::select(Some(id)); Wallets::select(Some(id));
}); });
// Show button to close opened wallet.
View::item_button(ui, [false, false], FOLDER_LOCK, || {
Wallets::close(id).unwrap()
});
} }
if Wallets::is_open(id) { let layout_size = ui.available_size();
ui.label("opened!"); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| {
} ui.add_space(7.0);
} ui.vertical(|ui| {
// Setup wallet name text.
let name_text = format!("{} {}", FOLDER, wallet.config.name);
let name_color = if is_selected { Colors::DARK_TEXT } else { Colors::TITLE };
ui.add_space(3.0);
View::ellipsize_text(ui, name_text, 18.0, name_color);
ui.add_space(-1.0);
// Setup wallet connection text.
let external_url = wallet.config.get_external_node_url();
let conn_text = if let Some(url) = external_url {
format!("{} {}", GLOBE_SIMPLE, url)
} else {
format!("{} {}", COMPUTER_TOWER, t!("network.node"))
};
View::ellipsize_text(ui, conn_text, 15.0, Colors::TEXT);
ui.add_space(1.0);
// Setup wallet status text.
let status_text = if Wallets::is_open(id) {
format!("{} {}", FOLDER_OPEN, t!("wallets.unlocked"))
} else {
format!("{} {}", FOLDER_LOCK, t!("wallets.locked"))
};
ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY));
ui.add_space(3.0);
})
});
});
ui.add_space(6.0);
} }
/// Draw floating button to show wallet creation [`Modal`]. /// Draw floating button to show wallet creation [`Modal`].
@ -237,7 +324,7 @@ impl WalletsContent {
.anchor(Align2::RIGHT_BOTTOM, egui::Vec2::new(-6.0 - right_margin, -6.0)) .anchor(Align2::RIGHT_BOTTOM, egui::Vec2::new(-6.0 - right_margin, -6.0))
.frame(egui::Frame::default()) .frame(egui::Frame::default())
.show(ui.ctx(), |ui| { .show(ui.ctx(), |ui| {
View::round_button(ui, PLUS, || { View::circle_button(ui, PLUS, || {
self.creation_content.show_name_pass_modal(); self.creation_content.show_name_pass_modal();
}); });
}); });
@ -373,7 +460,7 @@ impl WalletsContent {
&self, &self,
ui:&mut egui::Ui, ui:&mut egui::Ui,
is_list_empty: bool, is_list_empty: bool,
is_dual_panel: bool, dual_panel: bool,
is_wallet_showing: bool is_wallet_showing: bool
) -> f32 { ) -> f32 {
let is_wallet_creation = self.creation_content.can_go_back(); let is_wallet_creation = self.creation_content.can_go_back();
@ -382,7 +469,7 @@ impl WalletsContent {
} else { } else {
ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH ui.available_width() - Root::SIDE_PANEL_MIN_WIDTH
}; };
if is_dual_panel { if dual_panel {
let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64; let min_width = (Root::SIDE_PANEL_MIN_WIDTH + View::get_right_inset()) as i64;
max(min_width, available_width as i64) as f32 max(min_width, available_width as i64) as f32
} else { } else {

View file

@ -17,8 +17,8 @@ pub mod selection;
pub mod tx; pub mod tx;
pub mod keys; pub mod keys;
mod wallet; mod wallets;
pub use wallet::{Wallet, Wallets}; pub use wallets::{Wallet, Wallets};
mod config; mod config;
pub use config::*; pub use config::*;

View file

@ -46,7 +46,7 @@ lazy_static! {
pub struct Wallets { pub struct Wallets {
/// List of wallets. /// List of wallets.
list: Vec<Wallet>, list: Vec<Wallet>,
/// Selected [`Wallet`] identifier from config. /// Selected [`Wallet`] identifier.
selected_id: Option<i64>, selected_id: Option<i64>,
/// Identifiers of opened wallets. /// Identifiers of opened wallets.
opened_ids: BTreeSet<i64> opened_ids: BTreeSet<i64>
@ -61,7 +61,8 @@ impl Wallets {
Self { Self {
list: Self::load_wallets(&AppConfig::chain_type()), list: Self::load_wallets(&AppConfig::chain_type()),
selected_id: None, selected_id: None,
opened_ids: Default::default() } opened_ids: BTreeSet::default()
}
} }
/// Create new wallet and add it to state. /// Create new wallet and add it to state.
@ -198,7 +199,7 @@ impl Wallet {
mnemonic: String, mnemonic: String,
external_node_url: Option<String> external_node_url: Option<String>
) -> Result<Wallet, Error> { ) -> Result<Wallet, Error> {
let config = WalletConfig::create(name.clone(), external_node_url); let config = WalletConfig::create(name, external_node_url);
let wallet = Self::create_wallet_instance(config.clone())?; let wallet = Self::create_wallet_instance(config.clone())?;
let w = Wallet { let w = Wallet {
instance: wallet, instance: wallet,
@ -566,7 +567,7 @@ impl Wallet {
} }
/// Get summary info about the wallet. /// Get summary info about the wallet.
pub fn get_info<'a, T: ?Sized, C, K>( fn get_info<'a, T: ?Sized, C, K>(
wallet: &mut T, wallet: &mut T,
parent_key_id: &Identifier, parent_key_id: &Identifier,
minimum_confirmations: u64, minimum_confirmations: u64,