Web: Implement MonitorHandle (#3801)
Requires getting permission from the user to get "detailed" support. Also enables users to go fullscreen on specific monitors. Exposes platform-specific orientation API. Most functionality depends on browser support, currently only Chromium.
This commit is contained in:
parent
2e97ab3d4f
commit
a0bc3e5dc8
21 changed files with 1493 additions and 120 deletions
|
|
@ -1,4 +1,7 @@
|
|||
use std::cell::Ref;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
|
|
@ -9,6 +12,44 @@ pub struct Dispatcher<T: 'static>(Wrapper<T, Arc<Sender<Closure<T>>>, Closure<T>
|
|||
|
||||
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
||||
|
||||
impl<T> Clone for Dispatcher<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Debug for Dispatcher<T> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Dispatcher").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for Dispatcher<T> {}
|
||||
|
||||
impl<T> Hash for Dispatcher<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Ord for Dispatcher<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for Dispatcher<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.eq(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialOrd for Dispatcher<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dispatcher<T> {
|
||||
pub fn new(main_thread: MainThreadMarker, value: T) -> (Self, DispatchRunner<T>) {
|
||||
let (sender, receiver) = channel::<Closure<T>>();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::cell::{Ref, RefCell};
|
||||
use std::cmp;
|
||||
use std::future::Future;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -81,3 +83,29 @@ impl<V, S: Clone + Send, E> Clone for Wrapper<V, S, E> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, S: Clone + Send, E> Eq for Wrapper<V, S, E> {}
|
||||
|
||||
impl<V, S: Clone + Send, E> Hash for Wrapper<V, S, E> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Arc::as_ptr(&self.value.value).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, S: Clone + Send, E> Ord for Wrapper<V, S, E> {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
Arc::as_ptr(&self.value.value).cmp(&Arc::as_ptr(&other.value.value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, S: Clone + Send, E> PartialOrd for Wrapper<V, S, E> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, S: Clone + Send, E> PartialEq for Wrapper<V, S, E> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.value.value, &other.value.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use super::{backend, device, window};
|
||||
use super::{backend, device, window, HasMonitorPermissionFuture, MonitorPermissionFuture};
|
||||
use crate::application::ApplicationHandler;
|
||||
use crate::error::EventLoopError;
|
||||
use crate::error::{EventLoopError, NotSupportedError};
|
||||
use crate::event::Event;
|
||||
use crate::event_loop::ActiveEventLoop as RootActiveEventLoop;
|
||||
use crate::platform::web::{ActiveEventLoopExtWeb, PollStrategy, WaitUntilStrategy};
|
||||
|
|
@ -77,6 +77,18 @@ impl EventLoop {
|
|||
pub fn wait_until_strategy(&self) -> WaitUntilStrategy {
|
||||
self.elw.wait_until_strategy()
|
||||
}
|
||||
|
||||
pub fn has_multiple_screens(&self) -> Result<bool, NotSupportedError> {
|
||||
self.elw.has_multiple_screens()
|
||||
}
|
||||
|
||||
pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
|
||||
self.elw.request_detailed_monitor_permission().0
|
||||
}
|
||||
|
||||
pub fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture {
|
||||
self.elw.p.runner.monitor().has_detailed_monitor_permission_async()
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event<A: ApplicationHandler>(app: &mut A, target: &RootActiveEventLoop, event: Event) {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ use std::rc::{Rc, Weak};
|
|||
use js_sys::Function;
|
||||
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Document, KeyboardEvent, PageTransitionEvent, PointerEvent, WheelEvent};
|
||||
use web_sys::{Document, KeyboardEvent, Navigator, PageTransitionEvent, PointerEvent, WheelEvent};
|
||||
use web_time::{Duration, Instant};
|
||||
|
||||
use super::super::main_thread::MainThreadMarker;
|
||||
use super::super::monitor::MonitorHandler;
|
||||
use super::super::DeviceId;
|
||||
use super::backend;
|
||||
use super::state::State;
|
||||
|
|
@ -51,11 +52,13 @@ struct Execution {
|
|||
events: RefCell<VecDeque<EventWrapper>>,
|
||||
id: RefCell<u32>,
|
||||
window: web_sys::Window,
|
||||
navigator: Navigator,
|
||||
document: Document,
|
||||
#[allow(clippy::type_complexity)]
|
||||
all_canvases: RefCell<Vec<(WindowId, Weak<backend::Canvas>, DispatchRunner<Inner>)>>,
|
||||
redraw_pending: RefCell<HashSet<WindowId>>,
|
||||
destroy_pending: RefCell<VecDeque<WindowId>>,
|
||||
pub(crate) monitor: Rc<MonitorHandler>,
|
||||
page_transition_event_handle: RefCell<Option<backend::PageTransitionEventHandle>>,
|
||||
device_events: Cell<DeviceEvents>,
|
||||
on_mouse_move: OnEventHandle<PointerEvent>,
|
||||
|
|
@ -70,6 +73,8 @@ struct Execution {
|
|||
enum RunnerEnum {
|
||||
/// The `EventLoop` is created but not being run.
|
||||
Pending,
|
||||
/// The `EventLoop` is running some async initialization and is waiting to be started.
|
||||
Initializing(Runner),
|
||||
/// The `EventLoop` is being run.
|
||||
Running(Runner),
|
||||
/// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since
|
||||
|
|
@ -134,6 +139,8 @@ impl Shared {
|
|||
#[allow(clippy::disallowed_methods)]
|
||||
let window = web_sys::window().expect("only callable from inside the `Window`");
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let navigator = window.navigator();
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let document = window.document().expect("Failed to obtain document");
|
||||
|
||||
Shared(Rc::<Execution>::new_cyclic(|weak| {
|
||||
|
|
@ -144,6 +151,13 @@ impl Shared {
|
|||
}
|
||||
});
|
||||
|
||||
let monitor = MonitorHandler::new(
|
||||
main_thread,
|
||||
window.clone(),
|
||||
&navigator,
|
||||
WeakShared(weak.clone()),
|
||||
);
|
||||
|
||||
Execution {
|
||||
main_thread,
|
||||
proxy_spawner,
|
||||
|
|
@ -156,11 +170,13 @@ impl Shared {
|
|||
event_loop_recreation: Cell::new(false),
|
||||
events: RefCell::new(VecDeque::new()),
|
||||
window,
|
||||
navigator,
|
||||
document,
|
||||
id: RefCell::new(0),
|
||||
all_canvases: RefCell::new(Vec::new()),
|
||||
redraw_pending: RefCell::new(HashSet::new()),
|
||||
destroy_pending: RefCell::new(VecDeque::new()),
|
||||
monitor: Rc::new(monitor),
|
||||
page_transition_event_handle: RefCell::new(None),
|
||||
device_events: Cell::default(),
|
||||
on_mouse_move: RefCell::new(None),
|
||||
|
|
@ -182,6 +198,10 @@ impl Shared {
|
|||
&self.0.window
|
||||
}
|
||||
|
||||
pub fn navigator(&self) -> &Navigator {
|
||||
&self.0.navigator
|
||||
}
|
||||
|
||||
pub fn document(&self) -> &Document {
|
||||
&self.0.document
|
||||
}
|
||||
|
|
@ -199,17 +219,38 @@ impl Shared {
|
|||
self.0.destroy_pending.borrow_mut().push_back(id);
|
||||
}
|
||||
|
||||
pub(crate) fn start(&self, event_handler: Box<EventHandler>) {
|
||||
let mut runner = self.0.runner.borrow_mut();
|
||||
assert!(matches!(*runner, RunnerEnum::Pending));
|
||||
if self.0.monitor.is_initializing() {
|
||||
*runner = RunnerEnum::Initializing(Runner::new(event_handler));
|
||||
} else {
|
||||
*runner = RunnerEnum::Running(Runner::new(event_handler));
|
||||
|
||||
drop(runner);
|
||||
|
||||
self.init();
|
||||
self.set_listener();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start_delayed(&self) {
|
||||
let event_handler = match self.0.runner.replace(RunnerEnum::Pending) {
|
||||
RunnerEnum::Initializing(event_handler) => event_handler,
|
||||
// The event loop wasn't started yet.
|
||||
RunnerEnum::Pending => return,
|
||||
_ => unreachable!("event loop already started before waiting for initialization"),
|
||||
};
|
||||
*self.0.runner.borrow_mut() = RunnerEnum::Running(event_handler);
|
||||
|
||||
self.init();
|
||||
self.set_listener();
|
||||
}
|
||||
|
||||
// Set the event callback to use for the event loop runner
|
||||
// This the event callback is a fairly thin layer over the user-provided callback that closes
|
||||
// over a RootActiveEventLoop reference
|
||||
pub(crate) fn set_listener(&self, event_handler: Box<EventHandler>) {
|
||||
{
|
||||
let mut runner = self.0.runner.borrow_mut();
|
||||
assert!(matches!(*runner, RunnerEnum::Pending));
|
||||
*runner = RunnerEnum::Running(Runner::new(event_handler));
|
||||
}
|
||||
self.init();
|
||||
|
||||
fn set_listener(&self) {
|
||||
*self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition(
|
||||
self.window().clone(),
|
||||
{
|
||||
|
|
@ -236,6 +277,7 @@ impl Shared {
|
|||
|
||||
let runner = self.clone();
|
||||
let window = self.window().clone();
|
||||
let navigator = self.navigator().clone();
|
||||
*self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new(
|
||||
self.window().clone(),
|
||||
"pointermove",
|
||||
|
|
@ -263,7 +305,7 @@ impl Shared {
|
|||
}
|
||||
|
||||
// pointer move event
|
||||
let mut delta = backend::event::MouseDelta::init(&window, &event);
|
||||
let mut delta = backend::event::MouseDelta::init(&navigator, &event);
|
||||
runner.send_events(backend::event::pointer_move_event(event).flat_map(|event| {
|
||||
let delta = delta.delta(&event).to_physical(backend::scale_factor(&window));
|
||||
|
||||
|
|
@ -419,7 +461,7 @@ impl Shared {
|
|||
self.send_events::<EventWrapper>(iter::empty());
|
||||
}
|
||||
|
||||
pub fn init(&self) {
|
||||
fn init(&self) {
|
||||
// NB: For consistency all platforms must call `can_create_surfaces` even though Web
|
||||
// applications don't themselves have a formal surface destroy/create lifecycle.
|
||||
self.run_until_cleared(
|
||||
|
|
@ -501,8 +543,8 @@ impl Shared {
|
|||
match self.0.runner.try_borrow().as_ref().map(Deref::deref) {
|
||||
// If the runner is attached but not running, we always wake it up.
|
||||
Ok(RunnerEnum::Running(_)) => (),
|
||||
Ok(RunnerEnum::Pending) => {
|
||||
// The runner still hasn't been attached: queue this event and wait for it to be
|
||||
// The runner still hasn't been attached: queue this event and wait for it to be
|
||||
Ok(RunnerEnum::Pending | RunnerEnum::Initializing(_)) => {
|
||||
process_immediately = false;
|
||||
},
|
||||
// Some other code is mutating the runner, which most likely means
|
||||
|
|
@ -605,6 +647,8 @@ impl Shared {
|
|||
RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event.into()),
|
||||
// If the Runner has been destroyed, there is nothing to do.
|
||||
RunnerEnum::Destroyed => return,
|
||||
// This function should never be called if we are still waiting for something.
|
||||
RunnerEnum::Initializing(_) => unreachable!(),
|
||||
}
|
||||
|
||||
let is_closed = self.exiting();
|
||||
|
|
@ -730,6 +774,8 @@ impl Shared {
|
|||
Ok(RunnerEnum::Pending) => false,
|
||||
// The event loop is closed since it has been destroyed.
|
||||
Ok(RunnerEnum::Destroyed) => true,
|
||||
// The event loop is not closed since its still waiting to be started.
|
||||
Ok(RunnerEnum::Initializing(_)) => false,
|
||||
// Some other code is mutating the runner, which most likely means
|
||||
// the event loop is running and busy.
|
||||
Err(_) => false,
|
||||
|
|
@ -795,6 +841,14 @@ impl Shared {
|
|||
pub(crate) fn waker(&self) -> Waker<WeakShared> {
|
||||
self.0.proxy_spawner.waker()
|
||||
}
|
||||
|
||||
pub(crate) fn weak(&self) -> WeakShared {
|
||||
WeakShared(Rc::downgrade(&self.0))
|
||||
}
|
||||
|
||||
pub(crate) fn monitor(&self) -> &Rc<MonitorHandler> {
|
||||
&self.0.monitor
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use std::cell::Cell;
|
||||
use std::clone::Clone;
|
||||
use std::collections::vec_deque::IntoIter as VecDequeIter;
|
||||
use std::collections::VecDeque;
|
||||
use std::iter;
|
||||
use std::rc::Rc;
|
||||
|
||||
use web_sys::Element;
|
||||
|
||||
use super::super::monitor::MonitorHandle;
|
||||
use super::super::monitor::{MonitorHandle, MonitorPermissionFuture};
|
||||
use super::super::{lock, KeyEventExtra};
|
||||
use super::device::DeviceId;
|
||||
use super::runner::{EventWrapper, WeakShared};
|
||||
use super::window::WindowId;
|
||||
use super::{backend, runner, EventLoopProxy};
|
||||
use crate::error::NotSupportedError;
|
||||
use crate::event::{
|
||||
DeviceId as RootDeviceId, ElementState, Event, KeyEvent, Touch, TouchPhase, WindowEvent,
|
||||
};
|
||||
|
|
@ -61,7 +60,7 @@ impl ActiveEventLoop {
|
|||
event_loop_recreation: bool,
|
||||
) {
|
||||
self.runner.event_loop_recreation(event_loop_recreation);
|
||||
self.runner.set_listener(event_handler);
|
||||
self.runner.start(event_handler);
|
||||
}
|
||||
|
||||
pub fn generate_id(&self) -> WindowId {
|
||||
|
|
@ -594,12 +593,12 @@ impl ActiveEventLoop {
|
|||
canvas.on_context_menu();
|
||||
}
|
||||
|
||||
pub fn available_monitors(&self) -> VecDequeIter<MonitorHandle> {
|
||||
VecDeque::new().into_iter()
|
||||
pub fn available_monitors(&self) -> Vec<MonitorHandle> {
|
||||
self.runner.monitor().available_monitors()
|
||||
}
|
||||
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
None
|
||||
self.runner.monitor().primary_monitor()
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_05")]
|
||||
|
|
@ -653,7 +652,19 @@ impl ActiveEventLoop {
|
|||
}
|
||||
|
||||
pub(crate) fn is_cursor_lock_raw(&self) -> bool {
|
||||
lock::is_cursor_lock_raw(self.runner.window(), self.runner.document())
|
||||
lock::is_cursor_lock_raw(self.runner.navigator(), self.runner.document())
|
||||
}
|
||||
|
||||
pub(crate) fn has_multiple_screens(&self) -> Result<bool, NotSupportedError> {
|
||||
self.runner.monitor().is_extended().ok_or(NotSupportedError::new())
|
||||
}
|
||||
|
||||
pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
|
||||
self.runner.monitor().request_detailed_monitor_permission(self.runner.weak())
|
||||
}
|
||||
|
||||
pub(crate) fn has_detailed_monitor_permission(&self) -> bool {
|
||||
self.runner.monitor().has_detailed_monitor_permission()
|
||||
}
|
||||
|
||||
pub(crate) fn waker(&self) -> Waker<WeakShared> {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ use tracing::error;
|
|||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{console, Document, DomException, Element, Window};
|
||||
use web_sys::{console, Document, DomException, Element, Navigator};
|
||||
|
||||
pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool {
|
||||
pub(crate) fn is_cursor_lock_raw(navigator: &Navigator, document: &Document) -> bool {
|
||||
thread_local! {
|
||||
static IS_CURSOR_LOCK_RAW: OnceCell<bool> = const { OnceCell::new() };
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool {
|
|||
// TODO: Remove when Chrome can better advertise that they don't support unaccelerated
|
||||
// movement on Linux.
|
||||
// See <https://issues.chromium.org/issues/40833850>.
|
||||
if super::web_sys::chrome_linux(window) {
|
||||
if super::web_sys::chrome_linux(navigator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +39,8 @@ pub(crate) fn is_cursor_lock_raw(window: &Window, document: &Document) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn request_pointer_lock(window: &Window, document: &Document, element: &Element) {
|
||||
if is_cursor_lock_raw(window, document) {
|
||||
pub(crate) fn request_pointer_lock(navigator: &Navigator, document: &Document, element: &Element) {
|
||||
if is_cursor_lock_raw(navigator, document) {
|
||||
thread_local! {
|
||||
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|error: JsValue| {
|
||||
if let Some(error) = error.dyn_ref::<DomException>() {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ pub(crate) use self::event_loop::{
|
|||
PlatformSpecificEventLoopAttributes,
|
||||
};
|
||||
pub(crate) use self::keyboard::KeyEventExtra;
|
||||
pub use self::monitor::{MonitorHandle, VideoModeHandle};
|
||||
pub(crate) use self::monitor::{
|
||||
HasMonitorPermissionFuture, MonitorHandle, MonitorPermissionFuture, OrientationLockFuture,
|
||||
VideoModeHandle,
|
||||
};
|
||||
use self::web_sys as backend;
|
||||
pub use self::window::{PlatformSpecificWindowAttributes, Window, WindowId};
|
||||
pub(crate) use crate::icon::NoIcon as PlatformIcon;
|
||||
|
|
|
|||
|
|
@ -1,53 +1,802 @@
|
|||
use std::iter::Empty;
|
||||
use std::cell::{OnceCell, Ref, RefCell};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::iter::{self, Once};
|
||||
use std::mem;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::task::{ready, Context, Poll};
|
||||
|
||||
use dpi::LogicalSize;
|
||||
use js_sys::{Object, Promise};
|
||||
use tracing::error;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{
|
||||
console, DomException, Navigator, OrientationLockType, OrientationType, PermissionState,
|
||||
PermissionStatus, ScreenOrientation, Window,
|
||||
};
|
||||
|
||||
use super::event_loop::runner::WeakShared;
|
||||
use super::main_thread::MainThreadMarker;
|
||||
use super::r#async::{Dispatcher, Notified, Notifier};
|
||||
use super::web_sys::{Engine, EventListenerHandle};
|
||||
use crate::dpi::{PhysicalPosition, PhysicalSize};
|
||||
use crate::platform::web::{
|
||||
MonitorPermissionError, Orientation, OrientationData, OrientationLock, OrientationLockError,
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct MonitorHandle;
|
||||
#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct MonitorHandle(Dispatcher<Inner>);
|
||||
|
||||
impl MonitorHandle {
|
||||
fn new(main_thread: MainThreadMarker, inner: Inner) -> Self {
|
||||
Self(Dispatcher::new(main_thread, inner).0)
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
unreachable!()
|
||||
self.0.queue(|inner| match &inner.screen {
|
||||
Screen::Screen(_) => 0.,
|
||||
Screen::Detailed(screen) => screen.device_pixel_ratio(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn position(&self) -> PhysicalPosition<i32> {
|
||||
unreachable!()
|
||||
self.0.queue(|inner| {
|
||||
if let Screen::Detailed(screen) = &inner.screen {
|
||||
PhysicalPosition::new(screen.left(), screen.top())
|
||||
} else {
|
||||
PhysicalPosition::default()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
unreachable!()
|
||||
self.0.queue(|inner| {
|
||||
if let Screen::Detailed(screen) = &inner.screen {
|
||||
Some(screen.label())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
|
||||
unreachable!()
|
||||
None
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
unreachable!()
|
||||
self.0.queue(|inner| {
|
||||
let width = inner.screen.width().unwrap();
|
||||
let height = inner.screen.height().unwrap();
|
||||
|
||||
if let Some(Engine::Chromium) = inner.engine {
|
||||
PhysicalSize::new(width, height).cast()
|
||||
} else {
|
||||
LogicalSize::new(width, height)
|
||||
.to_physical(super::web_sys::scale_factor(&inner.window))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn video_modes(&self) -> Empty<VideoModeHandle> {
|
||||
unreachable!()
|
||||
pub fn video_modes(&self) -> Once<VideoModeHandle> {
|
||||
iter::once(VideoModeHandle(self.clone()))
|
||||
}
|
||||
|
||||
pub fn orientation(&self) -> OrientationData {
|
||||
self.0.queue(|inner| {
|
||||
let orientation =
|
||||
inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into());
|
||||
let angle = orientation.angle().unwrap();
|
||||
|
||||
match orientation.type_().unwrap() {
|
||||
OrientationType::LandscapePrimary => OrientationData {
|
||||
orientation: Orientation::Landscape,
|
||||
flipped: false,
|
||||
natural: angle == 0,
|
||||
},
|
||||
OrientationType::LandscapeSecondary => OrientationData {
|
||||
orientation: Orientation::Landscape,
|
||||
flipped: true,
|
||||
natural: angle == 180,
|
||||
},
|
||||
OrientationType::PortraitPrimary => OrientationData {
|
||||
orientation: Orientation::Portrait,
|
||||
flipped: false,
|
||||
natural: angle == 0,
|
||||
},
|
||||
OrientationType::PortraitSecondary => OrientationData {
|
||||
orientation: Orientation::Portrait,
|
||||
flipped: true,
|
||||
natural: angle == 180,
|
||||
},
|
||||
_ => {
|
||||
unreachable!("found unrecognized orientation: {}", orientation.type_string())
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn request_lock(&self, orientation_lock: OrientationLock) -> OrientationLockFuture {
|
||||
// Short-circuit without blocking.
|
||||
if let Some(support) = HAS_LOCK_SUPPORT.with(|support| support.get().cloned()) {
|
||||
if !support {
|
||||
return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported)));
|
||||
}
|
||||
}
|
||||
|
||||
self.0.queue(|inner| {
|
||||
let orientation =
|
||||
inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into());
|
||||
|
||||
if !HAS_LOCK_SUPPORT
|
||||
.with(|support| *support.get_or_init(|| !orientation.has_lock().is_undefined()))
|
||||
{
|
||||
return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported)));
|
||||
}
|
||||
|
||||
let future = JsFuture::from(orientation.lock(orientation_lock.to_js()).unwrap());
|
||||
let notifier = Notifier::new();
|
||||
let notified = notifier.notified();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
notifier.notify(future.await.map(|_| ()).map_err(OrientationLockError::from_js));
|
||||
});
|
||||
|
||||
OrientationLockFuture::Future(notified)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unlock(&self) -> Result<(), OrientationLockError> {
|
||||
// Short-circuit without blocking.
|
||||
if let Some(support) = HAS_LOCK_SUPPORT.with(|support| support.get().cloned()) {
|
||||
if !support {
|
||||
return Err(OrientationLockError::Unsupported);
|
||||
}
|
||||
}
|
||||
|
||||
self.0.queue(|inner| {
|
||||
let orientation =
|
||||
inner.orientation.get_or_init(|| inner.screen.orientation().unchecked_into());
|
||||
|
||||
if !HAS_LOCK_SUPPORT
|
||||
.with(|support| *support.get_or_init(|| !orientation.has_lock().is_undefined()))
|
||||
{
|
||||
return Err(OrientationLockError::Unsupported);
|
||||
}
|
||||
|
||||
orientation.unlock().map_err(OrientationLockError::from_js)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_internal(&self) -> Option<bool> {
|
||||
self.0.queue(|inner| {
|
||||
if let Screen::Detailed(screen) = &inner.screen {
|
||||
Some(screen.is_internal())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_detailed(&self) -> bool {
|
||||
self.0.queue(|inner| matches!(inner.screen, Screen::Detailed(_)))
|
||||
}
|
||||
|
||||
pub(crate) fn detailed(
|
||||
&self,
|
||||
main_thread: MainThreadMarker,
|
||||
) -> Option<Ref<'_, ScreenDetailed>> {
|
||||
let inner = self.0.value(main_thread);
|
||||
match &inner.screen {
|
||||
Screen::Screen(_) => None,
|
||||
Screen::Detailed(_) => Some(Ref::map(inner, |inner| {
|
||||
if let Screen::Detailed(detailed) = &inner.screen {
|
||||
detailed
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct VideoModeHandle;
|
||||
#[derive(Debug)]
|
||||
pub enum OrientationLockFuture {
|
||||
Future(Notified<Result<(), OrientationLockError>>),
|
||||
Ready(Option<Result<(), OrientationLockError>>),
|
||||
}
|
||||
|
||||
impl Future for OrientationLockFuture {
|
||||
type Output = Result<(), OrientationLockError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.get_mut() {
|
||||
Self::Future(notified) => Pin::new(notified).poll(cx).map(Option::unwrap),
|
||||
Self::Ready(result) => {
|
||||
Poll::Ready(result.take().expect("`OrientationLockFuture` polled after completion"))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrientationLock {
|
||||
fn to_js(self) -> OrientationLockType {
|
||||
match self {
|
||||
OrientationLock::Any => OrientationLockType::Any,
|
||||
OrientationLock::Natural => OrientationLockType::Natural,
|
||||
OrientationLock::Landscape { flipped: None } => OrientationLockType::Landscape,
|
||||
OrientationLock::Landscape { flipped: Some(flipped) } => {
|
||||
if flipped {
|
||||
OrientationLockType::LandscapeSecondary
|
||||
} else {
|
||||
OrientationLockType::LandscapePrimary
|
||||
}
|
||||
},
|
||||
OrientationLock::Portrait { flipped: None } => OrientationLockType::Portrait,
|
||||
OrientationLock::Portrait { flipped: Some(flipped) } => {
|
||||
if flipped {
|
||||
OrientationLockType::PortraitSecondary
|
||||
} else {
|
||||
OrientationLockType::PortraitPrimary
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrientationLockError {
|
||||
fn from_js(error: JsValue) -> Self {
|
||||
debug_assert!(error.has_type::<DomException>());
|
||||
let error: DomException = error.unchecked_into();
|
||||
|
||||
if let DomException::ABORT_ERR = error.code() {
|
||||
OrientationLockError::Busy
|
||||
} else {
|
||||
OrientationLockError::Unsupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct VideoModeHandle(pub(super) MonitorHandle);
|
||||
|
||||
impl VideoModeHandle {
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
unreachable!();
|
||||
self.0.size()
|
||||
}
|
||||
|
||||
pub fn bit_depth(&self) -> u16 {
|
||||
unreachable!();
|
||||
self.0 .0.queue(|inner| inner.screen.color_depth().unwrap()).try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> u32 {
|
||||
unreachable!();
|
||||
0
|
||||
}
|
||||
|
||||
pub fn monitor(&self) -> MonitorHandle {
|
||||
unreachable!();
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
window: WindowExt,
|
||||
engine: Option<Engine>,
|
||||
screen: Screen,
|
||||
orientation: OnceCell<ScreenOrientationExt>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn new(window: WindowExt, engine: Option<Engine>, screen: Screen) -> Self {
|
||||
Self { window, engine, screen, orientation: OnceCell::new() }
|
||||
}
|
||||
}
|
||||
|
||||
enum Screen {
|
||||
Screen(ScreenExt),
|
||||
Detailed(ScreenDetailed),
|
||||
}
|
||||
|
||||
impl Deref for Screen {
|
||||
type Target = ScreenExt;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Screen::Screen(screen) => screen,
|
||||
Screen::Detailed(screen) => screen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MonitorHandler {
|
||||
state: RefCell<State>,
|
||||
main_thread: MainThreadMarker,
|
||||
window: WindowExt,
|
||||
engine: Option<Engine>,
|
||||
screen: ScreenExt,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Unsupported,
|
||||
Initialize(Notified<Result<(), MonitorPermissionError>>),
|
||||
Permission { permission: PermissionStatusExt, _handle: EventListenerHandle<dyn Fn()> },
|
||||
Upgrade(Notified<Result<(), MonitorPermissionError>>),
|
||||
Detailed(ScreenDetails),
|
||||
}
|
||||
|
||||
impl MonitorHandler {
|
||||
pub fn new(
|
||||
main_thread: MainThreadMarker,
|
||||
window: Window,
|
||||
navigator: &Navigator,
|
||||
runner: WeakShared,
|
||||
) -> Self {
|
||||
let window: WindowExt = window.unchecked_into();
|
||||
let engine = super::web_sys::engine(navigator);
|
||||
let screen: ScreenExt = window.screen().unwrap().unchecked_into();
|
||||
|
||||
let state = if has_screen_details_support(&window) {
|
||||
let permissions = navigator.permissions().expect(
|
||||
"expected the Permissions API to be implemented if the Window Management API is \
|
||||
as well",
|
||||
);
|
||||
let descriptor: PermissionDescriptor = Object::new().unchecked_into();
|
||||
descriptor.set_name("window-management");
|
||||
let future = JsFuture::from(permissions.query(&descriptor).unwrap());
|
||||
|
||||
let window = window.clone();
|
||||
let notifier = Notifier::new();
|
||||
let notified = notifier.notified();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let permission: PermissionStatusExt = match future.await {
|
||||
Ok(permission) => permission.unchecked_into(),
|
||||
Err(error) => unreachable_error(
|
||||
&error,
|
||||
"retrieving permission for Window Management API failed even though its \
|
||||
implemented",
|
||||
),
|
||||
};
|
||||
|
||||
let screen_details = match permission.state() {
|
||||
PermissionState::Granted => {
|
||||
let screen_details = match JsFuture::from(window.screen_details()).await {
|
||||
Ok(screen_details) => screen_details.unchecked_into(),
|
||||
Err(error) => unreachable_error(
|
||||
&error,
|
||||
"getting screen details failed even though permission was granted",
|
||||
),
|
||||
};
|
||||
notifier.notify(Ok(()));
|
||||
Some(screen_details)
|
||||
},
|
||||
PermissionState::Denied => {
|
||||
notifier.notify(Err(MonitorPermissionError::Denied));
|
||||
None
|
||||
},
|
||||
PermissionState::Prompt => {
|
||||
notifier.notify(Err(MonitorPermissionError::Prompt));
|
||||
None
|
||||
},
|
||||
_ => {
|
||||
error!(
|
||||
"encountered unknown permission state: {}",
|
||||
permission.state_string()
|
||||
);
|
||||
notifier.notify(Err(MonitorPermissionError::Denied));
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Notifying `Future`s is not dependant on the lifetime of the runner,
|
||||
// because they can outlive it.
|
||||
if let Some(runner) = runner.upgrade() {
|
||||
let state = if let Some(screen_details) = screen_details {
|
||||
State::Detailed(screen_details)
|
||||
} else {
|
||||
// If permission is denied we listen for changes so we can catch external
|
||||
// permission granting.
|
||||
let handle =
|
||||
Self::setup_listener(runner.weak(), window, permission.clone());
|
||||
State::Permission { permission, _handle: handle }
|
||||
};
|
||||
|
||||
*runner.monitor().state.borrow_mut() = state;
|
||||
runner.start_delayed();
|
||||
}
|
||||
});
|
||||
|
||||
State::Initialize(notified)
|
||||
} else {
|
||||
State::Unsupported
|
||||
};
|
||||
|
||||
Self { state: RefCell::new(state), main_thread, window, engine, screen }
|
||||
}
|
||||
|
||||
fn setup_listener(
|
||||
runner: WeakShared,
|
||||
window: WindowExt,
|
||||
permission: PermissionStatus,
|
||||
) -> EventListenerHandle<dyn Fn()> {
|
||||
EventListenerHandle::new(
|
||||
permission.clone(),
|
||||
"change",
|
||||
Closure::new(move || {
|
||||
if let PermissionState::Granted = permission.state() {
|
||||
let future = JsFuture::from(window.screen_details());
|
||||
|
||||
let runner = runner.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let screen_details = match future.await {
|
||||
Ok(screen_details) => screen_details.unchecked_into(),
|
||||
Err(error) => unreachable_error(
|
||||
&error,
|
||||
"getting screen details failed even though permission was granted",
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(runner) = runner.upgrade() {
|
||||
// We drop the event listener handle here, which
|
||||
// doesn't drop it while we are running it, because
|
||||
// we are in a `spawn_local()` context.
|
||||
*runner.monitor().state.borrow_mut() = State::Detailed(screen_details);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_extended(&self) -> Option<bool> {
|
||||
self.screen.is_extended()
|
||||
}
|
||||
|
||||
pub fn is_initializing(&self) -> bool {
|
||||
matches!(self.state.borrow().deref(), State::Initialize(_))
|
||||
}
|
||||
|
||||
pub fn current_monitor(&self) -> MonitorHandle {
|
||||
if let State::Detailed(details) = self.state.borrow().deref() {
|
||||
MonitorHandle::new(
|
||||
self.main_thread,
|
||||
Inner::new(
|
||||
self.window.clone(),
|
||||
self.engine,
|
||||
Screen::Detailed(details.current_screen()),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
MonitorHandle::new(
|
||||
self.main_thread,
|
||||
Inner::new(self.window.clone(), self.engine, Screen::Screen(self.screen.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We have to return a `Vec` here because the iterator is otherwise not `Send` + `Sync`.
|
||||
pub fn available_monitors(&self) -> Vec<MonitorHandle> {
|
||||
if let State::Detailed(details) = self.state.borrow().deref() {
|
||||
details
|
||||
.screens()
|
||||
.into_iter()
|
||||
.map(move |screen| {
|
||||
MonitorHandle::new(
|
||||
self.main_thread,
|
||||
Inner::new(self.window.clone(), self.engine, Screen::Detailed(screen)),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![self.current_monitor()]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
if let State::Detailed(details) = self.state.borrow().deref() {
|
||||
details.screens().into_iter().find_map(|screen| {
|
||||
screen.is_primary().then(|| {
|
||||
MonitorHandle::new(
|
||||
self.main_thread,
|
||||
Inner::new(self.window.clone(), self.engine, Screen::Detailed(screen)),
|
||||
)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request_detailed_monitor_permission(
|
||||
&self,
|
||||
shared: WeakShared,
|
||||
) -> MonitorPermissionFuture {
|
||||
let state = self.state.borrow();
|
||||
let (notifier, notified) = match state.deref() {
|
||||
State::Unsupported => {
|
||||
return MonitorPermissionFuture::Ready(Some(Err(
|
||||
MonitorPermissionError::Unsupported,
|
||||
)))
|
||||
},
|
||||
State::Initialize(notified) => {
|
||||
return MonitorPermissionFuture::Initialize {
|
||||
runner: Dispatcher::new(self.main_thread, (shared, self.window.clone())).0,
|
||||
notified: notified.clone(),
|
||||
}
|
||||
},
|
||||
State::Permission { permission, .. } => {
|
||||
match permission.state() {
|
||||
PermissionState::Granted | PermissionState::Prompt => (),
|
||||
PermissionState::Denied => {
|
||||
return MonitorPermissionFuture::Ready(Some(Err(
|
||||
MonitorPermissionError::Denied,
|
||||
)))
|
||||
},
|
||||
_ => {
|
||||
error!(
|
||||
"encountered unknown permission state: {}",
|
||||
permission.state_string()
|
||||
);
|
||||
|
||||
return MonitorPermissionFuture::Ready(Some(Err(
|
||||
MonitorPermissionError::Denied,
|
||||
)));
|
||||
},
|
||||
}
|
||||
|
||||
drop(state);
|
||||
|
||||
let notifier = Notifier::new();
|
||||
let notified = notifier.notified();
|
||||
*self.state.borrow_mut() = State::Upgrade(notified.clone());
|
||||
|
||||
(notifier, notified)
|
||||
},
|
||||
// A request is already in progress.
|
||||
State::Upgrade(notified) => return MonitorPermissionFuture::Upgrade(notified.clone()),
|
||||
State::Detailed(_) => return MonitorPermissionFuture::Ready(Some(Ok(()))),
|
||||
};
|
||||
|
||||
let future = JsFuture::from(self.window.screen_details());
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match future.await {
|
||||
Ok(details) => {
|
||||
// Notifying `Future`s is not dependant on the lifetime of the runner, because
|
||||
// they can outlive it.
|
||||
notifier.notify(Ok(()));
|
||||
|
||||
if let Some(shared) = shared.upgrade() {
|
||||
*shared.monitor().state.borrow_mut() =
|
||||
State::Detailed(details.unchecked_into())
|
||||
}
|
||||
},
|
||||
Err(error) => unreachable_error(
|
||||
&error,
|
||||
"getting screen details failed even though permission was granted",
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
MonitorPermissionFuture::Upgrade(notified)
|
||||
}
|
||||
|
||||
pub fn has_detailed_monitor_permission_async(&self) -> HasMonitorPermissionFuture {
|
||||
match self.state.borrow().deref() {
|
||||
State::Unsupported | State::Permission { .. } | State::Upgrade(_) => {
|
||||
HasMonitorPermissionFuture::Ready(Some(false))
|
||||
},
|
||||
State::Initialize(notified) => HasMonitorPermissionFuture::Future(notified.clone()),
|
||||
State::Detailed(_) => HasMonitorPermissionFuture::Ready(Some(true)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_detailed_monitor_permission(&self) -> bool {
|
||||
match self.state.borrow().deref() {
|
||||
State::Unsupported | State::Permission { .. } | State::Upgrade(_) => false,
|
||||
State::Initialize(_) => {
|
||||
unreachable!("called `has_detailed_monitor_permission()` while initializing")
|
||||
},
|
||||
State::Detailed(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum MonitorPermissionFuture {
|
||||
Initialize {
|
||||
runner: Dispatcher<(WeakShared, WindowExt)>,
|
||||
notified: Notified<Result<(), MonitorPermissionError>>,
|
||||
},
|
||||
Upgrade(Notified<Result<(), MonitorPermissionError>>),
|
||||
Ready(Option<Result<(), MonitorPermissionError>>),
|
||||
}
|
||||
|
||||
impl MonitorPermissionFuture {
|
||||
fn upgrade(&mut self) {
|
||||
let notifier = Notifier::new();
|
||||
let notified = notifier.notified();
|
||||
let Self::Initialize { runner, .. } = mem::replace(self, Self::Upgrade(notified.clone()))
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
runner.dispatch(|(shared, window)| {
|
||||
let future = JsFuture::from(window.screen_details());
|
||||
|
||||
if let Some(shared) = shared.upgrade() {
|
||||
*shared.monitor().state.borrow_mut() = State::Upgrade(notified);
|
||||
}
|
||||
|
||||
let shared = shared.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match future.await {
|
||||
Ok(details) => {
|
||||
// Notifying `Future`s is not dependant on the lifetime
|
||||
// of
|
||||
// the runner, because
|
||||
// they can outlive it.
|
||||
notifier.notify(Ok(()));
|
||||
|
||||
if let Some(shared) = shared.upgrade() {
|
||||
*shared.monitor().state.borrow_mut() =
|
||||
State::Detailed(details.unchecked_into())
|
||||
}
|
||||
},
|
||||
Err(error) => unreachable_error(
|
||||
&error,
|
||||
"getting screen details failed even though permission was granted",
|
||||
),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for MonitorPermissionFuture {
|
||||
type Output = Result<(), MonitorPermissionError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
match this {
|
||||
Self::Initialize { notified, .. } => {
|
||||
if let Err(error) = ready!(Pin::new(notified).poll(cx).map(Option::unwrap)) {
|
||||
match error {
|
||||
MonitorPermissionError::Denied | MonitorPermissionError::Unsupported => {
|
||||
Poll::Ready(Err(error))
|
||||
},
|
||||
MonitorPermissionError::Prompt => {
|
||||
this.upgrade();
|
||||
Poll::Pending
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
},
|
||||
Self::Upgrade(notified) => Pin::new(notified).poll(cx).map(Option::unwrap),
|
||||
Self::Ready(result) => Poll::Ready(
|
||||
result.take().expect("`MonitorPermissionFuture` polled after completion"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HasMonitorPermissionFuture {
|
||||
Future(Notified<Result<(), MonitorPermissionError>>),
|
||||
Ready(Option<bool>),
|
||||
}
|
||||
|
||||
impl Future for HasMonitorPermissionFuture {
|
||||
type Output = bool;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.get_mut() {
|
||||
Self::Future(notified) => {
|
||||
Pin::new(notified).poll(cx).map(Option::unwrap).map(|result| result.is_ok())
|
||||
},
|
||||
Self::Ready(result) => Poll::Ready(
|
||||
result.take().expect("`MonitorPermissionFuture` polled after completion"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn unreachable_error(error: &JsValue, message: &str) -> ! {
|
||||
if let Some(error) = error.dyn_ref::<DomException>() {
|
||||
unreachable!("{message}. {}: {}", error.name(), error.message());
|
||||
} else {
|
||||
console::error_1(error);
|
||||
unreachable!("{message}");
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static HAS_LOCK_SUPPORT: OnceCell<bool> = const { OnceCell::new() };
|
||||
}
|
||||
|
||||
pub fn has_screen_details_support(window: &Window) -> bool {
|
||||
thread_local! {
|
||||
static HAS_SCREEN_DETAILS: OnceCell<bool> = const { OnceCell::new() };
|
||||
}
|
||||
|
||||
HAS_SCREEN_DETAILS.with(|support| {
|
||||
*support.get_or_init(|| {
|
||||
let window: &WindowExt = window.unchecked_ref();
|
||||
!window.has_screen_details().is_undefined()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[derive(Clone)]
|
||||
#[wasm_bindgen(extends = Window)]
|
||||
pub(crate) type WindowExt;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = getScreenDetails)]
|
||||
fn has_screen_details(this: &WindowExt) -> JsValue;
|
||||
|
||||
#[wasm_bindgen(method, js_name = getScreenDetails)]
|
||||
fn screen_details(this: &WindowExt) -> Promise;
|
||||
|
||||
type ScreenDetails;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = currentScreen)]
|
||||
fn current_screen(this: &ScreenDetails) -> ScreenDetailed;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn screens(this: &ScreenDetails) -> Vec<ScreenDetailed>;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[wasm_bindgen(extends = web_sys::Screen)]
|
||||
pub(crate) type ScreenExt;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = isExtended)]
|
||||
fn is_extended(this: &ScreenExt) -> Option<bool>;
|
||||
|
||||
#[wasm_bindgen(extends = ScreenOrientation)]
|
||||
type ScreenOrientationExt;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = type)]
|
||||
fn type_string(this: &ScreenOrientationExt) -> String;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = lock)]
|
||||
fn has_lock(this: &ScreenOrientationExt) -> JsValue;
|
||||
|
||||
#[wasm_bindgen(extends = ScreenExt)]
|
||||
pub(crate) type ScreenDetailed;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = devicePixelRatio)]
|
||||
fn device_pixel_ratio(this: &ScreenDetailed) -> f64;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = isInternal)]
|
||||
fn is_internal(this: &ScreenDetailed) -> bool;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = isPrimary)]
|
||||
fn is_primary(this: &ScreenDetailed) -> bool;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn label(this: &ScreenDetailed) -> String;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn left(this: &ScreenDetailed) -> i32;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn top(this: &ScreenDetailed) -> i32;
|
||||
|
||||
#[wasm_bindgen(extends = Object)]
|
||||
type PermissionDescriptor;
|
||||
|
||||
#[wasm_bindgen(method, setter, js_name = name)]
|
||||
fn set_name(this: &PermissionDescriptor, name: &str);
|
||||
|
||||
#[wasm_bindgen(extends = PermissionStatus)]
|
||||
type PermissionStatusExt;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = state)]
|
||||
fn state_string(this: &PermissionStatusExt) -> String;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use smol_str::SmolStr;
|
|||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{
|
||||
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent,
|
||||
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, Navigator,
|
||||
PointerEvent, WheelEvent,
|
||||
};
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
|||
use crate::error::OsError as RootOE;
|
||||
use crate::event::{Force, InnerSizeWriter, MouseButton, MouseScrollDelta};
|
||||
use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey};
|
||||
use crate::platform_impl::OsError;
|
||||
use crate::platform_impl::{Fullscreen, OsError};
|
||||
use crate::window::{WindowAttributes, WindowId as RootWindowId};
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -57,6 +57,7 @@ struct Handlers {
|
|||
|
||||
pub struct Common {
|
||||
pub window: web_sys::Window,
|
||||
navigator: Navigator,
|
||||
pub document: Document,
|
||||
/// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure
|
||||
/// the DPI factor is maintained. Note: this is read-only because we use a pointer to this
|
||||
|
|
@ -78,6 +79,7 @@ impl Canvas {
|
|||
main_thread: MainThreadMarker,
|
||||
id: WindowId,
|
||||
window: web_sys::Window,
|
||||
navigator: Navigator,
|
||||
document: Document,
|
||||
attr: WindowAttributes,
|
||||
) -> Result<Self, RootOE> {
|
||||
|
|
@ -116,6 +118,7 @@ impl Canvas {
|
|||
let common = Common {
|
||||
window: window.clone(),
|
||||
document: document.clone(),
|
||||
navigator,
|
||||
raw: Rc::new(canvas.clone()),
|
||||
style,
|
||||
old_size: Rc::default(),
|
||||
|
|
@ -142,8 +145,14 @@ impl Canvas {
|
|||
super::set_canvas_position(&common.document, &common.raw, &common.style, position);
|
||||
}
|
||||
|
||||
if attr.fullscreen.is_some() {
|
||||
fullscreen::request_fullscreen(&document, &canvas);
|
||||
if let Some(fullscreen) = attr.fullscreen {
|
||||
fullscreen::request_fullscreen(
|
||||
main_thread,
|
||||
&window,
|
||||
&document,
|
||||
&canvas,
|
||||
fullscreen.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if attr.active {
|
||||
|
|
@ -222,6 +231,11 @@ impl Canvas {
|
|||
&self.common.window
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn navigator(&self) -> &Navigator {
|
||||
&self.common.navigator
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn document(&self) -> &Document {
|
||||
&self.common.document
|
||||
|
|
@ -445,8 +459,14 @@ impl Canvas {
|
|||
}));
|
||||
}
|
||||
|
||||
pub fn request_fullscreen(&self) {
|
||||
fullscreen::request_fullscreen(self.document(), self.raw());
|
||||
pub(crate) fn request_fullscreen(&self, fullscreen: Fullscreen) {
|
||||
fullscreen::request_fullscreen(
|
||||
self.main_thread,
|
||||
self.window(),
|
||||
self.document(),
|
||||
self.raw(),
|
||||
fullscreen,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn exit_fullscreen(&self) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use dpi::{LogicalPosition, PhysicalPosition, Position};
|
|||
use smol_str::SmolStr;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{KeyboardEvent, MouseEvent, PointerEvent, WheelEvent};
|
||||
use web_sys::{KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent};
|
||||
|
||||
use super::Engine;
|
||||
use crate::event::{MouseButton, MouseScrollDelta};
|
||||
|
|
@ -108,8 +108,8 @@ pub enum MouseDelta {
|
|||
}
|
||||
|
||||
impl MouseDelta {
|
||||
pub fn init(window: &web_sys::Window, event: &PointerEvent) -> Self {
|
||||
match super::engine(window) {
|
||||
pub fn init(navigator: &Navigator, event: &PointerEvent) -> Self {
|
||||
match super::engine(navigator) {
|
||||
Some(Engine::Chromium) => Self::Chromium,
|
||||
// Firefox has wrong movement values in coalesced events.
|
||||
Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
use std::cell::OnceCell;
|
||||
|
||||
use js_sys::Promise;
|
||||
use js_sys::{Object, Promise};
|
||||
use tracing::error;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{Document, Element, HtmlCanvasElement};
|
||||
use web_sys::{console, Document, Element, HtmlCanvasElement, Window};
|
||||
|
||||
pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) {
|
||||
if is_fullscreen(document, canvas) {
|
||||
return;
|
||||
}
|
||||
use super::super::main_thread::MainThreadMarker;
|
||||
use super::super::monitor::{self, ScreenDetailed};
|
||||
use crate::platform_impl::Fullscreen;
|
||||
|
||||
pub(crate) fn request_fullscreen(
|
||||
main_thread: MainThreadMarker,
|
||||
window: &Window,
|
||||
document: &Document,
|
||||
canvas: &HtmlCanvasElement,
|
||||
fullscreen: Fullscreen,
|
||||
) {
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(extends = HtmlCanvasElement)]
|
||||
|
|
@ -19,21 +26,66 @@ pub fn request_fullscreen(document: &Document, canvas: &HtmlCanvasElement) {
|
|||
#[wasm_bindgen(method, js_name = requestFullscreen)]
|
||||
fn request_fullscreen(this: &RequestFullscreen) -> Promise;
|
||||
|
||||
#[wasm_bindgen(method, js_name = requestFullscreen)]
|
||||
fn request_fullscreen_with_options(
|
||||
this: &RequestFullscreen,
|
||||
options: &FullscreenOptions,
|
||||
) -> Promise;
|
||||
|
||||
#[wasm_bindgen(method, js_name = webkitRequestFullscreen)]
|
||||
fn webkit_request_fullscreen(this: &RequestFullscreen);
|
||||
|
||||
type FullscreenOptions;
|
||||
|
||||
#[wasm_bindgen(method, setter, js_name = screen)]
|
||||
fn set_screen(this: &FullscreenOptions, screen: &ScreenDetailed);
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|error| {
|
||||
console::error_1(&error);
|
||||
error!("Failed to transition to full screen mode")
|
||||
});
|
||||
}
|
||||
|
||||
if is_fullscreen(document, canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas: &RequestFullscreen = canvas.unchecked_ref();
|
||||
|
||||
if has_fullscreen_api_support(canvas) {
|
||||
thread_local! {
|
||||
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|_| ());
|
||||
}
|
||||
REJECT_HANDLER.with(|handler| {
|
||||
let _ = canvas.request_fullscreen().catch(handler);
|
||||
});
|
||||
} else {
|
||||
canvas.webkit_request_fullscreen();
|
||||
match fullscreen {
|
||||
Fullscreen::Exclusive(_) => error!("Exclusive full screen mode is not supported"),
|
||||
Fullscreen::Borderless(Some(monitor)) => {
|
||||
if !monitor::has_screen_details_support(window) {
|
||||
error!(
|
||||
"Fullscreen mode selecting a specific screen is not supported by this browser"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(monitor) = monitor.detailed(main_thread) {
|
||||
let options: FullscreenOptions = Object::new().unchecked_into();
|
||||
options.set_screen(&monitor);
|
||||
REJECT_HANDLER.with(|handler| {
|
||||
let _ = canvas.request_fullscreen_with_options(&options).catch(handler);
|
||||
});
|
||||
} else {
|
||||
error!(
|
||||
"Selecting a specific screen for fullscreen mode requires a detailed screen. \
|
||||
See `MonitorHandleExtWeb::is_detailed()`."
|
||||
)
|
||||
}
|
||||
},
|
||||
Fullscreen::Borderless(None) => {
|
||||
if has_fullscreen_api_support(canvas) {
|
||||
REJECT_HANDLER.with(|handler| {
|
||||
let _ = canvas.request_fullscreen().catch(handler);
|
||||
});
|
||||
} else {
|
||||
canvas.webkit_request_fullscreen();
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@ use js_sys::Array;
|
|||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{
|
||||
Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState, Window,
|
||||
};
|
||||
use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState};
|
||||
|
||||
pub use self::canvas::{Canvas, Style};
|
||||
pub use self::event::ButtonsState;
|
||||
|
|
@ -182,15 +180,15 @@ thread_local! {
|
|||
static USER_AGENT_DATA: OnceCell<UserAgentData> = const { OnceCell::new() };
|
||||
}
|
||||
|
||||
pub fn chrome_linux(window: &Window) -> bool {
|
||||
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).chrome_linux)
|
||||
pub fn chrome_linux(navigator: &Navigator) -> bool {
|
||||
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).chrome_linux)
|
||||
}
|
||||
|
||||
pub fn engine(window: &Window) -> Option<Engine> {
|
||||
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(window)).engine)
|
||||
pub fn engine(navigator: &Navigator) -> Option<Engine> {
|
||||
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).engine)
|
||||
}
|
||||
|
||||
fn user_agent(window: &Window) -> UserAgentData {
|
||||
fn user_agent(navigator: &Navigator) -> UserAgentData {
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(extends = Navigator)]
|
||||
|
|
@ -213,7 +211,7 @@ fn user_agent(window: &Window) -> UserAgentData {
|
|||
fn brand(this: &NavigatorUaBrandVersion) -> String;
|
||||
}
|
||||
|
||||
let navigator: NavigatorExt = window.navigator().unchecked_into();
|
||||
let navigator: &NavigatorExt = navigator.unchecked_ref();
|
||||
|
||||
if let Some(data) = navigator.user_agent_data() {
|
||||
let engine = 'engine: {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use std::cell::Ref;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use web_sys::HtmlCanvasElement;
|
||||
|
||||
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
||||
use super::monitor::MonitorHandle;
|
||||
use super::monitor::{MonitorHandle, MonitorHandler};
|
||||
use super::r#async::Dispatcher;
|
||||
use super::{backend, lock, ActiveEventLoop, Fullscreen};
|
||||
use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
|
||||
|
|
@ -24,6 +23,7 @@ pub struct Window {
|
|||
pub struct Inner {
|
||||
id: WindowId,
|
||||
pub window: web_sys::Window,
|
||||
monitor: Rc<MonitorHandler>,
|
||||
canvas: Rc<backend::Canvas>,
|
||||
destroy_fn: Option<Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
|
@ -33,11 +33,13 @@ impl Window {
|
|||
let id = target.generate_id();
|
||||
|
||||
let window = target.runner.window();
|
||||
let navigator = target.runner.navigator();
|
||||
let document = target.runner.document();
|
||||
let canvas = backend::Canvas::create(
|
||||
target.runner.main_thread(),
|
||||
id,
|
||||
window.clone(),
|
||||
navigator.clone(),
|
||||
document.clone(),
|
||||
attr,
|
||||
)?;
|
||||
|
|
@ -48,7 +50,13 @@ impl Window {
|
|||
let runner = target.runner.clone();
|
||||
let destroy_fn = Box::new(move || runner.notify_destroy_window(RootWI(id)));
|
||||
|
||||
let inner = Inner { id, window: window.clone(), canvas, destroy_fn: Some(destroy_fn) };
|
||||
let inner = Inner {
|
||||
id,
|
||||
window: window.clone(),
|
||||
monitor: Rc::clone(target.runner.monitor()),
|
||||
canvas,
|
||||
destroy_fn: Some(destroy_fn),
|
||||
};
|
||||
|
||||
let canvas = Rc::downgrade(&inner.canvas);
|
||||
let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner);
|
||||
|
|
@ -80,7 +88,7 @@ impl Window {
|
|||
|
||||
pub(crate) fn is_cursor_lock_raw(&self) -> bool {
|
||||
self.inner.queue(move |inner| {
|
||||
lock::is_cursor_lock_raw(inner.canvas.window(), inner.canvas.document())
|
||||
lock::is_cursor_lock_raw(inner.canvas.navigator(), inner.canvas.document())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +252,7 @@ impl Inner {
|
|||
match mode {
|
||||
CursorGrabMode::None => self.canvas.document().exit_pointer_lock(),
|
||||
CursorGrabMode::Locked => lock::request_pointer_lock(
|
||||
self.canvas.window(),
|
||||
self.canvas.navigator(),
|
||||
self.canvas.document(),
|
||||
self.canvas.raw(),
|
||||
),
|
||||
|
|
@ -312,8 +320,8 @@ impl Inner {
|
|||
|
||||
#[inline]
|
||||
pub(crate) fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
|
||||
if fullscreen.is_some() {
|
||||
self.canvas.request_fullscreen();
|
||||
if let Some(fullscreen) = fullscreen {
|
||||
self.canvas.request_fullscreen(fullscreen);
|
||||
} else {
|
||||
self.canvas.exit_fullscreen()
|
||||
}
|
||||
|
|
@ -365,17 +373,17 @@ impl Inner {
|
|||
|
||||
#[inline]
|
||||
pub fn current_monitor(&self) -> Option<MonitorHandle> {
|
||||
None
|
||||
Some(self.monitor.current_monitor())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
||||
VecDeque::new()
|
||||
pub fn available_monitors(&self) -> Vec<MonitorHandle> {
|
||||
self.monitor.available_monitors()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
None
|
||||
self.monitor.primary_monitor()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue