Toggle contacts popover when clicking on status bar icon

Antonio Scandurra created

Change summary

crates/contacts_status_item/src/contacts_popover.rs     | 26 ++++
crates/contacts_status_item/src/contacts_status_item.rs | 63 ++++++++-
crates/gpui/src/app.rs                                  | 29 +++-
crates/gpui/src/platform.rs                             |  5 
crates/gpui/src/platform/mac/status_item.rs             | 45 +++++-
crates/gpui/src/platform/mac/window.rs                  | 71 ++++++++--
crates/gpui/src/platform/test.rs                        | 11 +
crates/zed/src/main.rs                                  |  1 
crates/zed/src/zed.rs                                   |  1 
9 files changed, 208 insertions(+), 44 deletions(-)

Detailed changes

crates/contacts_status_item/src/contacts_popover.rs 🔗

@@ -0,0 +1,26 @@
+use gpui::{color::Color, elements::*, Entity, RenderContext, View};
+
+pub struct ContactsPopover;
+
+impl Entity for ContactsPopover {
+    type Event = ();
+}
+
+impl View for ContactsPopover {
+    fn ui_name() -> &'static str {
+        "ContactsPopover"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        Empty::new()
+            .contained()
+            .with_background_color(Color::red())
+            .boxed()
+    }
+}
+
+impl ContactsPopover {
+    pub fn new() -> Self {
+        Self
+    }
+}

crates/contacts_status_item/src/contacts_status_item.rs 🔗

@@ -1,6 +1,24 @@
-use gpui::{color::Color, elements::*, Appearance, Entity, RenderContext, View};
+mod contacts_popover;
 
-pub struct ContactsStatusItem;
+use contacts_popover::ContactsPopover;
+use gpui::{
+    actions,
+    color::Color,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
+    ViewHandle,
+};
+
+actions!(contacts_status_item, [ToggleContactsPopover]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(ContactsStatusItem::toggle_contacts_popover);
+}
+
+pub struct ContactsStatusItem {
+    popover: Option<ViewHandle<ContactsPopover>>,
+}
 
 impl Entity for ContactsStatusItem {
     type Event = ();
@@ -16,15 +34,46 @@ impl View for ContactsStatusItem {
             Appearance::Light | Appearance::VibrantLight => Color::black(),
             Appearance::Dark | Appearance::VibrantDark => Color::white(),
         };
-        Svg::new("icons/zed_22.svg")
-            .with_color(color)
-            .aligned()
-            .boxed()
+        MouseEventHandler::new::<Self, _, _>(0, cx, |_, _| {
+            Svg::new("icons/zed_22.svg")
+                .with_color(color)
+                .aligned()
+                .boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(ToggleContactsPopover);
+        })
+        .boxed()
     }
 }
 
 impl ContactsStatusItem {
     pub fn new() -> Self {
-        Self
+        Self { popover: None }
+    }
+
+    fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
+        match self.popover.take() {
+            Some(popover) => {
+                cx.remove_window(popover.window_id());
+            }
+            None => {
+                let window_bounds = cx.window_bounds();
+                let size = vec2f(360., 460.);
+                let origin = window_bounds.lower_left()
+                    + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
+                self.popover = Some(
+                    cx.add_window(
+                        gpui::WindowOptions {
+                            bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
+                            titlebar: None,
+                            center: false,
+                        },
+                        |_| ContactsPopover::new(),
+                    )
+                    .1,
+                );
+            }
+        }
     }
 }

crates/gpui/src/app.rs 🔗

@@ -1244,6 +1244,10 @@ impl MutableAppContext {
             .map_or(false, |window| window.is_fullscreen)
     }
 
+    pub fn window_bounds(&self, window_id: usize) -> RectF {
+        self.presenters_and_platform_windows[&window_id].1.bounds()
+    }
+
     pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
         let window_id = params.window_id;
         let view_id = params.view_id;
@@ -2032,10 +2036,12 @@ impl MutableAppContext {
             window_id,
         }));
 
-        let scene =
-            presenter
-                .borrow_mut()
-                .build_scene(window.size(), window.scale_factor(), false, self);
+        let scene = presenter.borrow_mut().build_scene(
+            window.content_size(),
+            window.scale_factor(),
+            false,
+            self,
+        );
         window.present_scene(scene);
         self.presenters_and_platform_windows
             .insert(window_id, (presenter.clone(), window));
@@ -2411,8 +2417,12 @@ impl MutableAppContext {
                 {
                     let mut presenter = presenter.borrow_mut();
                     presenter.invalidate(&mut invalidation, window.appearance(), self);
-                    let scene =
-                        presenter.build_scene(window.size(), window.scale_factor(), false, self);
+                    let scene = presenter.build_scene(
+                        window.content_size(),
+                        window.scale_factor(),
+                        false,
+                        self,
+                    );
                     window.present_scene(scene);
                 }
                 self.presenters_and_platform_windows
@@ -2477,7 +2487,8 @@ impl MutableAppContext {
                 window.appearance(),
                 self,
             );
-            let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
+            let scene =
+                presenter.build_scene(window.content_size(), window.scale_factor(), true, self);
             window.present_scene(scene);
         }
         self.presenters_and_platform_windows = presenters;
