diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e3d8fe..b8ec9346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Unreleased + - On macOS, drop the run closure on exit. - On Windows, location of `WindowEvent::Touch` are window client coordinates instead of screen coordinates. - On X11, fix delayed events after window redraw. - On macOS, add `WindowBuilderExt::with_disallow_hidpi` to have the option to turn off best resolution openGL surface. - On Windows, screen saver won't start if the window is in fullscreen mode. - Change all occurrences of the `new_user_event` method to `with_user_event`. +- On macOS, the dock and the menu bar are now hidden in fullscreen mode. +- `Window::set_fullscreen` now takes `Option` where `Fullscreen` + consists of `Fullscreen::Exclusive(VideoMode)` and + `Fullscreen::Borderless(MonitorHandle)` variants. + - Adds support for exclusive fullscreen mode. # 0.20.0 Alpha 2 (2019-07-09) diff --git a/FEATURES.md b/FEATURES.md index a89c0689..73994604 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -84,6 +84,9 @@ If your PR makes notable changes to Winit's features, please update this section - **Fullscreen**: The windows created by winit can be put into fullscreen mode. - **Fullscreen toggle**: The windows created by winit can be switched to and from fullscreen after creation. +- **Exclusive fullscreen**: Winit allows changing the video mode of the monitor + for fullscreen windows, and if applicable, captures the monitor for exclusive + use by this application. - **HiDPI support**: Winit assists developers in appropriately scaling HiDPI content. - **Popup / modal windows**: Windows can be created relative to the client area of other windows, and parent windows can be disabled in favor of popup windows. This feature also guarantees that popup windows @@ -157,6 +160,7 @@ Legend: |Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | |Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ | |Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ | +|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |❌ |❌ | |HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |✔️ | |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |❌ | diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index 27df2276..d4b83bb0 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -1,56 +1,35 @@ -use std::io::{self, Write}; -use winit::{ - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - monitor::MonitorHandle, - window::WindowBuilder, -}; +use std::io::{stdin, stdout, Write}; +use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::monitor::{MonitorHandle, VideoMode}; +use winit::window::{Fullscreen, WindowBuilder}; fn main() { let event_loop = EventLoop::new(); - #[cfg(target_os = "macos")] - let mut macos_use_simple_fullscreen = false; + print!("Please choose the fullscreen mode: (1) exclusive, (2) borderless: "); + stdout().flush().unwrap(); - let monitor = { - // On macOS there are two fullscreen modes "native" and "simple" - #[cfg(target_os = "macos")] - { - print!("Please choose the fullscreen mode: (1) native, (2) simple: "); - io::stdout().flush().unwrap(); + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); - let mut num = String::new(); - io::stdin().read_line(&mut num).unwrap(); - let num = num.trim().parse().ok().expect("Please enter a number"); - match num { - 2 => macos_use_simple_fullscreen = true, - _ => {} - } + let fullscreen = Some(match num { + 1 => Fullscreen::Exclusive(prompt_for_video_mode(&prompt_for_monitor(&event_loop))), + 2 => Fullscreen::Borderless(prompt_for_monitor(&event_loop)), + _ => panic!("Please enter a valid number"), + }); - // Prompt for monitor when using native fullscreen - if !macos_use_simple_fullscreen { - Some(prompt_for_monitor(&event_loop)) - } else { - None - } - } - - #[cfg(not(target_os = "macos"))] - Some(prompt_for_monitor(&event_loop)) - }; - - let mut is_fullscreen = monitor.is_some(); let mut is_maximized = false; let mut decorations = true; let window = WindowBuilder::new() .with_title("Hello world!") - .with_fullscreen(monitor) + .with_fullscreen(fullscreen.clone()) .build(&event_loop) .unwrap(); event_loop.run(move |event, _, control_flow| { - println!("{:?}", event); *control_flow = ControlFlow::Wait; match event { @@ -67,35 +46,14 @@ fn main() { } => match (virtual_code, state) { (VirtualKeyCode::Escape, _) => *control_flow = ControlFlow::Exit, (VirtualKeyCode::F, ElementState::Pressed) => { - #[cfg(target_os = "macos")] - { - if macos_use_simple_fullscreen { - use winit::platform::macos::WindowExtMacOS; - if WindowExtMacOS::set_simple_fullscreen(&window, !is_fullscreen) { - is_fullscreen = !is_fullscreen; - } - return; - } - } - - is_fullscreen = !is_fullscreen; - if !is_fullscreen { + if window.fullscreen().is_some() { window.set_fullscreen(None); } else { - window.set_fullscreen(Some(window.current_monitor())); + window.set_fullscreen(fullscreen.clone()); } } (VirtualKeyCode::S, ElementState::Pressed) => { println!("window.fullscreen {:?}", window.fullscreen()); - - #[cfg(target_os = "macos")] - { - use winit::platform::macos::WindowExtMacOS; - println!( - "window.simple_fullscreen {:?}", - WindowExtMacOS::simple_fullscreen(&window) - ); - } } (VirtualKeyCode::M, ElementState::Pressed) => { is_maximized = !is_maximized; @@ -121,10 +79,10 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { } print!("Please write the number of the monitor to use: "); - io::stdout().flush().unwrap(); + stdout().flush().unwrap(); let mut num = String::new(); - io::stdin().read_line(&mut num).unwrap(); + stdin().read_line(&mut num).unwrap(); let num = num.trim().parse().ok().expect("Please enter a number"); let monitor = event_loop .available_monitors() @@ -135,3 +93,24 @@ fn prompt_for_monitor(event_loop: &EventLoop<()>) -> MonitorHandle { monitor } + +fn prompt_for_video_mode(monitor: &MonitorHandle) -> VideoMode { + for (i, video_mode) in monitor.video_modes().enumerate() { + println!("Video mode #{}: {}", i, video_mode); + } + + print!("Please write the number of the video mode to use: "); + stdout().flush().unwrap(); + + let mut num = String::new(); + stdin().read_line(&mut num).unwrap(); + let num = num.trim().parse().ok().expect("Please enter a number"); + let video_mode = monitor + .video_modes() + .nth(num) + .expect("Please enter a valid ID"); + + println!("Using {}", video_mode); + + video_mode +} diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 39f7af3d..4f5dbc49 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; use winit::{ event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::{CursorIcon, WindowBuilder}, + window::{CursorIcon, Fullscreen, WindowBuilder}, }; const WINDOW_COUNT: usize = 3; @@ -19,11 +19,34 @@ fn main() { .with_inner_size(WINDOW_SIZE.into()) .build(&event_loop) .unwrap(); + + let mut video_modes: Vec<_> = window.current_monitor().video_modes().collect(); + let mut video_mode_id = 0usize; + let (tx, rx) = mpsc::channel(); window_senders.insert(window.id(), tx); thread::spawn(move || { while let Ok(event) = rx.recv() { match event { + WindowEvent::Moved { .. } => { + // We need to update our chosen video mode if the window + // was moved to an another monitor, so that the window + // appears on this monitor instead when we go fullscreen + let previous_video_mode = video_modes.iter().cloned().nth(video_mode_id); + video_modes = window.current_monitor().video_modes().collect(); + video_mode_id = video_mode_id.min(video_modes.len()); + let video_mode = video_modes.iter().nth(video_mode_id); + + // Different monitors may support different video modes, + // and the index we chose previously may now point to a + // completely different video mode, so notify the user + if video_mode != previous_video_mode.as_ref() { + println!( + "Window moved to another monitor, picked video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + } WindowEvent::KeyboardInput { input: KeyboardInput { @@ -44,9 +67,26 @@ fn main() { false => CursorIcon::Default, }), D => window.set_decorations(!state), - F => window.set_fullscreen(match state { - true => Some(window.current_monitor()), - false => None, + // Cycle through video modes + Right | Left => { + video_mode_id = match key { + Left => video_mode_id.saturating_sub(1), + Right => (video_modes.len() - 1).min(video_mode_id + 1), + _ => unreachable!(), + }; + println!( + "Picking video mode: {}", + video_modes.iter().nth(video_mode_id).unwrap() + ); + } + F => window.set_fullscreen(match (state, modifiers.alt) { + (true, false) => { + Some(Fullscreen::Borderless(window.current_monitor())) + } + (true, true) => Some(Fullscreen::Exclusive( + video_modes.iter().nth(video_mode_id).unwrap().clone(), + )), + (false, _) => None, }), G => window.set_cursor_grab(state).unwrap(), H => window.set_cursor_visible(!state), @@ -56,6 +96,7 @@ fn main() { println!("-> inner_position : {:?}", window.inner_position()); println!("-> outer_size : {:?}", window.outer_size()); println!("-> inner_size : {:?}", window.inner_size()); + println!("-> fullscreen : {:?}", window.fullscreen()); } L => window.set_min_inner_size(match state { true => Some(WINDOW_SIZE.into()), @@ -108,6 +149,7 @@ fn main() { | WindowEvent::KeyboardInput { input: KeyboardInput { + state: ElementState::Released, virtual_keycode: Some(VirtualKeyCode::Escape), .. }, diff --git a/examples/video_modes.rs b/examples/video_modes.rs index f8c6aa08..f923fa92 100644 --- a/examples/video_modes.rs +++ b/examples/video_modes.rs @@ -7,6 +7,6 @@ fn main() { println!("Listing available video modes:"); for mode in monitor.video_modes() { - println!("{:?}", mode); + println!("{}", mode); } } diff --git a/src/lib.rs b/src/lib.rs index 1290bcd1..a4a65f54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,6 @@ extern crate log; #[macro_use] extern crate serde; #[macro_use] -#[cfg(target_os = "windows")] extern crate derivative; #[macro_use] #[cfg(target_os = "windows")] diff --git a/src/monitor.rs b/src/monitor.rs index f27ef9d7..8e085a58 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -52,17 +52,41 @@ impl Iterator for AvailableMonitorsIter { /// - [`MonitorHandle::video_modes`][monitor_get]. /// /// [monitor_get]: ../monitor/struct.MonitorHandle.html#method.video_modes -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Derivative)] +#[derivative(Clone, Debug = "transparent", PartialEq, Eq, Hash)] pub struct VideoMode { - pub(crate) size: (u32, u32), - pub(crate) bit_depth: u16, - pub(crate) refresh_rate: u16, + pub(crate) video_mode: platform_impl::VideoMode, +} + +impl PartialOrd for VideoMode { + fn partial_cmp(&self, other: &VideoMode) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VideoMode { + fn cmp(&self, other: &VideoMode) -> std::cmp::Ordering { + // TODO: we can impl `Ord` for `PhysicalSize` once we switch from `f32` + // to `u32` there + let size: (u32, u32) = self.size().into(); + let other_size: (u32, u32) = other.size().into(); + self.monitor().cmp(&other.monitor()).then( + size.cmp(&other_size) + .then( + self.refresh_rate() + .cmp(&other.refresh_rate()) + .then(self.bit_depth().cmp(&other.bit_depth())), + ) + .reverse(), + ) + } } impl VideoMode { /// Returns the resolution of this video mode. + #[inline] pub fn size(&self) -> PhysicalSize { - self.size.into() + self.video_mode.size() } /// Returns the bit depth of this video mode, as in how many bits you have @@ -73,15 +97,37 @@ impl VideoMode { /// /// - **Wayland:** Always returns 32. /// - **iOS:** Always returns 32. + #[inline] pub fn bit_depth(&self) -> u16 { - self.bit_depth + self.video_mode.bit_depth() } /// Returns the refresh rate of this video mode. **Note**: the returned /// refresh rate is an integer approximation, and you shouldn't rely on this /// value to be exact. + #[inline] pub fn refresh_rate(&self) -> u16 { - self.refresh_rate + self.video_mode.refresh_rate() + } + + /// Returns the monitor that this video mode is valid for. Each monitor has + /// a separate set of valid video modes. + #[inline] + pub fn monitor(&self) -> MonitorHandle { + self.video_mode.monitor() + } +} + +impl std::fmt::Display for VideoMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}x{} @ {} Hz ({} bpp)", + self.size().width, + self.size().height, + self.refresh_rate(), + self.bit_depth() + ) } } @@ -90,7 +136,7 @@ impl VideoMode { /// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. /// /// [`Window`]: ../window/struct.Window.html -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct MonitorHandle { pub(crate) inner: platform_impl::MonitorHandle, } diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index dfb659f9..3141dea4 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -79,7 +79,7 @@ use std::fmt; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index c75b7ef6..90257077 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -1,18 +1,44 @@ use std::{ - collections::{HashSet, VecDeque}, + collections::{BTreeSet, VecDeque}, fmt, ops::{Deref, DerefMut}, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}, }; -use crate::platform_impl::platform::ffi::{ - id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger, -}; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, +} +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Inner { uiscreen: id, } @@ -25,6 +51,7 @@ impl Drop for Inner { } } +#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MonitorHandle { inner: Inner, } @@ -140,21 +167,24 @@ impl Inner { } } - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { let refresh_rate: NSInteger = unsafe { msg_send![self.uiscreen, maximumFramesPerSecond] }; let available_modes: id = unsafe { msg_send![self.uiscreen, availableModes] }; let available_mode_count: NSUInteger = unsafe { msg_send![available_modes, count] }; - let mut modes = HashSet::with_capacity(available_mode_count); + let mut modes = BTreeSet::new(); for i in 0..available_mode_count { let mode: id = unsafe { msg_send![available_modes, objectAtIndex: i] }; let size: CGSize = unsafe { msg_send![mode, size] }; - modes.insert(VideoMode { - size: (size.width as u32, size.height as u32), - bit_depth: 32, - refresh_rate: refresh_rate as u16, + modes.insert(RootVideoMode { + video_mode: VideoMode { + size: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate: refresh_rate as u16, + monitor: MonitorHandle::retained_new(self.uiscreen), + }, }); } diff --git a/src/platform_impl/ios/view.rs b/src/platform_impl/ios/view.rs index b174b75a..95047814 100644 --- a/src/platform_impl/ios/view.rs +++ b/src/platform_impl/ios/view.rs @@ -8,15 +8,14 @@ use objc::{ use crate::{ event::{DeviceId as RootDeviceId, Event, Touch, TouchPhase, WindowEvent}, platform::ios::MonitorHandleExtIOS, - window::{WindowAttributes, WindowId as RootWindowId}, -}; - -use crate::platform_impl::platform::{ - app_state::AppState, - event_loop, - ffi::{id, nil, CGFloat, CGPoint, CGRect, UIInterfaceOrientationMask, UITouchPhase}, - window::PlatformSpecificWindowBuilderAttributes, - DeviceId, + platform_impl::platform::{ + app_state::AppState, + event_loop, + ffi::{id, nil, CGFloat, CGPoint, CGRect, UIInterfaceOrientationMask, UITouchPhase}, + window::PlatformSpecificWindowBuilderAttributes, + DeviceId, + }, + window::{Fullscreen, WindowAttributes, WindowId as RootWindowId}, }; // requires main thread @@ -366,8 +365,12 @@ pub unsafe fn create_window( if let Some(hidpi_factor) = platform_attributes.hidpi_factor { let () = msg_send![window, setContentScaleFactor: hidpi_factor as CGFloat]; } - if let &Some(ref monitor) = &window_attributes.fullscreen { - let () = msg_send![window, setScreen:monitor.ui_screen()]; + match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(_)) => unimplemented!(), + Some(Fullscreen::Borderless(ref monitor)) => { + msg_send![window, setScreen:monitor.ui_screen()] + } + None => (), } window diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index ea4f022f..05b68ea4 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -17,7 +17,7 @@ use crate::{ ffi::{id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask}, monitor, view, EventLoopWindowTarget, MonitorHandle, }, - window::{CursorIcon, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; pub struct Inner { @@ -157,10 +157,11 @@ impl Inner { warn!("`Window::set_maximized` is ignored on iOS") } - pub fn set_fullscreen(&self, monitor: Option) { + pub fn set_fullscreen(&self, monitor: Option) { unsafe { match monitor { - Some(monitor) => { + Some(Fullscreen::Exclusive(_)) => unimplemented!("exclusive fullscreen on iOS"), // TODO + Some(Fullscreen::Borderless(monitor)) => { let uiscreen = monitor.ui_screen() as id; let current: id = msg_send![self.window, screen]; let bounds: CGRect = msg_send![uiscreen, bounds]; @@ -176,7 +177,7 @@ impl Inner { } } - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { unsafe { let monitor = self.current_monitor(); let uiscreen = monitor.inner.ui_screen(); @@ -189,7 +190,7 @@ impl Inner { && screen_space_bounds.size.width == screen_bounds.size.width && screen_space_bounds.size.height == screen_bounds.size.height { - Some(monitor) + Some(Fullscreen::Borderless(monitor)) } else { None } @@ -293,11 +294,12 @@ impl Window { // TODO: transparency, visible unsafe { - let screen = window_attributes - .fullscreen - .as_ref() - .map(|screen| screen.ui_screen() as _) - .unwrap_or_else(|| monitor::main_uiscreen().ui_screen()); + let screen = match window_attributes.fullscreen { + Some(Fullscreen::Exclusive(_)) => unimplemented!("exclusive fullscreen on iOS"), // TODO: do we set the frame to video mode bounds instead of screen bounds? + Some(Fullscreen::Borderless(ref monitor)) => monitor.ui_screen() as id, + None => monitor::main_uiscreen().ui_screen(), + }; + let screen_bounds: CGRect = msg_send![screen, bounds]; let frame = match window_attributes.inner_size { diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 258f4280..bd2656ed 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -13,8 +13,8 @@ use crate::{ event::Event, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, icon::Icon, - monitor::{MonitorHandle as RootMonitorHandle, VideoMode}, - window::{CursorIcon, WindowAttributes}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; mod dlopen; @@ -92,7 +92,7 @@ impl DeviceId { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum MonitorHandle { X(x11::MonitorHandle), Wayland(wayland::MonitorHandle), @@ -140,7 +140,7 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> Box> { + pub fn video_modes(&self) -> Box> { match self { MonitorHandle::X(m) => Box::new(m.video_modes()), MonitorHandle::Wayland(m) => Box::new(m.video_modes()), @@ -148,6 +148,46 @@ impl MonitorHandle { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum VideoMode { + X(x11::VideoMode), + Wayland(wayland::VideoMode), +} + +impl VideoMode { + #[inline] + pub fn size(&self) -> PhysicalSize { + match self { + &VideoMode::X(ref m) => m.size(), + &VideoMode::Wayland(ref m) => m.size(), + } + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + match self { + &VideoMode::X(ref m) => m.bit_depth(), + &VideoMode::Wayland(ref m) => m.bit_depth(), + } + } + + #[inline] + pub fn refresh_rate(&self) -> u16 { + match self { + &VideoMode::X(ref m) => m.refresh_rate(), + &VideoMode::Wayland(ref m) => m.refresh_rate(), + } + } + + #[inline] + pub fn monitor(&self) -> RootMonitorHandle { + match self { + &VideoMode::X(ref m) => m.monitor(), + &VideoMode::Wayland(ref m) => m.monitor(), + } + } +} + impl Window { #[inline] pub fn new( @@ -310,17 +350,15 @@ impl Window { } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { match self { &Window::X(ref w) => w.fullscreen(), - &Window::Wayland(ref w) => w.fullscreen().map(|monitor_id| RootMonitorHandle { - inner: MonitorHandle::Wayland(monitor_id), - }), + &Window::Wayland(ref w) => w.fullscreen(), } } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { + pub fn set_fullscreen(&self, monitor: Option) { match self { &Window::X(ref w) => w.set_fullscreen(monitor), &Window::Wayland(ref w) => w.set_fullscreen(monitor), diff --git a/src/platform_impl/linux/wayland/event_loop.rs b/src/platform_impl/linux/wayland/event_loop.rs index 7c3b21a0..2cdfb168 100644 --- a/src/platform_impl/linux/wayland/event_loop.rs +++ b/src/platform_impl/linux/wayland/event_loop.rs @@ -16,8 +16,11 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize}, event::ModifiersState, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, - monitor::VideoMode, - platform_impl::platform::sticky_exit_callback, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::{ + sticky_exit_callback, MonitorHandle as PlatformMonitorHandle, + VideoMode as PlatformVideoMode, + }, }; use super::{window::WindowStore, DeviceId, WindowId}; @@ -603,17 +606,67 @@ impl Drop for SeatData { * Monitor stuff */ +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, +} + +impl VideoMode { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + #[inline] + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(self.monitor.clone()), + } + } +} + +#[derive(Clone)] pub struct MonitorHandle { pub(crate) proxy: wl_output::WlOutput, pub(crate) mgr: OutputMgr, } -impl Clone for MonitorHandle { - fn clone(&self) -> MonitorHandle { - MonitorHandle { - proxy: self.proxy.clone(), - mgr: self.mgr.clone(), - } +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.native_identifier() == other.native_identifier() + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.native_identifier().cmp(&other.native_identifier()) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.native_identifier().hash(state); } } @@ -680,15 +733,20 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.mgr .with_info(&self.proxy, |_, info| info.modes.clone()) .unwrap_or(vec![]) .into_iter() - .map(|x| VideoMode { - size: (x.dimensions.0 as u32, x.dimensions.1 as u32), - refresh_rate: (x.refresh_rate as f32 / 1000.0).round() as u16, - bit_depth: 32, + .map(move |x| RootVideoMode { + video_mode: PlatformVideoMode::Wayland(VideoMode { + size: (x.dimensions.0 as u32, x.dimensions.1 as u32), + refresh_rate: (x.refresh_rate as f32 / 1000.0).round() as u16, + bit_depth: 32, + monitor: monitor.clone(), + }), }) } } diff --git a/src/platform_impl/linux/wayland/mod.rs b/src/platform_impl/linux/wayland/mod.rs index aa96e276..09cd66d1 100644 --- a/src/platform_impl/linux/wayland/mod.rs +++ b/src/platform_impl/linux/wayland/mod.rs @@ -3,7 +3,8 @@ pub use self::{ event_loop::{ - EventLoop, EventLoopProxy, EventLoopWindowTarget, MonitorHandle, WindowEventsSink, + EventLoop, EventLoopProxy, EventLoopWindowTarget, MonitorHandle, VideoMode, + WindowEventsSink, }, window::Window, }; diff --git a/src/platform_impl/linux/wayland/window.rs b/src/platform_impl/linux/wayland/window.rs index 773af760..b4a04aa7 100644 --- a/src/platform_impl/linux/wayland/window.rs +++ b/src/platform_impl/linux/wayland/window.rs @@ -8,10 +8,11 @@ use crate::{ error::{ExternalError, NotSupportedError, OsError as RootOsError}, monitor::MonitorHandle as RootMonitorHandle, platform_impl::{ + platform::wayland::event_loop::{available_monitors, primary_monitor}, MonitorHandle as PlatformMonitorHandle, PlatformSpecificWindowBuilderAttributes as PlAttributes, }, - window::{CursorIcon, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; use smithay_client_toolkit::{ @@ -25,7 +26,6 @@ use smithay_client_toolkit::{ }; use super::{make_wid, EventLoopWindowTarget, MonitorHandle, WindowId}; -use crate::platform_impl::platform::wayland::event_loop::{available_monitors, primary_monitor}; pub struct Window { surface: wl_surface::WlSurface, @@ -108,13 +108,19 @@ impl Window { } // Check for fullscreen requirements - if let Some(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - }) = attributes.fullscreen - { - frame.set_fullscreen(Some(&monitor_id.proxy)); - } else if attributes.maximized { - frame.set_maximized(); + match attributes.fullscreen { + Some(Fullscreen::Exclusive(_)) => { + panic!("Wayland doesn't support exclusive fullscreen") + } + Some(Fullscreen::Borderless(RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(ref monitor_id), + })) => frame.set_fullscreen(Some(&monitor_id.proxy)), + Some(Fullscreen::Borderless(_)) => unreachable!(), + None => { + if attributes.maximized { + frame.set_maximized(); + } + } } frame.set_resizable(attributes.resizable); @@ -252,25 +258,31 @@ impl Window { } } - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { if *(self.fullscreen.lock().unwrap()) { - Some(self.current_monitor()) + Some(Fullscreen::Borderless(RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(self.current_monitor()), + })) } else { None } } - pub fn set_fullscreen(&self, monitor: Option) { - if let Some(RootMonitorHandle { - inner: PlatformMonitorHandle::Wayland(ref monitor_id), - }) = monitor - { - self.frame - .lock() - .unwrap() - .set_fullscreen(Some(&monitor_id.proxy)); - } else { - self.frame.lock().unwrap().unset_fullscreen(); + pub fn set_fullscreen(&self, fullscreen: Option) { + match fullscreen { + Some(Fullscreen::Exclusive(_)) => { + panic!("Wayland doesn't support exclusive fullscreen") + } + Some(Fullscreen::Borderless(RootMonitorHandle { + inner: PlatformMonitorHandle::Wayland(ref monitor_id), + })) => { + self.frame + .lock() + .unwrap() + .set_fullscreen(Some(&monitor_id.proxy)); + } + Some(Fullscreen::Borderless(_)) => unreachable!(), + None => self.frame.lock().unwrap().unset_fullscreen(), } } diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 45f1e3d3..345ebeeb 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -11,7 +11,7 @@ mod window; mod xdisplay; pub use self::{ - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::UnownedWindow, xdisplay::{XConnection, XError, XNotSupported}, }; diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 72026257..33a694e0 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -4,47 +4,66 @@ use parking_lot::Mutex; use super::{ ffi::{ - RRCrtcChangeNotifyMask, RROutputPropertyNotifyMask, RRScreenChangeNotifyMask, True, Window, - XRRScreenResources, + RRCrtc, RRCrtcChangeNotifyMask, RRMode, RROutputPropertyNotifyMask, + RRScreenChangeNotifyMask, True, Window, XRRCrtcInfo, XRRScreenResources, }, util, XConnection, XError, }; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::{MonitorHandle as PlatformMonitorHandle, VideoMode as PlatformVideoMode}, }; -// Used to test XRandR < 1.5 code path. This should always be committed as false. -const FORCE_RANDR_COMPAT: bool = false; -// Also used for testing. This should always be committed as false. +// Used for testing. This should always be committed as false. const DISABLE_MONITOR_LIST_CACHING: bool = false; lazy_static! { - static ref XRANDR_VERSION: Mutex> = Mutex::default(); static ref MONITORS: Mutex>> = Mutex::default(); } -fn version_is_at_least(major: c_int, minor: c_int) -> bool { - if let Some((avail_major, avail_minor)) = *XRANDR_VERSION.lock() { - if avail_major == major { - avail_minor >= minor - } else { - avail_major > major - } - } else { - unreachable!(); - } -} - pub fn invalidate_cached_monitor_list() -> Option> { // We update this lazily. (*MONITORS.lock()).take() } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) native_mode: RRMode, + pub(crate) monitor: Option, +} + +impl VideoMode { + #[inline] + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + #[inline] + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + #[inline] + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + #[inline] + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: PlatformMonitorHandle::X(self.monitor.clone().unwrap()), + } + } +} + #[derive(Debug, Clone)] pub struct MonitorHandle { /// The actual id - id: u32, + pub(crate) id: RRCrtc, /// The name of the monitor pub(crate) name: String, /// The size of the monitor @@ -61,16 +80,43 @@ pub struct MonitorHandle { video_modes: Vec, } +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + impl MonitorHandle { - fn from_repr( + fn new( xconn: &XConnection, resources: *mut XRRScreenResources, - id: u32, - repr: util::MonitorRepr, + id: RRCrtc, + crtc: *mut XRRCrtcInfo, primary: bool, ) -> Option { - let (name, hidpi_factor, video_modes) = unsafe { xconn.get_output_info(resources, &repr)? }; - let (dimensions, position) = unsafe { (repr.size(), repr.position()) }; + let (name, hidpi_factor, video_modes) = unsafe { xconn.get_output_info(resources, crtc)? }; + let dimensions = unsafe { ((*crtc).width as u32, (*crtc).height as u32) }; + let position = unsafe { ((*crtc).x as i32, (*crtc).y as i32) }; let rect = util::AaRect::new(position, dimensions); Some(MonitorHandle { id, @@ -107,8 +153,14 @@ impl MonitorHandle { } #[inline] - pub fn video_modes(&self) -> impl Iterator { - self.video_modes.clone().into_iter() + pub fn video_modes(&self) -> impl Iterator { + let monitor = self.clone(); + self.video_modes.clone().into_iter().map(move |mut x| { + x.monitor = Some(monitor.clone()); + RootVideoMode { + video_mode: PlatformVideoMode::X(x), + } + }) } } @@ -139,8 +191,12 @@ impl XConnection { fn query_monitor_list(&self) -> Vec { unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + let root = (self.xlib.XDefaultRootWindow)(self.display); - let resources = if version_is_at_least(1, 3) { + let resources = if (major == 1 && minor >= 3) || major > 1 { (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) } else { // WARNING: this function is supposedly very slow, on the order of hundreds of ms. @@ -155,48 +211,19 @@ impl XConnection { let mut available; let mut has_primary = false; - if self.xrandr_1_5.is_some() && version_is_at_least(1, 5) && !FORCE_RANDR_COMPAT { - // We're in XRandR >= 1.5, enumerate monitors. This supports things like MST and - // videowalls. - let xrandr_1_5 = self.xrandr_1_5.as_ref().unwrap(); - let mut monitor_count = 0; - let monitors = - (xrandr_1_5.XRRGetMonitors)(self.display, root, 1, &mut monitor_count); - assert!(monitor_count >= 0); - available = Vec::with_capacity(monitor_count as usize); - for monitor_index in 0..monitor_count { - let monitor = monitors.offset(monitor_index as isize); - let is_primary = (*monitor).primary != 0; + let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); + available = Vec::with_capacity((*resources).ncrtc as usize); + for crtc_index in 0..(*resources).ncrtc { + let crtc_id = *((*resources).crtcs.offset(crtc_index as isize)); + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let is_active = (*crtc).width > 0 && (*crtc).height > 0 && (*crtc).noutput > 0; + if is_active { + let is_primary = *(*crtc).outputs.offset(0) == primary; has_primary |= is_primary; - MonitorHandle::from_repr( - self, - resources, - monitor_index as u32, - monitor.into(), - is_primary, - ) - .map(|monitor_id| available.push(monitor_id)); - } - (xrandr_1_5.XRRFreeMonitors)(monitors); - } else { - // We're in XRandR < 1.5, enumerate CRTCs. Everything will work except MST and - // videowall setups will also show monitors that aren't in the logical groups the user - // cares about. - let primary = (self.xrandr.XRRGetOutputPrimary)(self.display, root); - available = Vec::with_capacity((*resources).ncrtc as usize); - for crtc_index in 0..(*resources).ncrtc { - let crtc_id = *((*resources).crtcs.offset(crtc_index as isize)); - let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); - let is_active = (*crtc).width > 0 && (*crtc).height > 0 && (*crtc).noutput > 0; - if is_active { - let crtc = util::MonitorRepr::from(crtc); - let is_primary = crtc.get_output() == primary; - has_primary |= is_primary; - MonitorHandle::from_repr(self, resources, crtc_id as u32, crtc, is_primary) - .map(|monitor_id| available.push(monitor_id)); - } - (self.xrandr.XRRFreeCrtcInfo)(crtc); + MonitorHandle::new(self, resources, crtc_id, crtc, is_primary) + .map(|monitor_id| available.push(monitor_id)); } + (self.xrandr.XRRFreeCrtcInfo)(crtc); } // If no monitors were detected as being primary, we just pick one ourselves! @@ -236,19 +263,15 @@ impl XConnection { } pub fn select_xrandr_input(&self, root: Window) -> Result { - { - let mut version_lock = XRANDR_VERSION.lock(); - if version_lock.is_none() { - let mut major = 0; - let mut minor = 0; - let has_extension = - unsafe { (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor) }; - if has_extension != True { - panic!("[winit] XRandR extension not available."); - } - *version_lock = Some((major, minor)); - } - } + let has_xrandr = unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor) + }; + assert!( + has_xrandr == True, + "[winit] XRandR extension not available." + ); let mut event_offset = 0; let mut error_offset = 0; diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 1e9a41c6..1fbef41d 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,7 +1,10 @@ use std::{env, slice, str::FromStr}; -use super::*; -use crate::{dpi::validate_hidpi_factor, monitor::VideoMode}; +use super::{ + ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, + *, +}; +use crate::{dpi::validate_hidpi_factor, platform_impl::platform::x11::VideoMode}; pub fn calc_dpi_factor( (width_px, height_px): (u32, u32), @@ -34,47 +37,6 @@ pub fn calc_dpi_factor( dpi_factor } -pub enum MonitorRepr { - Monitor(*mut ffi::XRRMonitorInfo), - Crtc(*mut ffi::XRRCrtcInfo), -} - -impl MonitorRepr { - pub unsafe fn get_output(&self) -> ffi::RROutput { - match *self { - // Same member names, but different locations within the struct... - MonitorRepr::Monitor(monitor) => *((*monitor).outputs.offset(0)), - MonitorRepr::Crtc(crtc) => *((*crtc).outputs.offset(0)), - } - } - - pub unsafe fn size(&self) -> (u32, u32) { - match *self { - MonitorRepr::Monitor(monitor) => ((*monitor).width as u32, (*monitor).height as u32), - MonitorRepr::Crtc(crtc) => ((*crtc).width as u32, (*crtc).height as u32), - } - } - - pub unsafe fn position(&self) -> (i32, i32) { - match *self { - MonitorRepr::Monitor(monitor) => ((*monitor).x as i32, (*monitor).y as i32), - MonitorRepr::Crtc(crtc) => ((*crtc).x as i32, (*crtc).y as i32), - } - } -} - -impl From<*mut ffi::XRRMonitorInfo> for MonitorRepr { - fn from(monitor: *mut ffi::XRRMonitorInfo) -> Self { - MonitorRepr::Monitor(monitor) - } -} - -impl From<*mut ffi::XRRCrtcInfo> for MonitorRepr { - fn from(crtc: *mut ffi::XRRCrtcInfo) -> Self { - MonitorRepr::Crtc(crtc) - } -} - impl XConnection { // Retrieve DPI from Xft.dpi property pub unsafe fn get_xft_dpi(&self) -> Option { @@ -96,11 +58,11 @@ impl XConnection { } pub unsafe fn get_output_info( &self, - resources: *mut ffi::XRRScreenResources, - repr: &MonitorRepr, + resources: *mut XRRScreenResources, + crtc: *mut XRRCrtcInfo, ) -> Option<(String, f64, Vec)> { let output_info = - (self.xrandr.XRRGetOutputInfo)(self.display, resources, repr.get_output()); + (self.xrandr.XRRGetOutputInfo)(self.display, resources, *(*crtc).outputs.offset(0)); if output_info.is_null() { // When calling `XRRGetOutputInfo` on a virtual monitor (versus a physical display) // it's possible for it to return null. @@ -132,6 +94,10 @@ impl XConnection { size: (x.width, x.height), refresh_rate: (refresh_rate as f32 / 1000.0).round() as u16, bit_depth: bit_depth as u16, + native_mode: x.id, + // This is populated in `MonitorHandle::video_modes` as the + // video mode is returned to the user + monitor: None, } }); @@ -144,7 +110,7 @@ impl XConnection { dpi / 96. } else { calc_dpi_factor( - repr.size(), + ((*crtc).width as u32, (*crtc).height as u32), ( (*output_info).mm_width as u64, (*output_info).mm_height as u64, @@ -155,4 +121,61 @@ impl XConnection { (self.xrandr.XRRFreeOutputInfo)(output_info); Some((name, hidpi_factor, modes.collect())) } + pub fn set_crtc_config(&self, crtc_id: RRCrtc, mode_id: RRMode) -> Result<(), ()> { + unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + + let root = (self.xlib.XDefaultRootWindow)(self.display); + let resources = if (major == 1 && minor >= 3) || major > 1 { + (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) + } else { + (self.xrandr.XRRGetScreenResources)(self.display, root) + }; + + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let status = (self.xrandr.XRRSetCrtcConfig)( + self.display, + resources, + crtc_id, + CurrentTime, + (*crtc).x, + (*crtc).y, + mode_id, + (*crtc).rotation, + (*crtc).outputs.offset(0), + 1, + ); + + (self.xrandr.XRRFreeCrtcInfo)(crtc); + (self.xrandr.XRRFreeScreenResources)(resources); + + if status == Success as i32 { + Ok(()) + } else { + Err(()) + } + } + } + pub fn get_crtc_mode(&self, crtc_id: RRCrtc) -> RRMode { + unsafe { + let mut major = 0; + let mut minor = 0; + (self.xrandr.XRRQueryVersion)(self.display, &mut major, &mut minor); + + let root = (self.xlib.XDefaultRootWindow)(self.display); + let resources = if (major == 1 && minor >= 3) || major > 1 { + (self.xrandr.XRRGetScreenResourcesCurrent)(self.display, root) + } else { + (self.xrandr.XRRGetScreenResources)(self.display, root) + }; + + let crtc = (self.xrandr.XRRGetCrtcInfo)(self.display, resources, crtc_id); + let mode = (*crtc).mode; + (self.xrandr.XRRFreeCrtcInfo)(crtc); + (self.xrandr.XRRFreeScreenResources)(resources); + mode + } + } } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 33a16e02..e65d8836 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -16,12 +16,13 @@ use parking_lot::Mutex; use crate::{ dpi::{LogicalPosition, LogicalSize}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, - monitor::MonitorHandle as RootMonitorHandle, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::{ x11::{ime::ImeContextCreationError, MonitorHandle as X11MonitorHandle}, MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, + VideoMode as PlatformVideoMode, }, - window::{CursorIcon, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, }; use super::{ffi, util, EventLoopWindowTarget, ImeSender, WindowId, XConnection, XError}; @@ -46,9 +47,11 @@ pub struct SharedState { pub guessed_dpi: Option, pub last_monitor: Option, pub dpi_adjusted: Option<(f64, f64)>, - pub fullscreen: Option, - // Used to restore position after exiting fullscreen. + pub fullscreen: Option, + // Used to restore position after exiting fullscreen pub restore_position: Option<(i32, i32)>, + // Used to restore video mode after exiting fullscreen + pub desktop_video_mode: Option<(ffi::RRCrtc, ffi::RRMode)>, pub frame_extents: Option, pub min_inner_size: Option, pub max_inner_size: Option, @@ -408,6 +411,7 @@ impl UnownedWindow { if window_attrs.fullscreen.is_some() { window .set_fullscreen_inner(window_attrs.fullscreen.clone()) + .unwrap() .queue(); } if window_attrs.always_on_top { @@ -564,41 +568,122 @@ impl UnownedWindow { self.set_netwm(fullscreen.into(), (fullscreen_atom as c_long, 0, 0, 0)) } - fn set_fullscreen_inner(&self, monitor: Option) -> util::Flusher<'_> { - match monitor { + fn set_fullscreen_inner(&self, fullscreen: Option) -> Option> { + let mut shared_state_lock = self.shared_state.lock(); + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if old_fullscreen == fullscreen { + return None; + } + shared_state_lock.fullscreen = fullscreen.clone(); + + match (&old_fullscreen, &fullscreen) { + // Store the desktop video mode before entering exclusive + // fullscreen, so we can restore it upon exit, as XRandR does not + // provide a mechanism to set this per app-session or restore this + // to the desktop video mode as macOS and Windows do + ( + &None, + &Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: PlatformVideoMode::X(ref video_mode), + })), + ) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: PlatformVideoMode::X(ref video_mode), + })), + ) => { + let monitor = video_mode.monitor.as_ref().unwrap(); + shared_state_lock.desktop_video_mode = + Some((monitor.id, self.xconn.get_crtc_mode(monitor.id))); + } + // Restore desktop video mode upon exiting exclusive fullscreen + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let (monitor_id, mode_id) = shared_state_lock.desktop_video_mode.take().unwrap(); + self.xconn + .set_crtc_config(monitor_id, mode_id) + .expect("failed to restore desktop video mode"); + } + _ => (), + } + + drop(shared_state_lock); + + match fullscreen { None => { let flusher = self.set_fullscreen_hint(false); - if let Some(position) = self.shared_state.lock().restore_position.take() { + let mut shared_state_lock = self.shared_state.lock(); + if let Some(position) = shared_state_lock.restore_position.take() { self.set_position_inner(position.0, position.1).queue(); } - flusher + Some(flusher) } - Some(RootMonitorHandle { - inner: PlatformMonitorHandle::X(monitor), - }) => { + Some(fullscreen) => { + let (video_mode, monitor) = match fullscreen { + Fullscreen::Exclusive(RootVideoMode { + video_mode: PlatformVideoMode::X(ref video_mode), + }) => (Some(video_mode), video_mode.monitor.as_ref().unwrap()), + Fullscreen::Borderless(RootMonitorHandle { + inner: PlatformMonitorHandle::X(ref monitor), + }) => (None, monitor), + _ => unreachable!(), + }; + + if let Some(video_mode) = video_mode { + // FIXME: this is actually not correct if we're setting the + // video mode to a resolution higher than the current + // desktop resolution, because XRandR does not automatically + // reposition the monitors to the right and below this + // monitor. + // + // What ends up happening is we will get the fullscreen + // window showing up on those monitors as well, because + // their virtual position now overlaps with the monitor that + // we just made larger.. + // + // It'd be quite a bit of work to handle this correctly (and + // nobody else seems to bother doing this correctly either), + // so we're just leaving this broken. Fixing this would + // involve storing all CRTCs upon entering fullscreen, + // restoring them upon exit, and after entering fullscreen, + // repositioning displays to the right and below this + // display. I think there would still be edge cases that are + // difficult or impossible to handle correctly, e.g. what if + // a new monitor was plugged in while in fullscreen? + // + // I think we might just want to disallow setting the video + // mode higher than the current desktop video mode (I'm sure + // this will make someone unhappy, but it's very unusual for + // games to want to do this anyway). + self.xconn + .set_crtc_config(monitor.id, video_mode.native_mode) + .expect("failed to set video mode"); + } + let window_position = self.outer_position_physical(); self.shared_state.lock().restore_position = Some(window_position); let monitor_origin: (i32, i32) = monitor.position().into(); self.set_position_inner(monitor_origin.0, monitor_origin.1) .queue(); - self.set_fullscreen_hint(true) + Some(self.set_fullscreen_hint(true)) } - _ => unreachable!(), } } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { self.shared_state.lock().fullscreen.clone() } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - self.shared_state.lock().fullscreen = monitor.clone(); - self.set_fullscreen_inner(monitor) - .flush() - .expect("Failed to change window fullscreen state"); - self.invalidate_cached_frame_extents(); + pub fn set_fullscreen(&self, fullscreen: Option) { + if let Some(flusher) = self.set_fullscreen_inner(fullscreen) { + flusher + .flush() + .expect("Failed to change window fullscreen state"); + self.invalidate_cached_frame_extents(); + } } fn get_rect(&self) -> util::AaRect { diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 7ddcdf6f..aec0fc97 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -6,6 +6,13 @@ use cocoa::{ base::id, foundation::{NSInteger, NSUInteger}, }; +use core_foundation::{ + array::CFArrayRef, dictionary::CFDictionaryRef, string::CFStringRef, uuid::CFUUIDRef, +}; +use core_graphics::{ + base::CGError, + display::{CGDirectDisplayID, CGDisplayConfigRef}, +}; use objc; pub const NSNotFound: NSInteger = NSInteger::max_value(); @@ -108,3 +115,95 @@ pub enum NSWindowLevel { NSPopUpMenuWindowLevel = kCGPopUpMenuWindowLevelKey as _, NSScreenSaverWindowLevel = kCGScreenSaverWindowLevelKey as _, } + +pub type CGDisplayFadeInterval = f32; +pub type CGDisplayReservationInterval = f32; +pub type CGDisplayBlendFraction = f32; + +pub const kCGDisplayBlendNormal: f32 = 0.0; +pub const kCGDisplayBlendSolidColor: f32 = 1.0; + +pub type CGDisplayFadeReservationToken = u32; +pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0; + +pub type Boolean = u8; +pub const FALSE: Boolean = 0; +pub const TRUE: Boolean = 1; + +pub const kCGErrorSuccess: i32 = 0; +pub const kCGErrorFailure: i32 = 1000; +pub const kCGErrorIllegalArgument: i32 = 1001; +pub const kCGErrorInvalidConnection: i32 = 1002; +pub const kCGErrorInvalidContext: i32 = 1003; +pub const kCGErrorCannotComplete: i32 = 1004; +pub const kCGErrorNotImplemented: i32 = 1006; +pub const kCGErrorRangeCheck: i32 = 1007; +pub const kCGErrorTypeCheck: i32 = 1008; +pub const kCGErrorInvalidOperation: i32 = 1010; +pub const kCGErrorNoneAvailable: i32 = 1011; + +pub const IO1BitIndexedPixels: &str = "P"; +pub const IO2BitIndexedPixels: &str = "PP"; +pub const IO4BitIndexedPixels: &str = "PPPP"; +pub const IO8BitIndexedPixels: &str = "PPPPPPPP"; +pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB"; +pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB"; + +pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB"; +pub const kIO64BitDirectPixels: &str = "-16R16G16B16"; + +pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16"; +pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32"; + +pub const IOYUV422Pixels: &str = "Y4U2V2"; +pub const IO8BitOverlayPixels: &str = "O8"; + +pub type CGWindowLevel = i32; +pub type CGDisplayModeRef = *mut libc::c_void; + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGRestorePermanentDisplayConfiguration(); + pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError; + pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError; + pub fn CGConfigureDisplayFadeEffect( + config: CGDisplayConfigRef, + fadeOutSeconds: CGDisplayFadeInterval, + fadeInSeconds: CGDisplayFadeInterval, + fadeRed: f32, + fadeGreen: f32, + fadeBlue: f32, + ) -> CGError; + pub fn CGAcquireDisplayFadeReservation( + seconds: CGDisplayReservationInterval, + token: *mut CGDisplayFadeReservationToken, + ) -> CGError; + pub fn CGDisplayFade( + token: CGDisplayFadeReservationToken, + duration: CGDisplayFadeInterval, + startBlend: CGDisplayBlendFraction, + endBlend: CGDisplayBlendFraction, + redBlend: f32, + greenBlend: f32, + blueBlend: f32, + synchronous: Boolean, + ) -> CGError; + pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError; + pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef; + pub fn CGShieldingWindowLevel() -> CGWindowLevel; + pub fn CGDisplaySetDisplayMode( + display: CGDirectDisplayID, + mode: CGDisplayModeRef, + options: CFDictionaryRef, + ) -> CGError; + pub fn CGDisplayCopyAllDisplayModes( + display: CGDirectDisplayID, + options: CFDictionaryRef, + ) -> CFArrayRef; + pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize; + pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64; + pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef; + pub fn CGDisplayModeRetain(mode: CGDisplayModeRef); + pub fn CGDisplayModeRelease(mode: CGDisplayModeRef); +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 02b63515..c26385da 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -17,7 +17,7 @@ use std::{fmt, ops::Deref, sync::Arc}; pub use self::{ event_loop::{EventLoop, EventLoopWindowTarget, Proxy as EventLoopProxy}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::{Id as WindowId, PlatformSpecificWindowBuilderAttributes, UnownedWindow}, }; use crate::{ diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index cf74c644..c9408890 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -1,25 +1,119 @@ use std::{collections::VecDeque, fmt}; +use super::ffi; +use crate::{ + dpi::{PhysicalPosition, PhysicalSize}, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform_impl::platform::util::IdRef, +}; use cocoa::{ appkit::NSScreen, base::{id, nil}, foundation::{NSString, NSUInteger}, }; -use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayMode}; +use core_foundation::{ + array::{CFArrayGetCount, CFArrayGetValueAtIndex}, + base::{CFRelease, TCFType}, + string::CFString, +}; +use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; use core_video_sys::{ kCVReturnSuccess, kCVTimeIsIndefinite, CVDisplayLinkCreateWithCGDisplay, CVDisplayLinkGetNominalOutputVideoRefreshPeriod, CVDisplayLinkRelease, }; -use crate::{ - dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, - platform_impl::platform::util::IdRef, -}; +#[derive(Derivative)] +#[derivative(Debug, Clone, PartialEq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + #[derivative(Debug = "ignore", PartialEq = "ignore", Hash = "ignore")] + pub(crate) native_mode: NativeDisplayMode, +} -#[derive(Clone, PartialEq)] +pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef); + +unsafe impl Send for NativeDisplayMode {} + +impl Drop for NativeDisplayMode { + fn drop(&mut self) { + unsafe { + ffi::CGDisplayModeRelease(self.0); + } + } +} + +impl Clone for NativeDisplayMode { + fn clone(&self) -> Self { + unsafe { + ffi::CGDisplayModeRetain(self.0); + } + NativeDisplayMode(self.0) + } +} + +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Clone)] pub struct MonitorHandle(CGDirectDisplayID); +// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that +// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an +// unique identifier that persists even across system reboots +impl PartialEq for MonitorHandle { + fn eq(&self, other: &Self) -> bool { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + == ffi::CGDisplayCreateUUIDFromDisplayID(other.0) + } + } +} + +impl Eq for MonitorHandle {} + +impl PartialOrd for MonitorHandle { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for MonitorHandle { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0) + .cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0)) + } + } +} + +impl std::hash::Hash for MonitorHandle { + fn hash(&self, state: &mut H) { + unsafe { + ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state); + } + } +} + pub fn available_monitors() -> VecDeque { if let Ok(displays) = CGDisplay::active_displays() { let mut monitors = VecDeque::with_capacity(displays.len()); @@ -101,7 +195,7 @@ impl MonitorHandle { unsafe { NSScreen::backingScaleFactor(screen) as f64 } } - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { let cv_refresh_rate = unsafe { let mut display_link = std::ptr::null_mut(); assert_eq!( @@ -117,11 +211,27 @@ impl MonitorHandle { time.timeScale as i64 / time.timeValue }; - CGDisplayMode::all_display_modes(self.0, std::ptr::null()) - .expect("failed to obtain list of display modes") - .into_iter() - .map(move |mode| { - let cg_refresh_rate = mode.refresh_rate().round() as i64; + let monitor = self.clone(); + + unsafe { + let modes = { + let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null()); + assert!(!array.is_null(), "failed to get list of display modes"); + let array_count = CFArrayGetCount(array); + let modes: Vec<_> = (0..array_count) + .into_iter() + .map(move |i| { + let mode = CFArrayGetValueAtIndex(array, i) as *mut _; + ffi::CGDisplayModeRetain(mode); + mode + }) + .collect(); + CFRelease(array as *const _); + modes + }; + + modes.into_iter().map(move |mode| { + let cg_refresh_rate = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64; // CGDisplayModeGetRefreshRate returns 0.0 for any display that // isn't a CRT @@ -131,34 +241,55 @@ impl MonitorHandle { cv_refresh_rate }; - VideoMode { - size: (mode.width() as u32, mode.height() as u32), + let pixel_encoding = + CFString::wrap_under_create_rule(ffi::CGDisplayModeCopyPixelEncoding(mode)) + .to_string(); + let bit_depth = if pixel_encoding.eq_ignore_ascii_case(ffi::IO32BitDirectPixels) { + 32 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) { + 16 + } else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) { + 30 + } else { + unimplemented!() + }; + + let video_mode = VideoMode { + size: ( + ffi::CGDisplayModeGetPixelWidth(mode) as u32, + ffi::CGDisplayModeGetPixelHeight(mode) as u32, + ), refresh_rate: refresh_rate as u16, - bit_depth: mode.bit_depth() as u16, - } + bit_depth, + monitor: monitor.clone(), + native_mode: NativeDisplayMode(mode), + }; + + RootVideoMode { video_mode } }) + } } pub(crate) fn ns_screen(&self) -> Option { unsafe { - let native_id = self.native_identifier(); + let uuid = ffi::CGDisplayCreateUUIDFromDisplayID(self.0); let screens = NSScreen::screens(nil); let count: NSUInteger = msg_send![screens, count]; let key = IdRef::new(NSString::alloc(nil).init_str("NSScreenNumber")); - let mut matching_screen: Option = None; for i in 0..count { let screen = msg_send![screens, objectAtIndex: i as NSUInteger]; let device_description = NSScreen::deviceDescription(screen); let value: id = msg_send![device_description, objectForKey:*key]; if value != nil { - let screen_number: NSUInteger = msg_send![value, unsignedIntegerValue]; - if screen_number as u32 == native_id { - matching_screen = Some(screen); - break; + let other_native_id: NSUInteger = msg_send![value, unsignedIntegerValue]; + let other_uuid = + ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID); + if uuid == other_uuid { + return Some(screen); } } } - matching_screen + None } } } diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs index db7a61d7..661ec82e 100644 --- a/src/platform_impl/macos/observer.rs +++ b/src/platform_impl/macos/observer.rs @@ -1,6 +1,6 @@ use std::{self, os::raw::*, ptr, time::Instant}; -use crate::platform_impl::platform::app_state::AppState; +use crate::platform_impl::platform::{app_state::AppState, ffi}; #[link(name = "CoreFoundation", kind = "framework")] extern "C" { @@ -13,7 +13,7 @@ extern "C" { pub fn CFRunLoopObserverCreate( allocator: CFAllocatorRef, activities: CFOptionFlags, - repeats: Boolean, + repeats: ffi::Boolean, order: CFIndex, callout: CFRunLoopObserverCallBack, context: *mut CFRunLoopObserverContext, @@ -51,11 +51,6 @@ extern "C" { pub fn CFRelease(cftype: *const c_void); } -pub type Boolean = u8; -#[allow(dead_code)] -const FALSE: Boolean = 0; -const TRUE: Boolean = 1; - pub enum CFAllocator {} pub type CFAllocatorRef = *mut CFAllocator; pub enum CFRunLoop {} @@ -102,7 +97,7 @@ pub struct CFRunLoopSourceContext { pub retain: extern "C" fn(*const c_void) -> *const c_void, pub release: extern "C" fn(*const c_void), pub copyDescription: extern "C" fn(*const c_void) -> CFStringRef, - pub equal: extern "C" fn(*const c_void, *const c_void) -> Boolean, + pub equal: extern "C" fn(*const c_void, *const c_void) -> ffi::Boolean, pub hash: extern "C" fn(*const c_void) -> CFHashCode, pub schedule: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), pub cancel: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), @@ -162,8 +157,8 @@ impl RunLoop { let observer = CFRunLoopObserverCreate( ptr::null_mut(), flags, - TRUE, // Indicates we want this to run repeatedly - priority, // The lower the value, the sooner this will run + ffi::TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run handler, ptr::null_mut(), ); diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index bb8655fc..d7fe9592 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -206,7 +206,10 @@ extern "C" fn toggle_full_screen_callback(context: *mut c_void) { } } } - + // Window level must be restored from `CGShieldingWindowLevel() + // + 1` back to normal in order for `toggleFullScreen` to do + // anything + context.ns_window.setLevel_(0); context.ns_window.toggleFullScreen_(nil); } Box::from_raw(context_ptr); diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index a99e7f6c..d0b0a557 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -8,6 +8,23 @@ use std::{ }, }; +use crate::{ + dpi::{LogicalPosition, LogicalSize}, + error::{ExternalError, NotSupportedError, OsError as RootOsError}, + icon::Icon, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, + platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS}, + platform_impl::platform::{ + app_state::AppState, + ffi, + monitor::{self, MonitorHandle, VideoMode}, + util::{self, IdRef}, + view::{self, new_view}, + window_delegate::new_delegate, + OsError, + }, + window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWindowId}, +}; use cocoa::{ appkit::{ self, CGFloat, NSApp, NSApplication, NSApplicationActivationPolicy, @@ -17,30 +34,12 @@ use cocoa::{ base::{id, nil}, foundation::{NSAutoreleasePool, NSDictionary, NSPoint, NSRect, NSSize, NSString}, }; -use core_graphics::display::CGDisplay; +use core_graphics::display::{CGDisplay, CGDisplayMode}; use objc::{ declare::ClassDecl, runtime::{Class, Object, Sel, BOOL, NO, YES}, }; -use crate::{ - dpi::{LogicalPosition, LogicalSize}, - error::{ExternalError, NotSupportedError, OsError as RootOsError}, - icon::Icon, - monitor::MonitorHandle as RootMonitorHandle, - platform::macos::{ActivationPolicy, RequestUserAttentionType, WindowExtMacOS}, - platform_impl::platform::{ - app_state::AppState, - ffi, - monitor::{self, MonitorHandle}, - util::{self, IdRef}, - view::{self, new_view}, - window_delegate::new_delegate, - OsError, - }, - window::{CursorIcon, WindowAttributes, WindowId as RootWindowId}, -}; - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Id(pub usize); @@ -119,11 +118,14 @@ fn create_window( unsafe { let pool = NSAutoreleasePool::new(nil); let screen = match attrs.fullscreen { - Some(ref monitor_id) => { - let monitor_screen = monitor_id.inner.ns_screen(); + Some(Fullscreen::Borderless(RootMonitorHandle { inner: ref monitor })) + | Some(Fullscreen::Exclusive(RootVideoMode { + video_mode: VideoMode { ref monitor, .. }, + })) => { + let monitor_screen = monitor.ns_screen(); Some(monitor_screen.unwrap_or(appkit::NSScreen::mainScreen(nil))) } - _ => None, + None => None, }; let frame = match screen { Some(screen) => appkit::NSScreen::frame(screen), @@ -239,12 +241,15 @@ lazy_static! { #[derive(Default)] pub struct SharedState { pub resizable: bool, - pub fullscreen: Option, + pub fullscreen: Option, pub maximized: bool, pub standard_frame: Option, is_simple_fullscreen: bool, pub saved_style: Option, + /// Presentation options saved before entering `set_simple_fullscreen`, and + /// restored upon exiting it save_presentation_opts: Option, + pub saved_desktop_display_mode: Option<(CGDisplay, CGDisplayMode)>, } impl SharedState { @@ -362,16 +367,7 @@ impl UnownedWindow { let delegate = new_delegate(&window, fullscreen.is_some()); // Set fullscreen mode after we setup everything - if let Some(monitor) = fullscreen { - if monitor.inner != window.current_monitor().inner { - // To do this with native fullscreen, we probably need to - // warp the window... while we could use - // `enterFullScreenMode`, they're idiomatically different - // fullscreen modes, so we'd have to support both anyway. - unimplemented!(); - } - window.set_fullscreen(Some(monitor)); - } + window.set_fullscreen(fullscreen); // 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 @@ -601,22 +597,44 @@ impl UnownedWindow { } } + /// This is called when the window is exiting fullscreen, whether by the + /// user clicking on the green fullscreen button or programmatically by + /// `toggleFullScreen:` pub(crate) fn restore_state_from_fullscreen(&self) { - let maximized = { - trace!("Locked shared state in `restore_state_from_fullscreen`"); - let mut shared_state_lock = self.shared_state.lock().unwrap(); + trace!("Locked shared state in `restore_state_from_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); - shared_state_lock.fullscreen = None; + shared_state_lock.fullscreen = None; - let mask = self.saved_style(&mut *shared_state_lock); + let maximized = shared_state_lock.maximized; + let mask = self.saved_style(&mut *shared_state_lock); - self.set_style_mask_async(mask); - shared_state_lock.maximized - }; + drop(shared_state_lock); trace!("Unocked shared state in `restore_state_from_fullscreen`"); + + self.set_style_mask_async(mask); self.set_maximized(maximized); } + fn restore_display_mode(&self) { + trace!("Locked shared state in `restore_display_mode`"); + let shared_state_lock = self.shared_state.lock().unwrap(); + + if let Some(Fullscreen::Exclusive(RootVideoMode { ref video_mode })) = + shared_state_lock.fullscreen + { + unsafe { + ffi::CGRestorePermanentDisplayConfiguration(); + assert_eq!( + ffi::CGDisplayRelease(video_mode.monitor().inner.native_identifier()), + ffi::kCGErrorSuccess + ); + } + } + + trace!("Unlocked shared state in `restore_display_mode`"); + } + #[inline] pub fn set_maximized(&self, maximized: bool) { let is_zoomed = self.is_zoomed(); @@ -634,44 +652,159 @@ impl UnownedWindow { } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { let shared_state_lock = self.shared_state.lock().unwrap(); shared_state_lock.fullscreen.clone() } #[inline] - /// TODO: Right now set_fullscreen do not work on switching monitors - /// in fullscreen mode - pub fn set_fullscreen(&self, monitor: Option) { + pub fn set_fullscreen(&self, fullscreen: Option) { + trace!("Locked shared state in `set_fullscreen`"); let shared_state_lock = self.shared_state.lock().unwrap(); if shared_state_lock.is_simple_fullscreen { + trace!("Unlocked shared state in `set_fullscreen`"); return; } - - let not_fullscreen = { - trace!("Locked shared state in `set_fullscreen`"); - let current = &shared_state_lock.fullscreen; - match (current, monitor) { - (&Some(ref a), Some(ref b)) if a.inner != b.inner => { - // Our best bet is probably to move to the origin of the - // target monitor. - unimplemented!() - } - (&None, None) | (&Some(_), Some(_)) => return, - _ => (), - } + let old_fullscreen = shared_state_lock.fullscreen.clone(); + if fullscreen == old_fullscreen { trace!("Unlocked shared state in `set_fullscreen`"); - current.is_none() - }; + return; + } + trace!("Unlocked shared state in `set_fullscreen`"); + drop(shared_state_lock); - unsafe { - util::toggle_full_screen_async( - *self.ns_window, - *self.ns_view, - not_fullscreen, - Arc::downgrade(&self.shared_state), - ) - }; + // If the fullscreen is on a different monitor, we must move the window + // to that monitor before we toggle fullscreen (as `toggleFullScreen` + // does not take a screen parameter, but uses the current screen) + if let Some(ref fullscreen) = fullscreen { + let new_screen = match fullscreen { + Fullscreen::Borderless(RootMonitorHandle { inner: ref monitor }) => monitor, + Fullscreen::Exclusive(RootVideoMode { + video_mode: VideoMode { ref monitor, .. }, + }) => monitor, + } + .ns_screen() + .unwrap(); + + unsafe { + let old_screen = NSWindow::screen(*self.ns_window); + if old_screen != new_screen { + let mut screen_frame: NSRect = msg_send![new_screen, frame]; + // The coordinate system here has its origin at bottom-left + // and Y goes up + screen_frame.origin.y += screen_frame.size.height; + util::set_frame_top_left_point_async(*self.ns_window, screen_frame.origin); + } + } + } + + if let Some(Fullscreen::Exclusive(ref video_mode)) = fullscreen { + // Note: `enterFullScreenMode:withOptions:` seems to do the exact + // same thing as we're doing here (captures the display, sets the + // video mode, and hides the menu bar and dock), with the exception + // of that I couldn't figure out how to set the display mode with + // it. I think `enterFullScreenMode:withOptions:` is still using the + // older display mode API where display modes were of the type + // `CFDictionary`, but this has changed, so we can't obtain the + // correct parameter for this any longer. Apple's code samples for + // this function seem to just pass in "YES" for the display mode + // parameter, which is not consistent with the docs saying that it + // takes a `NSDictionary`.. + + let display_id = video_mode.monitor().inner.native_identifier(); + + let mut fade_token = ffi::kCGDisplayFadeReservationInvalidToken; + + unsafe { + // Fade to black (and wait for the fade to complete) to hide the + // flicker from capturing the display and switching display mode + if ffi::CGAcquireDisplayFadeReservation(5.0, &mut fade_token) + == ffi::kCGErrorSuccess + { + ffi::CGDisplayFade( + fade_token, + 0.3, + ffi::kCGDisplayBlendNormal, + ffi::kCGDisplayBlendSolidColor, + 0.0, + 0.0, + 0.0, + ffi::TRUE, + ); + } + + assert_eq!(ffi::CGDisplayCapture(display_id), ffi::kCGErrorSuccess); + } + + unsafe { + let result = ffi::CGDisplaySetDisplayMode( + display_id, + video_mode.video_mode.native_mode.0, + std::ptr::null(), + ); + assert!(result == ffi::kCGErrorSuccess, "failed to set video mode"); + + // After the display has been configured, fade back in + // asynchronously + if fade_token != ffi::kCGDisplayFadeReservationInvalidToken { + ffi::CGDisplayFade( + fade_token, + 0.6, + ffi::kCGDisplayBlendSolidColor, + ffi::kCGDisplayBlendNormal, + 0.0, + 0.0, + 0.0, + ffi::FALSE, + ); + ffi::CGReleaseDisplayFadeReservation(fade_token); + } + } + } + + match (&old_fullscreen, &fullscreen) { + (&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => unsafe { + // If we're already in fullscreen mode, calling + // `CGDisplayCapture` will place the shielding window on top of + // our window, which results in a black display and is not what + // we want. So, we must place our window on top of the shielding + // window. Unfortunately, this also makes our window be on top + // of the menu bar, and this looks broken, so we must make sure + // that the menu bar is disabled. This is done in the window + // delegate in `window:willUseFullScreenPresentationOptions:`. + msg_send![*self.ns_window, setLevel: ffi::CGShieldingWindowLevel() + 1]; + }, + (&Some(Fullscreen::Exclusive(_)), &None) => unsafe { + self.restore_display_mode(); + + util::toggle_full_screen_async( + *self.ns_window, + *self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + self.restore_display_mode(); + } + (&None, &Some(Fullscreen::Exclusive(_))) + | (&None, &Some(Fullscreen::Borderless(_))) + | (&Some(Fullscreen::Borderless(_)), &None) => unsafe { + // Wish it were this simple for all cases + util::toggle_full_screen_async( + *self.ns_window, + *self.ns_view, + old_fullscreen.is_none(), + Arc::downgrade(&self.shared_state), + ); + }, + _ => (), + } + + trace!("Locked shared state in `set_fullscreen`"); + let mut shared_state_lock = self.shared_state.lock().unwrap(); + shared_state_lock.fullscreen = fullscreen.clone(); + trace!("Unlocked shared state in `set_fullscreen`"); } #[inline] diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs index c9680d89..9bce9ce7 100644 --- a/src/platform_impl/macos/window_delegate.rs +++ b/src/platform_impl/macos/window_delegate.rs @@ -5,9 +5,9 @@ use std::{ }; use cocoa::{ - appkit::{self, NSView, NSWindow}, + appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow}, base::{id, nil}, - foundation::NSAutoreleasePool, + foundation::{NSAutoreleasePool, NSUInteger}, }; use objc::{ declare::ClassDecl, @@ -22,7 +22,7 @@ use crate::{ util::{self, IdRef}, window::{get_window_id, UnownedWindow}, }, - window::WindowId, + window::{Fullscreen, WindowId}, }; pub struct WindowDelegateState { @@ -182,6 +182,11 @@ lazy_static! { dragging_exited as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(window:willUseFullScreenPresentationOptions:), + window_will_use_fullscreen_presentation_options + as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + ); decl.add_method( sel!(windowDidEnterFullScreen:), window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id), @@ -408,6 +413,26 @@ extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Completed `windowWillEnterFullscreen:`"); } +extern "C" fn window_will_use_fullscreen_presentation_options( + _this: &Object, + _: Sel, + _: id, + _proposed_options: NSUInteger, +) -> NSUInteger { + // Generally, games will want to disable the menu bar and the dock. Ideally, + // this would be configurable by the user. Unfortunately because of our + // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is + // placed on top of the menu bar in exclusive fullscreen mode. This looks + // broken so we always disable the menu bar in exclusive fullscreen. We may + // still want to make this configurable for borderless fullscreen. Right now + // we don't, for consistency. If we do, it should be documented that the + // user-provided options are ignored in exclusive fullscreen. + (NSApplicationPresentationOptions::NSApplicationPresentationFullScreen + | NSApplicationPresentationOptions::NSApplicationPresentationHideDock + | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar) + .bits() +} + /// Invoked when entered fullscreen extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { trace!("Triggered `windowDidEnterFullscreen:`"); @@ -415,8 +440,21 @@ extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { state.with_window(|window| { let monitor = window.current_monitor(); trace!("Locked shared state in `window_did_enter_fullscreen`"); - window.shared_state.lock().unwrap().fullscreen = Some(monitor); - trace!("Unlocked shared state in `window_will_enter_fullscreen`"); + let mut shared_state = window.shared_state.lock().unwrap(); + match shared_state.fullscreen { + // Exclusive mode sets the state in `set_fullscreen` as the user + // can't enter exclusive mode by other means (like the + // fullscreen button on the window decorations) + Some(Fullscreen::Exclusive(_)) => (), + // `window_did_enter_fullscreen` was triggered and we're already + // in fullscreen, so we must've reached here by `set_fullscreen` + // as it updates the state + Some(Fullscreen::Borderless(_)) => (), + // Otherwise, we must've reached fullscreen by the user clicking + // on the green fullscreen button. Update state! + None => shared_state.fullscreen = Some(Fullscreen::Borderless(monitor)), + } + trace!("Unlocked shared state in `window_did_enter_fullscreen`"); }); state.initial_fullscreen = false; }); diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 6216fa29..34e9327d 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -4,7 +4,7 @@ use winapi::{self, shared::windef::HWND}; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, - monitor::MonitorHandle, + monitor::{MonitorHandle, VideoMode}, window::Window, }; diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index addb0175..cc2e1646 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -3,54 +3,64 @@ use winapi::{ minwindef::{BOOL, DWORD, LPARAM, TRUE, WORD}, windef::{HDC, HMONITOR, HWND, LPRECT, POINT}, }, - um::{wingdi, winnt::LONG, winuser}, + um::{wingdi, winuser}, }; use std::{ - collections::{HashSet, VecDeque}, + collections::{BTreeSet, VecDeque}, io, mem, ptr, }; use super::{util, EventLoop}; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, - monitor::VideoMode, + monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, platform_impl::platform::{ dpi::{dpi_to_scale_factor, get_monitor_dpi}, window::Window, }, }; -/// Win32 implementation of the main `MonitorHandle` object. #[derive(Derivative)] -#[derivative(Debug, Clone)] -pub struct MonitorHandle { - /// Monitor handle. - hmonitor: HMonitor, - #[derivative(Debug = "ignore")] - monitor_info: winuser::MONITORINFOEXW, - /// The system name of the monitor. - monitor_name: String, - /// True if this is the primary monitor. - primary: bool, - /// The position of the monitor in pixels on the desktop. - /// - /// A window that is positioned at these coordinates will overlap the monitor. - position: (i32, i32), - /// The current resolution in pixels on the monitor. - dimensions: (u32, u32), - /// DPI scale factor. - hidpi_factor: f64, +#[derivative(Debug, Clone, Eq, PartialEq, Hash)] +pub struct VideoMode { + pub(crate) size: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, + pub(crate) monitor: MonitorHandle, + #[derivative(Debug = "ignore", PartialEq = "ignore", Hash = "ignore")] + pub(crate) native_video_mode: wingdi::DEVMODEW, } +impl VideoMode { + pub fn size(&self) -> PhysicalSize { + self.size.into() + } + + pub fn bit_depth(&self) -> u16 { + self.bit_depth + } + + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } + + pub fn monitor(&self) -> RootMonitorHandle { + RootMonitorHandle { + inner: self.monitor.clone(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] +pub struct MonitorHandle(HMONITOR); + // Send is not implemented for HMONITOR, we have to wrap it and implement it manually. // For more info see: // https://github.com/retep998/winapi-rs/issues/360 // https://github.com/retep998/winapi-rs/issues/396 -#[derive(Debug, Clone)] -struct HMonitor(HMONITOR); -unsafe impl Send for HMonitor {} +unsafe impl Send for MonitorHandle {} unsafe extern "system" fn monitor_enum_proc( hmonitor: HMONITOR, @@ -59,7 +69,7 @@ unsafe extern "system" fn monitor_enum_proc( data: LPARAM, ) -> BOOL { let monitors = data as *mut VecDeque; - (*monitors).push_back(MonitorHandle::from_hmonitor(hmonitor)); + (*monitors).push_back(MonitorHandle::new(hmonitor)); TRUE // continue enumeration } @@ -79,12 +89,12 @@ pub fn available_monitors() -> VecDeque { pub fn primary_monitor() -> MonitorHandle { const ORIGIN: POINT = POINT { x: 0, y: 0 }; let hmonitor = unsafe { winuser::MonitorFromPoint(ORIGIN, winuser::MONITOR_DEFAULTTOPRIMARY) }; - MonitorHandle::from_hmonitor(hmonitor) + MonitorHandle::new(hmonitor) } pub fn current_monitor(hwnd: HWND) -> MonitorHandle { let hmonitor = unsafe { winuser::MonitorFromWindow(hwnd, winuser::MONITOR_DEFAULTTONEAREST) }; - MonitorHandle::from_hmonitor(hmonitor) + MonitorHandle::new(hmonitor) } impl EventLoop { @@ -125,73 +135,69 @@ pub(crate) fn get_monitor_info(hmonitor: HMONITOR) -> Result Self { - let monitor_info = get_monitor_info(hmonitor).expect("`GetMonitorInfoW` failed"); - let place = monitor_info.rcMonitor; - let dimensions = ( - (place.right - place.left) as u32, - (place.bottom - place.top) as u32, - ); - MonitorHandle { - hmonitor: HMonitor(hmonitor), - monitor_name: util::wchar_ptr_to_string(monitor_info.szDevice.as_ptr()), - primary: util::has_flag(monitor_info.dwFlags, winuser::MONITORINFOF_PRIMARY), - position: (place.left as i32, place.top as i32), - dimensions, - hidpi_factor: dpi_to_scale_factor(get_monitor_dpi(hmonitor).unwrap_or(96)), - monitor_info, - } + pub(crate) fn new(hmonitor: HMONITOR) -> Self { + MonitorHandle(hmonitor) } pub(crate) fn contains_point(&self, point: &POINT) -> bool { - let left = self.position.0 as LONG; - let right = left + self.dimensions.0 as LONG; - let top = self.position.1 as LONG; - let bottom = top + self.dimensions.1 as LONG; - point.x >= left && point.x <= right && point.y >= top && point.y <= bottom + let monitor_info = get_monitor_info(self.0).unwrap(); + point.x >= monitor_info.rcMonitor.left + && point.x <= monitor_info.rcMonitor.right + && point.y >= monitor_info.rcMonitor.top + && point.y <= monitor_info.rcMonitor.bottom } #[inline] pub fn name(&self) -> Option { - Some(self.monitor_name.clone()) + let monitor_info = get_monitor_info(self.0).unwrap(); + Some(util::wchar_ptr_to_string(monitor_info.szDevice.as_ptr())) } #[inline] pub fn native_identifier(&self) -> String { - self.monitor_name.clone() + self.name().unwrap() } #[inline] pub fn hmonitor(&self) -> HMONITOR { - self.hmonitor.0 + self.0 } #[inline] pub fn size(&self) -> PhysicalSize { - self.dimensions.into() + let monitor_info = get_monitor_info(self.0).unwrap(); + PhysicalSize { + width: (monitor_info.rcMonitor.right - monitor_info.rcMonitor.left) as f64, + height: (monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top) as f64, + } } #[inline] pub fn position(&self) -> PhysicalPosition { - self.position.into() + let monitor_info = get_monitor_info(self.0).unwrap(); + PhysicalPosition { + x: monitor_info.rcMonitor.left as f64, + y: monitor_info.rcMonitor.top as f64, + } } #[inline] pub fn hidpi_factor(&self) -> f64 { - self.hidpi_factor + dpi_to_scale_factor(get_monitor_dpi(self.0).unwrap_or(96)) } #[inline] - pub fn video_modes(&self) -> impl Iterator { + pub fn video_modes(&self) -> impl Iterator { // EnumDisplaySettingsExW can return duplicate values (or some of the // fields are probably changing, but we aren't looking at those fields - // anyway), so we're using a HashSet deduplicate - let mut modes = HashSet::new(); + // anyway), so we're using a BTreeSet deduplicate + let mut modes = BTreeSet::new(); let mut i = 0; loop { unsafe { - let device_name = self.monitor_info.szDevice.as_ptr(); + let monitor_info = get_monitor_info(self.0).unwrap(); + let device_name = monitor_info.szDevice.as_ptr(); let mut mode: wingdi::DEVMODEW = mem::zeroed(); mode.dmSize = mem::size_of_val(&mode) as WORD; if winuser::EnumDisplaySettingsExW(device_name, i, &mut mode, 0) == 0 { @@ -205,10 +211,14 @@ impl MonitorHandle { | wingdi::DM_DISPLAYFREQUENCY; assert!(mode.dmFields & REQUIRED_FIELDS == REQUIRED_FIELDS); - modes.insert(VideoMode { - size: (mode.dmPelsWidth, mode.dmPelsHeight), - bit_depth: mode.dmBitsPerPel as u16, - refresh_rate: mode.dmDisplayFrequency as u16, + modes.insert(RootVideoMode { + video_mode: VideoMode { + size: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate: mode.dmDisplayFrequency as u16, + monitor: self.clone(), + native_video_mode: mode, + }, }); } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 577ecfd7..39f6b478 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -46,7 +46,7 @@ use crate::{ window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorIcon, Icon, WindowAttributes}, + window::{CursorIcon, Fullscreen, Icon, WindowAttributes}, }; /// The Win32 implementation of the main `Window` object. @@ -327,7 +327,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::RESIZABLE, resizable) }); }); @@ -421,80 +421,177 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::MAXIMIZED, maximized) }); }); } #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { let window_state = self.window_state.lock(); window_state.fullscreen.clone() } #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - unsafe { - let window = self.window.clone(); - let window_state = Arc::clone(&self.window_state); + pub fn set_fullscreen(&self, fullscreen: Option) { + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); - match &monitor { - &Some(RootMonitorHandle { ref inner }) => { - let (x, y): (i32, i32) = inner.position().into(); - let (width, height): (u32, u32) = inner.size().into(); + let mut window_state_lock = window_state.lock(); + let old_fullscreen = window_state_lock.fullscreen.clone(); + if window_state_lock.fullscreen == fullscreen { + return; + } + window_state_lock.fullscreen = fullscreen.clone(); + drop(window_state_lock); - let mut monitor = monitor.clone(); - self.thread_executor.execute_in_thread(move || { - let mut window_state_lock = window_state.lock(); + self.thread_executor.execute_in_thread(move || { + let mut window_state_lock = window_state.lock(); - let client_rect = - util::get_client_rect(window.0).expect("get client rect failed!"); - window_state_lock.saved_window = Some(SavedWindow { - client_rect, - dpi_factor: window_state_lock.dpi_factor, - }); - - window_state_lock.fullscreen = monitor.take(); - WindowState::refresh_window_state( - window_state_lock, - window.0, - Some(RECT { - left: x, - top: y, - right: x + width as c_int, - bottom: y + height as c_int, - }), - ); - - mark_fullscreen(window.0, true); + // Save window bounds before entering fullscreen + match (&old_fullscreen, &fullscreen) { + (&None, &Some(_)) => { + let client_rect = util::get_client_rect(window.0).unwrap(); + window_state_lock.saved_window = Some(SavedWindow { + client_rect, + dpi_factor: window_state_lock.dpi_factor, }); } - &None => { - self.thread_executor.execute_in_thread(move || { - let mut window_state_lock = window_state.lock(); - window_state_lock.fullscreen = None; + _ => (), + } - if let Some(SavedWindow { - client_rect, - dpi_factor, - }) = window_state_lock.saved_window - { - window_state_lock.dpi_factor = dpi_factor; - window_state_lock.saved_window = None; + // Change video mode if we're transitioning to or from exclusive + // fullscreen + match (&old_fullscreen, &fullscreen) { + (&None, &Some(Fullscreen::Exclusive(ref video_mode))) + | ( + &Some(Fullscreen::Borderless(_)), + &Some(Fullscreen::Exclusive(ref video_mode)), + ) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Exclusive(ref video_mode))) => + { + let monitor = video_mode.monitor(); - WindowState::refresh_window_state( - window_state_lock, + let mut display_name = OsStr::new(&monitor.inner.native_identifier()) + .encode_wide() + .collect::>(); + // `encode_wide` does not add a null-terminator but + // `ChangeDisplaySettingsExW` requires a null-terminated + // string, so add it + display_name.push(0); + + let mut native_video_mode = video_mode.video_mode.native_video_mode.clone(); + + let res = unsafe { + winuser::ChangeDisplaySettingsExW( + display_name.as_ptr(), + &mut native_video_mode, + std::ptr::null_mut(), + winuser::CDS_FULLSCREEN, + std::ptr::null_mut(), + ) + }; + + debug_assert!(res != winuser::DISP_CHANGE_BADFLAGS); + debug_assert!(res != winuser::DISP_CHANGE_BADMODE); + debug_assert!(res != winuser::DISP_CHANGE_BADPARAM); + debug_assert!(res != winuser::DISP_CHANGE_FAILED); + assert_eq!(res, winuser::DISP_CHANGE_SUCCESSFUL); + } + (&Some(Fullscreen::Exclusive(_)), &None) + | (&Some(Fullscreen::Exclusive(_)), &Some(Fullscreen::Borderless(_))) => { + let res = unsafe { + winuser::ChangeDisplaySettingsExW( + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + winuser::CDS_FULLSCREEN, + std::ptr::null_mut(), + ) + }; + + debug_assert!(res != winuser::DISP_CHANGE_BADFLAGS); + debug_assert!(res != winuser::DISP_CHANGE_BADMODE); + debug_assert!(res != winuser::DISP_CHANGE_BADPARAM); + debug_assert!(res != winuser::DISP_CHANGE_FAILED); + assert_eq!(res, winuser::DISP_CHANGE_SUCCESSFUL); + } + _ => (), + } + + unsafe { + // There are some scenarios where calling `ChangeDisplaySettingsExW` takes long + // enough to execute that the DWM thinks our program has frozen and takes over + // our program's window. When that happens, the `SetWindowPos` call below gets + // eaten and the window doesn't get set to the proper fullscreen position. + // + // Calling `PeekMessageW` here notifies Windows that our process is still running + // fine, taking control back from the DWM and ensuring that the `SetWindowPos` call + // below goes through. + let mut msg = mem::zeroed(); + winuser::PeekMessageW(&mut msg, ptr::null_mut(), 0, 0, 0); + } + + // Update window style + WindowState::set_window_flags(window_state_lock, window.0, |f| { + f.set(WindowFlags::MARKER_FULLSCREEN, fullscreen.is_some()) + }); + + // Update window bounds + match &fullscreen { + Some(fullscreen) => { + let monitor = match fullscreen { + Fullscreen::Exclusive(ref video_mode) => video_mode.monitor(), + Fullscreen::Borderless(ref monitor) => monitor.clone(), + }; + + let position: (i32, i32) = monitor.position().into(); + let size: (u32, u32) = monitor.size().into(); + + unsafe { + winuser::SetWindowPos( + window.0, + ptr::null_mut(), + position.0, + position.1, + size.0 as i32, + size.1 as i32, + winuser::SWP_ASYNCWINDOWPOS | winuser::SWP_NOZORDER, + ); + winuser::UpdateWindow(window.0); + } + } + None => { + let mut window_state_lock = window_state.lock(); + if let Some(SavedWindow { + client_rect, + dpi_factor, + }) = window_state_lock.saved_window.take() + { + window_state_lock.dpi_factor = dpi_factor; + drop(window_state_lock); + + unsafe { + winuser::SetWindowPos( window.0, - Some(client_rect), + ptr::null_mut(), + client_rect.left, + client_rect.top, + client_rect.right - client_rect.left, + client_rect.bottom - client_rect.top, + winuser::SWP_ASYNCWINDOWPOS | winuser::SWP_NOZORDER, ); + winuser::UpdateWindow(window.0); } - - mark_fullscreen(window.0, false); - }); + } } } - } + + unsafe { + taskbar_mark_fullscreen(window.0, fullscreen.is_some()); + } + }); } #[inline] @@ -503,8 +600,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - let client_rect = util::get_client_rect(window.0).expect("get client rect failed!"); - WindowState::set_window_flags(window_state.lock(), window.0, Some(client_rect), |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::DECORATIONS, decorations) }); }); @@ -516,7 +612,7 @@ impl Window { let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { - WindowState::set_window_flags(window_state.lock(), window.0, None, |f| { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { f.set(WindowFlags::ALWAYS_ON_TOP, always_on_top) }); }); @@ -769,9 +865,7 @@ unsafe fn init( let window_state = { let window_state = WindowState::new(&attributes, window_icon, taskbar_icon, dpi_factor); let window_state = Arc::new(Mutex::new(window_state)); - WindowState::set_window_flags(window_state.lock(), real_window.0, None, |f| { - *f = window_flags - }); + WindowState::set_window_flags(window_state.lock(), real_window.0, |f| *f = window_flags); window_state }; @@ -865,7 +959,7 @@ pub fn com_initialized() { // is activated. If the window is not fullscreen, the Shell falls back to // heuristics to determine how the window should be treated, which means // that it could still consider the window as fullscreen. :( -unsafe fn mark_fullscreen(handle: HWND, fullscreen: bool) { +unsafe fn taskbar_mark_fullscreen(handle: HWND, fullscreen: bool) { com_initialized(); TASKBAR_LIST.with(|task_bar_list_ptr| { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 9d494508..5979f777 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -1,8 +1,7 @@ use crate::{ dpi::LogicalSize, - monitor::MonitorHandle, platform_impl::platform::{event_loop, icon::WinIcon, util}, - window::{CursorIcon, WindowAttributes}, + window::{CursorIcon, Fullscreen, WindowAttributes}, }; use parking_lot::MutexGuard; use std::{io, ptr}; @@ -29,7 +28,7 @@ pub struct WindowState { pub saved_window: Option, pub dpi_factor: f64, - pub fullscreen: Option, + pub fullscreen: Option, /// Used to supress duplicate redraw attempts when calling `request_redraw` multiple /// times in `EventsCleared`. pub queued_out_of_band_redraw: bool, @@ -84,6 +83,7 @@ bitflags! { WindowFlags::RESIZABLE.bits | WindowFlags::MAXIMIZED.bits ); + const FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; const INVISIBLE_AND_MASK = !WindowFlags::MAXIMIZED.bits; } @@ -122,32 +122,16 @@ impl WindowState { self.window_flags } - pub fn set_window_flags( - mut this: MutexGuard<'_, Self>, - window: HWND, - set_client_rect: Option, - f: F, - ) where + pub fn set_window_flags(mut this: MutexGuard<'_, Self>, window: HWND, f: F) + where F: FnOnce(&mut WindowFlags), { let old_flags = this.window_flags; f(&mut this.window_flags); - - let is_fullscreen = this.fullscreen.is_some(); - this.window_flags - .set(WindowFlags::MARKER_FULLSCREEN, is_fullscreen); let new_flags = this.window_flags; drop(this); - old_flags.apply_diff(window, new_flags, set_client_rect); - } - - pub fn refresh_window_state( - this: MutexGuard<'_, Self>, - window: HWND, - set_client_rect: Option, - ) { - Self::set_window_flags(this, window, set_client_rect, |_| ()); + old_flags.apply_diff(window, new_flags); } pub fn set_window_flags_in_place(&mut self, f: F) @@ -185,6 +169,7 @@ impl WindowFlags { fn mask(mut self) -> WindowFlags { if self.contains(WindowFlags::MARKER_FULLSCREEN) { self &= WindowFlags::FULLSCREEN_AND_MASK; + self |= WindowFlags::FULLSCREEN_OR_MASK; } if !self.contains(WindowFlags::VISIBLE) { self &= WindowFlags::INVISIBLE_AND_MASK; @@ -236,7 +221,7 @@ impl WindowFlags { } /// Adjust the window client rectangle to the return value, if present. - fn apply_diff(mut self, window: HWND, mut new: WindowFlags, set_client_rect: Option) { + fn apply_diff(mut self, window: HWND, mut new: WindowFlags) { self = self.mask(); new = new.mask(); @@ -295,45 +280,20 @@ impl WindowFlags { winuser::SetWindowLongW(window, winuser::GWL_STYLE, style as _); winuser::SetWindowLongW(window, winuser::GWL_EXSTYLE, style_ex as _); - match set_client_rect - .and_then(|r| util::adjust_window_rect_with_styles(window, style, style_ex, r)) - { - Some(client_rect) => { - let (x, y, w, h) = ( - client_rect.left, - client_rect.top, - client_rect.right - client_rect.left, - client_rect.bottom - client_rect.top, - ); - winuser::SetWindowPos( - window, - ptr::null_mut(), - x, - y, - w, - h, - winuser::SWP_NOZORDER - | winuser::SWP_FRAMECHANGED - | winuser::SWP_NOACTIVATE, - ); - } - None => { - // Refresh the window frame. - winuser::SetWindowPos( - window, - ptr::null_mut(), - 0, - 0, - 0, - 0, - winuser::SWP_NOZORDER - | winuser::SWP_NOMOVE - | winuser::SWP_NOSIZE - | winuser::SWP_FRAMECHANGED - | winuser::SWP_NOACTIVATE, - ); - } + let mut flags = winuser::SWP_NOZORDER + | winuser::SWP_NOMOVE + | winuser::SWP_NOSIZE + | winuser::SWP_FRAMECHANGED; + + // We generally don't want style changes here to affect window + // focus, but for fullscreen windows they must be activated + // (i.e. focused) so that they appear on top of the taskbar + if !new.contains(WindowFlags::MARKER_FULLSCREEN) { + flags |= winuser::SWP_NOACTIVATE; } + + // Refresh the window frame + winuser::SetWindowPos(window, ptr::null_mut(), 0, 0, 0, 0, flags); winuser::SendMessageW(window, *event_loop::SET_RETAIN_STATE_ON_SIZE_MSG_ID, 0, 0); } } diff --git a/src/window.rs b/src/window.rs index e095e38d..f983d791 100644 --- a/src/window.rs +++ b/src/window.rs @@ -5,7 +5,7 @@ use crate::{ dpi::{LogicalPosition, LogicalSize}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, - monitor::{AvailableMonitorsIter, MonitorHandle}, + monitor::{AvailableMonitorsIter, MonitorHandle, VideoMode}, platform_impl, }; @@ -45,6 +45,18 @@ impl fmt::Debug for Window { } } +impl Drop for Window { + fn drop(&mut self) { + // If the window is in exclusive fullscreen, we must restore the desktop + // video mode (generally this would be done on application exit, but + // closing the window doesn't necessarily always mean application exit, + // such as when there are multiple windows) + if let Some(Fullscreen::Exclusive(_)) = self.fullscreen() { + self.set_fullscreen(None); + } + } +} + /// Identifier of a window. Unique for each window. /// /// Can be obtained with `window.id()`. @@ -110,7 +122,7 @@ pub struct WindowAttributes { /// Whether the window should be set as fullscreen upon creation. /// /// The default is `None`. - pub fullscreen: Option, + pub fullscreen: Option, /// The title of the window in the title bar. /// @@ -222,14 +234,14 @@ impl WindowBuilder { self } - /// Sets the window fullscreen state. None means a normal window, Some(MonitorHandle) + /// Sets the window fullscreen state. None means a normal window, Some(Fullscreen) /// means a fullscreen window on that specific monitor /// /// ## Platform-specific /// /// - **Windows:** Screen saver is disabled in fullscreen mode. #[inline] - pub fn with_fullscreen(mut self, monitor: Option) -> WindowBuilder { + pub fn with_fullscreen(mut self, monitor: Option) -> WindowBuilder { self.window.fullscreen = monitor; self } @@ -295,7 +307,6 @@ impl WindowBuilder { self, window_target: &EventLoopWindowTarget, ) -> Result { - // building platform_impl::Window::new(&window_target.p, self.window, self.platform_specific) .map(|window| Window { window }) } @@ -537,11 +548,27 @@ impl Window { /// /// ## Platform-specific /// + /// - **macOS:** `Fullscreen::Exclusive` provides true exclusive mode with a + /// video mode change. *Caveat!* macOS doesn't provide task switching (or + /// spaces!) while in exclusive fullscreen mode. This mode should be used + /// when a video mode change is desired, but for a better user experience, + /// borderless fullscreen might be preferred. + /// + /// `Fullscreen::Borderless` provides a borderless fullscreen window on a + /// separate space. This is the idiomatic way for fullscreen games to work + /// on macOS. See [`WindowExtMacOs::set_simple_fullscreen`][simple] if + /// separate spaces are not preferred. + /// + /// The dock and the menu bar are always disabled in fullscreen mode. /// - **iOS:** Can only be called on the main thread. + /// - **Wayland:** Does not support exclusive fullscreen mode. /// - **Windows:** Screen saver is disabled in fullscreen mode. + /// + /// [simple]: + /// ../platform/macos/trait.WindowExtMacOS.html#tymethod.set_simple_fullscreen #[inline] - pub fn set_fullscreen(&self, monitor: Option) { - self.window.set_fullscreen(monitor) + pub fn set_fullscreen(&self, fullscreen: Option) { + self.window.set_fullscreen(fullscreen) } /// Gets the window's current fullscreen state. @@ -550,7 +577,7 @@ impl Window { /// /// - **iOS:** Can only be called on the main thread. #[inline] - pub fn fullscreen(&self) -> Option { + pub fn fullscreen(&self) -> Option { self.window.fullscreen() } @@ -759,3 +786,9 @@ impl Default for CursorIcon { CursorIcon::Default } } + +#[derive(Clone, Debug, PartialEq)] +pub enum Fullscreen { + Exclusive(VideoMode), + Borderless(MonitorHandle), +}