display.rs

  1use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay};
  2use anyhow::Result;
  3use cocoa::{
  4    appkit::NSScreen,
  5    base::{id, nil},
  6    foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString},
  7};
  8use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
  9use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
 10use objc::{msg_send, sel, sel_impl};
 11use uuid::Uuid;
 12
 13#[derive(Debug)]
 14pub(crate) struct MacDisplay(pub(crate) CGDirectDisplayID);
 15
 16unsafe impl Send for MacDisplay {}
 17
 18impl MacDisplay {
 19    /// Get the screen with the given [`DisplayId`].
 20    pub fn find_by_id(id: DisplayId) -> Option<Self> {
 21        Self::all().find(|screen| screen.id() == id)
 22    }
 23
 24    /// Get the primary screen - the one with the menu bar, and whose bottom left
 25    /// corner is at the origin of the AppKit coordinate system.
 26    pub fn primary() -> Self {
 27        // Instead of iterating through all active systems displays via `all()` we use the first
 28        // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList`
 29        // will always return a list of active displays (machine might be sleeping).
 30        //
 31        // The following is what Chromium does too:
 32        //
 33        // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56
 34        unsafe {
 35            let screens = NSScreen::screens(nil);
 36            let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0);
 37            let device_description = NSScreen::deviceDescription(screen);
 38            let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
 39            let screen_number = device_description.objectForKey_(screen_number_key);
 40            let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue];
 41            Self(screen_number)
 42        }
 43    }
 44
 45    /// Obtains an iterator over all currently active system displays.
 46    pub fn all() -> impl Iterator<Item = Self> {
 47        unsafe {
 48            // We're assuming there aren't more than 32 displays connected to the system.
 49            let mut displays = Vec::with_capacity(32);
 50            let mut display_count = 0;
 51            let result = CGGetActiveDisplayList(
 52                displays.capacity() as u32,
 53                displays.as_mut_ptr(),
 54                &mut display_count,
 55            );
 56
 57            if result == 0 {
 58                displays.set_len(display_count as usize);
 59                displays.into_iter().map(MacDisplay)
 60            } else {
 61                panic!("Failed to get active display list. Result: {result}");
 62            }
 63        }
 64    }
 65}
 66
 67#[link(name = "ApplicationServices", kind = "framework")]
 68extern "C" {
 69    fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
 70}
 71
 72/// Convert the given rectangle from Cocoa's coordinate space to GPUI's coordinate space.
 73///
 74/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
 75/// with the Y axis pointing upwards.
 76///
 77/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
 78/// screen, with the Y axis pointing downwards (matching CoreGraphics)
 79pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds<GlobalPixels> {
 80    let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
 81
 82    Bounds {
 83        origin: point(
 84            GlobalPixels(rect.origin.x as f32),
 85            GlobalPixels(
 86                primary_screen_size.height as f32 - rect.origin.y as f32 - rect.size.height as f32,
 87            ),
 88        ),
 89        size: size(
 90            GlobalPixels(rect.size.width as f32),
 91            GlobalPixels(rect.size.height as f32),
 92        ),
 93    }
 94}
 95
 96/// Convert the given rectangle from GPUI's coordinate system to Cocoa's native coordinate space.
 97///
 98/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
 99/// with the Y axis pointing upwards.
100///
101/// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
102/// screen, with the Y axis pointing downwards (matching CoreGraphics)
103pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds<GlobalPixels>) -> NSRect {
104    let primary_screen_height = MacDisplay::primary().bounds().size.height;
105
106    NSRect::new(
107        NSPoint::new(
108            bounds.origin.x.into(),
109            (primary_screen_height - bounds.origin.y - bounds.size.height).into(),
110        ),
111        NSSize::new(bounds.size.width.into(), bounds.size.height.into()),
112    )
113}
114
115impl PlatformDisplay for MacDisplay {
116    fn id(&self) -> DisplayId {
117        DisplayId(self.0)
118    }
119
120    fn uuid(&self) -> Result<Uuid> {
121        let cfuuid = unsafe { CGDisplayCreateUUIDFromDisplayID(self.0 as CGDirectDisplayID) };
122        anyhow::ensure!(
123            !cfuuid.is_null(),
124            "AppKit returned a null from CGDisplayCreateUUIDFromDisplayID"
125        );
126
127        let bytes = unsafe { CFUUIDGetUUIDBytes(cfuuid) };
128        Ok(Uuid::from_bytes([
129            bytes.byte0,
130            bytes.byte1,
131            bytes.byte2,
132            bytes.byte3,
133            bytes.byte4,
134            bytes.byte5,
135            bytes.byte6,
136            bytes.byte7,
137            bytes.byte8,
138            bytes.byte9,
139            bytes.byte10,
140            bytes.byte11,
141            bytes.byte12,
142            bytes.byte13,
143            bytes.byte14,
144            bytes.byte15,
145        ]))
146    }
147
148    fn bounds(&self) -> Bounds<GlobalPixels> {
149        unsafe {
150            // CGDisplayBounds is in "global display" coordinates, where 0 is
151            // the top left of the primary display.
152            let bounds = CGDisplayBounds(self.0);
153
154            Bounds {
155                origin: point(
156                    GlobalPixels(bounds.origin.x as f32),
157                    GlobalPixels(bounds.origin.y as f32),
158                ),
159                size: size(
160                    GlobalPixels(bounds.size.width as f32),
161                    GlobalPixels(bounds.size.height as f32),
162                ),
163            }
164        }
165    }
166}