P8: desktop sidebar (shell B), theme + density pickers
- Wide desktop (≥720px) now renders a left sidebar nav (Wallet/Activity/Send/ Receive/Settings) with a profile card instead of the bottom tab bar; narrow screens keep the bottom tabs. Both drive the same Tab state and screens. - Settings gains an Appearance group: tap to cycle theme (Dark/Light/Yellow, re-applies egui visuals live) and density (Comfy/Regular/Compact). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+196
-17
@@ -135,23 +135,45 @@ impl GoblinWalletView {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bottom tab bar.
|
||||
let bottom_inset = View::get_bottom_inset();
|
||||
egui::TopBottomPanel::bottom("goblin_tabs")
|
||||
.frame(egui::Frame {
|
||||
fill: t.bg,
|
||||
inner_margin: Margin {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: (4.0 + bottom_inset) as i8,
|
||||
},
|
||||
stroke: Stroke::new(1.0, t.line),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
self.tab_bar_ui(ui, wallet);
|
||||
});
|
||||
// Desktop (wide) shows a left sidebar (shell B); narrow/mobile shows a
|
||||
// bottom tab bar (shell A). Both drive the same Tab state and screens.
|
||||
let wide_desktop = View::is_desktop() && ui.available_width() >= 720.0;
|
||||
if wide_desktop {
|
||||
egui::SidePanel::left("goblin_sidebar")
|
||||
.resizable(false)
|
||||
.exact_width(244.0)
|
||||
.frame(egui::Frame {
|
||||
fill: t.bg,
|
||||
stroke: Stroke::new(1.0, t.line),
|
||||
inner_margin: Margin {
|
||||
left: (View::far_left_inset_margin(ui) + 16.0) as i8,
|
||||
right: 16,
|
||||
top: (View::get_top_inset() + 28.0) as i8,
|
||||
bottom: 20,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
self.sidebar_ui(ui, wallet);
|
||||
});
|
||||
} else {
|
||||
let bottom_inset = View::get_bottom_inset();
|
||||
egui::TopBottomPanel::bottom("goblin_tabs")
|
||||
.frame(egui::Frame {
|
||||
fill: t.bg,
|
||||
inner_margin: Margin {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: (4.0 + bottom_inset) as i8,
|
||||
},
|
||||
stroke: Stroke::new(1.0, t.line),
|
||||
..Default::default()
|
||||
})
|
||||
.show_inside(ui, |ui| {
|
||||
self.tab_bar_ui(ui, wallet);
|
||||
});
|
||||
}
|
||||
|
||||
// Central content.
|
||||
egui::CentralPanel::default()
|
||||
@@ -218,6 +240,118 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
|
||||
/// Desktop left sidebar: wordmark, nav items, profile card.
|
||||
fn sidebar_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
|
||||
let t = theme::tokens();
|
||||
// Wordmark.
|
||||
ui.horizontal(|ui| {
|
||||
widgets_logo(ui);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new("goblin")
|
||||
.font(FontId::new(20.0, fonts::bold()))
|
||||
.color(t.text),
|
||||
);
|
||||
});
|
||||
ui.add_space(28.0);
|
||||
|
||||
let has_requests = wallet
|
||||
.nostr_service()
|
||||
.map(|s| !s.store.pending_requests().is_empty())
|
||||
.unwrap_or(false);
|
||||
// (tab, icon, label, is_send_action, badge)
|
||||
let items = [
|
||||
(Some(Tab::Home), WALLET, "Wallet", false),
|
||||
(Some(Tab::Activity), CLOCK, "Activity", has_requests),
|
||||
(None, crate::gui::icons::ARROW_UP, "Send", false),
|
||||
(Some(Tab::Receive), ARROW_DOWN, "Receive", false),
|
||||
(Some(Tab::Me), USER_CIRCLE, "Settings", false),
|
||||
];
|
||||
for (tab, icon, label, badge) in items {
|
||||
let active = tab.map(|x| x == self.tab).unwrap_or(false);
|
||||
let (rect, resp) =
|
||||
ui.allocate_exact_size(Vec2::new(ui.available_width(), 44.0), Sense::click());
|
||||
if active || resp.hovered() {
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
eframe::epaint::CornerRadius::same(12),
|
||||
if active { t.surface2 } else { t.hover },
|
||||
);
|
||||
}
|
||||
let color = if active { t.text } else { t.text_dim };
|
||||
ui.painter().text(
|
||||
rect.left_center() + Vec2::new(14.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
icon,
|
||||
FontId::new(20.0, fonts::regular()),
|
||||
color,
|
||||
);
|
||||
ui.painter().text(
|
||||
rect.left_center() + Vec2::new(44.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
label,
|
||||
FontId::new(
|
||||
15.0,
|
||||
if active {
|
||||
fonts::semibold()
|
||||
} else {
|
||||
fonts::medium()
|
||||
},
|
||||
),
|
||||
color,
|
||||
);
|
||||
if badge {
|
||||
ui.painter()
|
||||
.circle_filled(rect.right_center() - Vec2::new(14.0, 0.0), 4.0, t.neg);
|
||||
}
|
||||
if resp.clicked() {
|
||||
match tab {
|
||||
Some(x) => self.tab = x,
|
||||
None => self.send = Some(SendFlow::default()),
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
|
||||
// Profile card pinned to the bottom.
|
||||
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||
let (handle, connected) = wallet
|
||||
.nostr_service()
|
||||
.map(|s| {
|
||||
let id = s.identity.read();
|
||||
let h = id
|
||||
.nip05
|
||||
.clone()
|
||||
.map(|n| format!("@{}", n.split('@').next().unwrap_or("")))
|
||||
.unwrap_or_else(|| data::short_npub(&hex_of(&id.npub)));
|
||||
(h, s.is_connected())
|
||||
})
|
||||
.unwrap_or_else(|| ("Anonymous".to_string(), false));
|
||||
w::card(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
w::avatar(ui, &handle, 36.0, 6);
|
||||
ui.add_space(10.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(
|
||||
RichText::new(&handle)
|
||||
.font(FontId::new(14.0, fonts::semibold()))
|
||||
.color(t.text),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(if connected {
|
||||
"synced · Tor"
|
||||
} else {
|
||||
"connecting…"
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn home_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let data = wallet.get_data();
|
||||
ScrollArea::vertical()
|
||||
@@ -629,6 +763,28 @@ impl GoblinWalletView {
|
||||
self.claim_ui(ui, wallet);
|
||||
}
|
||||
|
||||
ui.add_space(16.0);
|
||||
w::kicker(ui, "Appearance");
|
||||
ui.add_space(8.0);
|
||||
w::card(ui, |ui| {
|
||||
let theme_label = match crate::AppConfig::theme() {
|
||||
crate::gui::theme::ThemeKind::Light => "Light",
|
||||
crate::gui::theme::ThemeKind::Dark => "Dark",
|
||||
crate::gui::theme::ThemeKind::Yellow => "Yellow",
|
||||
};
|
||||
if settings_row_btn(ui, "Theme", theme_label) {
|
||||
cycle_theme(ui.ctx());
|
||||
}
|
||||
let density_label = match crate::AppConfig::density() {
|
||||
crate::gui::theme::DensityKind::Compact => "Compact",
|
||||
crate::gui::theme::DensityKind::Regular => "Regular",
|
||||
crate::gui::theme::DensityKind::Comfy => "Comfy",
|
||||
};
|
||||
if settings_row_btn(ui, "Density", density_label) {
|
||||
cycle_density();
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
settings_group(ui, "Wallet", |ui| {
|
||||
settings_row(ui, "Display unit", "ツ (grin)");
|
||||
@@ -933,6 +1089,29 @@ fn accept_policy_label(wallet: &Wallet) -> &'static str {
|
||||
.unwrap_or("Anyone")
|
||||
}
|
||||
|
||||
/// Cycle the color theme Dark → Light → Yellow → Dark and re-apply visuals.
|
||||
fn cycle_theme(ctx: &egui::Context) {
|
||||
use crate::gui::theme::ThemeKind;
|
||||
let next = match crate::AppConfig::theme() {
|
||||
ThemeKind::Dark => ThemeKind::Light,
|
||||
ThemeKind::Light => ThemeKind::Yellow,
|
||||
ThemeKind::Yellow => ThemeKind::Dark,
|
||||
};
|
||||
crate::AppConfig::set_theme(next);
|
||||
crate::setup_visuals(ctx);
|
||||
}
|
||||
|
||||
/// Cycle the density Comfy → Regular → Compact → Comfy.
|
||||
fn cycle_density() {
|
||||
use crate::gui::theme::DensityKind;
|
||||
let next = match crate::AppConfig::density() {
|
||||
DensityKind::Comfy => DensityKind::Regular,
|
||||
DensityKind::Regular => DensityKind::Compact,
|
||||
DensityKind::Compact => DensityKind::Comfy,
|
||||
};
|
||||
crate::AppConfig::set_density(next);
|
||||
}
|
||||
|
||||
/// Cycle the incoming-payment accept policy Anyone → Contacts → Ask → Anyone.
|
||||
fn cycle_accept_policy(wallet: &Wallet) {
|
||||
use crate::nostr::config::AcceptPolicy;
|
||||
|
||||
Reference in New Issue
Block a user