@@ -3749,6 +3760,10 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.toggle_window_full_screen(self.window_id)
     }
 
+    pub fn window_bounds(&self) -> RectF {
+        self.app.window_bounds(self.window_id)
+    }
+
     pub fn prompt(
         &self,
         level: PromptLevel,

crates/gpui/src/platform.rs 🔗

@@ -128,7 +128,8 @@ pub trait Window {
     fn zoom(&self);
     fn toggle_full_screen(&self);
 
-    fn size(&self) -> Vector2F;
+    fn bounds(&self) -> RectF;
+    fn content_size(&self) -> Vector2F;
     fn scale_factor(&self) -> f32;
     fn titlebar_height(&self) -> f32;
     fn present_scene(&mut self, scene: Scene);
@@ -140,6 +141,7 @@ pub trait Window {
 pub struct WindowOptions<'a> {
     pub bounds: WindowBounds,
     pub titlebar: Option<TitlebarOptions<'a>>,
+    pub center: bool,
 }
 
 #[derive(Debug)]
@@ -273,6 +275,7 @@ impl<'a> Default for WindowOptions<'a> {
                 appears_transparent: Default::default(),
                 traffic_light_position: Default::default(),
             }),
+            center: false,
         }
     }
 }

crates/gpui/src/platform/mac/status_item.rs 🔗

