Skip to content

Commit

Permalink
Web: Implement MonitorHandle (#3801)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
daxpedda authored Jul 23, 2024
1 parent 2e97ab3 commit a0bc3e5
Show file tree
Hide file tree
Showing 21 changed files with 1,494 additions and 121 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,21 @@ web_sys = { package = "web-sys", version = "0.3.64", features = [
'MessagePort',
'Navigator',
'Node',
'OrientationType',
'OrientationLockType',
'PageTransitionEvent',
'Permissions',
'PermissionState',
'PermissionStatus',
'PointerEvent',
'PremultiplyAlpha',
'ResizeObserver',
'ResizeObserverBoxOptions',
'ResizeObserverEntry',
'ResizeObserverOptions',
'ResizeObserverSize',
'Screen',
'ScreenOrientation',
'VisibilityState',
'Window',
'WheelEvent',
Expand Down
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ disallowed-methods = [
{ path = "web_sys::HtmlCanvasElement::height", reason = "Winit shouldn't touch the internal canvas size" },
{ path = "web_sys::HtmlCanvasElement::set_width", reason = "Winit shouldn't touch the internal canvas size" },
{ path = "web_sys::HtmlCanvasElement::set_height", reason = "Winit shouldn't touch the internal canvas size" },
{ path = "web_sys::Window::navigator", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" },
{ path = "web_sys::HtmlElement::style", reason = "cache this to reduce calls to JS" },
Expand Down
102 changes: 80 additions & 22 deletions examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::error::Error;
use std::fmt::Debug;
#[cfg(not(any(android_platform, ios_platform)))]
use std::num::NonZeroU32;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::Arc;
use std::{fmt, mem};

Expand All @@ -25,6 +26,8 @@ use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMac
use winit::platform::startup_notify::{
self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify,
};
#[cfg(web_platform)]
use winit::platform::web::{ActiveEventLoopExtWeb, CustomCursorExtWeb, WindowAttributesExtWeb};
use winit::window::{
Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection,
Theme, Window, WindowId,
Expand All @@ -43,26 +46,34 @@ fn main() -> Result<(), Box<dyn Error>> {
tracing::init();

let event_loop = EventLoop::new()?;
let _event_loop_proxy = event_loop.create_proxy();
let (sender, receiver) = mpsc::channel();

// Wire the user event from another thread.
#[cfg(not(web_platform))]
std::thread::spawn(move || {
// Wake up the `event_loop` once every second and dispatch a custom event
// from a different thread.
info!("Starting to send user event every second");
loop {
_event_loop_proxy.wake_up();
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
{
let event_loop_proxy = event_loop.create_proxy();
let sender = sender.clone();
std::thread::spawn(move || {
// Wake up the `event_loop` once every second and dispatch a custom event
// from a different thread.
info!("Starting to send user event every second");
loop {
let _ = sender.send(Action::Message);
event_loop_proxy.wake_up();
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
}

let app = Application::new(&event_loop);
let app = Application::new(&event_loop, receiver, sender);
Ok(event_loop.run_app(app)?)
}

/// Application state and event handling.
struct Application {
/// Trigger actions through proxy wake up.
receiver: Receiver<Action>,
sender: Sender<Action>,
/// Custom cursors assets.
custom_cursors: Vec<CustomCursor>,
/// Application icon.
Expand All @@ -76,7 +87,7 @@ struct Application {
}

impl Application {
fn new(event_loop: &EventLoop) -> Self {
fn new(event_loop: &EventLoop, receiver: Receiver<Action>, sender: Sender<Action>) -> Self {
// SAFETY: we drop the context right before the event loop is stopped, thus making it safe.
#[cfg(not(any(android_platform, ios_platform)))]
let context = Some(
Expand All @@ -103,6 +114,8 @@ impl Application {
];

Self {
receiver,
sender,
#[cfg(not(any(android_platform, ios_platform)))]
context,
custom_cursors,
Expand Down Expand Up @@ -138,7 +151,6 @@ impl Application {

#[cfg(web_platform)]
{
use winit::platform::web::WindowAttributesExtWeb;
window_attributes = window_attributes.with_append(true);
}

Expand All @@ -160,7 +172,23 @@ impl Application {
Ok(window_id)
}

fn handle_action(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, action: Action) {
fn handle_action_from_proxy(&mut self, _event_loop: &ActiveEventLoop, action: Action) {
match action {
#[cfg(web_platform)]
Action::DumpMonitors => self.dump_monitors(_event_loop),
Action::Message => {
info!("User wake up");
},
_ => unreachable!("Tried to execute invalid action without `WindowId`"),
}
}

fn handle_action_with_window(
&mut self,
event_loop: &ActiveEventLoop,
window_id: WindowId,
action: Action,
) {
// let cursor_position = self.cursor_position;
let window = self.windows.get_mut(&window_id).unwrap();
info!("Executing action: {action:?}");
Expand Down Expand Up @@ -217,6 +245,26 @@ impl Application {
}
},
Action::RequestResize => window.swap_dimensions(),
#[cfg(web_platform)]
Action::DumpMonitors => {
let future = event_loop.request_detailed_monitor_permission();
let proxy = event_loop.create_proxy();
let sender = self.sender.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Err(error) = future.await {
error!("{error}")
}

let _ = sender.send(Action::DumpMonitors);
proxy.wake_up();
});
},
#[cfg(not(web_platform))]
Action::DumpMonitors => self.dump_monitors(event_loop),
Action::Message => {
self.sender.send(Action::Message).unwrap();
event_loop.create_proxy().wake_up();
},
}
}

Expand Down Expand Up @@ -300,8 +348,10 @@ impl Application {
}

impl ApplicationHandler for Application {
fn proxy_wake_up(&mut self, _event_loop: &ActiveEventLoop) {
info!("User wake up");
fn proxy_wake_up(&mut self, event_loop: &ActiveEventLoop) {
while let Ok(action) = self.receiver.try_recv() {
self.handle_action_from_proxy(event_loop, action)
}
}

fn window_event(
Expand Down Expand Up @@ -369,7 +419,7 @@ impl ApplicationHandler for Application {
};

if let Some(action) = action {
self.handle_action(event_loop, window_id, action);
self.handle_action_with_window(event_loop, window_id, action);
}
}
},
Expand All @@ -378,7 +428,7 @@ impl ApplicationHandler for Application {
if let Some(action) =
state.is_pressed().then(|| Self::process_mouse_binding(button, &mods)).flatten()
{
self.handle_action(event_loop, window_id, action);
self.handle_action_with_window(event_loop, window_id, action);
}
},
WindowEvent::CursorLeft { .. } => {
Expand Down Expand Up @@ -703,8 +753,6 @@ impl WindowState {
) {
use std::time::Duration;

use winit::platform::web::CustomCursorExtWeb;

let cursors = vec![
custom_cursors[0].clone(),
custom_cursors[1].clone(),
Expand Down Expand Up @@ -886,6 +934,8 @@ enum Action {
#[cfg(macos_platform)]
CreateNewTab,
RequestResize,
DumpMonitors,
Message,
}

impl Action {
Expand Down Expand Up @@ -920,6 +970,14 @@ impl Action {
#[cfg(macos_platform)]
Action::CreateNewTab => "Create new tab",
Action::RequestResize => "Request a resize",
#[cfg(not(web_platform))]
Action::DumpMonitors => "Dump monitor information",
#[cfg(web_platform)]
Action::DumpMonitors => {
"Request permission to query detailed monitor information and dump monitor \
information"
},
Action::Message => "Prints a message through a user wake up",
}
}
}
Expand All @@ -942,8 +1000,6 @@ fn decode_cursor(bytes: &[u8]) -> CustomCursorSource {
fn url_custom_cursor() -> CustomCursorSource {
use std::sync::atomic::{AtomicU64, Ordering};

use winit::platform::web::CustomCursorExtWeb;

static URL_COUNTER: AtomicU64 = AtomicU64::new(0);

CustomCursor::from_url(
Expand Down Expand Up @@ -1041,6 +1097,7 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
Binding::new("R", ModifiersState::CONTROL, Action::ToggleResizable),
Binding::new("R", ModifiersState::ALT, Action::RequestResize),
// M.
Binding::new("M", ModifiersState::CONTROL.union(ModifiersState::ALT), Action::DumpMonitors),
Binding::new("M", ModifiersState::CONTROL, Action::ToggleMaximize),
Binding::new("M", ModifiersState::ALT, Action::Minimize),
// N.
Expand Down Expand Up @@ -1069,6 +1126,7 @@ const KEY_BINDINGS: &[Binding<&'static str>] = &[
Binding::new("T", ModifiersState::SUPER, Action::CreateNewTab),
#[cfg(macos_platform)]
Binding::new("O", ModifiersState::CONTROL, Action::CycleOptionAsAlt),
Binding::new("S", ModifiersState::CONTROL, Action::Message),
];

const MOUSE_BINDINGS: &[Binding<MouseButton>] = &[
Expand Down
7 changes: 7 additions & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ changelog entry.
- On Web, add `ActiveEventLoopExtWeb::is_cursor_lock_raw()` to determine if
`DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using
`CursorGrabMode::Locked`.
- On Web, implement `MonitorHandle` and `VideoModeHandle`.

Without prompting the user for permission, only the current monitor is returned. But when
prompting and being granted permission through
`ActiveEventLoop::request_detailed_monitor_permission()`, access to all monitors and their
information is available. This "detailed monitors" can be used in `Window::set_fullscreen()` as
well.

### Changed

Expand Down
17 changes: 16 additions & 1 deletion src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,15 @@ impl ActiveEventLoop {
}

/// Returns the list of all the monitors available on the system.
///
/// ## Platform-specific
///
/// **Web:** Only returns the current monitor without
#[cfg_attr(
any(web_platform, docsrs),
doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]."
)]
#[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")]
#[inline]
pub fn available_monitors(&self) -> impl Iterator<Item = MonitorHandle> {
let _span = tracing::debug_span!("winit::ActiveEventLoop::available_monitors",).entered();
Expand All @@ -394,7 +403,13 @@ impl ActiveEventLoop {
///
/// ## Platform-specific
///
/// **Wayland / Web:** Always returns `None`.
/// - **Wayland:** Always returns `None`.
/// - **Web:** Always returns `None` without
#[cfg_attr(
any(web_platform, docsrs),
doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]."
)]
#[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions.")]
#[inline]
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
let _span = tracing::debug_span!("winit::ActiveEventLoop::primary_monitor",).entered();
Expand Down
37 changes: 33 additions & 4 deletions src/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ impl VideoModeHandle {
}

/// Returns the refresh rate of this video mode in mHz.
///
/// ## Platform-specific
///
/// **Web:** Always returns `0`.
#[inline]
pub fn refresh_rate_millihertz(&self) -> u32 {
self.video_mode.refresh_rate_millihertz()
Expand Down Expand Up @@ -108,6 +112,15 @@ impl MonitorHandle {
/// Returns a human-readable name of the monitor.
///
/// Returns `None` if the monitor doesn't exist anymore.
///
/// ## Platform-specific
///
/// **Web:** Always returns [`None`] without
#[cfg_attr(
any(web_platform, docsrs),
doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]."
)]
#[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")]
#[inline]
pub fn name(&self) -> Option<String> {
self.inner.name()
Expand All @@ -121,6 +134,15 @@ impl MonitorHandle {

/// Returns the top-left corner position of the monitor relative to the larger full
/// screen area.
///
/// ## Platform-specific
///
/// **Web:** Always returns [`Default`] without
#[cfg_attr(
any(web_platform, docsrs),
doc = "[detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]."
)]
#[cfg_attr(not(any(web_platform, docsrs)), doc = "detailed monitor permissions.")]
#[inline]
pub fn position(&self) -> PhysicalPosition<i32> {
self.inner.position()
Expand All @@ -133,6 +155,10 @@ impl MonitorHandle {
///
/// When using exclusive fullscreen, the refresh rate of the [`VideoModeHandle`] that was
/// used to enter fullscreen should be used instead.
///
/// ## Platform-specific
///
/// **Web:** Always returns [`None`].
#[inline]
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
self.inner.refresh_rate_millihertz()
Expand All @@ -148,18 +174,21 @@ impl MonitorHandle {
/// - **X11:** Can be overridden using the `WINIT_X11_SCALE_FACTOR` environment variable.
/// - **Wayland:** May differ from [`Window::scale_factor`].
/// - **Android:** Always returns 1.0.
/// - **Web:** Always returns `0.0` without
#[cfg_attr(
any(web_platform, docsrs),
doc = " [detailed monitor permissions][crate::platform::web::ActiveEventLoopExtWeb::request_detailed_monitor_permission]."
)]
#[cfg_attr(not(any(web_platform, docsrs)), doc = " detailed monitor permissions.")]
///
#[rustfmt::skip]
/// [`Window::scale_factor`]: crate::window::Window::scale_factor
#[inline]
pub fn scale_factor(&self) -> f64 {
self.inner.scale_factor()
}

/// Returns all fullscreen video modes supported by this monitor.
///
/// ## Platform-specific
///
/// - **Web:** Always returns an empty iterator
#[inline]
pub fn video_modes(&self) -> impl Iterator<Item = VideoModeHandle> {
self.inner.video_modes().map(|video_mode| VideoModeHandle { video_mode })
Expand Down
Loading

0 comments on commit a0bc3e5

Please sign in to comment.