platform.rs

  1use crate::dispatcher::WebDispatcher;
  2use crate::display::WebDisplay;
  3use crate::keyboard::WebKeyboardLayout;
  4use crate::window::WebWindow;
  5use anyhow::Result;
  6use futures::channel::oneshot;
  7use gpui::{
  8    Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DummyKeyboardMapper,
  9    ForegroundExecutor, Keymap, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
 10    PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task,
 11    ThermalState, WindowAppearance, WindowParams,
 12};
 13use gpui_wgpu::WgpuContext;
 14use std::{
 15    borrow::Cow,
 16    cell::RefCell,
 17    path::{Path, PathBuf},
 18    rc::Rc,
 19    sync::Arc,
 20};
 21
 22static BUNDLED_FONTS: &[&[u8]] = &[
 23    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
 24    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
 25    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
 26    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
 27    include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
 28    include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
 29    include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
 30    include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
 31];
 32
 33pub struct WebPlatform {
 34    browser_window: web_sys::Window,
 35    background_executor: BackgroundExecutor,
 36    foreground_executor: ForegroundExecutor,
 37    text_system: Arc<dyn PlatformTextSystem>,
 38    active_window: RefCell<Option<AnyWindowHandle>>,
 39    active_display: Rc<dyn PlatformDisplay>,
 40    callbacks: RefCell<WebPlatformCallbacks>,
 41    wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
 42}
 43
 44#[derive(Default)]
 45struct WebPlatformCallbacks {
 46    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 47    quit: Option<Box<dyn FnMut()>>,
 48    reopen: Option<Box<dyn FnMut()>>,
 49    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 50    will_open_app_menu: Option<Box<dyn FnMut()>>,
 51    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 52    keyboard_layout_change: Option<Box<dyn FnMut()>>,
 53    thermal_state_change: Option<Box<dyn FnMut()>>,
 54}
 55
 56impl WebPlatform {
 57    pub fn new() -> Self {
 58        let browser_window =
 59            web_sys::window().expect("must be running in a browser window context");
 60        let dispatcher = Arc::new(WebDispatcher::new(browser_window.clone()));
 61        let background_executor = BackgroundExecutor::new(dispatcher.clone());
 62        let foreground_executor = ForegroundExecutor::new(dispatcher);
 63        let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
 64            "IBM Plex Sans",
 65        ));
 66        let fonts = BUNDLED_FONTS
 67            .iter()
 68            .map(|bytes| Cow::Borrowed(*bytes))
 69            .collect();
 70        if let Err(error) = text_system.add_fonts(fonts) {
 71            log::error!("failed to load bundled fonts: {error:#}");
 72        }
 73        let text_system: Arc<dyn PlatformTextSystem> = text_system;
 74        let active_display: Rc<dyn PlatformDisplay> =
 75            Rc::new(WebDisplay::new(browser_window.clone()));
 76
 77        Self {
 78            browser_window,
 79            background_executor,
 80            foreground_executor,
 81            text_system,
 82            active_window: RefCell::new(None),
 83            active_display,
 84            callbacks: RefCell::new(WebPlatformCallbacks::default()),
 85            wgpu_context: Rc::new(RefCell::new(None)),
 86        }
 87    }
 88}
 89
 90impl Platform for WebPlatform {
 91    fn background_executor(&self) -> BackgroundExecutor {
 92        self.background_executor.clone()
 93    }
 94
 95    fn foreground_executor(&self) -> ForegroundExecutor {
 96        self.foreground_executor.clone()
 97    }
 98
 99    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
100        self.text_system.clone()
101    }
102
103    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
104        let wgpu_context = self.wgpu_context.clone();
105        wasm_bindgen_futures::spawn_local(async move {
106            match WgpuContext::new_web().await {
107                Ok(context) => {
108                    log::info!("WebGPU context initialized successfully");
109                    *wgpu_context.borrow_mut() = Some(context);
110                    on_finish_launching();
111                }
112                Err(err) => {
113                    log::error!("Failed to initialize WebGPU context: {err:#}");
114                    on_finish_launching();
115                }
116            }
117        });
118    }
119
120    fn quit(&self) {
121        log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
122    }
123
124    fn restart(&self, _binary_path: Option<PathBuf>) {}
125
126    fn activate(&self, _ignoring_other_apps: bool) {}
127
128    fn hide(&self) {}
129
130    fn hide_other_apps(&self) {}
131
132    fn unhide_other_apps(&self) {}
133
134    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
135        vec![self.active_display.clone()]
136    }
137
138    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
139        Some(self.active_display.clone())
140    }
141
142    fn active_window(&self) -> Option<AnyWindowHandle> {
143        *self.active_window.borrow()
144    }
145
146    fn open_window(
147        &self,
148        handle: AnyWindowHandle,
149        params: WindowParams,
150    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
151        let context_ref = self.wgpu_context.borrow();
152        let context = context_ref.as_ref().ok_or_else(|| {
153            anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
154        })?;
155
156        let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
157        *self.active_window.borrow_mut() = Some(handle);
158        Ok(Box::new(window))
159    }
160
161    fn window_appearance(&self) -> WindowAppearance {
162        let Ok(Some(media_query)) = self
163            .browser_window
164            .match_media("(prefers-color-scheme: dark)")
165        else {
166            return WindowAppearance::Light;
167        };
168        if media_query.matches() {
169            WindowAppearance::Dark
170        } else {
171            WindowAppearance::Light
172        }
173    }
174
175    fn open_url(&self, url: &str) {
176        if let Err(error) = self.browser_window.open_with_url(url) {
177            log::warn!("Failed to open URL '{url}': {error:?}");
178        }
179    }
180
181    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
182        self.callbacks.borrow_mut().open_urls = Some(callback);
183    }
184
185    fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
186        Task::ready(Ok(()))
187    }
188
189    fn prompt_for_paths(
190        &self,
191        _options: PathPromptOptions,
192    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
193        let (tx, rx) = oneshot::channel();
194        tx.send(Err(anyhow::anyhow!(
195            "prompt_for_paths is not supported on the web"
196        )))
197        .ok();
198        rx
199    }
200
201    fn prompt_for_new_path(
202        &self,
203        _directory: &Path,
204        _suggested_name: Option<&str>,
205    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
206        let (sender, receiver) = oneshot::channel();
207        sender
208            .send(Err(anyhow::anyhow!(
209                "prompt_for_new_path is not supported on the web"
210            )))
211            .ok();
212        receiver
213    }
214
215    fn can_select_mixed_files_and_dirs(&self) -> bool {
216        false
217    }
218
219    fn reveal_path(&self, _path: &Path) {}
220
221    fn open_with_system(&self, _path: &Path) {}
222
223    fn on_quit(&self, callback: Box<dyn FnMut()>) {
224        self.callbacks.borrow_mut().quit = Some(callback);
225    }
226
227    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
228        self.callbacks.borrow_mut().reopen = Some(callback);
229    }
230
231    fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
232
233    fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
234
235    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
236        self.callbacks.borrow_mut().app_menu_action = Some(callback);
237    }
238
239    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
240        self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
241    }
242
243    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
244        self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
245    }
246
247    fn thermal_state(&self) -> ThermalState {
248        ThermalState::Nominal
249    }
250
251    fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
252        self.callbacks.borrow_mut().thermal_state_change = Some(callback);
253    }
254
255    fn compositor_name(&self) -> &'static str {
256        "Web"
257    }
258
259    fn app_path(&self) -> Result<PathBuf> {
260        Err(anyhow::anyhow!("app_path is not available on the web"))
261    }
262
263    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
264        Err(anyhow::anyhow!(
265            "path_for_auxiliary_executable is not available on the web"
266        ))
267    }
268
269    fn set_cursor_style(&self, style: CursorStyle) {
270        let css_cursor = match style {
271            CursorStyle::Arrow => "default",
272            CursorStyle::IBeam => "text",
273            CursorStyle::Crosshair => "crosshair",
274            CursorStyle::ClosedHand => "grabbing",
275            CursorStyle::OpenHand => "grab",
276            CursorStyle::PointingHand => "pointer",
277            CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
278                "ew-resize"
279            }
280            CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
281                "ns-resize"
282            }
283            CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
284            CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
285            CursorStyle::ResizeColumn => "col-resize",
286            CursorStyle::ResizeRow => "row-resize",
287            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
288            CursorStyle::OperationNotAllowed => "not-allowed",
289            CursorStyle::DragLink => "alias",
290            CursorStyle::DragCopy => "copy",
291            CursorStyle::ContextualMenu => "context-menu",
292            CursorStyle::None => "none",
293        };
294
295        if let Some(document) = self.browser_window.document() {
296            if let Some(body) = document.body() {
297                if let Err(error) = body.style().set_property("cursor", css_cursor) {
298                    log::warn!("Failed to set cursor style: {error:?}");
299                }
300            }
301        }
302    }
303
304    fn should_auto_hide_scrollbars(&self) -> bool {
305        true
306    }
307
308    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
309        None
310    }
311
312    fn write_to_clipboard(&self, _item: ClipboardItem) {}
313
314    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
315        Task::ready(Err(anyhow::anyhow!(
316            "credential storage is not available on the web"
317        )))
318    }
319
320    fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
321        Task::ready(Ok(None))
322    }
323
324    fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
325        Task::ready(Err(anyhow::anyhow!(
326            "credential storage is not available on the web"
327        )))
328    }
329
330    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
331        Box::new(WebKeyboardLayout)
332    }
333
334    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
335        Rc::new(DummyKeyboardMapper)
336    }
337
338    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
339        self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
340    }
341}