diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 055f7e3c..b48c20e8 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -172,7 +172,6 @@ impl Global for GlobalAppSettings {} pub struct AppSettings { /// Settings inner: Entity, - /// Event subscriptions _subscriptions: SmallVec<[Subscription; 2]>, } @@ -184,8 +183,6 @@ impl AppSettings { } /// The underlying settings entity, which notifies whenever any field changes. - /// Settings load asynchronously, so observers can watch it to pick up values - /// that arrive after construction. pub fn entity(&self) -> &Entity { &self.inner } diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 84028327..e768504d 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -34,7 +34,7 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); pub const TITLEBAR_HEIGHT: Pixels = px(36.0); /// Defines workspace tabbar height -pub const TABBAR_HEIGHT: Pixels = px(44.0); +pub const TABBAR_HEIGHT: Pixels = px(36.0); /// Defines default sidebar width pub const SIDEBAR_WIDTH: Pixels = px(240.); diff --git a/crates/ui/src/dock/mod.rs b/crates/ui/src/dock/mod.rs index 257515c1..575f0f84 100644 --- a/crates/ui/src/dock/mod.rs +++ b/crates/ui/src/dock/mod.rs @@ -24,7 +24,7 @@ use crate::menu::DropdownMenu as _; use crate::resizable::{resize_handle, resize_handle_appearance}; use crate::tab::Tab; use crate::tab::tab_bar::TabBar; -use crate::title_bar::{title_bar_drag_handlers, window_controls}; +use crate::title_bar::{TRAFFIC_LIGHT_PADDING, title_bar_drag_handlers, window_controls}; use crate::{IconName, Selectable, Sizable, StyledExt, h_flex, v_flex}; mod panel; @@ -429,6 +429,42 @@ impl TabGroupSkin { == Some(group.node()) } + /// Whether this group is the left dock's root with a single panel. + /// + /// Such a group draws no tab bar, so its panel owns the window's top-left + /// corner — including the space the macOS traffic lights overlay. + fn is_plain_left_group(&self, group: &TabGroupContext, cx: &App) -> bool { + let Some(area) = self.shared.area() else { + return false; + }; + let area = area.read(cx); + + area.layout(DockPlacement::Left) + .map(|tree| tree.root().id()) + == Some(group.node()) + && group.panels().len() == 1 + } + + /// Whether this group is the topmost-left group on screen, which sits under + /// the native macOS traffic lights. The left dock's group is leftmost while + /// it is open and holds a panel; the center's is leftmost otherwise. + fn is_leftmost_top_group(&self, group: &TabGroupContext, cx: &App) -> bool { + let Some(area) = self.shared.area() else { + return false; + }; + let area = area.read(cx); + + let left_open = + area.is_dock_open(DockPlacement::Left) && !area.is_empty(DockPlacement::Left, cx); + let tree = if left_open { + area.layout(DockPlacement::Left) + } else { + area.layout(DockPlacement::Center) + }; + + tree.and_then(|tree| left_top_group(tree.root())) == Some(group.node()) + } + fn render_toolbar( &self, group: &TabGroupContext, @@ -509,6 +545,8 @@ impl TabGroupSkin { let has_leading = left_button.is_some() || bottom_button.is_some(); let drag = tab_drag(group, ix, cx); let is_title_bar = self.is_title_bar_group(group, cx); + let needs_traffic_light_padding = + cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx); let trailing_chrome = is_title_bar .then(|| self.shared.chrome.trailing(window, cx)) .flatten(); @@ -532,6 +570,9 @@ impl TabGroupSkin { .children(bottom_button), ) }) + .when(needs_traffic_light_padding, |this| { + this.pl(px(TRAFFIC_LIGHT_PADDING)) + }) .child( div() .id("tab") @@ -612,6 +653,8 @@ impl TabGroupSkin { .position(|panel| panel.panel_id(cx) == displayed) }); let is_title_bar = self.is_title_bar_group(group, cx); + let needs_traffic_light_padding = + cfg!(target_os = "macos") && self.is_leftmost_top_group(group, cx); let trailing_chrome = is_title_bar .then(|| self.shared.chrome.trailing(window, cx)) .flatten(); @@ -640,6 +683,9 @@ impl TabGroupSkin { .track_scroll(&self.scroll_handle) .h(TABBAR_HEIGHT) .bg(cx.theme().panel_background) + .when(needs_traffic_light_padding, |this| { + this.pl(px(TRAFFIC_LIGHT_PADDING)) + }) .when(is_title_bar || has_leading, |this| { this.prefix( h_flex() @@ -834,6 +880,12 @@ impl TabGroupRenderer for TabGroupSkin { window: &mut Window, cx: &mut App, ) -> AnyElement { + // The left dock's only panel draws bare, so its content can own the + // window's top-left corner instead of a tab bar doing so. + if self.is_plain_left_group(group, cx) { + return Empty.into_any_element(); + } + let visible: Vec = group .panels() .iter() diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 02dff88c..97ced488 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -67,14 +67,10 @@ enum Command { } pub struct Workspace { - sidebar: Entity, - /// App's Dock Area dock: Entity, title_bar_chrome: Rc, - /// Async tasks tasks: Vec>>, - /// Event subscriptions _subscriptions: SmallVec<[Subscription; 6]>, } @@ -221,17 +217,25 @@ impl Workspace { }), ); - cx.defer_in(window, |this, window, cx| { + cx.defer_in(window, move |this, window, cx| { + let sidebar = PanelHandle::new(sidebar); + + this.dock.update(cx, |area, cx| { + let left = DockLayout::tabs().panel_view(Arc::new(sidebar), cx); + area.set_dock(DockPlacement::Left, left, window, cx); + area.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); + }); + let greeter = PanelHandle::new(greeter::init(window, cx)); let center = DockLayout::v_split() .child(DockLayout::tabs().panel_view(Arc::new(greeter), cx), None); - this.dock - .update(cx, |area, cx| area.set_center(center, window, cx)); + this.dock.update(cx, |area, cx| { + area.set_center(center, window, cx); + }); }); Self { - sidebar, dock, title_bar_chrome, tasks: vec![], @@ -725,20 +729,7 @@ impl Render for Workspace { .on_action(cx.listener(Self::on_command)) .relative() .size_full() - .child( - h_flex() - .size_full() - .child( - div() - .flex_shrink_0() - .h_full() - .w(SIDEBAR_WIDTH) - .border_r_1() - .border_color(cx.theme().border_variant) - .child(self.sidebar.clone()), - ) - .child(self.dock.clone()), - ) + .child(self.dock.clone()) // Notifications .children(notification_layer) // Modals diff --git a/crates/workspace/src/panels/greeter.rs b/crates/workspace/src/panels/greeter.rs index 6d62c200..a8178cbd 100644 --- a/crates/workspace/src/panels/greeter.rs +++ b/crates/workspace/src/panels/greeter.rs @@ -157,8 +157,11 @@ impl Render for GreeterPanel { .label("Change theme") .ghost() .small() - .on_click(cx.listener(move |_, _, _, cx| { - cx.dispatch_action(&Command::ToggleTheme); + .on_click(cx.listener(move |_, _, window, cx| { + window.dispatch_action( + Box::new(Command::ToggleTheme), + cx, + ); })), ), ), diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index f0bd516a..a7b616eb 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -368,14 +368,6 @@ impl Sidebar { .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); @@ -468,6 +460,18 @@ impl Panel for Sidebar { fn panel_id(&self) -> SharedString { "Sidebar".into() } + + fn title(&self, _cx: &App) -> AnyElement { + SharedString::from("Sidebar").into_any_element() + } + + fn closable(&self, _cx: &App) -> bool { + false + } + + fn zoomable(&self, _cx: &App) -> bool { + false + } } impl EventEmitter for Sidebar {} @@ -493,16 +497,18 @@ impl Render for Sidebar { .image_cache(retain_all("sidebar")) .size_full() .gap_2() + .bg(cx.theme().surface_background) + .border_r_1() + .border_color(cx.theme().border_variant) .child(self.render_user(window, cx)) .child( v_flex() .px_2() - .py_1() .gap_1() .child( NavItem::new("nav-inbox", "Inbox", Icon::new(IconName::Inbox).small()) - .on_click(|_event, _window, cx| { - cx.dispatch_action(&Command::ShowInbox) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::ShowInbox), cx) }), ) .child( @@ -516,27 +522,27 @@ impl Render for Sidebar { }) .on_click({ let sidebar = sidebar.clone(); - move |_event, _window, cx| { + move |_event, window, cx| { if let Err(error) = sidebar.update(cx, |this, cx| { this.new_requests = false; cx.notify(); }) { log::error!("Failed to clear new requests: {error}"); } - cx.dispatch_action(&Command::ShowRequests); + window.dispatch_action(Box::new(Command::ShowRequests), cx); } }), ) .child( NavItem::new("nav-browse", "Browse", Icon::new(IconName::Compass).small()) - .on_click(|_event, _window, cx| { - cx.dispatch_action(&Command::ShowBrowse) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::ShowBrowse), cx) }), ) .child( NavItem::new("nav-search", "Search", Icon::new(IconName::Search).small()) - .on_click(|_event, _window, cx| { - cx.dispatch_action(&Command::ShowSearch) + .on_click(|_event, window, cx| { + window.dispatch_action(Box::new(Command::ShowSearch), cx) }), ), )