Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions winit-appkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ mod window_delegate;

use std::os::raw::c_void;

use dpi::LogicalSize;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[doc(inline)]
Expand Down Expand Up @@ -187,6 +188,18 @@ pub trait WindowExtMacOS {

/// Getter for the [`WindowExtMacOS::set_unified_titlebar`].
fn unified_titlebar(&self) -> bool;

/// Sets the offset for the window controls (traffic lights) in logical points.
/// Positive values move right (x) and down (y) relative to the default position.
///
/// ```no_run
/// # use winit::dpi::LogicalSize;
/// # use winit::platform::macos::WindowExtMacOS;
/// # fn example(window: &dyn winit::window::Window) {
/// window.set_traffic_light_inset(LogicalSize::new(24.0, 8.0));
/// # }
/// ```
fn set_traffic_light_inset(&self, inset: LogicalSize<f64>);
}

impl WindowExtMacOS for dyn Window + '_ {
Expand Down Expand Up @@ -297,6 +310,10 @@ impl WindowExtMacOS for dyn Window + '_ {
let window = self.cast_ref::<AppKitWindow>().unwrap();
window.maybe_wait_on_main(|w| w.unified_titlebar())
}
fn set_traffic_light_inset(&self, inset: LogicalSize<f64>) {
let window = self.cast_ref::<AppKitWindow>().unwrap();
window.maybe_wait_on_main(move |w| w.set_traffic_light_inset(inset))
}
}

