Improve macOS/iOS/Web thread safety
Co-authored-by: daxpedda <daxpedda@gmail.com>
This commit is contained in:
parent
119462795a
commit
af6c343d0e
20 changed files with 552 additions and 724 deletions
|
|
@ -797,6 +797,14 @@ impl Window {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) {
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Self) -> R + Send) -> R {
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> WindowId {
|
||||
WindowId
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use icrate::Foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadMarker};
|
||||
use icrate::Foundation::{CGFloat, CGPoint, CGRect, CGSize, MainThreadBound, MainThreadMarker};
|
||||
use objc2::rc::Id;
|
||||
use objc2::runtime::AnyObject;
|
||||
use objc2::{class, msg_send};
|
||||
|
|
@ -31,9 +28,9 @@ use crate::{
|
|||
};
|
||||
|
||||
pub struct Inner {
|
||||
pub(crate) window: Id<WinitUIWindow>,
|
||||
pub(crate) view_controller: Id<WinitViewController>,
|
||||
pub(crate) view: Id<WinitView>,
|
||||
window: Id<WinitUIWindow>,
|
||||
view_controller: Id<WinitViewController>,
|
||||
view: Id<WinitView>,
|
||||
gl_or_metal_backed: bool,
|
||||
}
|
||||
|
||||
|
|
@ -76,68 +73,58 @@ impl Inner {
|
|||
pub fn pre_present_notify(&self) {}
|
||||
|
||||
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
unsafe {
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let position = LogicalPosition {
|
||||
x: safe_area.origin.x as f64,
|
||||
y: safe_area.origin.y as f64,
|
||||
};
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let position = LogicalPosition {
|
||||
x: safe_area.origin.x as f64,
|
||||
y: safe_area.origin.y as f64,
|
||||
};
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
|
||||
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
unsafe {
|
||||
let screen_frame = self.screen_frame();
|
||||
let position = LogicalPosition {
|
||||
x: screen_frame.origin.x as f64,
|
||||
y: screen_frame.origin.y as f64,
|
||||
};
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
let screen_frame = self.screen_frame();
|
||||
let position = LogicalPosition {
|
||||
x: screen_frame.origin.x as f64,
|
||||
y: screen_frame.origin.y as f64,
|
||||
};
|
||||
let scale_factor = self.scale_factor();
|
||||
Ok(position.to_physical(scale_factor))
|
||||
}
|
||||
|
||||
pub fn set_outer_position(&self, physical_position: Position) {
|
||||
unsafe {
|
||||
let scale_factor = self.scale_factor();
|
||||
let position = physical_position.to_logical::<f64>(scale_factor);
|
||||
let screen_frame = self.screen_frame();
|
||||
let new_screen_frame = CGRect {
|
||||
origin: CGPoint {
|
||||
x: position.x as _,
|
||||
y: position.y as _,
|
||||
},
|
||||
size: screen_frame.size,
|
||||
};
|
||||
let bounds = self.rect_from_screen_space(new_screen_frame);
|
||||
self.window.setBounds(bounds);
|
||||
}
|
||||
let scale_factor = self.scale_factor();
|
||||
let position = physical_position.to_logical::<f64>(scale_factor);
|
||||
let screen_frame = self.screen_frame();
|
||||
let new_screen_frame = CGRect {
|
||||
origin: CGPoint {
|
||||
x: position.x as _,
|
||||
y: position.y as _,
|
||||
},
|
||||
size: screen_frame.size,
|
||||
};
|
||||
let bounds = self.rect_from_screen_space(new_screen_frame);
|
||||
self.window.setBounds(bounds);
|
||||
}
|
||||
|
||||
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
||||
unsafe {
|
||||
let scale_factor = self.scale_factor();
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let size = LogicalSize {
|
||||
width: safe_area.size.width as f64,
|
||||
height: safe_area.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
let scale_factor = self.scale_factor();
|
||||
let safe_area = self.safe_area_screen_space();
|
||||
let size = LogicalSize {
|
||||
width: safe_area.size.width as f64,
|
||||
height: safe_area.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
|
||||
pub fn outer_size(&self) -> PhysicalSize<u32> {
|
||||
unsafe {
|
||||
let scale_factor = self.scale_factor();
|
||||
let screen_frame = self.screen_frame();
|
||||
let size = LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
let scale_factor = self.scale_factor();
|
||||
let screen_frame = self.screen_frame();
|
||||
let size = LogicalSize {
|
||||
width: screen_frame.size.width as f64,
|
||||
height: screen_frame.size.height as f64,
|
||||
};
|
||||
size.to_physical(scale_factor)
|
||||
}
|
||||
|
||||
pub fn request_inner_size(&self, _size: Size) -> Option<PhysicalSize<u32>> {
|
||||
|
|
@ -262,22 +249,20 @@ impl Inner {
|
|||
}
|
||||
|
||||
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
|
||||
unsafe {
|
||||
let monitor = self.current_monitor_inner();
|
||||
let uiscreen = monitor.ui_screen();
|
||||
let screen_space_bounds = self.screen_frame();
|
||||
let screen_bounds = uiscreen.bounds();
|
||||
let monitor = self.current_monitor_inner();
|
||||
let uiscreen = monitor.ui_screen();
|
||||
let screen_space_bounds = self.screen_frame();
|
||||
let screen_bounds = uiscreen.bounds();
|
||||
|
||||
// TODO: track fullscreen instead of relying on brittle float comparisons
|
||||
if screen_space_bounds.origin.x == screen_bounds.origin.x
|
||||
&& screen_space_bounds.origin.y == screen_bounds.origin.y
|
||||
&& screen_space_bounds.size.width == screen_bounds.size.width
|
||||
&& screen_space_bounds.size.height == screen_bounds.size.height
|
||||
{
|
||||
Some(Fullscreen::Borderless(Some(monitor)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
// TODO: track fullscreen instead of relying on brittle float comparisons
|
||||
if screen_space_bounds.origin.x == screen_bounds.origin.x
|
||||
&& screen_space_bounds.origin.y == screen_bounds.origin.y
|
||||
&& screen_space_bounds.size.width == screen_bounds.size.width
|
||||
&& screen_space_bounds.size.height == screen_bounds.size.height
|
||||
{
|
||||
Some(Fullscreen::Borderless(Some(monitor)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -375,32 +360,7 @@ impl Inner {
|
|||
}
|
||||
|
||||
pub struct Window {
|
||||
pub inner: Inner,
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
assert_main_thread!("`Window::drop` can only be run on the main thread on iOS");
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Window {}
|
||||
unsafe impl Sync for Window {}
|
||||
|
||||
impl Deref for Window {
|
||||
type Target = Inner;
|
||||
|
||||
fn deref(&self) -> &Inner {
|
||||
assert_main_thread!("`Window` methods can only be run on the main thread on iOS");
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Window {
|
||||
fn deref_mut(&mut self) -> &mut Inner {
|
||||
assert_main_thread!("`Window` methods can only be run on the main thread on iOS");
|
||||
&mut self.inner
|
||||
}
|
||||
inner: MainThreadBound<Inner>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -497,15 +457,25 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
let inner = Inner {
|
||||
window,
|
||||
view_controller,
|
||||
view,
|
||||
gl_or_metal_backed,
|
||||
};
|
||||
Ok(Window {
|
||||
inner: Inner {
|
||||
window,
|
||||
view_controller,
|
||||
view,
|
||||
gl_or_metal_backed,
|
||||
},
|
||||
inner: MainThreadBound::new(inner, mtm),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) {
|
||||
// For now, don't actually do queuing, since it may be less predictable
|
||||
self.maybe_wait_on_main(f)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Inner) -> R + Send) -> R {
|
||||
self.inner.get_on_main(|inner, _mtm| f(inner))
|
||||
}
|
||||
}
|
||||
|
||||
// WindowExtIOS
|
||||
|
|
@ -542,27 +512,23 @@ impl Inner {
|
|||
}
|
||||
|
||||
impl Inner {
|
||||
// requires main thread
|
||||
unsafe fn screen_frame(&self) -> CGRect {
|
||||
fn screen_frame(&self) -> CGRect {
|
||||
self.rect_to_screen_space(self.window.bounds())
|
||||
}
|
||||
|
||||
// requires main thread
|
||||
unsafe fn rect_to_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
fn rect_to_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
let screen_space = self.window.screen().coordinateSpace();
|
||||
self.window
|
||||
.convertRect_toCoordinateSpace(rect, &screen_space)
|
||||
}
|
||||
|
||||
// requires main thread
|
||||
unsafe fn rect_from_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
fn rect_from_screen_space(&self, rect: CGRect) -> CGRect {
|
||||
let screen_space = self.window.screen().coordinateSpace();
|
||||
self.window
|
||||
.convertRect_fromCoordinateSpace(rect, &screen_space)
|
||||
}
|
||||
|
||||
// requires main thread
|
||||
unsafe fn safe_area_screen_space(&self) -> CGRect {
|
||||
fn safe_area_screen_space(&self) -> CGRect {
|
||||
let bounds = self.window.bounds();
|
||||
if app_state::os_capabilities().safe_area {
|
||||
let safe_area = self.window.safeAreaInsets();
|
||||
|
|
@ -580,7 +546,7 @@ impl Inner {
|
|||
} else {
|
||||
let screen_frame = self.rect_to_screen_space(bounds);
|
||||
let status_bar_frame = {
|
||||
let app = UIApplication::shared(MainThreadMarker::new().unwrap_unchecked()).expect(
|
||||
let app = UIApplication::shared(MainThreadMarker::new().unwrap()).expect(
|
||||
"`Window::get_inner_position` cannot be called before `EventLoop::run` on iOS",
|
||||
);
|
||||
app.statusBarFrame()
|
||||
|
|
|
|||
|
|
@ -306,6 +306,14 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) {
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Self) -> R + Send) -> R {
|
||||
f(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> WindowId {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ use core_foundation::{
|
|||
base::CFRelease,
|
||||
data::{CFDataGetBytePtr, CFDataRef},
|
||||
};
|
||||
use icrate::Foundation::MainThreadMarker;
|
||||
use objc2::rc::Id;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::appkit::{NSEvent, NSEventModifierFlags};
|
||||
use super::util::Never;
|
||||
use super::window::WinitWindow;
|
||||
use crate::{
|
||||
dpi::LogicalSize,
|
||||
|
|
@ -16,10 +18,7 @@ use crate::{
|
|||
Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NativeKey, NativeKeyCode,
|
||||
},
|
||||
platform::{modifier_supplement::KeyEventExtModifierSupplement, scancode::KeyCodeExtScancode},
|
||||
platform_impl::platform::{
|
||||
ffi,
|
||||
util::{get_kbd_type, Never},
|
||||
},
|
||||
platform_impl::platform::ffi,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -75,7 +74,7 @@ pub fn get_modifierless_char(scancode: u16) -> Key {
|
|||
}
|
||||
layout = CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout;
|
||||
}
|
||||
let keyboard_type = get_kbd_type();
|
||||
let keyboard_type = MainThreadMarker::run_on_main(|_mtm| unsafe { ffi::LMGetKbdType() });
|
||||
|
||||
let mut result_len = 0;
|
||||
let mut dead_keys = 0;
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ use core_foundation::runloop::{
|
|||
kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext,
|
||||
CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
|
||||
};
|
||||
use icrate::Foundation::is_main_thread;
|
||||
use icrate::Foundation::MainThreadMarker;
|
||||
use objc2::rc::{autoreleasepool, Id};
|
||||
use objc2::{msg_send_id, ClassType};
|
||||
use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle};
|
||||
|
||||
use super::appkit::{NSApp, NSApplicationActivationPolicy, NSEvent, NSWindow};
|
||||
use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent, NSWindow};
|
||||
use crate::{
|
||||
error::EventLoopError,
|
||||
event::Event,
|
||||
|
|
@ -66,15 +66,8 @@ impl PanicInfo {
|
|||
}
|
||||
|
||||
pub struct EventLoopWindowTarget<T: 'static> {
|
||||
pub sender: mpsc::Sender<T>, // this is only here to be cloned elsewhere
|
||||
pub receiver: mpsc::Receiver<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for EventLoopWindowTarget<T> {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
EventLoopWindowTarget { sender, receiver }
|
||||
}
|
||||
mtm: MainThreadMarker,
|
||||
}
|
||||
|
||||
impl<T: 'static> EventLoopWindowTarget<T> {
|
||||
|
|
@ -97,11 +90,11 @@ impl<T: 'static> EventLoopWindowTarget<T> {
|
|||
|
||||
impl<T> EventLoopWindowTarget<T> {
|
||||
pub(crate) fn hide_application(&self) {
|
||||
NSApp().hide(None)
|
||||
NSApplication::shared(self.mtm).hide(None)
|
||||
}
|
||||
|
||||
pub(crate) fn hide_other_applications(&self) {
|
||||
NSApp().hideOtherApplications(None)
|
||||
NSApplication::shared(self.mtm).hideOtherApplications(None)
|
||||
}
|
||||
|
||||
pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
|
||||
|
|
@ -118,8 +111,10 @@ pub struct EventLoop<T: 'static> {
|
|||
/// it around here as well.
|
||||
_delegate: Id<ApplicationDelegate>,
|
||||
|
||||
sender: mpsc::Sender<T>,
|
||||
window_target: Rc<RootWindowTarget<T>>,
|
||||
panic_info: Rc<PanicInfo>,
|
||||
mtm: MainThreadMarker,
|
||||
|
||||
/// We make sure that the callback closure is dropped during a panic
|
||||
/// by making the event loop own it.
|
||||
|
|
@ -151,9 +146,8 @@ impl<T> EventLoop<T> {
|
|||
pub(crate) fn new(
|
||||
attributes: &PlatformSpecificEventLoopAttributes,
|
||||
) -> Result<Self, EventLoopError> {
|
||||
if !is_main_thread() {
|
||||
panic!("On macOS, `EventLoop` must be created on the main thread!");
|
||||
}
|
||||
let mtm = MainThreadMarker::new()
|
||||
.expect("On macOS, `EventLoop` must be created on the main thread!");
|
||||
|
||||
// This must be done before `NSApp()` (equivalent to sending
|
||||
// `sharedApplication`) is called anywhere else, or we'll end up
|
||||
|
|
@ -180,12 +174,16 @@ impl<T> EventLoop<T> {
|
|||
|
||||
let panic_info: Rc<PanicInfo> = Default::default();
|
||||
setup_control_flow_observers(Rc::downgrade(&panic_info));
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
Ok(EventLoop {
|
||||
_delegate: delegate,
|
||||
sender,
|
||||
window_target: Rc::new(RootWindowTarget {
|
||||
p: Default::default(),
|
||||
p: EventLoopWindowTarget { receiver, mtm },
|
||||
_marker: PhantomData,
|
||||
}),
|
||||
mtm,
|
||||
panic_info,
|
||||
_callback: None,
|
||||
})
|
||||
|
|
@ -233,7 +231,7 @@ impl<T> EventLoop<T> {
|
|||
self._callback = Some(Rc::clone(&callback));
|
||||
|
||||
let exit_code = autoreleasepool(|_| {
|
||||
let app = NSApp();
|
||||
let app = NSApplication::shared(self.mtm);
|
||||
|
||||
// A bit of juggling with the callback references to make sure
|
||||
// that `self.callback` is the only owner of the callback.
|
||||
|
|
@ -408,7 +406,7 @@ impl<T> EventLoop<T> {
|
|||
}
|
||||
|
||||
pub fn create_proxy(&self) -> EventLoopProxy<T> {
|
||||
EventLoopProxy::new(self.window_target.p.sender.clone())
|
||||
EventLoopProxy::new(self.sender.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,8 @@ mod view;
|
|||
mod window;
|
||||
mod window_delegate;
|
||||
|
||||
use std::{fmt, ops::Deref};
|
||||
use std::fmt;
|
||||
|
||||
use self::window::WinitWindow;
|
||||
use self::window_delegate::WinitWindowDelegate;
|
||||
pub(crate) use self::{
|
||||
event::KeyEventExtra,
|
||||
event_loop::{
|
||||
|
|
@ -29,11 +27,9 @@ pub(crate) use self::{
|
|||
monitor::{MonitorHandle, VideoMode},
|
||||
window::{PlatformSpecificWindowBuilderAttributes, WindowId},
|
||||
};
|
||||
use crate::{
|
||||
error::OsError as RootOsError, event::DeviceId as RootDeviceId, window::WindowAttributes,
|
||||
};
|
||||
use objc2::rc::{autoreleasepool, Id};
|
||||
use crate::event::DeviceId as RootDeviceId;
|
||||
|
||||
pub(crate) use self::window::Window;
|
||||
pub(crate) use crate::icon::NoIcon as PlatformIcon;
|
||||
pub(crate) use crate::platform_impl::Fullscreen;
|
||||
|
||||
|
|
@ -49,47 +45,12 @@ impl DeviceId {
|
|||
// Constant device ID; to be removed when if backend is updated to report real device IDs.
|
||||
pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId);
|
||||
|
||||
pub(crate) struct Window {
|
||||
pub(crate) window: Id<WinitWindow>,
|
||||
// We keep this around so that it doesn't get dropped until the window does.
|
||||
_delegate: Id<WinitWindowDelegate>,
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
// Ensure the window is closed
|
||||
util::close_sync(&self.window);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OsError {
|
||||
CGError(core_graphics::base::CGError),
|
||||
CreationError(&'static str),
|
||||
}
|
||||
|
||||
unsafe impl Send for Window {}
|
||||
unsafe impl Sync for Window {}
|
||||
|
||||
impl Deref for Window {
|
||||
type Target = WinitWindow;
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new<T: 'static>(
|
||||
_window_target: &EventLoopWindowTarget<T>,
|
||||
attributes: WindowAttributes,
|
||||
pl_attribs: PlatformSpecificWindowBuilderAttributes,
|
||||
) -> Result<Self, RootOsError> {
|
||||
let (window, _delegate) = autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs))?;
|
||||
Ok(Window { window, _delegate })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for OsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
mod r#async;
|
||||
|
||||
pub(crate) use self::r#async::*;
|
||||
|
||||
use core_graphics::display::CGDisplay;
|
||||
use icrate::Foundation::{CGFloat, NSNotFound, NSPoint, NSRange, NSRect, NSUInteger};
|
||||
|
||||
|
|
@ -50,6 +44,7 @@ impl Drop for TraceGuard {
|
|||
// For consistency with other platforms, this will...
|
||||
// 1. translate the bottom-left window corner into the top-left window corner
|
||||
// 2. translate the coordinate from a bottom-left origin coordinate system to a top-left one
|
||||
#[allow(clippy::unnecessary_cast)]
|
||||
pub fn bottom_left_to_top_left(rect: NSRect) -> f64 {
|
||||
CGDisplay::main().pixels_high() as f64 - (rect.origin.y + rect.size.height) as f64
|
||||
}
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use dispatch::Queue;
|
||||
use icrate::Foundation::{is_main_thread, CGFloat, NSPoint, NSSize, NSString};
|
||||
use objc2::rc::autoreleasepool;
|
||||
|
||||
use crate::{
|
||||
dpi::{LogicalPosition, LogicalSize},
|
||||
platform_impl::platform::{
|
||||
appkit::{NSScreen, NSWindow, NSWindowLevel, NSWindowStyleMask},
|
||||
ffi,
|
||||
window::WinitWindow,
|
||||
},
|
||||
};
|
||||
|
||||
// Unsafe wrapper type that allows us to dispatch things that aren't Send.
|
||||
// This should *only* be used to dispatch to the main queue.
|
||||
// While it is indeed not guaranteed that these types can safely be sent to
|
||||
// other threads, we know that they're safe to use on the main thread.
|
||||
struct MainThreadSafe<T>(T);
|
||||
|
||||
unsafe impl<T> Send for MainThreadSafe<T> {}
|
||||
|
||||
impl<T> Deref for MainThreadSafe<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn run_on_main<R: Send>(f: impl FnOnce() -> R + Send) -> R {
|
||||
if is_main_thread() {
|
||||
f()
|
||||
} else {
|
||||
Queue::main().exec_sync(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_style_mask(window: &NSWindow, mask: NSWindowStyleMask) {
|
||||
window.setStyleMask(mask);
|
||||
// If we don't do this, key handling will break
|
||||
// (at least until the window is clicked again/etc.)
|
||||
let _ = window.makeFirstResponder(Some(&window.contentView()));
|
||||
}
|
||||
|
||||
// Always use this function instead of trying to modify `styleMask` directly!
|
||||
// `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch.
|
||||
// Otherwise, this would vomit out errors about not being on the main thread
|
||||
// and fail to do anything.
|
||||
pub(crate) fn set_style_mask_sync(window: &NSWindow, mask: NSWindowStyleMask) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
set_style_mask(&window, mask);
|
||||
})
|
||||
}
|
||||
|
||||
// `setContentSize:` isn't thread-safe either, though it doesn't log any errors
|
||||
// and just fails silently. Anyway, GCD to the rescue!
|
||||
pub(crate) fn set_content_size_sync(window: &NSWindow, size: LogicalSize<f64>) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.setContentSize(NSSize::new(size.width as CGFloat, size.height as CGFloat));
|
||||
});
|
||||
}
|
||||
|
||||
// `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy
|
||||
// to log errors.
|
||||
pub(crate) fn set_frame_top_left_point_sync(window: &NSWindow, point: NSPoint) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.setFrameTopLeftPoint(point);
|
||||
});
|
||||
}
|
||||
|
||||
// `setFrameTopLeftPoint:` isn't thread-safe, and fails silently.
|
||||
pub(crate) fn set_level_sync(window: &NSWindow, level: NSWindowLevel) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.setLevel(level);
|
||||
});
|
||||
}
|
||||
|
||||
// `setIgnoresMouseEvents_:` isn't thread-safe, and fails silently.
|
||||
pub(crate) fn set_ignore_mouse_events_sync(window: &NSWindow, ignore: bool) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.setIgnoresMouseEvents(ignore);
|
||||
});
|
||||
}
|
||||
|
||||
// `toggleFullScreen` is thread-safe, but our additional logic to account for
|
||||
// window styles isn't.
|
||||
pub(crate) fn toggle_full_screen_sync(window: &WinitWindow, not_fullscreen: bool) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
// `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
|
||||
// set a normal style temporarily. The previous state will be
|
||||
// restored in `WindowDelegate::window_did_exit_fullscreen`.
|
||||
if not_fullscreen {
|
||||
let curr_mask = window.styleMask();
|
||||
let required =
|
||||
NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
|
||||
if !curr_mask.contains(required) {
|
||||
set_style_mask(&window, required);
|
||||
window
|
||||
.lock_shared_state("toggle_full_screen_sync")
|
||||
.saved_style = Some(curr_mask);
|
||||
}
|
||||
}
|
||||
// Window level must be restored from `CGShieldingWindowLevel()
|
||||
// + 1` back to normal in order for `toggleFullScreen` to do
|
||||
// anything
|
||||
window.setLevel(NSWindowLevel::Normal);
|
||||
window.toggleFullScreen(None);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn restore_display_mode_sync(ns_screen: u32) {
|
||||
run_on_main(move || {
|
||||
unsafe { ffi::CGRestorePermanentDisplayConfiguration() };
|
||||
assert_eq!(
|
||||
unsafe { ffi::CGDisplayRelease(ns_screen) },
|
||||
ffi::kCGErrorSuccess
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// `setMaximized` is not thread-safe
|
||||
pub(crate) fn set_maximized_sync(window: &WinitWindow, is_zoomed: bool, maximized: bool) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
let mut shared_state = window.lock_shared_state("set_maximized_sync");
|
||||
// Save the standard frame sized if it is not zoomed
|
||||
if !is_zoomed {
|
||||
shared_state.standard_frame = Some(window.frame());
|
||||
}
|
||||
|
||||
shared_state.maximized = maximized;
|
||||
|
||||
if shared_state.fullscreen.is_some() {
|
||||
// Handle it in window_did_exit_fullscreen
|
||||
return;
|
||||
}
|
||||
|
||||
if window
|
||||
.styleMask()
|
||||
.contains(NSWindowStyleMask::NSResizableWindowMask)
|
||||
{
|
||||
drop(shared_state);
|
||||
// Just use the native zoom if resizable
|
||||
window.zoom(None);
|
||||
} else {
|
||||
// if it's not resizable, we set the frame directly
|
||||
let new_rect = if maximized {
|
||||
let screen = NSScreen::main().expect("no screen found");
|
||||
screen.visibleFrame()
|
||||
} else {
|
||||
shared_state.saved_standard_frame()
|
||||
};
|
||||
drop(shared_state);
|
||||
window.setFrame_display(new_rect, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// `orderOut:` isn't thread-safe. Calling it from another thread actually works,
|
||||
// but with an odd delay.
|
||||
pub(crate) fn order_out_sync(window: &NSWindow) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.orderOut(None);
|
||||
});
|
||||
}
|
||||
|
||||
// `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread
|
||||
// actually works, but with an odd delay.
|
||||
pub(crate) fn make_key_and_order_front_sync(window: &NSWindow) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.makeKeyAndOrderFront(None);
|
||||
});
|
||||
}
|
||||
|
||||
// `setTitle:` isn't thread-safe. Calling it from another thread invalidates the
|
||||
// window drag regions, which throws an exception when not done in the main
|
||||
// thread
|
||||
pub(crate) fn set_title_sync(window: &NSWindow, title: &str) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.setTitle(&NSString::from_str(title));
|
||||
});
|
||||
}
|
||||
|
||||
// `close:` is thread-safe, but we want the event to be triggered from the main
|
||||
// thread. Though, it's a good idea to look into that more...
|
||||
pub(crate) fn close_sync(window: &NSWindow) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
autoreleasepool(move |_| {
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_ime_cursor_area_sync(
|
||||
window: &WinitWindow,
|
||||
logical_spot: LogicalPosition<f64>,
|
||||
size: LogicalSize<f64>,
|
||||
) {
|
||||
let window = MainThreadSafe(window);
|
||||
run_on_main(move || {
|
||||
window.view().set_ime_cursor_area(logical_spot, size);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn get_kbd_type() -> u8 {
|
||||
run_on_main(|| unsafe { ffi::LMGetKbdType() })
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ use crate::{
|
|||
platform_impl::platform::{
|
||||
app_state::AppState,
|
||||
appkit::NSWindowOrderingMode,
|
||||
event_loop::EventLoopWindowTarget,
|
||||
ffi,
|
||||
monitor::{self, MonitorHandle, VideoMode},
|
||||
util,
|
||||
|
|
@ -36,8 +37,8 @@ use crate::{
|
|||
};
|
||||
use core_graphics::display::{CGDisplay, CGPoint};
|
||||
use icrate::Foundation::{
|
||||
is_main_thread, CGFloat, NSArray, NSCopying, NSInteger, NSObject, NSPoint, NSRect, NSSize,
|
||||
NSString,
|
||||
CGFloat, MainThreadBound, MainThreadMarker, NSArray, NSCopying, NSInteger, NSObject, NSPoint,
|
||||
NSRect, NSSize, NSString,
|
||||
};
|
||||
use objc2::declare::{Ivar, IvarDrop};
|
||||
use objc2::rc::{autoreleasepool, Id};
|
||||
|
|
@ -50,6 +51,47 @@ use super::appkit::{
|
|||
NSWindowTabbingMode, NSWindowTitleVisibility,
|
||||
};
|
||||
|
||||
pub(crate) struct Window {
|
||||
window: MainThreadBound<Id<WinitWindow>>,
|
||||
// We keep this around so that it doesn't get dropped until the window does.
|
||||
_delegate: MainThreadBound<Id<WinitWindowDelegate>>,
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
self.window
|
||||
.get_on_main(|window, _| autoreleasepool(|_| window.close()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new<T: 'static>(
|
||||
_window_target: &EventLoopWindowTarget<T>,
|
||||
attributes: WindowAttributes,
|
||||
pl_attribs: PlatformSpecificWindowBuilderAttributes,
|
||||
) -> Result<Self, RootOsError> {
|
||||
let mtm = MainThreadMarker::new()
|
||||
.expect("windows can only be created on the main thread on macOS");
|
||||
let (window, _delegate) = autoreleasepool(|_| WinitWindow::new(attributes, pl_attribs))?;
|
||||
Ok(Window {
|
||||
window: MainThreadBound::new(window, mtm),
|
||||
_delegate: MainThreadBound::new(_delegate, mtm),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WinitWindow) + Send + 'static) {
|
||||
// For now, don't actually do queuing, since it may be less predictable
|
||||
self.maybe_wait_on_main(f)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(
|
||||
&self,
|
||||
f: impl FnOnce(&WinitWindow) -> R + Send,
|
||||
) -> R {
|
||||
self.window.get_on_main(|window, _mtm| f(window))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct WindowId(pub usize);
|
||||
|
||||
|
|
@ -250,16 +292,12 @@ impl Drop for SharedStateMutexGuard<'_> {
|
|||
|
||||
impl WinitWindow {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn new(
|
||||
fn new(
|
||||
attrs: WindowAttributes,
|
||||
pl_attrs: PlatformSpecificWindowBuilderAttributes,
|
||||
) -> Result<(Id<Self>, Id<WinitWindowDelegate>), RootOsError> {
|
||||
trace_scope!("WinitWindow::new");
|
||||
|
||||
if !is_main_thread() {
|
||||
panic!("Windows can only be created on the main thread on macOS");
|
||||
}
|
||||
|
||||
let this = autoreleasepool(|_| {
|
||||
let screen = match attrs.fullscreen.clone().map(Into::into) {
|
||||
Some(Fullscreen::Borderless(Some(monitor)))
|
||||
|
|
@ -537,16 +575,21 @@ impl WinitWindow {
|
|||
SharedStateMutexGuard::new(self.shared_state.lock().unwrap(), called_from_fn)
|
||||
}
|
||||
|
||||
fn set_style_mask_sync(&self, mask: NSWindowStyleMask) {
|
||||
util::set_style_mask_sync(self, mask);
|
||||
fn set_style_mask(&self, mask: NSWindowStyleMask) {
|
||||
self.setStyleMask(mask);
|
||||
// If we don't do this, key handling will break
|
||||
// (at least until the window is clicked again/etc.)
|
||||
let _ = self.makeFirstResponder(Some(&self.contentView()));
|
||||
}
|
||||
}
|
||||
|
||||
impl WinitWindow {
|
||||
pub fn id(&self) -> WindowId {
|
||||
WindowId(self as *const Self as usize)
|
||||
}
|
||||
|
||||
pub fn set_title(&self, title: &str) {
|
||||
util::set_title_sync(self, title);
|
||||
self.setTitle(&NSString::from_str(title))
|
||||
}
|
||||
|
||||
pub fn set_transparent(&self, transparent: bool) {
|
||||
|
|
@ -555,8 +598,8 @@ impl WinitWindow {
|
|||
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
match visible {
|
||||
true => util::make_key_and_order_front_sync(self),
|
||||
false => util::order_out_sync(self),
|
||||
true => self.makeKeyAndOrderFront(None),
|
||||
false => self.orderOut(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -595,7 +638,7 @@ impl WinitWindow {
|
|||
pub fn set_outer_position(&self, position: Position) {
|
||||
let scale_factor = self.scale_factor();
|
||||
let position = position.to_logical(scale_factor);
|
||||
util::set_frame_top_left_point_sync(self, util::window_position(position));
|
||||
self.setFrameTopLeftPoint(util::window_position(position));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -617,7 +660,8 @@ impl WinitWindow {
|
|||
#[inline]
|
||||
pub fn request_inner_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
|
||||
let scale_factor = self.scale_factor();
|
||||
util::set_content_size_sync(self, size.to_logical(scale_factor));
|
||||
let size: LogicalSize<f64> = size.to_logical(scale_factor);
|
||||
self.setContentSize(NSSize::new(size.width as CGFloat, size.height as CGFloat));
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -725,7 +769,7 @@ impl WinitWindow {
|
|||
} else {
|
||||
mask &= !NSWindowStyleMask::NSResizableWindowMask;
|
||||
}
|
||||
self.set_style_mask_sync(mask);
|
||||
self.set_style_mask(mask);
|
||||
}
|
||||
// Otherwise, we don't change the mask until we exit fullscreen.
|
||||
}
|
||||
|
|
@ -753,7 +797,7 @@ impl WinitWindow {
|
|||
|
||||
// This must happen before the button's "enabled" status has been set,
|
||||
// hence we do it synchronously.
|
||||
self.set_style_mask_sync(mask);
|
||||
self.set_style_mask(mask);
|
||||
|
||||
// We edit the button directly instead of using `NSResizableWindowMask`,
|
||||
// since that mask also affect the resizability of the window (which is
|
||||
|
|
@ -849,7 +893,7 @@ impl WinitWindow {
|
|||
|
||||
#[inline]
|
||||
pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> {
|
||||
util::set_ignore_mouse_events_sync(self, !hittest);
|
||||
self.setIgnoresMouseEvents(!hittest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -862,14 +906,14 @@ impl WinitWindow {
|
|||
NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
|
||||
let needs_temp_mask = !curr_mask.contains(required);
|
||||
if needs_temp_mask {
|
||||
self.set_style_mask_sync(required);
|
||||
self.set_style_mask(required);
|
||||
}
|
||||
|
||||
let is_zoomed = self.isZoomed();
|
||||
|
||||
// Roll back temp styles
|
||||
if needs_temp_mask {
|
||||
self.set_style_mask_sync(curr_mask);
|
||||
self.set_style_mask(curr_mask);
|
||||
}
|
||||
|
||||
is_zoomed
|
||||
|
|
@ -900,7 +944,7 @@ impl WinitWindow {
|
|||
|
||||
drop(shared_state_lock);
|
||||
|
||||
self.set_style_mask_sync(mask);
|
||||
self.set_style_mask(mask);
|
||||
self.set_maximized(maximized);
|
||||
}
|
||||
|
||||
|
|
@ -929,7 +973,38 @@ impl WinitWindow {
|
|||
if is_zoomed == maximized {
|
||||
return;
|
||||
};
|
||||
util::set_maximized_sync(self, is_zoomed, maximized);
|
||||
|
||||
let mut shared_state = self.lock_shared_state("set_maximized");
|
||||
// Save the standard frame sized if it is not zoomed
|
||||
if !is_zoomed {
|
||||
shared_state.standard_frame = Some(self.frame());
|
||||
}
|
||||
|
||||
shared_state.maximized = maximized;
|
||||
|
||||
if shared_state.fullscreen.is_some() {
|
||||
// Handle it in window_did_exit_fullscreen
|
||||
return;
|
||||
}
|
||||
|
||||
if self
|
||||
.styleMask()
|
||||
.contains(NSWindowStyleMask::NSResizableWindowMask)
|
||||
{
|
||||
drop(shared_state);
|
||||
// Just use the native zoom if resizable
|
||||
self.zoom(None);
|
||||
} else {
|
||||
// if it's not resizable, we set the frame directly
|
||||
let new_rect = if maximized {
|
||||
let screen = NSScreen::main().expect("no screen found");
|
||||
screen.visibleFrame()
|
||||
} else {
|
||||
shared_state.saved_standard_frame()
|
||||
};
|
||||
drop(shared_state);
|
||||
self.setFrame_display(new_rect, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -985,7 +1060,7 @@ impl WinitWindow {
|
|||
// 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_sync(self, screen_frame.origin);
|
||||
self.setFrameTopLeftPoint(screen_frame.origin);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1061,22 +1136,43 @@ impl WinitWindow {
|
|||
|
||||
self.lock_shared_state("set_fullscreen").fullscreen = fullscreen.clone();
|
||||
|
||||
match (&old_fullscreen, &fullscreen) {
|
||||
(&None, &Some(_)) => {
|
||||
util::toggle_full_screen_sync(self, old_fullscreen.is_none());
|
||||
fn toggle_fullscreen(window: &WinitWindow) {
|
||||
// Window level must be restored from `CGShieldingWindowLevel()
|
||||
// + 1` back to normal in order for `toggleFullScreen` to do
|
||||
// anything
|
||||
window.setLevel(NSWindowLevel::Normal);
|
||||
window.toggleFullScreen(None);
|
||||
}
|
||||
|
||||
match (old_fullscreen, fullscreen) {
|
||||
(None, Some(_)) => {
|
||||
// `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
|
||||
// set a normal style temporarily. The previous state will be
|
||||
// restored in `WindowDelegate::window_did_exit_fullscreen`.
|
||||
let curr_mask = self.styleMask();
|
||||
let required = NSWindowStyleMask::NSTitledWindowMask
|
||||
| NSWindowStyleMask::NSResizableWindowMask;
|
||||
if !curr_mask.contains(required) {
|
||||
self.set_style_mask(required);
|
||||
self.lock_shared_state("set_fullscreen").saved_style = Some(curr_mask);
|
||||
}
|
||||
toggle_fullscreen(self);
|
||||
}
|
||||
(&Some(Fullscreen::Borderless(_)), &None) => {
|
||||
(Some(Fullscreen::Borderless(_)), None) => {
|
||||
// State is restored by `window_did_exit_fullscreen`
|
||||
util::toggle_full_screen_sync(self, old_fullscreen.is_none());
|
||||
toggle_fullscreen(self);
|
||||
}
|
||||
(&Some(Fullscreen::Exclusive(ref video_mode)), &None) => {
|
||||
(Some(Fullscreen::Exclusive(ref video_mode)), None) => {
|
||||
unsafe {
|
||||
util::restore_display_mode_sync(video_mode.monitor().native_identifier())
|
||||
ffi::CGRestorePermanentDisplayConfiguration();
|
||||
assert_eq!(
|
||||
ffi::CGDisplayRelease(video_mode.monitor().native_identifier()),
|
||||
ffi::kCGErrorSuccess
|
||||
);
|
||||
};
|
||||
// Rest of the state is restored by `window_did_exit_fullscreen`
|
||||
util::toggle_full_screen_sync(self, old_fullscreen.is_none());
|
||||
toggle_fullscreen(self);
|
||||
}
|
||||
(&Some(Fullscreen::Borderless(_)), &Some(Fullscreen::Exclusive(_))) => {
|
||||
(Some(Fullscreen::Borderless(_)), Some(Fullscreen::Exclusive(_))) => {
|
||||
// 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
|
||||
|
|
@ -1099,7 +1195,7 @@ impl WinitWindow {
|
|||
NSWindowLevel(unsafe { ffi::CGShieldingWindowLevel() } as NSInteger + 1);
|
||||
self.setLevel(window_level);
|
||||
}
|
||||
(&Some(Fullscreen::Exclusive(ref video_mode)), &Some(Fullscreen::Borderless(_))) => {
|
||||
(Some(Fullscreen::Exclusive(ref video_mode)), Some(Fullscreen::Borderless(_))) => {
|
||||
let presentation_options = self
|
||||
.lock_shared_state("set_fullscreen")
|
||||
.save_presentation_opts
|
||||
|
|
@ -1111,7 +1207,11 @@ impl WinitWindow {
|
|||
NSApp().setPresentationOptions(presentation_options);
|
||||
|
||||
unsafe {
|
||||
util::restore_display_mode_sync(video_mode.monitor().native_identifier())
|
||||
ffi::CGRestorePermanentDisplayConfiguration();
|
||||
assert_eq!(
|
||||
ffi::CGDisplayRelease(video_mode.monitor().native_identifier()),
|
||||
ffi::kCGErrorSuccess
|
||||
);
|
||||
};
|
||||
|
||||
// Restore the normal window level following the Borderless fullscreen
|
||||
|
|
@ -1156,7 +1256,7 @@ impl WinitWindow {
|
|||
}
|
||||
new_mask
|
||||
};
|
||||
self.set_style_mask_sync(new_mask);
|
||||
self.set_style_mask(new_mask);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1171,7 +1271,7 @@ impl WinitWindow {
|
|||
WindowLevel::AlwaysOnBottom => NSWindowLevel::BELOW_NORMAL,
|
||||
WindowLevel::Normal => NSWindowLevel::Normal,
|
||||
};
|
||||
util::set_level_sync(self, level);
|
||||
self.setLevel(level);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1191,7 +1291,7 @@ impl WinitWindow {
|
|||
let scale_factor = self.scale_factor();
|
||||
let logical_spot = spot.to_logical(scale_factor);
|
||||
let size = size.to_logical(scale_factor);
|
||||
util::set_ime_cursor_area_sync(self, logical_spot, size);
|
||||
self.view().set_ime_cursor_area(logical_spot, size);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -1209,7 +1309,7 @@ impl WinitWindow {
|
|||
|
||||
if !is_minimized && is_visible {
|
||||
NSApp().activateIgnoringOtherApps(true);
|
||||
util::make_key_and_order_front_sync(self);
|
||||
self.makeKeyAndOrderFront(None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1263,9 +1363,9 @@ impl WinitWindow {
|
|||
fn toggle_style_mask(&self, mask: NSWindowStyleMask, on: bool) {
|
||||
let current_style_mask = self.styleMask();
|
||||
if on {
|
||||
util::set_style_mask_sync(self, current_style_mask | mask);
|
||||
self.set_style_mask(current_style_mask | mask);
|
||||
} else {
|
||||
util::set_style_mask_sync(self, current_style_mask & (!mask));
|
||||
self.set_style_mask(current_style_mask & (!mask));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1360,7 +1460,7 @@ impl WindowExtMacOS for WinitWindow {
|
|||
true
|
||||
} else {
|
||||
let new_mask = self.saved_style(&mut shared_state_lock);
|
||||
self.set_style_mask_sync(new_mask);
|
||||
self.set_style_mask(new_mask);
|
||||
shared_state_lock.is_simple_fullscreen = false;
|
||||
|
||||
let save_presentation_opts = shared_state_lock.save_presentation_opts;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,14 @@ impl Window {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) {
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Self) -> R + Send) -> R {
|
||||
f(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn id(&self) -> WindowId {
|
||||
WindowId {
|
||||
|
|
|
|||
|
|
@ -96,16 +96,6 @@ impl<const SYNC: bool, T, E> MainThreadSafe<SYNC, T, E> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn with_mut<R>(&self, f: impl FnOnce(&mut T) -> R) -> Option<R> {
|
||||
Self::MAIN_THREAD.with(|is_main_thread| {
|
||||
if *is_main_thread.deref() {
|
||||
Some(f(self.value.write().unwrap().as_mut().unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<const SYNC: bool, T, E> Clone for MainThreadSafe<SYNC, T, E> {
|
||||
|
|
@ -219,17 +209,15 @@ impl<T> AsyncReceiver<T> {
|
|||
|
||||
pub struct Dispatcher<T: 'static>(MainThreadSafe<true, T, Closure<T>>);
|
||||
|
||||
pub enum Closure<T> {
|
||||
Ref(Box<dyn FnOnce(&T) + Send>),
|
||||
RefMut(Box<dyn FnOnce(&mut T) + Send>),
|
||||
}
|
||||
pub struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
||||
|
||||
impl<T> Dispatcher<T> {
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Option<Self> {
|
||||
MainThreadSafe::new(value, |value, closure| match closure {
|
||||
Closure::Ref(f) => f(value.read().unwrap().as_ref().unwrap()),
|
||||
Closure::RefMut(f) => f(value.write().unwrap().as_mut().unwrap()),
|
||||
MainThreadSafe::new(value, |value, Closure(closure)| {
|
||||
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything
|
||||
// funny with it here. See `Self::queue()`.
|
||||
closure(value.read().unwrap().as_ref().unwrap())
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
|
|
@ -238,30 +226,26 @@ impl<T> Dispatcher<T> {
|
|||
if self.is_main_thread() {
|
||||
self.0.with(f).unwrap()
|
||||
} else {
|
||||
self.0.send(Closure::Ref(Box::new(f)))
|
||||
self.0.send(Closure(Box::new(f)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch_mut(&self, f: impl 'static + FnOnce(&mut T) + Send) {
|
||||
if self.is_main_thread() {
|
||||
self.0.with_mut(f).unwrap()
|
||||
} else {
|
||||
self.0.send(Closure::RefMut(Box::new(f)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue<R: 'static + Send>(&self, f: impl 'static + FnOnce(&T) -> R + Send) -> R {
|
||||
pub fn queue<R: Send>(&self, f: impl FnOnce(&T) -> R + Send) -> R {
|
||||
if self.is_main_thread() {
|
||||
self.0.with(f).unwrap()
|
||||
} else {
|
||||
let pair = Arc::new((Mutex::new(None), Condvar::new()));
|
||||
let closure = Closure::Ref(Box::new({
|
||||
let closure = Box::new({
|
||||
let pair = pair.clone();
|
||||
move |value| {
|
||||
move |value: &T| {
|
||||
*pair.0.lock().unwrap() = Some(f(value));
|
||||
pair.1.notify_one();
|
||||
}
|
||||
}));
|
||||
}) as Box<dyn FnOnce(&T) + Send>;
|
||||
// SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is
|
||||
// safe because this function won't return until `f` has finished executing. See
|
||||
// `Self::new()`.
|
||||
let closure = Closure(unsafe { std::mem::transmute(closure) });
|
||||
|
||||
self.0.send(closure);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ pub use self::resize_scaling::ResizeScaleHandle;
|
|||
pub use self::timeout::{IdleCallback, Timeout};
|
||||
|
||||
use crate::dpi::{LogicalPosition, LogicalSize};
|
||||
use crate::platform::web::WindowExtWebSys;
|
||||
use crate::window::Window;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use web_sys::{
|
||||
CssStyleDeclaration, Document, Element, HtmlCanvasElement, PageTransitionEvent, VisibilityState,
|
||||
|
|
@ -52,12 +50,6 @@ pub fn on_page_transition(
|
|||
}
|
||||
}
|
||||
|
||||
impl WindowExtWebSys for Window {
|
||||
fn canvas(&self) -> Option<HtmlCanvasElement> {
|
||||
self.window.canvas()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_factor(window: &web_sys::Window) -> f64 {
|
||||
window.device_pixel_ratio()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,24 +13,23 @@ use super::r#async::Dispatcher;
|
|||
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::vec_deque::IntoIter as VecDequeIter;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Window {
|
||||
id: WindowId,
|
||||
has_focus: Arc<AtomicBool>,
|
||||
pub inner: Dispatcher<Inner>,
|
||||
inner: Dispatcher<Inner>,
|
||||
}
|
||||
|
||||
pub struct Inner {
|
||||
id: WindowId,
|
||||
pub window: web_sys::Window,
|
||||
document: Document,
|
||||
canvas: Rc<RefCell<backend::Canvas>>,
|
||||
previous_pointer: RefCell<&'static str>,
|
||||
destroy_fn: Option<Box<dyn FnOnce()>>,
|
||||
has_focus: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -55,41 +54,42 @@ impl Window {
|
|||
let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id)));
|
||||
|
||||
let has_focus = canvas.borrow().has_focus.clone();
|
||||
let window = Window {
|
||||
let inner = Inner {
|
||||
id,
|
||||
window: window.clone(),
|
||||
document: document.clone(),
|
||||
canvas,
|
||||
previous_pointer: RefCell::new("auto"),
|
||||
destroy_fn: Some(destroy_fn),
|
||||
has_focus,
|
||||
inner: Dispatcher::new(Inner {
|
||||
window: window.clone(),
|
||||
document: document.clone(),
|
||||
canvas,
|
||||
previous_pointer: RefCell::new("auto"),
|
||||
destroy_fn: Some(destroy_fn),
|
||||
})
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
window.set_title(&attr.title);
|
||||
window.set_maximized(attr.maximized);
|
||||
window.set_visible(attr.visible);
|
||||
window.set_window_icon(attr.window_icon);
|
||||
inner.set_title(&attr.title);
|
||||
inner.set_maximized(attr.maximized);
|
||||
inner.set_visible(attr.visible);
|
||||
inner.set_window_icon(attr.window_icon);
|
||||
|
||||
Ok(window)
|
||||
Ok(Window {
|
||||
inner: Dispatcher::new(inner).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Inner) + Send + 'static) {
|
||||
self.inner.dispatch(f)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Inner) -> R + Send) -> R {
|
||||
self.inner.queue(f)
|
||||
}
|
||||
|
||||
pub fn canvas(&self) -> Option<HtmlCanvasElement> {
|
||||
self.inner.with(|inner| inner.canvas.borrow().raw().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
pub fn set_title(&self, title: &str) {
|
||||
if self
|
||||
.inner
|
||||
.with(|inner| inner.canvas.borrow().set_attribute("alt", title))
|
||||
.is_none()
|
||||
{
|
||||
let title = title.to_owned();
|
||||
self.inner
|
||||
.dispatch(move |inner| inner.canvas.borrow().set_attribute("alt", &title));
|
||||
}
|
||||
self.canvas.borrow().set_attribute("alt", title)
|
||||
}
|
||||
|
||||
pub fn set_transparent(&self, _transparent: bool) {}
|
||||
|
|
@ -104,21 +104,17 @@ impl Window {
|
|||
}
|
||||
|
||||
pub fn request_redraw(&self) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
inner.canvas.borrow().request_animation_frame();
|
||||
});
|
||||
self.canvas.borrow().request_animation_frame();
|
||||
}
|
||||
|
||||
pub fn pre_present_notify(&self) {}
|
||||
|
||||
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
self.inner.queue(|inner| {
|
||||
Ok(inner
|
||||
.canvas
|
||||
.borrow()
|
||||
.position()
|
||||
.to_physical(inner.scale_factor()))
|
||||
})
|
||||
Ok(self
|
||||
.canvas
|
||||
.borrow()
|
||||
.position()
|
||||
.to_physical(self.scale_factor()))
|
||||
}
|
||||
|
||||
pub fn inner_position(&self) -> Result<PhysicalPosition<i32>, NotSupportedError> {
|
||||
|
|
@ -127,17 +123,15 @@ impl Window {
|
|||
}
|
||||
|
||||
pub fn set_outer_position(&self, position: Position) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
let canvas = inner.canvas.borrow();
|
||||
let position = position.to_logical::<f64>(inner.scale_factor());
|
||||
let canvas = self.canvas.borrow();
|
||||
let position = position.to_logical::<f64>(self.scale_factor());
|
||||
|
||||
backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position)
|
||||
});
|
||||
backend::set_canvas_position(canvas.document(), canvas.raw(), canvas.style(), position)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inner_size(&self) -> PhysicalSize<u32> {
|
||||
self.inner.queue(|inner| inner.canvas.borrow().inner_size())
|
||||
self.canvas.borrow().inner_size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -148,43 +142,24 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn request_inner_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
|
||||
self.inner.dispatch(move |inner| {
|
||||
let size = size.to_logical(inner.scale_factor());
|
||||
let canvas = inner.canvas.borrow();
|
||||
backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size);
|
||||
});
|
||||
|
||||
let size = size.to_logical(self.scale_factor());
|
||||
let canvas = self.canvas.borrow();
|
||||
backend::set_canvas_size(canvas.document(), canvas.raw(), canvas.style(), size);
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
let dimensions =
|
||||
dimensions.map(|dimensions| dimensions.to_logical(inner.scale_factor()));
|
||||
let canvas = inner.canvas.borrow();
|
||||
backend::set_canvas_min_size(
|
||||
canvas.document(),
|
||||
canvas.raw(),
|
||||
canvas.style(),
|
||||
dimensions,
|
||||
)
|
||||
})
|
||||
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
||||
let canvas = self.canvas.borrow();
|
||||
backend::set_canvas_min_size(canvas.document(), canvas.raw(), canvas.style(), dimensions)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_max_inner_size(&self, dimensions: Option<Size>) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
let dimensions =
|
||||
dimensions.map(|dimensions| dimensions.to_logical(inner.scale_factor()));
|
||||
let canvas = inner.canvas.borrow();
|
||||
backend::set_canvas_max_size(
|
||||
canvas.document(),
|
||||
canvas.raw(),
|
||||
canvas.style(),
|
||||
dimensions,
|
||||
)
|
||||
})
|
||||
let dimensions = dimensions.map(|dimensions| dimensions.to_logical(self.scale_factor()));
|
||||
let canvas = self.canvas.borrow();
|
||||
backend::set_canvas_max_size(canvas.document(), canvas.raw(), canvas.style(), dimensions)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -216,19 +191,13 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
self.inner.queue(|inner| inner.scale_factor())
|
||||
super::backend::scale_factor(&self.window)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
*inner.previous_pointer.borrow_mut() = cursor.name();
|
||||
backend::set_canvas_style_property(
|
||||
inner.canvas.borrow().raw(),
|
||||
"cursor",
|
||||
cursor.name(),
|
||||
);
|
||||
});
|
||||
*self.previous_pointer.borrow_mut() = cursor.name();
|
||||
backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", cursor.name());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -238,36 +207,31 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
|
||||
self.inner.queue(move |inner| {
|
||||
let lock = match mode {
|
||||
CursorGrabMode::None => false,
|
||||
CursorGrabMode::Locked => true,
|
||||
CursorGrabMode::Confined => {
|
||||
return Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
};
|
||||
let lock = match mode {
|
||||
CursorGrabMode::None => false,
|
||||
CursorGrabMode::Locked => true,
|
||||
CursorGrabMode::Confined => {
|
||||
return Err(ExternalError::NotSupported(NotSupportedError::new()))
|
||||
}
|
||||
};
|
||||
|
||||
inner
|
||||
.canvas
|
||||
.borrow()
|
||||
.set_cursor_lock(lock)
|
||||
.map_err(ExternalError::Os)
|
||||
})
|
||||
self.canvas
|
||||
.borrow()
|
||||
.set_cursor_lock(lock)
|
||||
.map_err(ExternalError::Os)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_cursor_visible(&self, visible: bool) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
if !visible {
|
||||
backend::set_canvas_style_property(inner.canvas.borrow().raw(), "cursor", "none");
|
||||
} else {
|
||||
backend::set_canvas_style_property(
|
||||
inner.canvas.borrow().raw(),
|
||||
"cursor",
|
||||
&inner.previous_pointer.borrow(),
|
||||
);
|
||||
}
|
||||
});
|
||||
if !visible {
|
||||
backend::set_canvas_style_property(self.canvas.borrow().raw(), "cursor", "none");
|
||||
} else {
|
||||
backend::set_canvas_style_property(
|
||||
self.canvas.borrow().raw(),
|
||||
"cursor",
|
||||
&self.previous_pointer.borrow(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -309,24 +273,20 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub(crate) fn fullscreen(&self) -> Option<Fullscreen> {
|
||||
self.inner.queue(|inner| {
|
||||
if inner.canvas.borrow().is_fullscreen() {
|
||||
Some(Fullscreen::Borderless(Some(MonitorHandle)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
if self.canvas.borrow().is_fullscreen() {
|
||||
Some(Fullscreen::Borderless(Some(MonitorHandle)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
|
||||
self.inner.dispatch(move |inner| {
|
||||
if fullscreen.is_some() {
|
||||
inner.canvas.borrow().request_fullscreen();
|
||||
} else if inner.canvas.borrow().is_fullscreen() {
|
||||
backend::exit_fullscreen(&inner.document);
|
||||
}
|
||||
});
|
||||
if fullscreen.is_some() {
|
||||
self.canvas.borrow().request_fullscreen();
|
||||
} else if self.canvas.borrow().is_fullscreen() {
|
||||
backend::exit_fullscreen(&self.document);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -365,9 +325,7 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn focus_window(&self) {
|
||||
self.inner.dispatch(|inner| {
|
||||
let _ = inner.canvas.borrow().raw().focus();
|
||||
})
|
||||
let _ = self.canvas.borrow().raw().focus();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -381,8 +339,8 @@ impl Window {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn available_monitors(&self) -> VecDequeIter<MonitorHandle> {
|
||||
VecDeque::new().into_iter()
|
||||
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
||||
VecDeque::new()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -412,14 +370,12 @@ impl Window {
|
|||
|
||||
#[inline]
|
||||
pub fn theme(&self) -> Option<Theme> {
|
||||
self.inner.queue(|inner| {
|
||||
backend::is_dark_mode(&inner.window).map(|is_dark_mode| {
|
||||
if is_dark_mode {
|
||||
Theme::Dark
|
||||
} else {
|
||||
Theme::Light
|
||||
}
|
||||
})
|
||||
backend::is_dark_mode(&self.window).map(|is_dark_mode| {
|
||||
if is_dark_mode {
|
||||
Theme::Dark
|
||||
} else {
|
||||
Theme::Light
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -437,23 +393,13 @@ impl Window {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
impl Drop for Inner {
|
||||
fn drop(&mut self) {
|
||||
self.inner.dispatch_mut(|inner| {
|
||||
if let Some(destroy_fn) = inner.destroy_fn.take() {
|
||||
destroy_fn();
|
||||
}
|
||||
});
|
||||
if let Some(destroy_fn) = self.destroy_fn.take() {
|
||||
destroy_fn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
#[inline]
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
super::backend::scale_factor(&self.window)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct WindowId(pub(crate) u32);
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ impl Window {
|
|||
unsafe { init(w_attr, pl_attr, event_loop) }
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) {
|
||||
// TODO: Use `thread_executor` here
|
||||
f(self)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(&self, f: impl FnOnce(&Self) -> R + Send) -> R {
|
||||
// TODO: Use `thread_executor` here
|
||||
f(self)
|
||||
}
|
||||
|
||||
fn window_state_lock(&self) -> MutexGuard<'_, WindowState> {
|
||||
self.window_state.lock().unwrap()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue