redesign dock and titlebar

This commit is contained in:
2026-09-18 13:21:40 +07:00
parent 6ff1eeddbb
commit 2ccbfcd4a8
5 changed files with 313 additions and 178 deletions
+124 -59
View File
@@ -24,6 +24,7 @@ use crate::menu::DropdownMenu as _;
use crate::resizable::{resize_handle, resize_handle_appearance}; use crate::resizable::{resize_handle, resize_handle_appearance};
use crate::tab::Tab; use crate::tab::Tab;
use crate::tab::tab_bar::TabBar; use crate::tab::tab_bar::TabBar;
use crate::title_bar::{title_bar_drag_handlers, window_controls};
use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex}; use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex};
mod panel; mod panel;
@@ -31,12 +32,34 @@ pub use panel::*;
actions!(dock, [ToggleZoom, ClosePanel]); actions!(dock, [ToggleZoom, ClosePanel]);
pub type TitleBarRenderer = fn(&mut Window, &mut App) -> AnyElement;
#[derive(Default)]
pub struct TitleBarChrome {
trailing: Cell<Option<TitleBarRenderer>>,
}
impl TitleBarChrome {
pub fn set_trailing(&self, renderer: TitleBarRenderer) {
self.trailing.set(Some(renderer));
}
fn trailing(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
self.trailing.get().map(|render| render(window, cx))
}
}
pub fn dock_area( pub fn dock_area(
id: impl Into<SharedString>, id: impl Into<SharedString>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Entity<DockArea> { ) -> (Entity<DockArea>, Rc<TitleBarChrome>) {
let shared = Rc::new(SkinShared::default()); let chrome = Rc::new(TitleBarChrome::default());
let shared = Rc::new(SkinShared {
area: RefCell::new(None),
resizing: Cell::new(None),
chrome: chrome.clone(),
});
let area = cx.new(|cx| { let area = cx.new(|cx| {
DockArea::new(id, None, window, cx).with_renderer(Rc::new(DockSkin { DockArea::new(id, None, window, cx).with_renderer(Rc::new(DockSkin {
shared: shared.clone(), shared: shared.clone(),
@@ -44,7 +67,7 @@ pub fn dock_area(
}); });
*shared.area.borrow_mut() = Some(area.downgrade()); *shared.area.borrow_mut() = Some(area.downgrade());
area (area, chrome)
} }
pub fn add_panel( pub fn add_panel(
@@ -167,6 +190,7 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
struct SkinShared { struct SkinShared {
area: RefCell<Option<WeakEntity<DockArea>>>, area: RefCell<Option<WeakEntity<DockArea>>>,
resizing: Cell<Option<DockPlacement>>, resizing: Cell<Option<DockPlacement>>,
chrome: Rc<TitleBarChrome>,
} }
impl SkinShared { impl SkinShared {
@@ -394,6 +418,17 @@ impl TabGroupSkin {
} }
} }
fn is_title_bar_group(&self, group: &TabGroupContext, cx: &App) -> bool {
let Some(area) = self.shared.area() else {
return false;
};
area.read(cx)
.layout(DockPlacement::Center)
.and_then(|tree| left_top_group(tree.root()))
== Some(group.node())
}
fn render_toolbar( fn render_toolbar(
&self, &self,
group: &TabGroupContext, group: &TabGroupContext,
@@ -473,16 +508,17 @@ impl TabGroupSkin {
let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx); let right_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
let has_leading = left_button.is_some() || bottom_button.is_some(); let has_leading = left_button.is_some() || bottom_button.is_some();
let drag = tab_drag(group, ix, cx); let drag = tab_drag(group, ix, cx);
let is_title_bar = self.is_title_bar_group(group, cx);
let trailing_chrome = is_title_bar
.then(|| self.shared.chrome.trailing(window, cx))
.flatten();
h_flex() let bar = h_flex()
.id("tab-title-bar")
.justify_between() .justify_between()
.items_center() .items_center()
.line_height(rems(1.0)) .line_height(rems(1.0))
.h(TABBAR_HEIGHT) .h(TABBAR_HEIGHT)
.py_2()
.pl_3()
.pr_2()
.rounded_t(cx.theme().radius_lg)
.bg(cx.theme().panel_background) .bg(cx.theme().panel_background)
.when(left_button.is_some(), |this| this.pl_2()) .when(left_button.is_some(), |this| this.pl_2())
.when(right_button.is_some(), |this| this.pr_2()) .when(right_button.is_some(), |this| this.pr_2())
@@ -499,9 +535,9 @@ impl TabGroupSkin {
.child( .child(
div() div()
.id("tab") .id("tab")
.flex_1() .flex_initial()
.min_w_0()
.px_2() .px_2()
.min_w_16()
.overflow_hidden() .overflow_hidden()
.whitespace_nowrap() .whitespace_nowrap()
.child( .child(
@@ -524,6 +560,14 @@ impl TabGroupSkin {
}) })
}), }),
) )
.child({
let space = div().id("tab-title-space").flex_1().h_full();
if is_title_bar {
title_bar_drag_handlers(space, window, cx).into_any_element()
} else {
space.into_any_element()
}
})
.child( .child(
h_flex() h_flex()
.flex_shrink_0() .flex_shrink_0()
@@ -532,7 +576,18 @@ impl TabGroupSkin {
.child(self.render_toolbar(group, window, cx)) .child(self.render_toolbar(group, window, cx))
.children(right_button), .children(right_button),
) )
.into_any_element() .when_some(trailing_chrome, |this, chrome| this.child(chrome));
if is_title_bar {
h_flex()
.h(TABBAR_HEIGHT)
.bg(cx.theme().panel_background)
.child(bar.flex_1())
.child(window_controls())
.into_any_element()
} else {
bar.into_any_element()
}
} }
fn render_tabs( fn render_tabs(
@@ -556,13 +611,36 @@ impl TabGroupSkin {
.iter() .iter()
.position(|panel| panel.panel_id(cx) == displayed) .position(|panel| panel.panel_id(cx) == displayed)
}); });
let is_title_bar = self.is_title_bar_group(group, cx);
let trailing_chrome = is_title_bar
.then(|| self.shared.chrome.trailing(window, cx))
.flatten();
let empty_space = div()
.id("tab-bar-empty-space")
.h_full()
.flex_grow_1()
.min_w_16()
.when(droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().surface_background))
.on_drop({
let group = TabGroupContext::clone(group);
move |drag: &DragPanel, window, cx| {
let ix = (drag.source() == group.node()).then(|| tabs_count - 1);
group.drop_panel(drag.clone(), ix, false, window, cx);
}
})
});
let empty_space = if is_title_bar {
title_bar_drag_handlers(empty_space, window, cx).into_any_element()
} else {
empty_space.into_any_element()
};
TabBar::new("tab-bar") let bar = TabBar::new("tab-bar")
.track_scroll(&self.scroll_handle) .track_scroll(&self.scroll_handle)
.h(TABBAR_HEIGHT) .h(TABBAR_HEIGHT)
.bg(cx.theme().panel_background) .bg(cx.theme().panel_background)
.rounded_t(cx.theme().radius_lg) .when(is_title_bar || has_leading, |this| {
.when(has_leading, |this| {
this.prefix( this.prefix(
h_flex() h_flex()
.items_center() .items_center()
@@ -639,26 +717,7 @@ impl TabGroupSkin {
}) })
}) })
})) }))
.last_empty_space( .last_empty_space(empty_space)
// Empty space so a panel can be moved past the last tab.
div()
.id("tab-bar-empty-space")
.h_full()
.flex_grow_1()
.min_w_16()
.when(droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| {
this.bg(cx.theme().surface_background)
})
.on_drop({
let group = TabGroupContext::clone(group);
move |drag: &DragPanel, window, cx| {
let ix = (drag.source() == group.node()).then(|| tabs_count - 1);
group.drop_panel(drag.clone(), ix, false, window, cx);
}
})
}),
)
.when(!collapsed, |this| { .when(!collapsed, |this| {
this.suffix( this.suffix(
h_flex() h_flex()
@@ -669,10 +728,22 @@ impl TabGroupSkin {
.px_0p5() .px_0p5()
.gap_1() .gap_1()
.child(self.render_toolbar(group, window, cx)) .child(self.render_toolbar(group, window, cx))
.children(right_button), .children(right_button)
.children(trailing_chrome),
) )
}) });
.into_any_element()
if is_title_bar {
h_flex()
.h(TABBAR_HEIGHT)
.w_full()
.bg(cx.theme().panel_background)
.child(bar.flex_1())
.child(window_controls())
.into_any_element()
} else {
bar.into_any_element()
}
} }
fn dock_toggle_button( fn dock_toggle_button(
@@ -738,28 +809,23 @@ impl TabGroupSkin {
} }
impl TabGroupRenderer for TabGroupSkin { impl TabGroupRenderer for TabGroupSkin {
fn frame(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> Stateful<Div> { fn frame(&self, group: &TabGroupContext, _: &mut Window, _cx: &mut App) -> Stateful<Div> {
div() div().id("tab-panel").when(!group.is_collapsed(), |this| {
.id("tab-panel") this.on_action({
.p_1() let group = TabGroupContext::clone(group);
.rounded(cx.theme().radius_lg) move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(!group.is_collapsed(), |this| {
this.on_action({
let group = TabGroupContext::clone(group);
move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
})
.on_action({
let group = TabGroupContext::clone(group);
move |_: &ClosePanel, window, cx| {
let Some(panel) = group.active_panel() else {
return;
};
let panel = panel.panel_id(cx);
group.close(panel, window, cx);
}
})
}) })
.on_action({
let group = TabGroupContext::clone(group);
move |_: &ClosePanel, window, cx| {
let Some(panel) = group.active_panel() else {
return;
};
let panel = panel.panel_id(cx);
group.close(panel, window, cx);
}
})
})
} }
fn render_tab_bar( fn render_tab_bar(
@@ -811,7 +877,6 @@ impl TabGroupRenderer for TabGroupSkin {
.child( .child(
div() div()
.size_full() .size_full()
.rounded_b(cx.theme().radius_lg)
.bg(cx.theme().panel_background) .bg(cx.theme().panel_background)
.overflow_hidden() .overflow_hidden()
.child(panel.cached(StyleRefinement::default().v_flex().size_full())), .child(panel.cached(StyleRefinement::default().v_flex().size_full())),
+56 -4
View File
@@ -2,9 +2,10 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement, AnyElement, App, ClickEvent, Context, Decorations, Div, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _, MouseButton, ParentElement, Pixels, Render, RenderOnce, Stateful,
StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div, px, StatefulInteractiveElement as _, StyleRefinement, Styled, TitlebarOptions, Window,
WindowControlArea, div, px,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -210,10 +211,61 @@ impl RenderOnce for ControlIcon {
#[derive(IntoElement)] #[derive(IntoElement)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
struct WindowControls { pub(crate) struct WindowControls {
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>, on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
} }
pub(crate) fn window_controls() -> WindowControls {
WindowControls {
on_close_window: None,
}
}
pub fn title_bar_drag_handlers(
this: Stateful<Div>,
window: &mut Window,
cx: &mut App,
) -> Stateful<Div> {
let state = window.use_state(cx, |_, _| TitleBarState { should_move: false });
let this = if cfg!(target_family = "wasm") {
this
} else {
this.window_control_area(WindowControlArea::Drag)
};
this.on_mouse_down_out(window.listener_for(&state, |state, _, _, _| {
state.should_move = false;
}))
.on_mouse_down(
MouseButton::Left,
window.listener_for(&state, |state, _, _, _| {
state.should_move = true;
}),
)
.on_mouse_up(
MouseButton::Left,
window.listener_for(&state, |state, _, _, _| {
state.should_move = false;
}),
)
.on_mouse_move(window.listener_for(&state, |state, _, window, _| {
if state.should_move {
state.should_move = false;
window.start_window_move();
}
}))
.on_click(|event, window, _| {
if event.click_count() == 2 {
if cfg!(target_os = "macos") {
window.titlebar_double_click();
} else {
window.zoom_window();
}
}
})
}
impl RenderOnce for WindowControls { impl RenderOnce for WindowControls {
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
if cfg!(target_os = "macos") || cfg!(target_family = "wasm") { if cfg!(target_os = "macos") || cfg!(target_family = "wasm") {
+21 -108
View File
@@ -1,3 +1,4 @@
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
@@ -8,8 +9,8 @@ use common::download_dir;
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
Render, SharedString, Styled, Subscription, Task, Window, div, px, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{PersonRegistry, shorten_pubkey}; use person::{PersonRegistry, shorten_pubkey};
@@ -17,12 +18,11 @@ use serde::Deserialize;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent}; use state::{NostrRegistry, StateEvent};
use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry}; use theme::{ActiveTheme, SIDEBAR_WIDTH, Theme, ThemeRegistry};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle}; use ui::dock::{self, ClosePanel, DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::{Notification, NotificationKind}; use ui::notification::{Notification, NotificationKind};
use ui::{Icon, IconName, Root, Sizable, TitleBar, WindowExtension, h_flex, v_flex}; use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
use crate::dialogs::import::ImportIdentity; use crate::dialogs::import::ImportIdentity;
use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::restore::RestoreEncryption;
@@ -63,6 +63,7 @@ pub struct Workspace {
sidebar: Entity<Sidebar>, sidebar: Entity<Sidebar>,
/// App's Dock Area /// App's Dock Area
dock: Entity<DockArea>, dock: Entity<DockArea>,
title_bar_chrome: Rc<dock::TitleBarChrome>,
/// Async tasks /// Async tasks
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
@@ -78,7 +79,7 @@ impl Workspace {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let sidebar = cx.new(|cx| Sidebar::new(window, cx)); let sidebar = cx.new(|cx| Sidebar::new(window, cx));
let dock = dock::dock_area("coop", window, cx); let (dock, title_bar_chrome) = dock::dock_area("coop", window, cx);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -225,6 +226,7 @@ impl Workspace {
Self { Self {
sidebar, sidebar,
dock, dock,
title_bar_chrome,
tasks: vec![], tasks: vec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
@@ -518,95 +520,14 @@ impl Workspace {
}); });
} }
fn titlebar_left(&mut self, cx: &mut Context<Self>) -> impl IntoElement { fn titlebar_right(_window: &mut Window, cx: &mut App) -> AnyElement {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
h_flex()
.flex_shrink_0()
.gap_2()
.when_none(&current_user, |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Import your identity to continue")),
)
})
.when_some(current_user.as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(public_key, cx);
let avatar = profile.avatar();
let name = profile.name();
this.child(
Button::new("current-user")
.child(Avatar::new(avatar.clone()).xsmall())
.small()
.caret()
.compact()
.transparent()
.dropdown_menu(move |this, _window, cx| {
let avatar = avatar.clone();
let name = name.clone();
this.min_w(px(256.))
.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1p5()
.text_xs()
.text_color(cx.theme().text_muted)
.child(Avatar::new(avatar.clone()).xsmall())
.child(name.clone())
}))
.separator()
.menu_with_icon(
"Profile",
IconName::Profile,
Box::new(Command::ShowProfile),
)
.menu_with_icon(
"Contact List",
IconName::Book,
Box::new(Command::ShowContactList),
)
.menu_with_icon(
"Backup",
IconName::UserKey,
Box::new(Command::ShowBackup),
)
.menu_with_icon(
"Themes",
IconName::Sun,
Box::new(Command::ToggleTheme),
)
// Only offer in-app updates when auto-update is
// enabled (managed channels update themselves).
.when(AutoUpdater::is_available(cx), |this| {
this.separator().menu_with_icon(
"Check for Updates",
IconName::Device,
Box::new(Command::Update),
)
})
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Command::ShowSettings),
)
}),
)
})
}
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let auto_updater = AutoUpdater::try_global(cx); let auto_updater = AutoUpdater::try_global(cx);
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx); let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let Some(public_key) = nostr.read(cx).current_user() else { let Some(public_key) = nostr.read(cx).current_user() else {
return div(); return div().into_any_element();
}; };
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
@@ -635,11 +556,11 @@ impl Workspace {
.tooltip("Quit and relaunch into the installed update") .tooltip("Quit and relaunch into the installed update")
.small() .small()
.ghost() .ghost()
.on_click(cx.listener(|_this, _event, _window, cx| { .on_click(|_event, _window, cx| {
if let Some(auto_updater) = AutoUpdater::try_global(cx) { if let Some(auto_updater) = AutoUpdater::try_global(cx) {
auto_updater.update(cx, |this, cx| this.restart(cx)); auto_updater.update(cx, |this, cx| this.restart(cx));
} }
})), }),
) )
}) })
.when(nip4e_enabled, |this| { .when(nip4e_enabled, |this| {
@@ -764,6 +685,7 @@ impl Workspace {
) )
}), }),
) )
.into_any_element()
} }
} }
@@ -772,33 +694,24 @@ impl Render for Workspace {
let modal_layer = Root::render_modal_layer(window, cx); let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx);
self.title_bar_chrome.set_trailing(Self::titlebar_right);
div() div()
.id("workspace") .id("workspace")
.on_action(cx.listener(Self::on_command)) .on_action(cx.listener(Self::on_command))
.relative() .relative()
.size_full() .size_full()
.child( .child(
v_flex() h_flex()
.size_full() .size_full()
// Title Bar
.child( .child(
TitleBar::new() div()
.child(self.titlebar_left(cx)) .flex_shrink_0()
.child(self.titlebar_right(cx)), .h_full()
.w(SIDEBAR_WIDTH)
.child(self.sidebar.clone()),
) )
// Main .child(self.dock.clone()),
.child(
h_flex()
.size_full()
.child(
div()
.flex_shrink_0()
.h_full()
.w(SIDEBAR_WIDTH)
.child(self.sidebar.clone()),
)
.child(self.dock.clone()),
),
) )
// Notifications // Notifications
.children(notification_layer) .children(notification_layer)
+106 -6
View File
@@ -2,28 +2,36 @@ use std::collections::HashSet;
use std::ops::Range; use std::ops::Range;
use anyhow::Error; use anyhow::Error;
use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::{DebouncedDelay, TimestampExt}; use common::{DebouncedDelay, TimestampExt};
use entry::RoomEntry; use entry::RoomEntry;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, UniformListScrollHandle, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task,
Window, div, retain_all, uniform_list, UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
}; };
use instant::Duration; use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry}; use state::{FIND_DELAY, NostrRegistry};
use theme::{ActiveTheme, SIDEBAR_WIDTH}; use theme::{ActiveTheme, SIDEBAR_WIDTH, TABBAR_HEIGHT};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent}; use ui::dock::{Panel, PanelEvent};
use ui::indicator::Indicator; use ui::indicator::Indicator;
use ui::input::{Input, InputEvent, InputState}; use ui::input::{Input, InputEvent, InputState};
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar; use ui::scroll::Scrollbar;
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; use ui::{
Icon, IconName, Selectable, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex,
title_bar_drag_handlers, v_flex,
};
use crate::Command;
mod entry; mod entry;
@@ -485,6 +493,97 @@ impl Sidebar {
}) })
.collect() .collect()
} }
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let current_user = nostr.read(cx).current_user();
title_bar_drag_handlers(
h_flex()
.id("sidebar-user")
.w_full()
.h(TABBAR_HEIGHT)
.flex_shrink_0()
.items_center()
.gap_2()
.px_2()
.when(cfg!(target_os = "macos"), |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING))
})
.when_none(&current_user, |this| {
this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Import your identity to continue")),
)
})
.when_some(current_user.as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(public_key, cx);
let avatar = profile.avatar();
let name = profile.name();
this.child(
Button::new("current-user")
.child(Avatar::new(avatar.clone()).xsmall())
.small()
.caret()
.compact()
.transparent()
.dropdown_menu(move |this, _window, cx| {
let avatar = avatar.clone();
let name = name.clone();
this.min_w(px(256.))
.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.gap_1p5()
.text_xs()
.text_color(cx.theme().text_muted)
.child(Avatar::new(avatar.clone()).xsmall())
.child(name.clone())
}))
.separator()
.menu_with_icon(
"Profile",
IconName::Profile,
Box::new(Command::ShowProfile),
)
.menu_with_icon(
"Contact List",
IconName::Book,
Box::new(Command::ShowContactList),
)
.menu_with_icon(
"Backup",
IconName::UserKey,
Box::new(Command::ShowBackup),
)
.menu_with_icon(
"Themes",
IconName::Sun,
Box::new(Command::ToggleTheme),
)
.when(AutoUpdater::is_available(cx), |this| {
this.separator().menu_with_icon(
"Check for Updates",
IconName::Device,
Box::new(Command::Update),
)
})
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Command::ShowSettings),
)
}),
)
}),
window,
cx,
)
}
} }
impl Panel for Sidebar { impl Panel for Sidebar {
@@ -502,7 +601,7 @@ impl Focusable for Sidebar {
} }
impl Render for Sidebar { impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let logged_in = nostr.read(cx).current_user().is_some(); let logged_in = nostr.read(cx).current_user().is_some();
@@ -524,6 +623,7 @@ impl Render for Sidebar {
.image_cache(retain_all("sidebar")) .image_cache(retain_all("sidebar"))
.size_full() .size_full()
.gap_2() .gap_2()
.child(self.render_user(window, cx))
.child( .child(
h_flex().px_2().py_1().child( h_flex().px_2().py_1().child(
Input::new(&self.find_input) Input::new(&self.find_input)
+6 -1
View File
@@ -9,6 +9,7 @@ use gpui::{
use gpui_platform::application; use gpui_platform::application;
use nostr_sdk::prelude::SecretKey; use nostr_sdk::prelude::SecretKey;
use state::{APP_ID, CLIENT_NAME}; use state::{APP_ID, CLIENT_NAME};
use theme::TABBAR_HEIGHT;
use ui::Root; use ui::Root;
actions!(coop, [Quit]); actions!(coop, [Quit]);
@@ -66,9 +67,13 @@ fn main() {
app_id: Some(APP_ID.to_owned()), app_id: Some(APP_ID.to_owned()),
titlebar: Some(TitlebarOptions { titlebar: Some(TitlebarOptions {
title: Some(SharedString::new_static(CLIENT_NAME)), title: Some(SharedString::new_static(CLIENT_NAME)),
traffic_light_position: Some(point(px(9.0), px(9.0))), traffic_light_position: Some(point(
px(9.0),
px(TABBAR_HEIGHT / px(2.) - 14. / 2.),
)),
appears_transparent: true, appears_transparent: true,
}), }),
app_owns_titlebar_drag: true,
..Default::default() ..Default::default()
}; };