/// Corresponds to `NSApplicationActivationPolicy`.
Expand Down Expand Up @@ -332,6 +349,7 @@ pub struct WindowAttributesMacOS {
pub(crate) title_hidden: bool,
pub(crate) titlebar_hidden: bool,
pub(crate) titlebar_buttons_hidden: bool,
pub(crate) traffic_light_inset: Option<LogicalSize<f64>>,
pub(crate) fullsize_content_view: bool,
pub(crate) disallow_hidpi: bool,
pub(crate) has_shadow: bool,
Expand Down Expand Up @@ -372,6 +390,24 @@ impl WindowAttributesMacOS {
self
}

/// Sets the offset for the window controls (traffic lights) in logical points.
/// Positive values move right (x) and down (y) relative to the default position.
///
/// This applies an offset from the default position; it does not change the native
/// spacing between the buttons. No effect if titlebar buttons are hidden.
///
/// ```no_run
/// # use winit::dpi::LogicalSize;
/// # use winit::platform::macos::WindowAttributesMacOS;
/// let attrs =
/// WindowAttributesMacOS::default().with_traffic_light_inset(LogicalSize::new(24.0, 8.0));
/// ```
#[inline]
pub fn with_traffic_light_inset(mut self, inset: LogicalSize<f64>) -> Self {
self.traffic_light_inset = Some(inset);
self
}

/// Hides the window title.
#[inline]
pub fn with_title_hidden(mut self, title_hidden: bool) -> Self {
Expand Down Expand Up @@ -459,6 +495,7 @@ impl Default for WindowAttributesMacOS {
title_hidden: false,
titlebar_hidden: false,
titlebar_buttons_hidden: false,
traffic_light_inset: None,
fullsize_content_view: false,
disallow_hidpi: false,
has_shadow: true,
Expand Down
98 changes: 97 additions & 1 deletion winit-appkit/src/window_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ use super::view::WinitView;
use super::window::{WinitPanel, WinitWindow, window_id};
use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS};

// Cached geometry for the native traffic-light buttons derived from
// NSWindow::standardWindowButton(...) frames (AppKit does not expose a
// dedicated struct for this).
// `spacing` is the horizontal delta between adjacent buttons.
#[derive(Clone, Copy, Debug)]
struct TrafficLightBase {
x: f64,
y: f64,
spacing: f64,
}

#[derive(Debug)]
pub(crate) struct State {
/// Strong reference to the global application state.
Expand All @@ -79,6 +90,8 @@ pub(crate) struct State {

/// The current resize increments for the window content.
surface_resize_increments: Cell<NSSize>,
traffic_light_inset: Cell<Option<LogicalSize<f64>>>,
traffic_light_base: Cell<Option<TrafficLightBase>>,
/// Whether the window is showing decorations.
decorations: Cell<bool>,
resizable: Cell<bool>,
Expand Down Expand Up @@ -143,6 +156,7 @@ define_class!(
trace_scope!("windowDidResize:");
// NOTE: WindowEvent::SurfaceResized is reported using NSViewFrameDidChangeNotification.
self.emit_move_event();
self.apply_traffic_light_inset();
}

#[unsafe(method(windowWillStartLiveResize:))]
Expand Down Expand Up @@ -273,6 +287,7 @@ define_class!(
trace_scope!("windowDidEnterFullScreen:");
self.ivars().initial_fullscreen.set(false);
self.ivars().in_fullscreen_transition.set(false);
self.apply_traffic_light_inset();
if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() {
self.set_fullscreen(target_fullscreen);
}
Expand All @@ -285,6 +300,7 @@ define_class!(

self.restore_state_from_fullscreen();
self.ivars().in_fullscreen_transition.set(false);
self.apply_traffic_light_inset();
if let Some(target_fullscreen) = self.ivars().target_fullscreen.take() {
self.set_fullscreen(target_fullscreen);
}
Expand Down Expand Up @@ -806,6 +822,8 @@ impl WindowDelegate {
previous_position: Cell::new(flip_window_screen_coordinates(window.frame())),
previous_scale_factor: Cell::new(scale_factor),
surface_resize_increments: Cell::new(surface_resize_increments),
traffic_light_inset: Cell::new(macos_attrs.traffic_light_inset),
traffic_light_base: Cell::new(None),
decorations: Cell::new(attrs.decorations),
resizable: Cell::new(attrs.resizable),
maximized: Cell::new(attrs.maximized),
Expand Down Expand Up @@ -853,6 +871,10 @@ impl WindowDelegate {
// Set fullscreen mode after we setup everything
delegate.set_fullscreen(attrs.fullscreen);

if let Some(inset) = macos_attrs.traffic_light_inset {
delegate.set_traffic_light_inset(inset);
}

// Setting the window as key has to happen *after* we set the fullscreen
// state, since otherwise we'll briefly see the window at normal size
// before it transitions.
Expand Down Expand Up @@ -930,6 +952,73 @@ impl WindowDelegate {
self.queue_event(WindowEvent::Moved(position));
}

fn apply_traffic_light_inset(&self) {
// Nothing to do if no inset was configured.
let Some(inset) = self.ivars().traffic_light_inset.get() else {
return;
};

// Fetch standard buttons; if any are hidden, clear cached base.
let window = self.window();
let Some(close) = window.standardWindowButton(NSWindowButton::CloseButton) else {
return;
};
if close.isHidden() {
self.ivars().traffic_light_base.set(None);
return;
}
let Some(miniaturize) = window.standardWindowButton(NSWindowButton::MiniaturizeButton)
else {
return;
};
if miniaturize.isHidden() {
self.ivars().traffic_light_base.set(None);
return;
}
let Some(zoom) = window.standardWindowButton(NSWindowButton::ZoomButton) else {
return;
};
if zoom.isHidden() {
self.ivars().traffic_light_base.set(None);
return;
}

// Capture the current default geometry as a candidate base.
let close_rect = close.frame();
let spacing = miniaturize.frame().origin.x - close_rect.origin.x; // Horizontal delta between buttons.
let current = TrafficLightBase { x: close_rect.origin.x, y: close_rect.origin.y, spacing };

// If frames no longer match cached base + inset (AppKit reset), refresh base.
let base = match self.ivars().traffic_light_base.get() {
Some(base) => {
let expected_x = base.x + inset.width;
let expected_y = base.y - inset.height;
let drift = (close_rect.origin.x - expected_x).abs() > 0.5
|| (close_rect.origin.y - expected_y).abs() > 0.5
|| (spacing - base.spacing).abs() > 0.5;
if drift {
self.ivars().traffic_light_base.set(Some(current));
current
} else {
base
}
},
None => {
self.ivars().traffic_light_base.set(Some(current));
current
},
};

// Apply inset relative to base while preserving native spacing.
let target_y = base.y - inset.height;
for (index, button) in [close, miniaturize, zoom].into_iter().enumerate() {
let mut rect = button.frame();
rect.origin.x = base.x + inset.width + (index as f64 * base.spacing);
rect.origin.y = target_y;
button.setFrameOrigin(rect.origin);
}
}

fn set_style_mask(&self, mask: NSWindowStyleMask) {
self.window().setStyleMask(mask);
// If we don't do this, key handling will break
Expand All @@ -938,7 +1027,9 @@ impl WindowDelegate {
}

pub fn set_title(&self, title: &str) {
self.window().setTitle(&NSString::from_str(title))
self.window().setTitle(&NSString::from_str(title));
// AppKit can reset standard button positions when the title changes.
self.apply_traffic_light_inset();
}

pub fn set_transparent(&self, transparent: bool) {
Expand Down Expand Up @@ -2029,6 +2120,11 @@ impl WindowExtMacOS for WindowDelegate {

window.toolbar().is_some() && window.toolbarStyle() == NSWindowToolbarStyle::Unified
}

fn set_traffic_light_inset(&self, inset: LogicalSize<f64>) {
self.ivars().traffic_light_inset.set(Some(inset));
self.apply_traffic_light_inset();
}
}

const DEFAULT_STANDARD_FRAME: NSRect =
Expand Down
147 changes: 147 additions & 0 deletions winit/examples/traffic_lights.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! macOS traffic-light inset demo.
//!
//! macOS only; on other platforms this example is a no-op for insets.
//! Arrow keys adjust the inset, Shift uses a larger step, R resets, Esc exits.

use std::error::Error;

use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, KeyEvent, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{Key, ModifiersState, NamedKey};
#[cfg(macos_platform)]
use winit::platform::macos::{WindowAttributesMacOS, WindowExtMacOS};
#[cfg(web_platform)]
use winit::platform::web::WindowAttributesWeb;
use winit::window::{Window, WindowAttributes, WindowId};

#[path = "util/fill.rs"]
mod fill;
#[path = "util/tracing.rs"]
mod tracing;

const DEFAULT_INSET_X: f64 = 0.0;
const DEFAULT_INSET_Y: f64 = 0.0;
const STEP_FINE: f64 = 1.0;
const STEP_COARSE: f64 = 8.0;

#[derive(Debug)]
struct App {
window: Option<Box<dyn Window>>,
inset: LogicalSize<f64>,
modifiers: ModifiersState,
}

impl Default for App {
fn default() -> Self {
Self {
window: None,
inset: LogicalSize::new(DEFAULT_INSET_X, DEFAULT_INSET_Y),
modifiers: ModifiersState::default(),
}
}
}

impl App {
fn title(&self) -> String {
format!("Traffic lights inset: x={:.1}, y={:.1}", self.inset.width, self.inset.height)
}

fn apply_inset(&self) {
let Some(window) = self.window.as_ref() else {
return;
};

#[cfg(macos_platform)]
window.set_traffic_light_inset(self.inset);
window.set_title(&self.title());
}

fn set_inset(&mut self, inset: LogicalSize<f64>) {
self.inset = inset;
self.apply_inset();
}

fn nudge_inset(&mut self, dx: f64, dy: f64) {
self.inset.width += dx;
self.inset.height += dy;
self.apply_inset();
}
}

impl ApplicationHandler for App {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes = WindowAttributes::default().with_title(self.title());
#[cfg(web_platform)]
let window_attributes = window_attributes
.with_platform_attributes(Box::new(WindowAttributesWeb::default().with_append(true)));
#[cfg(macos_platform)]
let window_attributes = window_attributes.with_platform_attributes(Box::new(
WindowAttributesMacOS::default().with_traffic_light_inset(self.inset),
));

self.window = match event_loop.create_window(window_attributes) {
Ok(window) => Some(window),
Err(err) => {
eprintln!("error creating window: {err}");
event_loop.exit();
return;
},
};

self.apply_inset();
}

fn window_event(&mut self, event_loop: &dyn ActiveEventLoop, _: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::ModifiersChanged(modifiers) => {
self.modifiers = modifiers.state();
},
WindowEvent::KeyboardInput {
event: KeyEvent { state: ElementState::Pressed, key_without_modifiers: key, .. },
is_synthetic: false,
..
} => {
let step = if self.modifiers.shift_key() { STEP_COARSE } else { STEP_FINE };

match key.as_ref() {
Key::Named(NamedKey::ArrowLeft) => self.nudge_inset(-step, 0.0),
Key::Named(NamedKey::ArrowRight) => self.nudge_inset(step, 0.0),
Key::Named(NamedKey::ArrowUp) => self.nudge_inset(0.0, -step),
Key::Named(NamedKey::ArrowDown) => self.nudge_inset(0.0, step),
Key::Character("r") => {
self.set_inset(LogicalSize::new(DEFAULT_INSET_X, DEFAULT_INSET_Y))
},
Key::Named(NamedKey::Escape) => event_loop.exit(),
_ => (),
}
},
WindowEvent::SurfaceResized(_) => {
self.window.as_ref().expect("resize event without a window").request_redraw();
},
WindowEvent::RedrawRequested => {
let window = self.window.as_ref().expect("redraw request without a window");
window.pre_present_notify();
fill::fill_window(window.as_ref());
},
_ => (),
}
}
}

fn main() -> Result<(), Box<dyn Error>> {
#[cfg(web_platform)]
console_error_panic_hook::set_once();

tracing::init();

println!("Traffic-light inset demo (macOS only).");
println!("Arrow keys adjust X/Y. Shift = coarse step.");
println!("R resets. Esc closes.");

let event_loop = EventLoop::new()?;
event_loop.run_app(App::default())?;
Ok(())
}
Loading