On Windows and macOS, add API to enable/disable window controls (#2537)

* On Windows and macOS, add API to enable/disable window controls

* fix build

* missing import

* use `WindowButtons` flags

* rename to `[set_]enabled_buttons`

* add example, fix windows impl for minimize

* macOS: Fix button enabling close/minimize while disabling maximized

* Update src/platform_impl/windows/window.rs

Co-authored-by: Kirill Chibisov <contact@kchibisov.com>

* compose the flags on a sep line, use `bool::then`

Co-authored-by: Mads Marquart <mads@marquart.dk>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
This commit is contained in:
Amr Bashir 2022-11-29 12:03:51 +02:00 committed by GitHub
parent 28e34c2e1b
commit 94688a62f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 343 additions and 36 deletions

View file

@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre
# Unreleased
- On Windows and MacOS, add API to enable/disable window buttons (close, minimize, ...etc).
- On Windows, macOS, X11 and Wayland, add `Window::set_theme`.
- **Breaking:** Remove `WindowExtWayland::wayland_set_csd_theme` and `WindowBuilderExtX11::with_gtk_theme_variant`.
- On Windows, revert window background to an empty brush to avoid white flashes when changing scaling.

View file

@ -0,0 +1,68 @@
#![allow(clippy::single_match)]
// This example is used by developers to test various window functions.
use simple_logger::SimpleLogger;
use winit::{
dpi::LogicalSize,
event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent},
event_loop::{DeviceEventFilter, EventLoop},
window::{WindowBuilder, WindowButtons},
};
fn main() {
SimpleLogger::new().init().unwrap();
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title("A fantastic window!")
.with_inner_size(LogicalSize::new(300.0, 300.0))
.build(&event_loop)
.unwrap();
eprintln!("Window Button keys:");
eprintln!(" (F) Toggle close button");
eprintln!(" (G) Toggle maximize button");
eprintln!(" (H) Toggle minimize button");
event_loop.set_device_event_filter(DeviceEventFilter::Never);
event_loop.run(move |event, _, control_flow| {
control_flow.set_wait();
match event {
Event::WindowEvent {
event:
WindowEvent::KeyboardInput {
input:
KeyboardInput {
virtual_keycode: Some(key),
state: ElementState::Pressed,
..
},
..
},
..
} => match key {
VirtualKeyCode::F => {
let buttons = window.enabled_buttons();
window.set_enabled_buttons(buttons ^ WindowButtons::CLOSE);
}
VirtualKeyCode::G => {
let buttons = window.enabled_buttons();
window.set_enabled_buttons(buttons ^ WindowButtons::MAXIMIZE);
}
VirtualKeyCode::H => {
let buttons = window.enabled_buttons();
window.set_enabled_buttons(buttons ^ WindowButtons::MINIMIZE);
}
_ => (),
},
Event::WindowEvent {
event: WindowEvent::CloseRequested,
window_id,
} if window_id == window.id() => control_flow.set_exit(),
_ => (),
}
});
}

View file

@ -24,7 +24,7 @@ use crate::{
error,
event::{self, StartCause, VirtualKeyCode},
event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW},
window::{self, CursorGrabMode, Theme, WindowLevel},
window::{self, CursorGrabMode, Theme, WindowButtons, WindowLevel},
};
fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option<event::VirtualKeyCode> {
@ -959,6 +959,12 @@ impl Window {
false
}
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {}
pub fn enabled_buttons(&self) -> WindowButtons {
WindowButtons::all()
}
pub fn set_minimized(&self, _minimized: bool) {}
pub fn set_maximized(&self, _maximized: bool) {}

View file

@ -23,7 +23,7 @@ use crate::{
monitor, view, EventLoopWindowTarget, Fullscreen, MonitorHandle,
},
window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes,
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
WindowId as RootWindowId, WindowLevel,
},
};
@ -172,6 +172,17 @@ impl Inner {
false
}
#[inline]
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {
warn!("`Window::set_enabled_buttons` is ignored on iOS");
}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
warn!("`Window::enabled_buttons` is ignored on iOS");
WindowButtons::all()
}
pub fn scale_factor(&self) -> f64 {
unsafe {
let hidpi: CGFloat = msg_send![self.view, contentScaleFactor];

View file

@ -39,7 +39,10 @@ use crate::{
ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW,
},
icon::Icon,
window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowLevel},
window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
WindowLevel,
},
};
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
@ -401,6 +404,16 @@ impl Window {
x11_or_wayland!(match self; Window(w) => w.is_resizable())
}
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
x11_or_wayland!(match self; Window(w) => w.set_enabled_buttons(buttons))
}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
x11_or_wayland!(match self; Window(w) => w.enabled_buttons())
}
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
x11_or_wayland!(match self; Window(w) => w.set_cursor_icon(cursor))

