commit fdaec372123b98a99c45d61dc7cd7c668436f1e7
parent 5490c513db866561c746295c0a163a766caf070c
Author: kernelkind <kernelkind@gmail.com>
Date: Fri, 10 Oct 2025 21:23:04 -0400
chrome: ui polish
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
1 file changed, 384 insertions(+), 252 deletions(-)
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -6,12 +6,15 @@ use crate::ChromeOptions;
use bitflags::bitflags;
use eframe::CreationContext;
use egui::{
- vec2, Button, Color32, CornerRadius, Label, Layout, Rect, RichText, ThemePreference, Widget,
+ vec2, Color32, CornerRadius, Label, Layout, Margin, Rect, RichText, Sense, ThemePreference, Ui,
+ Widget,
};
use egui_extras::{Size, StripBuilder};
use egui_nav::RouteResponse;
use egui_nav::{NavAction, NavDrawer};
use nostrdb::{ProfileRecord, Transaction};
+use notedeck::fonts::get_font_size;
+use notedeck::name::get_display_name;
use notedeck::ui::is_compiled_as_mobile;
use notedeck::AppResponse;
use notedeck::DrawerRouter;
@@ -23,9 +26,7 @@ use notedeck::{
};
use notedeck_columns::{timeline::TimelineKind, Damus};
use notedeck_dave::{Dave, DaveAvatar};
-use notedeck_ui::{
- app_images, expanding_button, AnimationHelper, ProfilePic, ICON_EXPANSION_MULTIPLE, ICON_WIDTH,
-};
+use notedeck_ui::{app_images, expanding_button, galley_centered_pos, ProfilePic};
use std::collections::HashMap;
#[derive(Default)]
@@ -213,33 +214,44 @@ impl Chrome {
.returning(self.nav.returning)
.drawer_focused(self.nav.drawer_focused)
.drag(is_compiled_as_mobile())
- .opened_offset(100.0);
+ .opened_offset(240.0);
let resp = drawer.show_mut(ui, |ui, route| match route {
ChromeRoute::Chrome => {
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
CornerRadius::ZERO,
- ui.visuals().panel_fill,
- );
- _ = ui.vertical_centered(|ui| {
- self.topdown_sidebar(ui, app_ctx.i18n);
- });
-
- ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
- let options = if amt_keyboard_open > 0.0 {
- SidebarOptions::Compact
+ if ui.visuals().dark_mode {
+ egui::Color32::BLACK
} else {
- SidebarOptions::default()
- };
- let response = bottomup_sidebar(self, app_ctx, ui, options);
+ egui::Color32::WHITE
+ },
+ );
+ egui::Frame::new()
+ .inner_margin(Margin::same(16))
+ .show(ui, |ui| {
+ let options = if amt_keyboard_open > 0.0 {
+ SidebarOptions::Compact
+ } else {
+ SidebarOptions::default()
+ };
+
+ let response = ui
+ .with_layout(Layout::top_down(egui::Align::Min), |ui| {
+ topdown_sidebar(self, app_ctx, ui, options)
+ })
+ .inner;
+
+ ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| {
+ ui.add(milestone_name(app_ctx.i18n));
+ });
- RouteResponse {
- response,
- can_take_drag_from: Vec::new(),
- }
- })
- .inner
+ RouteResponse {
+ response,
+ can_take_drag_from: Vec::new(),
+ }
+ })
+ .inner
}
ChromeRoute::App => {
let resp = self.apps[self.active as usize].update(app_ctx, ui);
@@ -329,53 +341,6 @@ impl Chrome {
action
}
-
- fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
- // macos needs a bit of space to make room for window
- // minimize/close buttons
- if cfg!(target_os = "macos") {
- ui.add_space(30.0);
- } else {
- // we still want *some* padding so that it aligns with the + button regardless
- ui.add_space(notedeck_ui::constants::FRAME_MARGIN.into());
- }
-
- if ui.add(expand_side_panel_button()).clicked() {
- self.nav.close();
- }
-
- ui.add_space(4.0);
- ui.add(milestone_name(i18n));
- //let dark_mode = ui.ctx().style().visuals.dark_mode;
-
- for (i, app) in self.apps.iter_mut().enumerate() {
- let r = match app {
- NotedeckApp::Columns(_columns_app) => columns_button(ui),
-
- NotedeckApp::Dave(dave) => {
- ui.add_space(24.0);
- let rect = dave_sidebar_rect(ui);
- dave_button(dave.avatar_mut(), ui, rect)
- }
-
- NotedeckApp::ClnDash(_clndash) => clndash_button(ui),
-
- NotedeckApp::Notebook(_notebook) => notebook_button(ui),
-
- NotedeckApp::Other(_other) => {
- // app provides its own button rendering ui?
- panic!("TODO: implement other apps")
- }
- };
-
- ui.add_space(4.0);
-
- if r.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
- self.active = i as i32;
- self.nav.close();
- }
- }
- }
}
impl notedeck::App for Chrome {
@@ -390,86 +355,42 @@ impl notedeck::App for Chrome {
}
fn milestone_name<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
- |ui: &mut egui::Ui| -> egui::Response {
- ui.vertical_centered(|ui| {
- let font = egui::FontId::new(
- notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny),
- egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()),
- );
- ui.add(
- Label::new(
- RichText::new(tr!(i18n, "BETA", "Beta version label"))
- .color(ui.style().visuals.noninteractive().fg_stroke.color)
- .font(font),
- )
- .selectable(false),
- )
- .on_hover_text(tr!(
- i18n,
- "Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
- "Beta product warning message"
- ))
- .on_hover_cursor(egui::CursorIcon::Help)
- })
- .inner
- }
-}
+ let text = if notedeck::ui::is_compiled_as_mobile() {
+ tr!(
+ i18n,
+ "Damus Android BETA",
+ "Damus android beta version label"
+ )
+ } else {
+ tr!(
+ i18n,
+ "Damus Notedeck BETA",
+ "Damus notedeck beta version label"
+ )
+ };
-fn expand_side_panel_button() -> impl Widget {
|ui: &mut egui::Ui| -> egui::Response {
- let img_size = 40.0;
- let img = app_images::damus_image()
- .max_width(img_size)
- .sense(egui::Sense::click());
-
- ui.add(img)
+ let font = egui::FontId::new(
+ notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny),
+ egui::FontFamily::Name(notedeck::fonts::NamedFontFamily::Bold.as_str().into()),
+ );
+ ui.add(
+ Label::new(
+ RichText::new(text)
+ .color(ui.style().visuals.noninteractive().fg_stroke.color)
+ .font(font),
+ )
+ .selectable(false),
+ )
+ .on_hover_text(tr!(
+ i18n,
+ "Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
+ "Beta product warning message"
+ ))
+ .on_hover_cursor(egui::CursorIcon::Help)
}
}
-fn support_button(ui: &mut egui::Ui) -> egui::Response {
- expanding_button(
- "help-button",
- 16.0,
- app_images::help_light_image(),
- app_images::help_dark_image(),
- ui,
- false,
- )
-}
-
-fn settings_button(ui: &mut egui::Ui) -> egui::Response {
- expanding_button(
- "settings-button",
- 32.0,
- app_images::settings_light_image(),
- app_images::settings_dark_image(),
- ui,
- false,
- )
-}
-
-fn columns_button(ui: &mut egui::Ui) -> egui::Response {
- expanding_button(
- "columns-button",
- 40.0,
- app_images::columns_image(),
- app_images::columns_image(),
- ui,
- false,
- )
-}
-
-fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
- expanding_button(
- "accounts-button",
- 24.0,
- app_images::profile_image().tint(ui.visuals().text_color()),
- app_images::profile_image(),
- ui,
- false,
- )
-}
-
fn clndash_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"clndash-button",
@@ -492,14 +413,6 @@ fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
)
}
-fn dave_sidebar_rect(ui: &mut egui::Ui) -> Rect {
- let size = vec2(60.0, 60.0);
- let available = ui.available_rect_before_wrap();
- let center_x = available.center().x;
- let center_y = available.top();
- egui::Rect::from_center_size(egui::pos2(center_x, center_y), size)
-}
-
fn dave_button(avatar: Option<&mut DaveAvatar>, ui: &mut egui::Ui, rect: Rect) -> egui::Response {
if let Some(avatar) = avatar {
avatar.render(rect, ui)
@@ -529,33 +442,6 @@ pub fn get_account_url<'a>(
}
}
-fn wallet_button() -> impl Widget {
- |ui: &mut egui::Ui| -> egui::Response {
- let img_size = 24.0;
-
- let max_size = img_size * ICON_EXPANSION_MULTIPLE;
-
- let img = if !ui.visuals().dark_mode {
- app_images::wallet_light_image()
- } else {
- app_images::wallet_dark_image()
- }
- .max_width(img_size);
-
- let helper = AnimationHelper::new(ui, "wallet-icon", vec2(max_size, max_size));
-
- let cur_img_size = helper.scale_1d_pos(img_size);
- img.paint_at(
- ui,
- helper
- .get_animation_rect()
- .shrink((max_size - cur_img_size) / 2.0),
- );
-
- helper.take_animation_response()
- }
-}
-
fn chrome_handle_app_action(
chrome: &mut Chrome,
ctx: &mut AppContext,
@@ -661,34 +547,60 @@ fn columns_route_to_profile(
}
}
-fn pfp_button(ctx: &mut AppContext, ui: &mut egui::Ui) -> egui::Response {
- let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
- let helper = AnimationHelper::new(ui, "pfp-button", egui::vec2(max_size, max_size));
-
- let min_pfp_size = ICON_WIDTH;
- let cur_pfp_size = helper.scale_1d_pos(min_pfp_size);
-
- let txn = Transaction::new(ctx.ndb).expect("should be able to create txn");
- let profile_url = get_account_url(&txn, ctx.ndb, ctx.accounts.get_selected_account());
-
- let mut widget = ProfilePic::new(ctx.img_cache, profile_url).size(cur_pfp_size);
-
- ui.put(helper.get_animation_rect(), &mut widget);
-
- helper.take_animation_response()
-}
-
/// The section of the chrome sidebar that starts at the
/// bottom and goes up
-fn bottomup_sidebar(
+fn topdown_sidebar(
chrome: &mut Chrome,
ctx: &mut AppContext,
ui: &mut egui::Ui,
options: SidebarOptions,
) -> Option<ChromePanelAction> {
- ui.add_space(8.0);
+ let previous_spacing = ui.spacing().item_spacing;
+ ui.spacing_mut().item_spacing.y = 12.0;
+
+ let loc = &mut ctx.i18n;
+
+ // macos needs a bit of space to make room for window
+ // minimize/close buttons
+ if cfg!(target_os = "macos") {
+ ui.add_space(8.0);
+ }
+
+ let txn = Transaction::new(ctx.ndb).expect("should be able to create txn");
+ let profile = ctx
+ .ndb
+ .get_profile_by_pubkey(&txn, ctx.accounts.get_selected_account().key.pubkey.bytes());
+
+ let disp_name = get_display_name(profile.as_ref().ok());
+ let name = if let Some(username) = disp_name.username {
+ format!("@{username}")
+ } else {
+ disp_name.username_or_displayname().to_owned()
+ };
+
+ let selected_acc = ctx.accounts.get_selected_account();
+ let profile_url = get_account_url(&txn, ctx.ndb, selected_acc);
+ if let Ok(profile) = profile {
+ get_profile_url_owned(Some(profile))
+ } else {
+ get_profile_url_owned(None)
+ };
+
+ let pfp_resp = ui.add(&mut ProfilePic::new(ctx.img_cache, profile_url).size(64.0));
+
+ ui.horizontal_wrapped(|ui| {
+ ui.add(egui::Label::new(
+ RichText::new(name)
+ .color(ui.visuals().weak_text_color())
+ .size(16.0),
+ ));
+ });
- let pfp_resp = pfp_button(ctx, ui).on_hover_cursor(egui::CursorIcon::PointingHand);
+ if let Some(npub) = selected_acc.key.pubkey.npub() {
+ if ui.add(copy_npub(&npub, 200.0)).clicked() {
+ ui.ctx().copy_text(npub);
+ }
+ }
// we skip this whole function in compact mode
if options.contains(SidebarOptions::Compact) {
@@ -701,47 +613,214 @@ fn bottomup_sidebar(
};
}
- let accounts_resp = accounts_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
- let settings_resp = settings_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
-
- let theme_action = match ui.ctx().theme() {
- egui::Theme::Dark => {
- let resp = ui
- .add(Button::new("☀").frame(false))
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .on_hover_text(tr!(
- ctx.i18n,
- "Switch to light mode",
- "Hover text for light mode toggle button"
- ));
- if resp.clicked() {
- Some(ChromePanelAction::SaveTheme(ThemePreference::Light))
- } else {
- None
- }
+ let mut action = None;
+
+ let theme = ui.ctx().theme();
+
+ StripBuilder::new(ui)
+ .sizes(Size::exact(40.0), 6)
+ .clip(true)
+ .vertical(|mut strip| {
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ let profile_img = if ui.visuals().dark_mode {
+ app_images::profile_image()
+ } else {
+ app_images::profile_image().tint(ui.visuals().text_color())
+ }
+ .max_size(ui.available_size());
+ ui.add(profile_img);
+ },
+ tr!(loc, "Profile", "Button to go to the user's profile"),
+ )
+ .clicked()
+ {
+ action = Some(ChromePanelAction::Profile(
+ ctx.accounts.get_selected_account().key.pubkey,
+ ));
+ }
+ });
+
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ let account_img = if ui.visuals().dark_mode {
+ app_images::accounts_image()
+ } else {
+ app_images::accounts_image().tint(ui.visuals().text_color())
+ }
+ .max_size(ui.available_size());
+ ui.add(account_img);
+ },
+ tr!(loc, "Accounts", "Button to go to the accounts view"),
+ )
+ .clicked()
+ {
+ action = Some(ChromePanelAction::Account);
+ }
+ });
+
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ let img = if ui.visuals().dark_mode {
+ app_images::wallet_dark_image()
+ } else {
+ app_images::wallet_light_image()
+ };
+
+ ui.add(img);
+ },
+ tr!(loc, "Wallet", "Button to go to the wallet view"),
+ )
+ .clicked()
+ {
+ action = Some(ChromePanelAction::Wallet);
+ }
+ });
+
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ ui.add(if ui.visuals().dark_mode {
+ app_images::settings_dark_image()
+ } else {
+ app_images::settings_light_image()
+ });
+ },
+ tr!(loc, "Settings", "Button to go to the settings view"),
+ )
+ .clicked()
+ {
+ action = Some(ChromePanelAction::Settings);
+ }
+ });
+
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ let c = match theme {
+ egui::Theme::Dark => "🔆",
+ egui::Theme::Light => "🌒",
+ };
+
+ let painter = ui.painter();
+ let galley = painter.layout_no_wrap(
+ c.to_owned(),
+ NotedeckTextStyle::Heading3.get_font_id(ui.ctx()),
+ ui.visuals().text_color(),
+ );
+
+ painter.galley(
+ galley_centered_pos(&galley, ui.available_rect_before_wrap().center()),
+ galley,
+ ui.visuals().text_color(),
+ );
+ },
+ tr!(loc, "Theme", "Button to change the theme (light or dark)"),
+ )
+ .clicked()
+ {
+ match theme {
+ egui::Theme::Dark => {
+ action = Some(ChromePanelAction::SaveTheme(ThemePreference::Light));
+ }
+ egui::Theme::Light => {
+ action = Some(ChromePanelAction::SaveTheme(ThemePreference::Dark));
+ }
+ }
+ }
+ });
+
+ strip.strip(|b| {
+ if drawer_item(
+ b,
+ |ui| {
+ ui.add(if ui.visuals().dark_mode {
+ app_images::help_dark_image()
+ } else {
+ app_images::help_light_image()
+ });
+ },
+ tr!(loc, "Support", "Button to go to the support view"),
+ )
+ .clicked()
+ {
+ action = Some(ChromePanelAction::Support);
+ }
+ });
+ });
+
+ for (i, app) in chrome.apps.iter_mut().enumerate() {
+ if chrome.active == i as i32 {
+ continue;
}
- egui::Theme::Light => {
- let resp = ui
- .add(Button::new("🌙").frame(false))
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .on_hover_text(tr!(
- ctx.i18n,
- "Switch to dark mode",
- "Hover text for dark mode toggle button"
- ));
- if resp.clicked() {
- Some(ChromePanelAction::SaveTheme(ThemePreference::Dark))
- } else {
- None
+
+ let text = match &app {
+ NotedeckApp::Dave(_) => tr!(loc, "Dave", "Button to go to the Dave app"),
+ NotedeckApp::Columns(_) => tr!(loc, "Columns", "Button to go to the Columns app"),
+ NotedeckApp::Notebook(_) => {
+ tr!(loc, "Notebook", "Button to go to the Notebook app")
}
- }
- };
+ NotedeckApp::ClnDash(_) => tr!(loc, "ClnDash", "Button to go to the ClnDash app"),
+ NotedeckApp::Other(_) => tr!(loc, "Other", "Button to go to the Other app"),
+ };
- let support_resp = support_button(ui).on_hover_cursor(egui::CursorIcon::PointingHand);
+ StripBuilder::new(ui)
+ .size(Size::exact(40.0))
+ .clip(true)
+ .vertical(|mut strip| {
+ strip.strip(|b| {
+ let resp = drawer_item(
+ b,
+ |ui| {
+ match app {
+ NotedeckApp::Columns(_columns_app) => {
+ ui.add(app_images::columns_image());
+ }
+
+ NotedeckApp::Dave(dave) => {
+ dave_button(
+ dave.avatar_mut(),
+ ui,
+ Rect::from_center_size(
+ ui.available_rect_before_wrap().center(),
+ vec2(30.0, 30.0),
+ ),
+ );
+ }
+
+ NotedeckApp::ClnDash(_clndash) => {
+ clndash_button(ui);
+ }
+
+ NotedeckApp::Notebook(_notebook) => {
+ notebook_button(ui);
+ }
+
+ NotedeckApp::Other(_other) => {
+ // app provides its own button rendering ui?
+ panic!("TODO: implement other apps")
+ }
+ }
+ },
+ text,
+ )
+ .on_hover_cursor(egui::CursorIcon::PointingHand);
- let wallet_resp = ui
- .add(wallet_button())
- .on_hover_cursor(egui::CursorIcon::PointingHand);
+ if resp.clicked() {
+ chrome.active = i as i32;
+ chrome.nav.close();
+ }
+ })
+ });
+ }
if ctx.args.options.contains(NotedeckOptions::Debug) {
let r = ui
@@ -791,21 +870,74 @@ fn bottomup_sidebar(
}
}
- if pfp_resp.clicked() {
- let pk = ctx.accounts.get_selected_account().key.pubkey;
- Some(ChromePanelAction::Profile(pk))
- } else if accounts_resp.clicked() {
- Some(ChromePanelAction::Account)
- } else if settings_resp.clicked() {
- Some(ChromePanelAction::Settings)
- } else if theme_action.is_some() {
- theme_action
- } else if support_resp.clicked() {
- Some(ChromePanelAction::Support)
- } else if wallet_resp.clicked() {
- Some(ChromePanelAction::Wallet)
- } else {
- None
+ ui.spacing_mut().item_spacing = previous_spacing;
+
+ action
+}
+
+fn drawer_item(builder: StripBuilder, icon: impl FnOnce(&mut Ui), text: String) -> egui::Response {
+ builder
+ .cell_layout(Layout::left_to_right(egui::Align::Center))
+ .sense(Sense::click())
+ .size(Size::exact(24.0))
+ .size(Size::exact(8.0)) // free space
+ .size(Size::remainder())
+ .horizontal(|mut strip| {
+ strip.cell(icon);
+
+ strip.empty();
+
+ strip.cell(|ui| {
+ ui.add(drawer_label(ui.ctx(), &text));
+ });
+ })
+ .on_hover_cursor(egui::CursorIcon::PointingHand)
+}
+
+fn drawer_label(ctx: &egui::Context, text: &str) -> egui::Label {
+ egui::Label::new(RichText::new(text).size(get_font_size(ctx, &NotedeckTextStyle::Heading2)))
+ .selectable(false)
+}
+
+fn copy_npub<'a>(npub: &'a String, width: f32) -> impl Widget + use<'a> {
+ move |ui: &mut egui::Ui| -> egui::Response {
+ let size = vec2(width, 24.0);
+ let (rect, mut resp) = ui.allocate_exact_size(size, egui::Sense::click());
+ resp = resp.on_hover_cursor(egui::CursorIcon::Copy);
+
+ let painter = ui.painter_at(rect);
+
+ painter.rect_filled(
+ rect,
+ CornerRadius::same(32),
+ if resp.hovered() {
+ ui.visuals().widgets.active.bg_fill
+ } else {
+ // ui.visuals().panel_fill
+ ui.visuals().widgets.inactive.bg_fill
+ },
+ );
+
+ let text =
+ Label::new(RichText::new(npub).size(get_font_size(ui.ctx(), &NotedeckTextStyle::Tiny)))
+ .truncate()
+ .selectable(false);
+
+ let (label_rect, copy_rect) = {
+ let rect = rect.shrink(4.0);
+ let (l, r) = rect.split_left_right_at_x(rect.right() - 24.0);
+ (l, r.shrink2(vec2(4.0, 0.0)))
+ };
+
+ app_images::copy_to_clipboard_image()
+ .tint(ui.visuals().text_color())
+ .maintain_aspect_ratio(true)
+ // .max_size(vec2(24.0, 24.0))
+ .paint_at(ui, copy_rect);
+
+ ui.put(label_rect, text);
+
+ resp
}
}