notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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:
Mcrates/notedeck_chrome/src/chrome.rs | 636++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
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 } }