View file

@ -18,7 +18,9 @@ use crate::platform_impl::{
Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError,
PlatformSpecificWindowBuilderAttributes as PlatformAttributes,
};
use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes};
use crate::window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
};
use super::env::WindowingFeatures;
use super::event_loop::WinitState;
@ -421,6 +423,14 @@ impl Window {
self.resizeable.load(Ordering::Relaxed)
}
#[inline]
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
WindowButtons::all()
}
#[inline]
pub fn scale_factor(&self) -> u32 {
// The scale factor from `get_surface_scale_factor` is always greater than zero, so

View file

@ -21,7 +21,8 @@ use crate::{
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{
CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes, WindowLevel,
CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes,
WindowButtons, WindowLevel,
},
};
@ -1280,6 +1281,14 @@ impl UnownedWindow {
self.shared_state_lock().is_resizable
}
#[inline]
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
WindowButtons::all()
}
#[inline]
pub fn xlib_display(&self) -> *mut c_void {
self.xconn.display as _

View file

@ -1,5 +1,5 @@
use objc2::foundation::NSObject;
use objc2::{extern_class, ClassType};
use objc2::{extern_class, extern_methods, ClassType};
use super::{NSResponder, NSView};
@ -12,3 +12,13 @@ extern_class!(
type Super = NSView;
}
);
extern_methods!(
unsafe impl NSControl {
#[sel(setEnabled:)]
pub fn setEnabled(&self, enabled: bool);
#[sel(isEnabled)]
pub fn isEnabled(&self) -> bool;
}
);

View file

