use crate::dispatcher::WebDispatcher;use crate::display::WebDisplay;use crate::events::EventListenerHandle;use crate::http_client::FetchHttpClient;use crate::keyboard::WebKeyboardLayout;use crate::window::WebWindow;use anyhow::Result;use futures::channel::oneshot;use gpui::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper, ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, ThermalState, WindowAppearance, WindowKind, WindowParams, popup::PopupNotSupportedError,};use gpui_wgpu::WgpuContext;use std::{ borrow::Cow, cell::{Cell, RefCell}, path::{Path, PathBuf}, rc::Rc, sync::Arc,};use wasm_bindgen::prelude::*; static BUNDLED_FONTS: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"), include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"), include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"), include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"), include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"), include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"), include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"), include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),]; pub struct WebPlatform { browser_window: web_sys::Window, dispatcher: Arc<WebDispatcher>, background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, text_system: Arc<dyn PlatformTextSystem>, active_window: RefCell<Option<AnyWindowHandle>>, active_display: Rc<dyn PlatformDisplay>, callbacks: RefCell<WebPlatformCallbacks>, wgpu_context: Rc<RefCell<Option<WgpuContext>>>, cursor_visible: Rc<Cell<bool>>, last_cursor_css: Rc<Cell<&'static str>>, _cursor_restore_listeners: Vec<EventListenerHandle>,} #[derive(Default)]struct WebPlatformCallbacks { open_urls: Option<Box<dyn FnMut(Vec<String>)>>, quit: Option<Box<dyn FnMut()>>, reopen: Option<Box<dyn FnMut()>>, app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>, will_open_app_menu: Option<Box<dyn FnMut()>>, validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>, keyboard_layout_change: Option<Box<dyn FnMut()>>, thermal_state_change: Option<Box<dyn FnMut()>>,} impl WebPlatform { pub fn new(allow_multi_threading: bool) -> Self { let browser_window = web_sys::window().expect("must be running in a browser window context"); let dispatcher = Arc::new(WebDispatcher::new( browser_window.clone(), allow_multi_threading, )); let background_executor = BackgroundExecutor::new(dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(dispatcher.clone()); let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts( "IBM Plex Sans", )); let fonts = BUNDLED_FONTS .iter() .map(|bytes| Cow::Borrowed(*bytes)) .collect(); if let Err(error) = text_system.add_fonts(fonts) { log::error!("failed to load bundled fonts: {error:#}"); } let text_system: Arc<dyn PlatformTextSystem> = text_system; let active_display: Rc<dyn PlatformDisplay> = Rc::new(WebDisplay::new(browser_window.clone())); let cursor_visible = Rc::new(Cell::new(true)); let last_cursor_css = Rc::new(Cell::new("default")); let cursor_restore_listeners = cursor_restore_listeners( &browser_window, cursor_visible.clone(), last_cursor_css.clone(), ); Self { browser_window, dispatcher, background_executor, foreground_executor, text_system, active_window: RefCell::new(None), active_display, callbacks: RefCell::new(WebPlatformCallbacks::default()), wgpu_context: Rc::new(RefCell::new(None)), cursor_visible, last_cursor_css, _cursor_restore_listeners: cursor_restore_listeners, } } /// Returns an HTTP client that runs browser Fetch operations on this platform's main thread. pub fn fetch_http_client(&self) -> FetchHttpClient { FetchHttpClient::new(self.dispatcher.clone()) } /// Returns a browser Fetch HTTP client with the given reported user agent. pub fn fetch_http_client_with_user_agent( &self, user_agent: &str, ) -> anyhow::Result<FetchHttpClient> { FetchHttpClient::with_user_agent(self.dispatcher.clone(), user_agent) }} impl Platform for WebPlatform { fn background_executor(&self) -> BackgroundExecutor { self.background_executor.clone() } fn foreground_executor(&self) -> ForegroundExecutor { self.foreground_executor.clone() } fn text_system(&self) -> Arc<dyn PlatformTextSystem> { self.text_system.clone() } fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) { let wgpu_context = self.wgpu_context.clone(); let browser_window = self.browser_window.clone(); wasm_bindgen_futures::spawn_local(async move { match WgpuContext::new_web().await { Ok(context) => { log::info!("WebGPU context initialized successfully"); *wgpu_context.borrow_mut() = Some(context); on_finish_launching(); } Err(err) => { // Without a GPU context nothing can ever render, so // launching the app would only produce confusing // downstream errors. Leave a message in the page instead. log::error!("Failed to initialize WebGPU context: {err:#}"); show_webgpu_unavailable_message(&browser_window); } } }); } fn quit(&self) { log::warn!("WebPlatform::quit called, but quitting is not supported in the browser ."); } fn restart(&self, _binary_path: Option<PathBuf>) {} fn activate(&self, _ignoring_other_apps: bool) {} fn hide(&self) {} fn hide_other_apps(&self) {} fn unhide_other_apps(&self) {} fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { vec![self.active_display.clone()] } fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> { Some(self.active_display.clone()) } fn active_window(&self) -> Option<AnyWindowHandle> { *self.active_window.borrow() } fn open_window( &self, handle: AnyWindowHandle, params: WindowParams, ) -> anyhow::Result<Box<dyn PlatformWindow>> { // Native popups are not implemented on the web yet. Rejecting lets callers fall back to // gpui's in-window popovers. if let WindowKind::AnchoredPopup(_) = params.kind { return Err(PopupNotSupportedError.into()); } let context_ref = self.wgpu_context.borrow(); let context = context_ref.as_ref().ok_or_else(|| { anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") })?; let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; *self.active_window.borrow_mut() = Some(handle); Ok(Box::new(window)) } fn window_appearance(&self) -> WindowAppearance { let Ok(Some(media_query)) = self .browser_window .match_media("(prefers-color-scheme: dark)") else { return WindowAppearance::Light; }; if media_query.matches() { WindowAppearance::Dark } else { WindowAppearance::Light } } fn open_url(&self, url: &str) { if let Err(error) = self.browser_window.open_with_url(url) { log::warn!("Failed to open URL '{url}': {error:?}"); } } fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) { self.callbacks.borrow_mut().open_urls = Some(callback); } fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> { Task::ready(Ok(())) } fn prompt_for_paths( &self, _options: PathPromptOptions, ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> { let (tx, rx) = oneshot::channel(); tx.send(Err(anyhow::anyhow!( "prompt_for_paths is not supported on the web" ))) .ok(); rx } fn prompt_for_new_path( &self, _directory: &Path, _suggested_name: Option<&str>, ) -> oneshot::Receiver<Result<Option<PathBuf>>> { let (sender, receiver) = oneshot::channel(); sender .send(Err(anyhow::anyhow!( "prompt_for_new_path is not supported on the web" ))) .ok(); receiver } fn can_select_mixed_files_and_dirs(&self) -> bool { false } fn reveal_path(&self, _path: &Path) {} fn open_with_system(&self, _path: &Path) {} fn on_quit(&self, callback: Box<dyn FnMut()>) { self.callbacks.borrow_mut().quit = Some(callback); } fn on_reopen(&self, callback: Box<dyn FnMut()>) { self.callbacks.borrow_mut().reopen = Some(callback); } fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {} fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {} fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {} fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) { self.callbacks.borrow_mut().app_menu_action = Some(callback); } fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) { self.callbacks.borrow_mut().will_open_app_menu = Some(callback); } fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) { self.callbacks.borrow_mut().validate_app_menu_command = Some(callback); } fn thermal_state(&self) -> ThermalState { ThermalState::Nominal } fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) { self.callbacks.borrow_mut().thermal_state_change = Some(callback); } fn compositor_name(&self) -> &'static str { "Web" } fn app_path(&self) -> Result<PathBuf> { Err(anyhow::anyhow!("app_path is not available on the web")) } fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> { Err(anyhow::anyhow!( "path_for_auxiliary_executable is not available on the web" )) } fn set_cursor_style(&self, style: CursorStyle) { let css_cursor = match style { CursorStyle::Arrow => "default", CursorStyle::IBeam => "text", CursorStyle::Crosshair => "crosshair", CursorStyle::ClosedHand => "grabbing", CursorStyle::OpenHand => "grab", CursorStyle::PointingHand => "pointer", CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => { "ew-resize" } CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => { "ns-resize" } CursorStyle::ResizeUpLeftDownRight => "nesw-resize", CursorStyle::ResizeUpRightDownLeft => "nwse-resize", CursorStyle::ResizeColumn => "col-resize", CursorStyle::ResizeRow => "row-resize", CursorStyle::IBeamCursorForVerticalLayout => "vertical-text", CursorStyle::OperationNotAllowed => "not-allowed", CursorStyle::DragLink => "alias", CursorStyle::DragCopy => "copy", CursorStyle::ContextualMenu => "context-menu", }; self.last_cursor_css.set(css_cursor); if self.cursor_visible.get() { set_body_cursor(&self.browser_window, css_cursor); } } fn hide_cursor_until_mouse_moves(&self) { if !self.cursor_visible.replace(false) { return; } set_body_cursor(&self.browser_window, "none"); } fn is_cursor_visible(&self) -> bool { self.cursor_visible.get() } fn should_auto_hide_scrollbars(&self) -> bool { true } fn read_from_clipboard(&self) -> Option<ClipboardItem> { None } fn write_to_clipboard(&self, item: ClipboardItem) { if let Some(text) = item.text() && let Some(window) = web_sys::window() { // Fire-and-forget; called synchronously inside the user's input // event, which satisfies the browser's user-activation requirement. drop(window.navigator().clipboard().write_text(&text)); } } fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!( "credential storage is not available on the web" ))) } fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> { Task::ready(Ok(None)) } fn delete_credentials(&self, _url: &str) -> Task<Result<()>> { Task::ready(Err(anyhow::anyhow!( "credential storage is not available on the web" ))) } fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> { Box::new(WebKeyboardLayout) } fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> { Rc::new(DummyKeyboardMapper) } fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) { self.callbacks.borrow_mut().keyboard_layout_change = Some(callback); }} fn cursor_restore_listeners( browser_window: &web_sys::Window, cursor_visible: Rc<Cell<bool>>, last_cursor_css: Rc<Cell<&'static str>>,) -> Vec<EventListenerHandle> { let mut handles = Vec::new(); let Some(document) = browser_window.document() else { return handles; }; let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| { let browser_window = browser_window.clone(); let cursor_visible = cursor_visible.clone(); let last_cursor_css = last_cursor_css.clone(); handles.push(EventListenerHandle::add( target, event_name, move |_event: JsValue| { if !cursor_visible.replace(true) { set_body_cursor(&browser_window, last_cursor_css.get()); } }, )); }; let document_target: &web_sys::EventTarget = document.as_ref(); let window_target: &web_sys::EventTarget = browser_window.as_ref(); add_listener(document_target, "mousemove"); add_listener(document_target, "mouseenter"); add_listener(window_target, "blur"); add_listener(document_target, "visibilitychange"); handles} fn show_webgpu_unavailable_message(browser_window: &web_sys::Window) { let Some(document) = browser_window.document() else { return; }; let Some(body) = document.body() else { return; }; let Ok(message) = document.create_element("p") else { return; }; message.set_text_content(Some( "Failed to initialize WebGPU. This application requires a browser with WebGPU support.", )); body.append_child(&message).ok();} fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) { if let Some(document) = browser_window.document() && let Some(body) = document.body() && let Err(error) = body.style().set_property("cursor", css_cursor) { log::warn!("Failed to set cursor style: {error:?}"); }}
use crate::dispatcher::WebDispatcher;
use crate::display::WebDisplay;
use crate::events::EventListenerHandle;
use crate::http_client::FetchHttpClient;
use crate::keyboard::WebKeyboardLayout;
use crate::window::WebWindow;
use anyhow::Result;
use futures::channel::oneshot;
use gpui::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
ThermalState, WindowAppearance, WindowKind, WindowParams, popup::PopupNotSupportedError,
};
use gpui_wgpu::WgpuContext;
use std::{
borrow::Cow,
cell::{Cell, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use wasm_bindgen::prelude::*;
static BUNDLED_FONTS: &[&[u8]] = &[
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
];
pub struct WebPlatform {
browser_window: web_sys::Window,
dispatcher: Arc<WebDispatcher>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
active_window: RefCell<Option<AnyWindowHandle>>,
active_display: Rc<dyn PlatformDisplay>,
callbacks: RefCell<WebPlatformCallbacks>,
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
_cursor_restore_listeners: Vec<EventListenerHandle>,
}
#[derive(Default)]
struct WebPlatformCallbacks {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
quit: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>,
app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
will_open_app_menu: Option<Box<dyn FnMut()>>,
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
keyboard_layout_change: Option<Box<dyn FnMut()>>,
thermal_state_change: Option<Box<dyn FnMut()>>,
}
impl WebPlatform {
pub fn new(allow_multi_threading: bool) -> Self {
let browser_window =
web_sys::window().expect("must be running in a browser window context");
let dispatcher = Arc::new(WebDispatcher::new(
browser_window.clone(),
allow_multi_threading,
));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher.clone());
let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
"IBM Plex Sans",
));
let fonts = BUNDLED_FONTS
.iter()
.map(|bytes| Cow::Borrowed(*bytes))
.collect();
if let Err(error) = text_system.add_fonts(fonts) {
log::error!("failed to load bundled fonts: {error:#}");
}
let text_system: Arc<dyn PlatformTextSystem> = text_system;
let active_display: Rc<dyn PlatformDisplay> =
Rc::new(WebDisplay::new(browser_window.clone()));
let cursor_visible = Rc::new(Cell::new(true));
let last_cursor_css = Rc::new(Cell::new("default"));
let cursor_restore_listeners = cursor_restore_listeners(
&browser_window,
cursor_visible.clone(),
last_cursor_css.clone(),
);
Self {
browser_window,
dispatcher,
background_executor,
foreground_executor,
text_system,
active_window: RefCell::new(None),
active_display,
callbacks: RefCell::new(WebPlatformCallbacks::default()),
wgpu_context: Rc::new(RefCell::new(None)),
cursor_visible,
last_cursor_css,
_cursor_restore_listeners: cursor_restore_listeners,
}
}
/// Returns an HTTP client that runs browser Fetch operations on this platform's main thread.
pub fn fetch_http_client(&self) -> FetchHttpClient {
FetchHttpClient::new(self.dispatcher.clone())
}
/// Returns a browser Fetch HTTP client with the given reported user agent.
pub fn fetch_http_client_with_user_agent(
&self,
user_agent: &str,
) -> anyhow::Result<FetchHttpClient> {
FetchHttpClient::with_user_agent(self.dispatcher.clone(), user_agent)
}
}
impl Platform for WebPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
let wgpu_context = self.wgpu_context.clone();
let browser_window = self.browser_window.clone();
wasm_bindgen_futures::spawn_local(async move {
match WgpuContext::new_web().await {
Ok(context) => {
log::info!("WebGPU context initialized successfully");
*wgpu_context.borrow_mut() = Some(context);
on_finish_launching();
}
Err(err) => {
// Without a GPU context nothing can ever render, so
// launching the app would only produce confusing
// downstream errors. Leave a message in the page instead.
log::error!("Failed to initialize WebGPU context: {err:#}");
show_webgpu_unavailable_message(&browser_window);
}
}
});
}
fn quit(&self) {
log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
}
fn restart(&self, _binary_path: Option<PathBuf>) {}
fn activate(&self, _ignoring_other_apps: bool) {}
fn hide(&self) {}
fn hide_other_apps(&self) {}
fn unhide_other_apps(&self) {}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![self.active_display.clone()]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.active_display.clone())
}
fn active_window(&self) -> Option<AnyWindowHandle> {
*self.active_window.borrow()
}
fn open_window(
&self,
handle: AnyWindowHandle,
params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
// Native popups are not implemented on the web yet. Rejecting lets callers fall back to
// gpui's in-window popovers.
if let WindowKind::AnchoredPopup(_) = params.kind {
return Err(PopupNotSupportedError.into());
}
let context_ref = self.wgpu_context.borrow();
let context = context_ref.as_ref().ok_or_else(|| {
anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
})?;
let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
*self.active_window.borrow_mut() = Some(handle);
Ok(Box::new(window))
}
fn window_appearance(&self) -> WindowAppearance {
let Ok(Some(media_query)) = self
.browser_window
.match_media("(prefers-color-scheme: dark)")
else {
return WindowAppearance::Light;
};
if media_query.matches() {
WindowAppearance::Dark
} else {
WindowAppearance::Light
}
}
fn open_url(&self, url: &str) {
if let Err(error) = self.browser_window.open_with_url(url) {
log::warn!("Failed to open URL '{url}': {error:?}");
}
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.callbacks.borrow_mut().open_urls = Some(callback);
}
fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn prompt_for_paths(
&self,
_options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
let (tx, rx) = oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"prompt_for_paths is not supported on the web"
)))
.ok();
rx
}
fn prompt_for_new_path(
&self,
_directory: &Path,
_suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (sender, receiver) = oneshot::channel();
sender
.send(Err(anyhow::anyhow!(
"prompt_for_new_path is not supported on the web"
)))
.ok();
receiver
}
fn can_select_mixed_files_and_dirs(&self) -> bool {
false
}
fn reveal_path(&self, _path: &Path) {}
fn open_with_system(&self, _path: &Path) {}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().reopen = Some(callback);
}
fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.callbacks.borrow_mut().app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
}
fn thermal_state(&self) -> ThermalState {
ThermalState::Nominal
}
fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().thermal_state_change = Some(callback);
}
fn compositor_name(&self) -> &'static str {
"Web"
}
fn app_path(&self) -> Result<PathBuf> {
Err(anyhow::anyhow!("app_path is not available on the web"))
}
fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
Err(anyhow::anyhow!(
"path_for_auxiliary_executable is not available on the web"
))
}
fn set_cursor_style(&self, style: CursorStyle) {
let css_cursor = match style {
CursorStyle::Arrow => "default",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",
CursorStyle::OpenHand => "grab",
CursorStyle::PointingHand => "pointer",
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
"ew-resize"
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
"ns-resize"
}
CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
};
self.last_cursor_css.set(css_cursor);
if self.cursor_visible.get() {
set_body_cursor(&self.browser_window, css_cursor);
}
}
fn hide_cursor_until_mouse_moves(&self) {
if !self.cursor_visible.replace(false) {
return;
}
set_body_cursor(&self.browser_window, "none");
}
fn is_cursor_visible(&self) -> bool {
self.cursor_visible.get()
}
fn should_auto_hide_scrollbars(&self) -> bool {
true
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
None
}
fn write_to_clipboard(&self, item: ClipboardItem) {
if let Some(text) = item.text()
&& let Some(window) = web_sys::window()
{
// Fire-and-forget; called synchronously inside the user's input
// event, which satisfies the browser's user-activation requirement.
drop(window.navigator().clipboard().write_text(&text));
}
}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
Task::ready(Ok(None))
}
fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
Task::ready(Err(anyhow::anyhow!(
"credential storage is not available on the web"
)))
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(WebKeyboardLayout)
}
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
Rc::new(DummyKeyboardMapper)
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
}
}
fn cursor_restore_listeners(
browser_window: &web_sys::Window,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
) -> Vec<EventListenerHandle> {
let mut handles = Vec::new();
let Some(document) = browser_window.document() else {
return handles;
};
let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {
let browser_window = browser_window.clone();
let cursor_visible = cursor_visible.clone();
let last_cursor_css = last_cursor_css.clone();
handles.push(EventListenerHandle::add(
target,
event_name,
move |_event: JsValue| {
if !cursor_visible.replace(true) {
set_body_cursor(&browser_window, last_cursor_css.get());
}
},
));
};
let document_target: &web_sys::EventTarget = document.as_ref();
let window_target: &web_sys::EventTarget = browser_window.as_ref();
add_listener(document_target, "mousemove");
add_listener(document_target, "mouseenter");
add_listener(window_target, "blur");
add_listener(document_target, "visibilitychange");
handles
}
fn show_webgpu_unavailable_message(browser_window: &web_sys::Window) {
let Some(document) = browser_window.document() else {
return;
};
let Some(body) = document.body() else {
return;
};
let Ok(message) = document.create_element("p") else {
return;
};
message.set_text_content(Some(
"Failed to initialize WebGPU. This application requires a browser with WebGPU support.",
));
body.append_child(&message).ok();
}
fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) {
if let Some(document) = browser_window.document()
&& let Some(body) = document.body()
&& let Err(error) = body.style().set_property("cursor", css_cursor)
{
log::warn!("Failed to set cursor style: {error:?}");
}
}