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