redesign dock and titlebar
This commit is contained in:
+108
-43
@@ -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),
|
||||||
)
|
)
|
||||||
|
.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()
|
.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),
|
||||||
)
|
)
|
||||||
})
|
});
|
||||||
|
|
||||||
|
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()
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
bar.into_any_element()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dock_toggle_button(
|
fn dock_toggle_button(
|
||||||
@@ -738,13 +809,8 @@ 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")
|
|
||||||
.p_1()
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_xs())
|
|
||||||
.when(!group.is_collapsed(), |this| {
|
|
||||||
this.on_action({
|
this.on_action({
|
||||||
let group = TabGroupContext::clone(group);
|
let group = TabGroupContext::clone(group);
|
||||||
move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
|
move |_: &ToggleZoom, window, cx| group.toggle_zoom(window, cx)
|
||||||
@@ -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())),
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
+14
-101
@@ -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(¤t_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,21 +694,13 @@ 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(
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
// Title Bar
|
|
||||||
.child(
|
|
||||||
TitleBar::new()
|
|
||||||
.child(self.titlebar_left(cx))
|
|
||||||
.child(self.titlebar_right(cx)),
|
|
||||||
)
|
|
||||||
// Main
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -798,7 +712,6 @@ impl Render for Workspace {
|
|||||||
.child(self.sidebar.clone()),
|
.child(self.sidebar.clone()),
|
||||||
)
|
)
|
||||||
.child(self.dock.clone()),
|
.child(self.dock.clone()),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
// Notifications
|
// Notifications
|
||||||
.children(notification_layer)
|
.children(notification_layer)
|
||||||
|
|||||||
@@ -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(¤t_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
@@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user