use std::cell::Ref; use std::fmt; use std::rc::Rc; use std::sync::Arc; use dpi::{LogicalPosition, LogicalSize}; use web_sys::HtmlCanvasElement; use super::main_thread::{MainThreadMarker, MainThreadSafe}; use super::monitor::MonitorHandler; use super::r#async::Dispatcher; use super::{backend, lock, ActiveEventLoop}; use crate::dpi::{LogicalInsets, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{NotSupportedError, RequestError}; use crate::icon::Icon; use crate::monitor::MonitorHandle as RootMonitorHandle; use crate::window::{ Cursor, CursorGrabMode, Fullscreen as RootFullscreen, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as RootWindow, WindowAttributes, WindowButtons, WindowId, WindowLevel, }; pub struct Window { inner: Dispatcher, } impl fmt::Debug for Window { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Window").finish_non_exhaustive() } } pub struct Inner { id: WindowId, pub window: web_sys::Window, monitor: Rc, safe_area: Rc, canvas: Rc, destroy_fn: Option>, } impl Window { pub(crate) fn new( target: &ActiveEventLoop, attr: WindowAttributes, ) -> Result { 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, )?; let canvas = Rc::new(canvas); target.register(&canvas, id); let runner = target.runner.clone(); let destroy_fn = Box::new(move || runner.notify_destroy_window(id)); let inner = Inner { id, window: window.clone(), monitor: Rc::clone(target.runner.monitor()), safe_area: Rc::clone(target.runner.safe_area()), canvas, destroy_fn: Some(destroy_fn), }; let canvas = Rc::downgrade(&inner.canvas); let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner); target.runner.add_canvas(id, canvas, runner); Ok(Window { inner: dispatcher }) } pub fn canvas(&self) -> Option> { MainThreadMarker::new() .map(|main_thread| Ref::map(self.inner.value(main_thread), |inner| inner.canvas.raw())) } pub(crate) fn prevent_default(&self) -> bool { self.inner.queue(|inner| inner.canvas.prevent_default.get()) } pub(crate) fn set_prevent_default(&self, prevent_default: bool) { self.inner.dispatch(move |inner| inner.canvas.prevent_default.set(prevent_default)) } pub(crate) fn is_cursor_lock_raw(&self) -> bool { self.inner.queue(move |inner| { lock::is_cursor_lock_raw(inner.canvas.navigator(), inner.canvas.document()) }) } } impl RootWindow for Window { fn id(&self) -> WindowId { self.inner.queue(|inner| inner.id) } fn scale_factor(&self) -> f64 { self.inner.queue(Inner::scale_factor) } fn request_redraw(&self) { self.inner.dispatch(|inner| inner.canvas.request_animation_frame()) } fn pre_present_notify(&self) {} fn reset_dead_keys(&self) { // Not supported } fn surface_position(&self) -> PhysicalPosition { // Note: the canvas element has no window decorations. (0, 0).into() } fn outer_position(&self) -> Result, RequestError> { Ok(self.inner.queue(|inner| inner.canvas.position().to_physical(inner.scale_factor()))) } fn set_outer_position(&self, position: Position) { self.inner.dispatch(move |inner| { let position = position.to_logical::(inner.scale_factor()); backend::set_canvas_position( inner.canvas.document(), inner.canvas.raw(), inner.canvas.style(), position, ) }) } fn surface_size(&self) -> PhysicalSize { self.inner.queue(|inner| inner.canvas.surface_size()) } fn request_surface_size(&self, size: Size) -> Option> { self.inner.queue(|inner| { let size = size.to_logical(self.scale_factor()); backend::set_canvas_size( inner.canvas.document(), inner.canvas.raw(), inner.canvas.style(), size, ); None }) } fn outer_size(&self) -> PhysicalSize { // Note: the canvas element has no window decorations, so this is equal to `surface_size`. self.surface_size() } fn safe_area(&self) -> PhysicalInsets { self.inner.queue(|inner| { let (safe_start_pos, safe_size) = inner.safe_area.get(); let safe_end_pos = LogicalPosition::new( safe_start_pos.x + safe_size.width, safe_start_pos.y + safe_size.height, ); let surface_start_pos = inner.canvas.position(); let surface_size = LogicalSize::new( backend::style_size_property(inner.canvas.style(), "width"), backend::style_size_property(inner.canvas.style(), "height"), ); let surface_end_pos = LogicalPosition::new( surface_start_pos.x + surface_size.width, surface_start_pos.y + surface_size.height, ); let top = f64::max(safe_start_pos.y - surface_start_pos.y, 0.); let left = f64::max(safe_start_pos.x - surface_start_pos.x, 0.); let bottom = f64::max(surface_end_pos.y - safe_end_pos.y, 0.); let right = f64::max(surface_end_pos.x - safe_end_pos.x, 0.); let insets = LogicalInsets::new(top, left, bottom, right); insets.to_physical(inner.scale_factor()) }) } fn set_min_surface_size(&self, min_size: Option) { self.inner.dispatch(move |inner| { let dimensions = min_size.map(|min_size| min_size.to_logical(inner.scale_factor())); backend::set_canvas_min_size( inner.canvas.document(), inner.canvas.raw(), inner.canvas.style(), dimensions, ) }) } fn set_max_surface_size(&self, max_size: Option) { self.inner.dispatch(move |inner| { let dimensions = max_size.map(|dimensions| dimensions.to_logical(inner.scale_factor())); backend::set_canvas_max_size( inner.canvas.document(), inner.canvas.raw(), inner.canvas.style(), dimensions, ) }) } fn surface_resize_increments(&self) -> Option> { None } fn set_surface_resize_increments(&self, _: Option) { // Intentionally a no-op: users can't resize canvas elements } fn set_title(&self, title: &str) { self.inner.queue(|inner| inner.canvas.set_attribute("alt", title)) } fn set_transparent(&self, _: bool) {} fn set_blur(&self, _: bool) {} fn set_visible(&self, _: bool) { // Intentionally a no-op } fn is_visible(&self) -> Option { None } fn set_resizable(&self, _: bool) { // Intentionally a no-op: users can't resize canvas elements } fn is_resizable(&self) -> bool { true } fn set_enabled_buttons(&self, _: WindowButtons) {} fn enabled_buttons(&self) -> WindowButtons { WindowButtons::all() } fn set_minimized(&self, _: bool) { // Intentionally a no-op, as canvases cannot be 'minimized' } fn is_minimized(&self) -> Option { // Canvas cannot be 'minimized' Some(false) } fn set_maximized(&self, _: bool) { // Intentionally a no-op, as canvases cannot be 'maximized' } fn is_maximized(&self) -> bool { // Canvas cannot be 'maximized' false } fn set_fullscreen(&self, fullscreen: Option) { self.inner.dispatch(move |inner| { if let Some(fullscreen) = fullscreen { inner.canvas.request_fullscreen(fullscreen.into()); } else { inner.canvas.exit_fullscreen() } }) } fn fullscreen(&self) -> Option { self.inner.queue(|inner| { if inner.canvas.is_fullscreen() { Some(RootFullscreen::Borderless(None)) } else { None } }) } fn set_decorations(&self, _: bool) { // Intentionally a no-op, no canvas decorations } fn is_decorated(&self) -> bool { true } fn set_window_level(&self, _: WindowLevel) { // Intentionally a no-op, no window ordering } fn set_window_icon(&self, _: Option) { // Currently an intentional no-op } fn set_ime_cursor_area(&self, _: Position, _: Size) { // Currently not implemented } fn set_ime_allowed(&self, _: bool) { // Currently not implemented } fn set_ime_purpose(&self, _: ImePurpose) { // Currently not implemented } fn focus_window(&self) { self.inner.dispatch(|inner| { let _ = inner.canvas.raw().focus(); }) } fn has_focus(&self) -> bool { self.inner.queue(|inner| inner.canvas.has_focus.get()) } fn request_user_attention(&self, _: Option) { // Currently an intentional no-op } fn set_theme(&self, _: Option) {} fn theme(&self) -> Option { self.inner.queue(|inner| { backend::is_dark_mode(&inner.window).map(|is_dark_mode| { if is_dark_mode { Theme::Dark } else { Theme::Light } }) }) } fn set_content_protected(&self, _: bool) {} fn title(&self) -> String { String::new() } fn set_cursor(&self, cursor: Cursor) { self.inner.dispatch(move |inner| inner.canvas.cursor.set_cursor(cursor)) } fn set_cursor_position(&self, _: Position) -> Result<(), RequestError> { Err(NotSupportedError::new("set_cursor_position is not supported").into()) } fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> { Ok(self.inner.queue(|inner| { match mode { CursorGrabMode::None => inner.canvas.document().exit_pointer_lock(), CursorGrabMode::Locked => lock::request_pointer_lock( inner.canvas.navigator(), inner.canvas.document(), inner.canvas.raw(), ), CursorGrabMode::Confined => { return Err(NotSupportedError::new("confined cursor mode is not supported")) }, } Ok(()) })?) } fn set_cursor_visible(&self, visible: bool) { self.inner.dispatch(move |inner| inner.canvas.cursor.set_cursor_visible(visible)) } fn drag_window(&self) -> Result<(), RequestError> { Err(NotSupportedError::new("drag_window is not supported").into()) } fn drag_resize_window(&self, _: ResizeDirection) -> Result<(), RequestError> { Err(NotSupportedError::new("drag_resize_window is not supported").into()) } fn show_window_menu(&self, _: Position) {} fn set_cursor_hittest(&self, _: bool) -> Result<(), RequestError> { Err(NotSupportedError::new("set_cursor_hittest is not supported").into()) } fn current_monitor(&self) -> Option { Some(self.inner.queue(|inner| inner.monitor.current_monitor()).into()) } fn available_monitors(&self) -> Box> { Box::new( self.inner .queue(|inner| inner.monitor.available_monitors()) .into_iter() .map(RootMonitorHandle::from), ) } fn primary_monitor(&self) -> Option { self.inner.queue(|inner| inner.monitor.primary_monitor()).map(RootMonitorHandle::from) } fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } } impl rwh_06::HasWindowHandle for Window { fn window_handle(&self) -> Result, rwh_06::HandleError> { MainThreadMarker::new() .map(|main_thread| { let inner = self.inner.value(main_thread); // SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid. let canvas: &wasm_bindgen::JsValue = inner.canvas.raw(); let window_handle = rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast()); // SAFETY: The pointer won't be invalidated as long as `Window` lives, which the // lifetime is bound to. unsafe { rwh_06::WindowHandle::borrow_raw(rwh_06::RawWindowHandle::WebCanvas( window_handle, )) } }) .ok_or(rwh_06::HandleError::Unavailable) } } impl rwh_06::HasDisplayHandle for Window { fn display_handle(&self) -> Result, rwh_06::HandleError> { Ok(rwh_06::DisplayHandle::web()) } } impl Inner { #[inline] pub fn scale_factor(&self) -> f64 { super::backend::scale_factor(&self.window) } } impl Drop for Inner { fn drop(&mut self) { if let Some(destroy_fn) = self.destroy_fn.take() { destroy_fn(); } } } #[derive(Clone, Debug)] pub struct PlatformSpecificWindowAttributes { pub(crate) canvas: Option>>, pub(crate) prevent_default: bool, pub(crate) focusable: bool, pub(crate) append: bool, } impl PartialEq for PlatformSpecificWindowAttributes { fn eq(&self, other: &Self) -> bool { (match (&self.canvas, &other.canvas) { (Some(this), Some(other)) => Arc::ptr_eq(this, other), (None, None) => true, _ => false, }) && self.prevent_default.eq(&other.prevent_default) && self.focusable.eq(&other.focusable) && self.append.eq(&other.append) } } impl PlatformSpecificWindowAttributes { pub(crate) fn set_canvas(&mut self, canvas: Option) { let Some(canvas) = canvas else { self.canvas = None; return; }; let main_thread = MainThreadMarker::new() .expect("received a `HtmlCanvasElement` outside the window context"); self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas))); } } impl Default for PlatformSpecificWindowAttributes { fn default() -> Self { Self { canvas: None, prevent_default: true, focusable: true, append: false } } }