@ -180,6 +180,12 @@ extern_methods!(
#[sel(isResizable)]
pub fn isResizable(&self) -> bool;
#[sel(isMiniaturizable)]
pub fn isMiniaturizable(&self) -> bool;
#[sel(hasCloseBox)]
pub fn hasCloseBox(&self) -> bool;
#[sel(isMiniaturized)]
pub fn isMiniaturized(&self) -> bool;

View file

@ -29,7 +29,7 @@ use crate::{
Fullscreen, OsError,
},
window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes,
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
WindowId as RootWindowId, WindowLevel,
},
};
@ -269,6 +269,14 @@ impl WinitWindow {
masks &= !NSWindowStyleMask::NSResizableWindowMask;
}
if !attrs.enabled_buttons.contains(WindowButtons::MINIMIZE) {
masks &= !NSWindowStyleMask::NSMiniaturizableWindowMask;
}
if !attrs.enabled_buttons.contains(WindowButtons::CLOSE) {
masks &= !NSWindowStyleMask::NSClosableWindowMask;
}
if pl_attrs.fullsize_content_view {
masks |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
}
@ -333,6 +341,12 @@ impl WinitWindow {
this.setMovableByWindowBackground(true);
}
if !attrs.enabled_buttons.contains(WindowButtons::MAXIMIZE) {
if let Some(button) = this.standardWindowButton(NSWindowButton::Zoom) {
button.setEnabled(false);
}
}
if let Some(increments) = attrs.resize_increments {
let increments = increments.to_logical(this.scale_factor());
let (w, h) = (increments.width, increments.height);
@ -624,6 +638,53 @@ impl WinitWindow {
self.isResizable()
}
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let mut mask = self.styleMask();
if buttons.contains(WindowButtons::CLOSE) {
mask |= NSWindowStyleMask::NSClosableWindowMask;
} else {
mask &= !NSWindowStyleMask::NSClosableWindowMask;
}
if buttons.contains(WindowButtons::MINIMIZE) {
mask |= NSWindowStyleMask::NSMiniaturizableWindowMask;
} else {
mask &= !NSWindowStyleMask::NSMiniaturizableWindowMask;
}
// This must happen before the button's "enabled" status has been set,
// hence we do it synchronously.
self.set_style_mask_sync(mask);
// We edit the button directly instead of using `NSResizableWindowMask`,
// since that mask also affect the resizability of the window (which is
// controllable by other means in `winit`).
if let Some(button) = self.standardWindowButton(NSWindowButton::Zoom) {
button.setEnabled(buttons.contains(WindowButtons::MAXIMIZE));
}
}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
let mut buttons = WindowButtons::empty();
if self.isMiniaturizable() {
buttons |= WindowButtons::MINIMIZE;
}
if self
.standardWindowButton(NSWindowButton::Zoom)
.map(|b| b.isEnabled())
.unwrap_or(true)
{
buttons |= WindowButtons::MAXIMIZE;
}
if self.hasCloseBox() {
buttons |= WindowButtons::CLOSE;
}
buttons
}
pub fn set_cursor_icon(&self, icon: CursorIcon) {
let view = self.view();
let mut cursor_state = view.state.cursor_state.lock().unwrap();

View file

@ -3,8 +3,8 @@ use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
use crate::event;
use crate::icon::Icon;
use crate::window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowId as RootWI,
WindowLevel,
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
WindowId as RootWI, WindowLevel,
};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle};
@ -172,6 +172,14 @@ impl Window {
true
}
#[inline]
pub fn set_enabled_buttons(&self, _buttons: WindowButtons) {}
#[inline]
pub fn enabled_buttons(&self) -> WindowButtons {
WindowButtons::all()
}
#[inline]
pub fn scale_factor(&self) -> f64 {
super::backend::scale_factor()

View file

@ -71,7 +71,10 @@ use crate::{
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
Fullscreen, Parent, PlatformSpecificWindowBuilderAttributes, WindowId,
},
window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowLevel},
window::{
CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowButtons,
WindowLevel,
},
};
/// The Win32 implementation of the main `Window` object.
@ -263,6 +266,44 @@ impl Window {
window_state.window_flags.contains(WindowFlags::RESIZABLE)
}
#[inline]
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
let window = self.window.clone();
let window_state = Arc::clone(&self.window_state);
self.thread_executor.execute_in_thread(move || {
let _ = &window;
WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| {
f.set(
WindowFlags::MINIMIZABLE,
buttons.contains(WindowButtons::MINIMIZE),
);
f.set(
WindowFlags::MAXIMIZABLE,
buttons.contains(WindowButtons::MAXIMIZE),
);
f.set(
WindowFlags::CLOSABLE,
buttons.contains(WindowButtons::CLOSE),
)
});
});
}
pub fn enabled_buttons(&self) -> WindowButtons {
let mut buttons = WindowButtons::empty();
let window_state = self.window_state_lock();
if window_state.window_flags.contains(WindowFlags::MINIMIZABLE) {
buttons |= WindowButtons::MINIMIZE;
}
if window_state.window_flags.contains(WindowFlags::MAXIMIZABLE) {
buttons |= WindowButtons::MAXIMIZE;
}
if window_state.window_flags.contains(WindowFlags::CLOSABLE) {
buttons |= WindowButtons::CLOSE;
}
buttons
}
/// Returns the `hwnd` of this window.
#[inline]
pub fn hwnd(&self) -> HWND {
@ -943,6 +984,8 @@ impl<'a, T: 'static> InitData<'a, T> {
// attribute is correctly applied.
win.set_visible(attributes.visible);
win.set_enabled_buttons(attributes.enabled_buttons);
if attributes.fullscreen.is_some() {
win.set_fullscreen(attributes.fullscreen);
force_window_active(win.window.0);
@ -1012,6 +1055,9 @@ where
window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent);
// WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured.
window_flags.set(WindowFlags::RESIZABLE, attributes.resizable);
// Will be changed later using `window.set_enabled_buttons` but we need to set a default here
// so the diffing later can work.
window_flags.set(WindowFlags::CLOSABLE, true);
let parent = match pl_attribs.parent {
Parent::ChildOf(parent) => {

View file

@ -11,8 +11,9 @@ use windows_sys::Win32::{
Foundation::{HWND, RECT},
Graphics::Gdi::InvalidateRgn,
UI::WindowsAndMessaging::{
AdjustWindowRectEx, GetMenu, GetWindowLongW, SendMessageW, SetWindowLongW, SetWindowPos,
ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_BOTTOM, HWND_NOTOPMOST, HWND_TOPMOST,
AdjustWindowRectEx, EnableMenuItem, GetMenu, GetSystemMenu, GetWindowLongW, SendMessageW,
SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_BOTTOM,
HWND_NOTOPMOST, HWND_TOPMOST, MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, SC_CLOSE,
SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION,
SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW,
WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD,
@ -77,38 +78,41 @@ bitflags! {
bitflags! {
pub struct WindowFlags: u32 {
const RESIZABLE = 1 << 0;
const VISIBLE = 1 << 1;
const ON_TASKBAR = 1 << 2;
const ALWAYS_ON_TOP = 1 << 3;
const ALWAYS_ON_BOTTOM = 1 << 4;
const NO_BACK_BUFFER = 1 << 5;
const TRANSPARENT = 1 << 6;
const CHILD = 1 << 7;
const MAXIMIZED = 1 << 8;
const POPUP = 1 << 9;
const MINIMIZABLE = 1 << 1;
const MAXIMIZABLE = 1 << 2;
const CLOSABLE = 1 << 3;
const VISIBLE = 1 << 4;
const ON_TASKBAR = 1 << 5;
const ALWAYS_ON_TOP = 1 << 6;
const ALWAYS_ON_BOTTOM = 1 << 7;
const NO_BACK_BUFFER = 1 << 8;
const TRANSPARENT = 1 << 9;
const CHILD = 1 << 10;
const MAXIMIZED = 1 << 11;
const POPUP = 1 << 12;
/// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is
/// included here to make masking easier.
const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 10;
const MARKER_BORDERLESS_FULLSCREEN = 1 << 11;
const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 13;
const MARKER_BORDERLESS_FULLSCREEN = 1 << 14;
/// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`.
/// In most cases, it's okay to let those parameters change the state. However, when we're
/// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to
/// effect our stored state, because the purpose of `apply_diff` is to update the actual
/// window's state to match our stored state. This controls whether to accept those changes.
const MARKER_RETAIN_STATE_ON_SIZE = 1 << 12;
const MARKER_RETAIN_STATE_ON_SIZE = 1 << 15;
const MARKER_IN_SIZE_MOVE = 1 << 13;
const MARKER_IN_SIZE_MOVE = 1 << 16;
const MINIMIZED = 1 << 14;
const MINIMIZED = 1 << 17;
const IGNORE_CURSOR_EVENT = 1 << 15;
const IGNORE_CURSOR_EVENT = 1 << 18;
/// Fully decorated window (incl. caption, border and drop shadow).
const MARKER_DECORATIONS = 1 << 16;
const MARKER_DECORATIONS = 1 << 19;
/// Drop shadow for undecorated windows.
const MARKER_UNDECORATED_SHADOW = 1 << 17;
const MARKER_UNDECORATED_SHADOW = 1 << 20;
const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits;
}
@ -237,16 +241,17 @@ impl WindowFlags {
pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) {
// Required styles to properly support common window functionality like aero snap.
let mut style = WS_CAPTION
| WS_MINIMIZEBOX
| WS_BORDER
| WS_CLIPSIBLINGS
| WS_CLIPCHILDREN
| WS_SYSMENU;
let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU;
let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES;
if self.contains(WindowFlags::RESIZABLE) {
style |= WS_SIZEBOX | WS_MAXIMIZEBOX;
style |= WS_SIZEBOX;
}
if self.contains(WindowFlags::MAXIMIZABLE) {
style |= WS_MAXIMIZEBOX;
}
if self.contains(WindowFlags::MINIMIZABLE) {
style |= WS_MINIMIZEBOX;
}
if self.contains(WindowFlags::VISIBLE) {
style |= WS_VISIBLE;
@ -350,6 +355,18 @@ impl WindowFlags {
}
}
if diff.contains(WindowFlags::CLOSABLE) || new.contains(WindowFlags::CLOSABLE) {
let flags = MF_BYCOMMAND
| new
.contains(WindowFlags::CLOSABLE)
.then(|| MF_ENABLED)
.unwrap_or(MF_DISABLED);
unsafe {
EnableMenuItem(GetSystemMenu(window, 0), SC_CLOSE, flags);
}
}
if !new.contains(WindowFlags::VISIBLE) {
unsafe {
ShowWindow(window, SW_HIDE);

View file

@ -126,6 +126,7 @@ pub(crate) struct WindowAttributes {
pub max_inner_size: Option<Size>,
pub position: Option<Position>,
pub resizable: bool,
pub enabled_buttons: WindowButtons,
pub title: String,
pub fullscreen: Option<platform_impl::Fullscreen>,
pub maximized: bool,
@ -148,6 +149,7 @@ impl Default for WindowAttributes {
max_inner_size: None,
position: None,
resizable: true,
enabled_buttons: WindowButtons::all(),
title: "winit window".to_owned(),
maximized: false,
fullscreen: None,
@ -244,6 +246,17 @@ impl WindowBuilder {
self
}
/// Sets the enabled window buttons.
///
/// The default is [`WindowButtons::all`]
///
/// See [`Window::set_enabled_buttons`] for details.
#[inline]
pub fn with_enabled_buttons(mut self, buttons: WindowButtons) -> Self {
self.window.enabled_buttons = buttons;
self
}
/// Sets the initial title of the window in the title bar.
///
/// The default is `"winit window"`.
@ -755,6 +768,26 @@ impl Window {
self.window.is_resizable()
}
/// Sets the enabled window buttons.
///
/// ## Platform-specific
///
/// - **Wayland / X11:** Not implemented.
/// - **Web / iOS / Android:** Unsupported.
pub fn set_enabled_buttons(&self, buttons: WindowButtons) {
self.window.set_enabled_buttons(buttons)
}
/// Gets the enabled window buttons.
///
/// ## Platform-specific
///
/// - **Wayland / X11:** Not implemented. Always returns [`WindowButtons::all`].
/// - **Web / iOS / Android:** Unsupported. Always returns [`WindowButtons::all`].
pub fn enabled_buttons(&self) -> WindowButtons {
self.window.enabled_buttons()
}
/// Sets the window to minimized or back
///
/// ## Platform-specific
@ -1441,6 +1474,14 @@ impl Default for UserAttentionType {
}
}
bitflags! {
pub struct WindowButtons: u32 {
const CLOSE = 1 << 0;
const MINIMIZE = 1 << 1;
const MAXIMIZE = 1 << 2;
}
}
/// A window level groups windows with respect to their z-position.
///
/// The relative ordering between windows in different window levels is fixed.