1
0
forked from GRIN/grim

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:
Claude
2026-06-10 02:33:32 -04:00
parent 32696438d3
commit 906fee9c71
+196 -17
View File
@@ -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;