From 0eec6881fc98eb2f84caf301d7adddc31f71a611 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 5 Jun 2025 18:53:17 -0700 Subject: [PATCH] Initial tab bar --- Cargo.lock | 3 +- Cargo.toml | 2 +- assets/icons/home-toolbar.png | Bin 0 -> 1114 bytes assets/icons/home-toolbar.svg | 4 + assets/icons/notifications.svg | 4 + assets/icons/notifications_dark_4x.png | Bin 0 -> 1529 bytes crates/notedeck_chrome/Cargo.toml | 1 + crates/notedeck_chrome/src/chrome.rs | 175 +++++++++++++++++++++---- 8 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 assets/icons/home-toolbar.png create mode 100644 assets/icons/home-toolbar.svg create mode 100644 assets/icons/notifications.svg create mode 100644 assets/icons/notifications_dark_4x.png diff --git a/Cargo.lock b/Cargo.lock index 9d81cbf2..4068d3fb 100644 --- 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 index b6eeab35..4f58fa8c 100644 --- 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 new file mode 100644 index 0000000000000000000000000000000000000000..1e636e5ec5189bc7221d51c8a472262c67858535 GIT binary patch literal 1114 zcmV-g1f~0lP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0063uBQgL0010qN zS#tmY0zd!&0zd%vSf(5R00YuVL_t(|+U;B2OH@G|J>#xoNDBpiRP-&`tB`J>MWB(O z{sq3-Q}0DR`WRBU){Br)3H}En1xhdau$D>mlu%-p_VtpHm>+w3anp6~ox6AE-kHtr zd03X;{$_sX%$z%S=I$;KA;SL+5nk7kH)VzyGA)+tsi`La&&S&1!NQV^VDKl?eS$XH^FJ@RVnWZ1`Y0AM*&%oLrKSjlI~ zi?lr_`?B&Pf6l|E>8q{d;B6jKUMnW8M8TU%>)dv>M4|11rtf7P<@)Y%zX;z-`ULa_ zK#zPt7O-TcU`Nilm4Yv5&`M!VxgjfsJ5r%rDSWSnI6$UWi;(JgY?A}N_50tNnC{Tz zfO%|_L)-oaRM)I^r0TjeZ+)|+vnmzIcDGO-OmK`U^qL_5mrbvd%NP^c% z#iRg_YAqPvC4fnE7$vBS4kmL1pvg8P-)PDe@XsPEVA3`b-%{Njal=Euc@@*>@G%&r=)^R; zgQy+A46qx61vIO`LsC4_t*$)-tVOEe#`k&Fx&WlGzu>kN(B9)}3Sc$#o}jg{(y9P7 zXkJC7Xs|2bgl2o-q+WB~RY<;Vy+T@%e;)OoJs1^GCPgN2SCM~R5>N&$=vqh~fQ&MK zQ;L+@6`=oW;E}L zjA0Bw0x + + + diff --git a/assets/icons/notifications.svg b/assets/icons/notifications.svg new file mode 100644 index 00000000..f56d4b3a --- /dev/null +++ b/assets/icons/notifications.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/notifications_dark_4x.png b/assets/icons/notifications_dark_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..1a45b96fbabc9eca6a298a1b183e1721aa8aa2ea GIT binary patch literal 1529 zcmV004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0063uBQgL0010qN zS#tmY0zd!&0zd%vSf(5R00nVLL_t(|+U=WLY!y`$hQEyzT6%$0u~bB$fYE}N5M#w6 z)R@>rK}dKrsfkHnc+n6AnxKX^Z$yHzPhNtQL=!Yd3rdNJ)!2lK35k(vDlH%oKs;6H z4X9=LK!|YGo;iEY>^TSG`5*t8wdVh4&Dpc}+H;_Q0%E}=kyH#bn8!>eQ3?z*#4&m} zVdT0uAPZ?^DfN_D4i3^qCmrTs5;rZr^eWjP-Qt37*&tQ<)_bk^S<))QPU5d%Sl*GD z`PO||e2Hw7kx1hIGx3^~=3D+E>CH;~74*uY{E0WxBBQb7i-?TMdP!s~%p`xjd=Ojw z6?`HSb1lDAc6*aABGQrVuOX}A$ppTjG4zx%4%5dOMt~Bks9`QQg+_F++>GYLfQ+M! zrvncU(ZNpkF_^}e@eohaNbsHB&NI>9)!7(m;njdcDc)r}10liXG||k>0Y}%FHxoA? zi};T5wga@WiN76EOy)(JtXCdoiP@W|0V&}pYAgp*tl`@Ti5hszT8Ca1nDbE-jf-Z0 z7OVW@tcoNbI$6OH3qOZ-(F8@RfXw1&iWPl~Rh*4@BLJrIDSAFM!d>P!_axCOu+}0! zMN?Mt;Vez06kEdDC`lqzK&m)GNjgb0UuPx%G8)*bG@Pf#oN`SNr2@@LKD5P>51njR zTrtnOCx}vkZthN_QatK3aE4(D-%y#xb({IF;m#G1g-SlW7h693#ruk@mHWIKu*`ji zXp1c$U>m~@SBq_+K{2~SCk^g6#~z2P#WNs=hZVWYi+q5cim7*9)nzd)dh{F|Q8PKy(zXmQU#<9`jYCuopE_gMdvlY7qW?z9D%zz#f z{60cD+ho56suh!RjC4c4Vx}c+;CHVEbhc`@Kvx6$e*AF*8IWorBLe-eIWZvG@j>71PJzL=5y;miEn=f-)G8YRd!kRU+~MONVZSWSEs^ zs=)J>4ZWQt1>Nf_;<@mU&<37Va#VE(eq?!K2H;CIfjGx36EblOuRx2Xx$$v=^5G-J zRIolQAfy5^heN72FhCtc2^uJ4pK79AV7>`_afVf3v!%81VWRS3kZp>a$mZaHzzWDp zK2zi{i#@M}H7fbu@;${$^SS->fC@+{TP*a}#N|W3b>C-8(C0WokI*ZYesqe~#N`9L zZCN4DlKIlu#oJV84bJhu~rQOrb^bGzdAo4Sk!WI88uBr_Y# zHb<`|^dmGk-x@=)B%RO(<~b$G7t~T#8}Q$6@Cl_({{{@lnVxT*0XW5MleQJ2Dqw_- z`PLZ#VYPAF7m}B~epc)YZ@61EvReA`t@j#q$s=~HVGEiX?q}LnYqDa?US%>Ex$2cU zz-fLl-8s5)0Rj7Zc#-r&nXR}LbQ#SQ)=&fhTV|3#MR&*o>Y(-&C* f1r$)g{}cZLCob)CtP@;q00000NkvXXu0mjfQdh)y literal 0 HcmV?d00001 diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml index 6dac8b07..be2a8a86 100644 --- 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 index 37638b2f..a1c6c1f9 100644 --- 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, } @@ -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 { - 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 { let mut got_action: Option = 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,8 @@ 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); } }); }); @@ -206,6 +214,90 @@ impl Chrome { 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 { + let mut got_action: Option = 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)) + } + }); + }); + + got_action + } + + fn toolbar(&mut self, ui: &mut egui::Ui) -> Option { + 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 { + 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??