@@ -1,5 +1,8 @@
 use crate::{
-    geometry::vector::{vec2f, Vector2F},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     platform::{
         self,
         mac::{
@@ -10,7 +13,7 @@ use crate::{
     Event, FontSystem, Scene,
 };
 use cocoa::{
-    appkit::{NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
+    appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
     base::{id, nil, YES},
     foundation::{NSPoint, NSRect, NSSize, NSString},
 };
@@ -163,7 +166,7 @@ impl StatusItem {
                 let state = state.borrow();
                 let layer = state.renderer.layer();
                 let scale_factor = state.scale_factor();
-                let size = state.size() * scale_factor;
+                let size = state.content_size() * scale_factor;
                 layer.set_contents_scale(scale_factor.into());
                 layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
             }
@@ -235,8 +238,12 @@ impl platform::Window for StatusItem {
         unimplemented!()
     }
 
-    fn size(&self) -> Vector2F {
-        self.0.borrow().size()
+    fn bounds(&self) -> RectF {
+        self.0.borrow().bounds()
+    }
+
+    fn content_size(&self) -> Vector2F {
+        self.0.borrow().content_size()
     }
 
     fn scale_factor(&self) -> f32 {
@@ -264,10 +271,28 @@ impl platform::Window for StatusItem {
 }
 
 impl StatusItemState {
-    fn size(&self) -> Vector2F {
+    fn bounds(&self) -> RectF {
+        unsafe {
+            let window: id = msg_send![self.native_item.button(), window];
+            let screen_frame = window.screen().visibleFrame();
+            let window_frame = NSWindow::frame(window);
+            let origin = vec2f(
+                window_frame.origin.x as f32,
+                (window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
+                    as f32,
+            );
+            let size = vec2f(
+                window_frame.size.width as f32,
+                window_frame.size.height as f32,
+            );
+            RectF::new(origin, size)
+        }
+    }
+
+    fn content_size(&self) -> Vector2F {
         unsafe {
             let NSSize { width, height, .. } =
-                NSWindow::frame(self.native_item.button().superview().superview()).size;
+                NSView::frame(self.native_item.button().superview().superview()).size;
             vec2f(width as f32, height as f32)
         }
     }
@@ -275,7 +300,7 @@ impl StatusItemState {
     fn scale_factor(&self) -> f32 {
         unsafe {
             let window: id = msg_send![self.native_item.button(), window];
-            window.screen().backingScaleFactor() as f32
+            NSScreen::backingScaleFactor(window.screen()) as f32
         }
     }
 }
@@ -292,7 +317,9 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     unsafe {
         if let Some(state) = get_state(this).upgrade() {
             let mut state_borrow = state.as_ref().borrow_mut();
-            if let Some(event) = Event::from_native(native_event, Some(state_borrow.size().y())) {
+            if let Some(event) =
+                Event::from_native(native_event, Some(state_borrow.content_size().y()))
+            {
                 if let Some(mut callback) = state_borrow.event_callback.take() {
                     drop(state_borrow);
                     callback(event);

crates/gpui/src/platform/mac/window.rs 🔗

@@ -335,12 +335,6 @@ impl Window {
         unsafe {
             let pool = NSAutoreleasePool::new(nil);
 
-            let frame = match options.bounds {
-                WindowBounds::Maximized => RectF::new(Default::default(), vec2f(1024., 768.)),
-                WindowBounds::Fixed(rect) => rect,
-            }
-            .to_ns_rect();
-
             let mut style_mask;
             if let Some(titlebar) = options.titlebar.as_ref() {
                 style_mask = NSWindowStyleMask::NSClosableWindowMask
@@ -357,16 +351,31 @@ impl Window {
 
             let native_window: id = msg_send![WINDOW_CLASS, alloc];
             let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
-                frame,
+                RectF::new(Default::default(), vec2f(1024., 768.)).to_ns_rect(),
                 style_mask,
                 NSBackingStoreBuffered,
                 NO,
             );
             assert!(!native_window.is_null());
 
-            if matches!(options.bounds, WindowBounds::Maximized) {
-                let screen = native_window.screen();
-                native_window.setFrame_display_(screen.visibleFrame(), YES);
+            let screen = native_window.screen();
+            match options.bounds {
+                WindowBounds::Maximized => {
+                    native_window.setFrame_display_(screen.visibleFrame(), YES);
+                }
+                WindowBounds::Fixed(top_left_bounds) => {
+                    let frame = screen.visibleFrame();
+                    let bottom_left_bounds = RectF::new(
+                        vec2f(
+                            top_left_bounds.origin_x(),
+                            frame.size.height as f32
+                                - top_left_bounds.origin_y()
+                                - top_left_bounds.height(),
+                        ),
+                        top_left_bounds.size(),
+                    );
+                    native_window.setFrame_display_(bottom_left_bounds.to_ns_rect(), YES);
+                }
             }
 
             let native_view: id = msg_send![VIEW_CLASS, alloc];
@@ -438,7 +447,10 @@ impl Window {
             native_window.setContentView_(native_view.autorelease());
             native_window.makeFirstResponder_(native_view);
 
-            native_window.center();
+            if options.center {
+                native_window.center();
+            }
+
             native_window.makeKeyAndOrderFront_(nil);
             let _: () = msg_send![
                 native_window,
@@ -633,8 +645,12 @@ impl platform::Window for Window {
             .detach();
     }
 
-    fn size(&self) -> Vector2F {
-        self.0.as_ref().borrow().size()
+    fn bounds(&self) -> RectF {
+        self.0.as_ref().borrow().bounds()
+    }
+
+    fn content_size(&self) -> Vector2F {
+        self.0.as_ref().borrow().content_size()
     }
 
     fn scale_factor(&self) -> f32 {
@@ -706,7 +722,24 @@ impl WindowState {
         }
     }
 
-    fn size(&self) -> Vector2F {
+    fn bounds(&self) -> RectF {
+        unsafe {
+            let screen_frame = self.native_window.screen().visibleFrame();
+            let window_frame = NSWindow::frame(self.native_window);
+            let origin = vec2f(
+                window_frame.origin.x as f32,
+                (window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
+                    as f32,
+            );
+            let size = vec2f(
+                window_frame.size.width as f32,
+                window_frame.size.height as f32,
+            );
+            RectF::new(origin, size)
+        }
+    }
+
+    fn content_size(&self) -> Vector2F {
         let NSSize { width, height, .. } =
             unsafe { NSView::frame(self.native_window.contentView()) }.size;
         vec2f(width as f32, height as f32)
@@ -783,7 +816,8 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
 
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
+    let event =
+        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
 
     if let Some(event) = event {
         if key_equivalent {
@@ -875,7 +909,8 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let weak_window_state = Rc::downgrade(&window_state);
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
+    let event =
+        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
     if let Some(event) = event {
         match &event {
             Event::MouseMoved(
@@ -1060,7 +1095,7 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
 
     unsafe {
         let scale_factor = window_state_borrow.scale_factor() as f64;
-        let size = window_state_borrow.size();
+        let size = window_state_borrow.content_size();
         let drawable_size: NSSize = NSSize {
             width: size.x() as f64 * scale_factor,
             height: size.y() as f64 * scale_factor,
@@ -1087,7 +1122,7 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
     let window_state = unsafe { get_window_state(this) };
     let window_state_borrow = window_state.as_ref().borrow();
 
-    if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
+    if window_state_borrow.content_size() == vec2f(size.width as f32, size.height as f32) {
         return;
     }
 

crates/gpui/src/platform/test.rs 🔗

@@ -1,6 +1,9 @@
 use super::{AppVersion, CursorStyle, WindowBounds};
 use crate::{
-    geometry::vector::{vec2f, Vector2F},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     keymap, Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
@@ -283,7 +286,11 @@ impl super::Window for Window {
 
     fn toggle_full_screen(&self) {}
 
-    fn size(&self) -> Vector2F {
+    fn bounds(&self) -> RectF {
+        RectF::new(Default::default(), self.size)
+    }
+
+    fn content_size(&self) -> Vector2F {
         self.size
     }
 

crates/zed/src/main.rs 🔗

@@ -105,6 +105,7 @@ fn main() {
         watch_settings_file(default_settings, settings_file, themes.clone(), cx);
         watch_keymap_file(keymap_file, cx);
 
+        contacts_status_item::init(cx);
         context_menu::init(cx);
         project::Project::init(&client);
         client::Channel::init(&client);

crates/zed/src/zed.rs 🔗

@@ -335,6 +335,7 @@ pub fn build_window_options() -> WindowOptions<'static> {
             appears_transparent: true,
             traffic_light_position: Some(vec2f(8., 8.)),
         }),
+        center: false,
     }
 }