451 lines · 14.9 KB · Rust
RawDownload
use crate::dispatcher::WebDispatcher;use crate::display::WebDisplay;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,    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);        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,            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,        }    }} 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();        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) => {                    log::error!("Failed to initialize WebGPU context: {err:#}");                    on_finish_launching();                }            }        });    }     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);    }} struct EventListenerHandle {    target: web_sys::EventTarget,    event_name: &'static str,    closure: Closure<dyn FnMut(JsValue)>,} impl Drop for EventListenerHandle {    fn drop(&mut self) {        self.target            .remove_event_listener_with_callback(                self.event_name,                self.closure.as_ref().unchecked_ref(),            )            .ok();    }} 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 make_restore_handler = |browser_window: web_sys::Window| {        let cursor_visible = cursor_visible.clone();        let last_cursor_css = last_cursor_css.clone();        Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {            if !cursor_visible.replace(true) {                set_body_cursor(&browser_window, last_cursor_css.get());            }        })    };     let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {        let closure = make_restore_handler(browser_window.clone());        target            .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())            .ok();        handles.push(EventListenerHandle {            target: target.clone(),            event_name,            closure,        });    };     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 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:?}");    }}