diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 151fb79ce..86260ee5f 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -190,6 +190,9 @@ pub use web::{WebLogger, WebRunner}; #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] mod native; +#[cfg(target_os = "macos")] +pub use native::macos::WindowChromeMetrics; + #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] pub use native::run::EframeWinitApplication; diff --git a/crates/eframe/src/native/macos.rs b/crates/eframe/src/native/macos.rs new file mode 100644 index 000000000..b1f2552e5 --- /dev/null +++ b/crates/eframe/src/native/macos.rs @@ -0,0 +1,76 @@ +use egui::Vec2; +use objc2_app_kit::{NSView, NSWindow, NSWindowButton}; +use raw_window_handle::{AppKitWindowHandle, RawWindowHandle}; + +/// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons) +/// on the native macOS window. +/// +/// This is very useful together with [`egui::ViewportBuilder::with_fullsize_content_view`]. +#[derive(Debug)] +pub struct WindowChromeMetrics { + /// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons), + /// including margins. + /// + /// The unit here is in "native scale", which means it needs to be divided by [`egui::Context::zoom_factor`] + /// to get the size in egui points. + pub traffic_lights_size: Vec2, +} + +impl WindowChromeMetrics { + /// Get the window chrome metrics for a given window handle. + pub fn from_window_handle(window_handle: &RawWindowHandle) -> Option { + window_chrome_metrics(window_handle) + } +} + +fn window_chrome_metrics(window_handle: &RawWindowHandle) -> Option { + let RawWindowHandle::AppKit(appkit_handle) = window_handle else { + return None; + }; + + let ns_view = ns_view_from_handle(appkit_handle)?; + let ns_window = ns_view.window()?; + + Some(WindowChromeMetrics { + traffic_lights_size: traffic_lights_metrics(&ns_window)?, + }) +} + +fn traffic_lights_metrics(ns_window: &NSWindow) -> Option { + // Button order is CloseButton, MiniaturizeButton, ZoomButton: + let close_button = ns_window + .standardWindowButton(NSWindowButton::CloseButton)? + .frame(); + let zoom_button = ns_window + .standardWindowButton(NSWindowButton::ZoomButton)? + .frame(); + + let left_margin = close_button.origin.x; + let right_margin = left_margin; // for symmetry + + let total_width = zoom_button.origin.x + zoom_button.size.width + right_margin; + + let top_margin = close_button.origin.y; + let bottom_margin = top_margin; // Usually symmetric + let total_height = top_margin + close_button.size.height + bottom_margin; + + Some(Vec2::new(total_width as f32, total_height as f32)) +} + +fn ns_view_from_handle(handle: &AppKitWindowHandle) -> Option<&NSView> { + let ns_view_ptr = handle.ns_view.as_ptr().cast::(); + + // Validate the pointer is non-null + if ns_view_ptr.is_null() { + None + } else { + // SAFETY: + // - We've verified the pointer is non-null + // - The pointer comes from the windowing system, so it should be valid + // - NSView pointers from AppKit are expected to remain valid for the window lifetime + #[expect(unsafe_code)] + unsafe { + ns_view_ptr.as_ref() + } + } +} diff --git a/crates/eframe/src/native/mod.rs b/crates/eframe/src/native/mod.rs index eb9413717..771964ae7 100644 --- a/crates/eframe/src/native/mod.rs +++ b/crates/eframe/src/native/mod.rs @@ -3,6 +3,9 @@ mod epi_integration; mod event_loop_context; pub mod run; +#[cfg(target_os = "macos")] +pub(crate) mod macos; + /// File storage which can be used by native backends. #[cfg(feature = "persistence")] pub mod file_storage;