notedeck

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

commit 0eec6881fc98eb2f84caf301d7adddc31f71a611
parent bcd9c61d46e8050d680f72200a32730e9f10ea4c
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  5 Jun 2025 18:53:17 -0700

Initial tab bar

Diffstat:
MCargo.lock | 3++-
MCargo.toml | 2+-
Aassets/icons/home-toolbar.png | 0
Aassets/icons/home-toolbar.svg | 4++++
Aassets/icons/notifications.svg | 4++++
Aassets/icons/notifications_dark_4x.png | 0
Mcrates/notedeck_chrome/Cargo.toml | 1+
Mcrates/notedeck_chrome/src/chrome.rs | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
8 files changed, 162 insertions(+), 27 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "egui_tabs" version = "0.2.1" -source = "git+https://github.com/damus-io/egui-tabs?rev=881d86bdf8b424563bf0869eaab5ab9a69e012a4#881d86bdf8b424563bf0869eaab5ab9a69e012a4" +source = "git+https://github.com/damus-io/egui-tabs?rev=6eb91740577b374a8a6658c09c9a4181299734d0#6eb91740577b374a8a6658c09c9a4181299734d0" dependencies = [ "egui", "egui_extras", @@ -3198,6 +3198,7 @@ dependencies = [ "egui", "egui-winit", "egui_extras", + "egui_tabs", "nostrdb", "notedeck", "notedeck_columns", diff --git a/Cargo.toml b/Cargo.toml @@ -24,7 +24,7 @@ egui-wgpu = "0.31.1" egui_extras = { version = "0.31.1", features = ["all_loaders"] } egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] } egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "0f0cbdd3184f3ff5fdf69ada08416ffc58a70d7a" } -egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "881d86bdf8b424563bf0869eaab5ab9a69e012a4" } +egui_tabs = { git = "https://github.com/damus-io/egui-tabs", rev = "6eb91740577b374a8a6658c09c9a4181299734d0" } #egui_virtual_list = "0.6.0" egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" } ehttp = "0.5.0" diff --git a/assets/icons/home-toolbar.png b/assets/icons/home-toolbar.png Binary files differ. diff --git a/assets/icons/home-toolbar.svg b/assets/icons/home-toolbar.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.12" d="M6 14V8H10V14" fill="white"/> +<path d="M5.99992 14V9.06667C5.99992 8.69327 5.99992 8.5066 6.07259 8.364C6.1365 8.23853 6.23849 8.1366 6.36392 8.07267C6.50654 8 6.69325 8 7.06659 8H8.93325C9.30665 8 9.49332 8 9.63592 8.07267C9.76139 8.1366 9.86332 8.23853 9.92725 8.364C9.99992 8.5066 9.99992 8.69327 9.99992 9.06667V14M1.33325 6.33333L7.35992 1.81333C7.58945 1.64121 7.70419 1.55514 7.83019 1.52196C7.94145 1.49268 8.05838 1.49268 8.16965 1.52197C8.29565 1.55514 8.41038 1.64121 8.63992 1.81333L14.6666 6.33333M2.66659 5.33333V11.8667C2.66659 12.6134 2.66659 12.9868 2.81191 13.272C2.93975 13.5229 3.14372 13.7269 3.3946 13.8547C3.67982 14 4.05318 14 4.79992 14H11.1999C11.9467 14 12.3201 14 12.6053 13.8547C12.8561 13.7269 13.0601 13.5229 13.1879 13.272C13.3333 12.9868 13.3333 12.6134 13.3333 11.8667V5.33333L9.27992 2.29333C8.82092 1.94907 8.59138 1.77695 8.33932 1.71059C8.11685 1.65203 7.88298 1.65203 7.66052 1.71059C7.40845 1.77695 7.17892 1.94907 6.71992 2.29333L2.66659 5.33333Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/notifications.svg b/assets/icons/notifications.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.12" d="M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" fill="white"/> +<path d="M9.33342 14H6.66675M12.0001 5.33337C12.0001 4.27251 11.5787 3.25509 10.8286 2.50495C10.0784 1.7548 9.06095 1.33337 8.00008 1.33337C6.93922 1.33337 5.92182 1.7548 5.17167 2.50495C4.42152 3.25509 4.0001 4.27251 4.0001 5.33337C4.0001 7.39351 3.48041 8.80404 2.89987 9.73697C2.41018 10.524 2.16534 10.9174 2.17431 11.0272C2.18426 11.1488 2.21 11.1951 2.30794 11.2678C2.3964 11.3334 2.79516 11.3334 3.59266 11.3334H12.4075C13.205 11.3334 13.6038 11.3334 13.6922 11.2678C13.7902 11.1951 13.8159 11.1488 13.8259 11.0272C13.8349 10.9174 13.59 10.524 13.1003 9.73697C12.5197 8.80404 12.0001 7.39351 12.0001 5.33337Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/notifications_dark_4x.png b/assets/icons/notifications_dark_4x.png Binary files differ. diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml @@ -10,6 +10,7 @@ description = "The nostr browser" [dependencies] eframe = { workspace = true } +egui_tabs = { workspace = true } egui_extras = { workspace = true } egui = { workspace = true } notedeck_columns = { workspace = true } diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -2,12 +2,12 @@ //#[cfg(target_arch = "wasm32")] //use wasm_bindgen::prelude::*; use crate::app::NotedeckApp; -use egui::{Button, Label, Layout, RichText, ThemePreference, Widget, vec2}; +use egui::{vec2, Button, Label, Layout, Rect, RichText, ThemePreference, Widget}; use egui_extras::{Size, StripBuilder}; use nostrdb::{ProfileRecord, Transaction}; use notedeck::{ - App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType, - profile::get_profile_url, + profile::get_profile_url, App, AppAction, AppContext, NotedeckTextStyle, UserAccount, + WalletType, }; use notedeck_columns::Damus; use notedeck_dave::{Dave, DaveAvatar}; @@ -19,6 +19,7 @@ pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; pub struct Chrome { active: i32, open: bool, + tab_selected: i32, apps: Vec<NotedeckApp>, } @@ -26,17 +27,25 @@ impl Default for Chrome { fn default() -> Self { Self { active: 0, + tab_selected: 0, open: true, apps: vec![], } } } +pub enum ToolbarAction { + Notifications, + Dave, + Home, +} + pub enum ChromePanelAction { Support, Settings, Account, Wallet, + Toolbar(ToolbarAction), SaveTheme(ThemePreference), } @@ -67,6 +76,10 @@ impl ChromePanelAction { ctx.theme.save(*theme); } + Self::Toolbar(_toolbar_action) => { + tracing::info!("toolbar action"); + } + Self::Support => { Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Support); } @@ -135,21 +148,16 @@ impl Chrome { self.active = app; } - /// Show the side menu or bar, depending on if we're on a narrow - /// or wide screen. - /// - /// The side menu should hover over the screen, while the side bar - /// is collapsible but persistent on the screen. - fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> { - ui.spacing_mut().item_spacing.x = 0.0; - + /// The chrome side panel + fn panel( + &mut self, + app_ctx: &mut AppContext, + builder: StripBuilder, + amt_open: f32, + ) -> Option<ChromePanelAction> { let mut got_action: Option<ChromePanelAction> = None; - let side_panel_width: f32 = 70.0; - let open_id = egui::Id::new("chrome_open"); - let amt_open = ui.ctx().animate_bool(open_id, self.open) * side_panel_width; - - StripBuilder::new(ui) + builder .size(Size::exact(amt_open)) // collapsible sidebar .size(Size::remainder()) // the main app contents .clip(true) @@ -172,7 +180,7 @@ impl Chrome { }); ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { - if let Some(action) = bottomup_sidebar(ctx, ui) { + if let Some(action) = bottomup_sidebar(app_ctx, ui) { got_action = Some(action); } }); @@ -197,8 +205,48 @@ impl Chrome { ); */ - if let Some(action) = self.apps[self.active as usize].update(ctx, ui) { - chrome_handle_app_action(self, ctx, action, ui); + if let Some(action) = self.apps[self.active as usize].update(app_ctx, ui) { + chrome_handle_app_action(self, app_ctx, action, ui); + } + }); + }); + + got_action + } + + /// How far is the chrome panel expanded? + fn amount_open(&self, ui: &mut egui::Ui) -> f32 { + let open_id = egui::Id::new("chrome_open"); + let side_panel_width: f32 = 70.0; + ui.ctx().animate_bool(open_id, self.open) * side_panel_width + } + + fn toolbar_height() -> f32 { + 60.0 + } + + /// On narrow layouts, we have a toolbar + fn toolbar_chrome( + &mut self, + ctx: &mut AppContext, + ui: &mut egui::Ui, + ) -> Option<ChromePanelAction> { + let mut got_action: Option<ChromePanelAction> = None; + let amt_open = self.amount_open(ui); + + StripBuilder::new(ui) + .size(Size::remainder()) // top cell + .size(Size::exact(Self::toolbar_height())) // bottom cell + .vertical(|mut strip| { + strip.strip(|builder| { + // the chrome panel is nested above the toolbar + + got_action = self.panel(ctx, builder, amt_open); + }); + + strip.cell(|ui| { + if let Some(action) = self.toolbar(ui) { + got_action = Some(ChromePanelAction::Toolbar(action)) } }); }); @@ -206,6 +254,50 @@ impl Chrome { got_action } + fn toolbar(&mut self, ui: &mut egui::Ui) -> Option<ToolbarAction> { + let _tab_res = egui_tabs::Tabs::new(3) + .selected(self.tab_selected) + //.hover_bg(TabColor::none()) + //.selected_fg(TabColor::none()) + //.selected_bg(TabColor::none()) + //.hover_bg(TabColor::none()) + //.hover_bg(TabColor::custom(egui::Color32::RED)) + .height(Self::toolbar_height()) + .layout(Layout::centered_and_justified(egui::Direction::TopDown)) + .show(ui, |ui, state| { + let index = state.index(); + + if index == 0 { + home_button(ui); + } else if index == 1 { + if let Some(dave) = self.get_dave() { + let rect = dave_toolbar_rect(ui); + let _dave_resp = dave_button(dave.avatar_mut(), ui, rect); + } + } else if index == 2 { + notifications_button(ui); + } + }); + + None + } + + /// Show the side menu or bar, depending on if we're on a narrow + /// or wide screen. + /// + /// The side menu should hover over the screen, while the side bar + /// is collapsible but persistent on the screen. + fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> { + ui.spacing_mut().item_spacing.x = 0.0; + + if notedeck::ui::is_narrow(ui.ctx()) { + self.toolbar_chrome(ctx, ui) + } else { + let amt_open = self.amount_open(ui); + self.panel(ctx, StripBuilder::new(ui), amt_open) + } + } + fn topdown_sidebar(&mut self, ui: &mut egui::Ui) { // macos needs a bit of space to make room for window // minimize/close buttons @@ -236,7 +328,8 @@ impl Chrome { ui.add_space(32.0); if let Some(dave) = self.get_dave() { - let dave_resp = dave_button(dave.avatar_mut(), ui); + let rect = dave_sidebar_rect(ui); + let dave_resp = dave_button(dave.avatar_mut(), ui, rect); if dave_resp.clicked() { self.active = 1; } else if dave_resp.hovered() { @@ -340,17 +433,49 @@ fn settings_button(ui: &mut egui::Ui) -> egui::Response { ) } +fn notifications_button(ui: &mut egui::Ui) -> egui::Response { + expanding_button( + "notifications-button", + 24.0, + &egui::include_image!("../../../assets/icons/notifications_dark_4x.png"), + &egui::include_image!("../../../assets/icons/notifications_dark_4x.png"), + ui, + ) +} + +fn home_button(ui: &mut egui::Ui) -> egui::Response { + expanding_button( + "home-button", + 24.0, + &egui::include_image!("../../../assets/icons/home-toolbar.png"), + &egui::include_image!("../../../assets/icons/home-toolbar.png"), + ui, + ) +} + fn columns_button(ui: &mut egui::Ui) -> egui::Response { let btn = egui::include_image!("../../../assets/icons/columns_80.png"); expanding_button("columns-button", 40.0, &btn, &btn, ui) } -fn dave_button(avatar: Option<&mut DaveAvatar>, 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_toolbar_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.center().y; + 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 { - let size = vec2(60.0, 60.0); - let available = ui.available_rect_before_wrap(); - let center_x = available.center().x; - let rect = egui::Rect::from_center_size(egui::pos2(center_x, available.top()), size); avatar.render(rect, ui) } else { // plain icon if wgpu device not available??