diff --git a/CHANGELOG.md b/CHANGELOG.md index 15832d210b..14acb6cfb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - Rename several functions to improve both internal consistency and compliance with Rust API guidelines. - Remove `WindowBuilder::multitouch` field, since it was only implemented on a few platforms. Multitouch is always enabled now. - **Breaking:** On macOS, change `ns` identifiers to use snake_case for consistency with iOS's `ui` identifiers. +- Add `MonitorHandle::video_modes` method for retrieving supported video modes for the given monitor. # Version 0.19.1 (2019-04-08) diff --git a/Cargo.toml b/Cargo.toml index 0599ec6927..ea087739d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ lazy_static = "1" libc = "0.2" log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } +derivative = "1.0.2" [dev-dependencies] image = "0.21" @@ -33,6 +34,7 @@ objc = "0.2.3" cocoa = "0.18.4" core-foundation = "0.6" core-graphics = "0.17.3" +core-video-sys = "0.1.2" dispatch = "0.1.4" objc = "0.2.3" diff --git a/FEATURES.md b/FEATURES.md index 515ff5979d..769fb9481a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -92,6 +92,7 @@ If your PR makes notable changes to Winit's features, please update this section ### System Information - **Monitor list**: Retrieve the list of monitors and their metadata, including which one is primary. +- **Video mode query**: Monitors can be queried for their supported fullscreen video modes (consisting of resolution, refresh rate, and bit depth). ### Input Handling - **Mouse events**: Generating mouse events associated with pointer motion, click, and scrolling events. @@ -160,9 +161,10 @@ Legend: |Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |❌ | ### System information -|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| -|------------ | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | +|Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| +|---------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | +|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A** | +|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |❌ | ### Input handling |Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |Emscripten| diff --git a/examples/video_modes.rs b/examples/video_modes.rs new file mode 100644 index 0000000000..535dc19587 --- /dev/null +++ b/examples/video_modes.rs @@ -0,0 +1,14 @@ +extern crate winit; + +use winit::event_loop::EventLoop; + +fn main() { + let event_loop = EventLoop::new(); + let monitor = event_loop.primary_monitor(); + + println!("Listing available video modes:"); + + for mode in monitor.video_modes() { + println!("{:?}", mode); + } +} diff --git a/src/lib.rs b/src/lib.rs index 347f8e1cf2..bfacb19fe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,8 @@ extern crate log; #[cfg(feature = "serde")] #[macro_use] extern crate serde; +#[macro_use] +extern crate derivative; #[cfg(target_os = "windows")] extern crate winapi; @@ -101,6 +103,8 @@ extern crate dispatch; extern crate core_foundation; #[cfg(target_os = "macos")] extern crate core_graphics; +#[cfg(target_os = "macos")] +extern crate core_video_sys; #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] extern crate x11_dl; #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "windows"))] diff --git a/src/monitor.rs b/src/monitor.rs index 8fab7ca44c..c70cfb8d32 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -44,6 +44,45 @@ impl Iterator for AvailableMonitorsIter { } } +/// Describes a fullscreen video mode of a monitor. +/// +/// Can be acquired with: +/// - [`MonitorHandle::video_modes`][monitor_get]. +/// +/// [monitor_get]: ../monitor/struct.MonitorHandle.html#method.video_modes +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct VideoMode { + pub(crate) dimensions: (u32, u32), + pub(crate) bit_depth: u16, + pub(crate) refresh_rate: u16, +} + +impl VideoMode { + /// Returns the resolution of this video mode. + pub fn dimensions(&self) -> PhysicalSize { + self.dimensions.into() + } + + /// Returns the bit depth of this video mode, as in how many bits you have + /// available per color. This is generally 24 bits or 32 bits on modern + /// systems, depending on whether the alpha channel is counted or not. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Always returns 32. + /// - **iOS:** Always returns 32. + pub fn bit_depth(&self) -> u16 { + self.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. + pub fn refresh_rate(&self) -> u16 { + self.refresh_rate + } +} + /// Handle to a monitor. /// /// Allows you to retrieve information about a given monitor and can be used in [`Window`] creation. @@ -88,4 +127,10 @@ impl MonitorHandle { pub fn hidpi_factor(&self) -> f64 { self.inner.hidpi_factor() } + + /// Returns all fullscreen video modes supported by this monitor. + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.inner.video_modes() + } } diff --git a/src/platform_impl/ios/monitor.rs b/src/platform_impl/ios/monitor.rs index 14e7d3790c..3bfa0f32e5 100644 --- a/src/platform_impl/ios/monitor.rs +++ b/src/platform_impl/ios/monitor.rs @@ -1,18 +1,13 @@ use std::{ - collections::VecDeque, + collections::{HashSet, VecDeque}, fmt, ops::{Deref, DerefMut}, }; use dpi::{PhysicalPosition, PhysicalSize}; +use monitor::VideoMode; -use platform_impl::platform::ffi::{ - id, - nil, - CGFloat, - CGRect, - NSUInteger, -}; +use platform_impl::platform::ffi::{id, nil, CGFloat, CGRect, CGSize, NSInteger, NSUInteger}; pub struct Inner { uiscreen: id, @@ -134,6 +129,27 @@ impl Inner { scale as f64 } } + + 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); + + 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 { + dimensions: (size.width as u32, size.height as u32), + bit_depth: 32, + refresh_rate: refresh_rate as u16, + }); + } + + modes.into_iter() + } } // MonitorHandleExtIOS diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 3b316a25d8..ca0b067000 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -14,7 +14,7 @@ use icon::Icon; use error::{ExternalError, NotSupportedError, OsError as RootOsError}; use event::Event; use event_loop::{EventLoopClosed, ControlFlow, EventLoopWindowTarget as RootELW}; -use monitor::MonitorHandle as RootMonitorHandle; +use monitor::{MonitorHandle as RootMonitorHandle, VideoMode}; use window::{WindowAttributes, CursorIcon}; use self::x11::{XConnection, XError}; use self::x11::ffi::XVisualInfo; @@ -142,6 +142,14 @@ impl MonitorHandle { &MonitorHandle::Wayland(ref m) => m.hidpi_factor() as f64, } } + + #[inline] + pub fn video_modes(&self) -> Box> { + match self { + MonitorHandle::X(m) => Box::new(m.video_modes()), + MonitorHandle::Wayland(m) => Box::new(m.video_modes()), + } + } } impl Window { diff --git a/src/platform_impl/linux/wayland/event_loop.rs b/src/platform_impl/linux/wayland/event_loop.rs index c73a225f3a..03d2c24051 100644 --- a/src/platform_impl/linux/wayland/event_loop.rs +++ b/src/platform_impl/linux/wayland/event_loop.rs @@ -9,6 +9,7 @@ use event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW} use event::ModifiersState; use dpi::{PhysicalPosition, PhysicalSize}; use platform_impl::platform::sticky_exit_callback; +use monitor::VideoMode; use super::window::WindowStore; use super::WindowId; @@ -584,6 +585,20 @@ impl MonitorHandle { .with_info(&self.proxy, |_, info| info.scale_factor) .unwrap_or(1) } + + #[inline] + pub fn video_modes(&self) -> impl Iterator + { + self.mgr + .with_info(&self.proxy, |_, info| info.modes.clone()) + .unwrap_or(vec![]) + .into_iter() + .map(|x| VideoMode { + dimensions: (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 + }) + } } pub fn primary_monitor(outputs: &OutputMgr) -> MonitorHandle { diff --git a/src/platform_impl/linux/x11/monitor.rs b/src/platform_impl/linux/x11/monitor.rs index 638ebce18d..716ddaaa41 100644 --- a/src/platform_impl/linux/x11/monitor.rs +++ b/src/platform_impl/linux/x11/monitor.rs @@ -3,6 +3,7 @@ use std::os::raw::*; use parking_lot::Mutex; use dpi::{PhysicalPosition, PhysicalSize}; +use monitor::VideoMode; use super::{util, XConnection, XError}; use super::ffi::{ RRCrtcChangeNotifyMask, @@ -56,6 +57,8 @@ pub struct MonitorHandle { pub(crate) hidpi_factor: f64, /// Used to determine which windows are on this monitor pub(crate) rect: util::AaRect, + /// Supported video modes on this monitor + video_modes: Vec, } impl MonitorHandle { @@ -66,7 +69,7 @@ impl MonitorHandle { repr: util::MonitorRepr, primary: bool, ) -> Option { - let (name, hidpi_factor) = unsafe { xconn.get_output_info(resources, &repr)? }; + let (name, hidpi_factor, video_modes) = unsafe { xconn.get_output_info(resources, &repr)? }; let (dimensions, position) = unsafe { (repr.dimensions(), repr.position()) }; let rect = util::AaRect::new(position, dimensions); Some(MonitorHandle { @@ -77,6 +80,7 @@ impl MonitorHandle { position, primary, rect, + video_modes, }) } @@ -101,6 +105,11 @@ impl MonitorHandle { pub fn hidpi_factor(&self) -> f64 { self.hidpi_factor } + + #[inline] + pub fn video_modes(&self) -> impl Iterator { + self.video_modes.clone().into_iter() + } } impl XConnection { diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 3abbf849c7..80c617b1ef 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,6 +1,7 @@ use std::{env, slice}; use std::str::FromStr; +use monitor::VideoMode; use dpi::validate_hidpi_factor; use super::*; @@ -101,7 +102,7 @@ impl XConnection { &self, resources: *mut ffi::XRRScreenResources, repr: &MonitorRepr, - ) -> Option<(String, f64)> { + ) -> Option<(String, f64, Vec)> { let output_info = (self.xrandr.XRRGetOutputInfo)( self.display, resources, @@ -114,6 +115,33 @@ impl XConnection { let _ = self.check_errors(); // discard `BadRROutput` error return None; } + + let screen = (self.xlib.XDefaultScreen)(self.display); + let bit_depth = (self.xlib.XDefaultDepth)(self.display, screen); + + let output_modes = + slice::from_raw_parts((*output_info).modes, (*output_info).nmode as usize); + let resource_modes = slice::from_raw_parts((*resources).modes, (*resources).nmode as usize); + + let modes = resource_modes + .iter() + // XRROutputInfo contains an array of mode ids that correspond to + // modes in the array in XRRScreenResources + .filter(|x| output_modes.iter().any(|id| x.id == *id)) + .map(|x| { + let refresh_rate = if x.dotClock > 0 && x.hTotal > 0 && x.vTotal > 0 { + x.dotClock as u64 * 1000 / (x.hTotal as u64 * x.vTotal as u64) + } else { + 0 + }; + + VideoMode { + dimensions: (x.width, x.height), + refresh_rate: (refresh_rate as f32 / 1000.0).round() as u16, + bit_depth: bit_depth as u16, + } + }); + let name_slice = slice::from_raw_parts( (*output_info).name as *mut u8, (*output_info).nameLen as usize, @@ -129,6 +157,6 @@ impl XConnection { }; (self.xrandr.XRRFreeOutputInfo)(output_info); - Some((name, hidpi_factor)) + Some((name, hidpi_factor, modes.collect())) } } diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index 8b87a81f85..d9d924ffd2 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -1,9 +1,18 @@ use std::{collections::VecDeque, fmt}; -use cocoa::{appkit::NSScreen, base::{id, nil}, foundation::{NSString, NSUInteger}}; -use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; +use cocoa::{ + appkit::NSScreen, + base::{id, nil}, + foundation::{NSString, NSUInteger}, +}; +use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayMode}; +use core_video_sys::{ + kCVReturnSuccess, kCVTimeIsIndefinite, CVDisplayLinkCreateWithCGDisplay, + CVDisplayLinkGetNominalOutputVideoRefreshPeriod, CVDisplayLinkRelease, +}; use dpi::{PhysicalPosition, PhysicalSize}; +use monitor::VideoMode; use platform_impl::platform::util::IdRef; #[derive(Clone, PartialEq)] @@ -93,6 +102,44 @@ impl MonitorHandle { unsafe { NSScreen::backingScaleFactor(screen) as f64 } } + pub fn video_modes(&self) -> impl Iterator { + let cv_refresh_rate = unsafe { + let mut display_link = std::ptr::null_mut(); + assert_eq!( + CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link), + kCVReturnSuccess + ); + let time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link); + CVDisplayLinkRelease(display_link); + + // This value is indefinite if an invalid display link was specified + assert!(time.flags & kCVTimeIsIndefinite == 0); + + 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; + + // CGDisplayModeGetRefreshRate returns 0.0 for any display that + // isn't a CRT + let refresh_rate = if cg_refresh_rate > 0 { + cg_refresh_rate + } else { + cv_refresh_rate + }; + + VideoMode { + dimensions: (mode.width() as u32, mode.height() as u32), + refresh_rate: refresh_rate as u16, + bit_depth: mode.bit_depth() as u16, + } + }) + } + pub(crate) fn ns_screen(&self) -> Option { unsafe { let native_id = self.native_identifier(); diff --git a/src/platform_impl/windows/monitor.rs b/src/platform_impl/windows/monitor.rs index ccbe581b83..01de9da64a 100644 --- a/src/platform_impl/windows/monitor.rs +++ b/src/platform_impl/windows/monitor.rs @@ -1,21 +1,25 @@ -use winapi::shared::minwindef::{BOOL, DWORD, LPARAM, TRUE}; +use winapi::shared::minwindef::{BOOL, DWORD, LPARAM, TRUE, WORD}; use winapi::shared::windef::{HDC, HMONITOR, HWND, LPRECT, POINT}; use winapi::um::winnt::LONG; -use winapi::um::winuser; +use winapi::um::{wingdi, winuser}; -use std::{mem, ptr, io}; -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; +use std::{io, mem, ptr}; -use super::{EventLoop, util}; +use super::{util, EventLoop}; use dpi::{PhysicalPosition, PhysicalSize}; +use monitor::VideoMode; use platform_impl::platform::dpi::{dpi_to_scale_factor, get_monitor_dpi}; use platform_impl::platform::window::Window; /// Win32 implementation of the main `MonitorHandle` object. -#[derive(Debug, Clone)] +#[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. @@ -130,6 +134,7 @@ impl MonitorHandle { 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, } } @@ -170,4 +175,39 @@ impl MonitorHandle { pub fn hidpi_factor(&self) -> f64 { self.hidpi_factor } + + #[inline] + 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(); + let mut i = 0; + + loop { + unsafe { + let device_name = self.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 { + break; + } + i += 1; + + const REQUIRED_FIELDS: DWORD = wingdi::DM_BITSPERPEL + | wingdi::DM_PELSWIDTH + | wingdi::DM_PELSHEIGHT + | wingdi::DM_DISPLAYFREQUENCY; + assert!(mode.dmFields & REQUIRED_FIELDS == REQUIRED_FIELDS); + + modes.insert(VideoMode { + dimensions: (mode.dmPelsWidth, mode.dmPelsHeight), + bit_depth: mode.dmBitsPerPel as u16, + refresh_rate: mode.dmDisplayFrequency as u16, + }); + } + } + + modes.into_iter() + } }