Initial tab bar

This commit is contained in:
William Casarin
2025-06-05 18:53:17 -07:00
parent bcd9c61d46
commit 0eec6881fc
8 changed files with 162 additions and 27 deletions

3
Cargo.lock generated
View File

@@ -1477,7 +1477,7 @@ dependencies = [
[[package]] [[package]]
name = "egui_tabs" name = "egui_tabs"
version = "0.2.1" 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 = [ dependencies = [
"egui", "egui",
"egui_extras", "egui_extras",
@@ -3198,6 +3198,7 @@ dependencies = [
"egui", "egui",
"egui-winit", "egui-winit",
"egui_extras", "egui_extras",
"egui_tabs",
"nostrdb", "nostrdb",
"notedeck", "notedeck",
"notedeck_columns", "notedeck_columns",

View File

@@ -24,7 +24,7 @@ egui-wgpu = "0.31.1"
egui_extras = { version = "0.31.1", features = ["all_loaders"] } egui_extras = { version = "0.31.1", features = ["all_loaders"] }
egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] } egui-winit = { version = "0.31.1", features = ["android-game-activity", "clipboard"] }
egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "0f0cbdd3184f3ff5fdf69ada08416ffc58a70d7a" } 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 = "0.6.0"
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" } egui_virtual_list = { git = "https://github.com/jb55/hello_egui", rev = "a66b6794f5e707a2f4109633770e02b02fb722e1" }
ehttp = "0.5.0" ehttp = "0.5.0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -10,6 +10,7 @@ description = "The nostr browser"
[dependencies] [dependencies]
eframe = { workspace = true } eframe = { workspace = true }
egui_tabs = { workspace = true }
egui_extras = { workspace = true } egui_extras = { workspace = true }
egui = { workspace = true } egui = { workspace = true }
notedeck_columns = { workspace = true } notedeck_columns = { workspace = true }

View File

@@ -2,12 +2,12 @@
//#[cfg(target_arch = "wasm32")] //#[cfg(target_arch = "wasm32")]
//use wasm_bindgen::prelude::*; //use wasm_bindgen::prelude::*;
use crate::app::NotedeckApp; 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 egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType, profile::get_profile_url, App, AppAction, AppContext, NotedeckTextStyle, UserAccount,
profile::get_profile_url, WalletType,
}; };
use notedeck_columns::Damus; use notedeck_columns::Damus;
use notedeck_dave::{Dave, DaveAvatar}; use notedeck_dave::{Dave, DaveAvatar};
@@ -19,6 +19,7 @@ pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2;
pub struct Chrome { pub struct Chrome {
active: i32, active: i32,
open: bool, open: bool,
tab_selected: i32,
apps: Vec<NotedeckApp>, apps: Vec<NotedeckApp>,
} }
@@ -26,17 +27,25 @@ impl Default for Chrome {
fn default() -> Self { fn default() -> Self {
Self { Self {
active: 0, active: 0,
tab_selected: 0,
open: true, open: true,
apps: vec![], apps: vec![],
} }
} }
} }
pub enum ToolbarAction {
Notifications,
Dave,
Home,
}
pub enum ChromePanelAction { pub enum ChromePanelAction {
Support, Support,
Settings, Settings,
Account, Account,
Wallet, Wallet,
Toolbar(ToolbarAction),
SaveTheme(ThemePreference), SaveTheme(ThemePreference),
} }
@@ -67,6 +76,10 @@ impl ChromePanelAction {
ctx.theme.save(*theme); ctx.theme.save(*theme);
} }
Self::Toolbar(_toolbar_action) => {
tracing::info!("toolbar action");
}
Self::Support => { Self::Support => {
Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Support); Self::columns_navigate(ctx, chrome, notedeck_columns::Route::Support);
} }
@@ -135,21 +148,16 @@ impl Chrome {
self.active = app; self.active = app;
} }
/// Show the side menu or bar, depending on if we're on a narrow /// The chrome side panel
/// or wide screen. fn panel(
/// &mut self,
/// The side menu should hover over the screen, while the side bar app_ctx: &mut AppContext,
/// is collapsible but persistent on the screen. builder: StripBuilder,
fn show(&mut self, ctx: &mut AppContext, ui: &mut egui::Ui) -> Option<ChromePanelAction> { amt_open: f32,
ui.spacing_mut().item_spacing.x = 0.0; ) -> Option<ChromePanelAction> {
let mut got_action: Option<ChromePanelAction> = None; let mut got_action: Option<ChromePanelAction> = None;
let side_panel_width: f32 = 70.0;
let open_id = egui::Id::new("chrome_open"); builder
let amt_open = ui.ctx().animate_bool(open_id, self.open) * side_panel_width;
StripBuilder::new(ui)
.size(Size::exact(amt_open)) // collapsible sidebar .size(Size::exact(amt_open)) // collapsible sidebar
.size(Size::remainder()) // the main app contents .size(Size::remainder()) // the main app contents
.clip(true) .clip(true)
@@ -172,7 +180,7 @@ impl Chrome {
}); });
ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { 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); got_action = Some(action);
} }
}); });
@@ -197,8 +205,8 @@ impl Chrome {
); );
*/ */
if let Some(action) = self.apps[self.active as usize].update(ctx, ui) { if let Some(action) = self.apps[self.active as usize].update(app_ctx, ui) {
chrome_handle_app_action(self, ctx, action, ui); chrome_handle_app_action(self, app_ctx, action, ui);
} }
}); });
}); });
@@ -206,6 +214,90 @@ impl Chrome {
got_action 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))
}
});
});
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) { fn topdown_sidebar(&mut self, ui: &mut egui::Ui) {
// macos needs a bit of space to make room for window // macos needs a bit of space to make room for window
// minimize/close buttons // minimize/close buttons
@@ -236,7 +328,8 @@ impl Chrome {
ui.add_space(32.0); ui.add_space(32.0);
if let Some(dave) = self.get_dave() { 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() { if dave_resp.clicked() {
self.active = 1; self.active = 1;
} else if dave_resp.hovered() { } 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 { fn columns_button(ui: &mut egui::Ui) -> egui::Response {
let btn = egui::include_image!("../../../assets/icons/columns_80.png"); let btn = egui::include_image!("../../../assets/icons/columns_80.png");
expanding_button("columns-button", 40.0, &btn, &btn, ui) 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 { 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) avatar.render(rect, ui)
} else { } else {
// plain icon if wgpu device not available?? // plain icon if wgpu device not available??