Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:01:21.461Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

platform.rs

452 lines · 15.2 KB · rust
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, WindowKind, WindowParams, popup::PopupNotSupportedError,
12};
13use gpui_wgpu::WgpuContext;
14use std::{
15    borrow::Cow,
16    cell::{Cell, RefCell},
17    path::{Path, PathBuf},
18    rc::Rc,
19    sync::Arc,
20};
21use wasm_bindgen::prelude::*;
22
23static BUNDLED_FONTS: &[&[u8]] = &[
24    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"),
25    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf"),
26    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.ttf"),
27    include_bytes!("../../../assets/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.ttf"),
28    include_bytes!("../../../assets/fonts/lilex/Lilex-Regular.ttf"),
29    include_bytes!("../../../assets/fonts/lilex/Lilex-Bold.ttf"),
30    include_bytes!("../../../assets/fonts/lilex/Lilex-Italic.ttf"),
31    include_bytes!("../../../assets/fonts/lilex/Lilex-BoldItalic.ttf"),
32];
33
34pub struct WebPlatform {
35    browser_window: web_sys::Window,
36    background_executor: BackgroundExecutor,
37    foreground_executor: ForegroundExecutor,
38    text_system: Arc<dyn PlatformTextSystem>,
39    active_window: RefCell<Option<AnyWindowHandle>>,
40    active_display: Rc<dyn PlatformDisplay>,
41    callbacks: RefCell<WebPlatformCallbacks>,
42    wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
43    cursor_visible: Rc<Cell<bool>>,
44    last_cursor_css: Rc<Cell<&'static str>>,
45    _cursor_restore_listeners: Vec<EventListenerHandle>,
46}
47
48#[derive(Default)]
49struct WebPlatformCallbacks {
50    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
51    quit: Option<Box<dyn FnMut()>>,
52    reopen: Option<Box<dyn FnMut()>>,
53    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
54    will_open_app_menu: Option<Box<dyn FnMut()>>,
55    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
56    keyboard_layout_change: Option<Box<dyn FnMut()>>,
57    thermal_state_change: Option<Box<dyn FnMut()>>,
58}
59
60impl WebPlatform {
61    pub fn new(allow_multi_threading: bool) -> Self {
62        let browser_window =
63            web_sys::window().expect("must be running in a browser window context");
64        let dispatcher = Arc::new(WebDispatcher::new(
65            browser_window.clone(),
66            allow_multi_threading,
67        ));
68        let background_executor = BackgroundExecutor::new(dispatcher.clone());
69        let foreground_executor = ForegroundExecutor::new(dispatcher);
70        let text_system = Arc::new(gpui_wgpu::CosmicTextSystem::new_without_system_fonts(
71            "IBM Plex Sans",
72        ));
73        let fonts = BUNDLED_FONTS
74            .iter()
75            .map(|bytes| Cow::Borrowed(*bytes))
76            .collect();
77        if let Err(error) = text_system.add_fonts(fonts) {
78            log::error!("failed to load bundled fonts: {error:#}");
79        }
80        let text_system: Arc<dyn PlatformTextSystem> = text_system;
81        let active_display: Rc<dyn PlatformDisplay> =
82            Rc::new(WebDisplay::new(browser_window.clone()));
83
84        let cursor_visible = Rc::new(Cell::new(true));
85        let last_cursor_css = Rc::new(Cell::new("default"));
86        let cursor_restore_listeners = cursor_restore_listeners(
87            &browser_window,
88            cursor_visible.clone(),
89            last_cursor_css.clone(),
90        );
91
92        Self {
93            browser_window,
94            background_executor,
95            foreground_executor,
96            text_system,
97            active_window: RefCell::new(None),
98            active_display,
99            callbacks: RefCell::new(WebPlatformCallbacks::default()),
100            wgpu_context: Rc::new(RefCell::new(None)),
101            cursor_visible,
102            last_cursor_css,
103            _cursor_restore_listeners: cursor_restore_listeners,
104        }
105    }
106}
107
108impl Platform for WebPlatform {
109    fn background_executor(&self) -> BackgroundExecutor {
110        self.background_executor.clone()
111    }
112
113    fn foreground_executor(&self) -> ForegroundExecutor {
114        self.foreground_executor.clone()
115    }
116
117    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
118        self.text_system.clone()
119    }
120
121    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
122        let wgpu_context = self.wgpu_context.clone();
123        wasm_bindgen_futures::spawn_local(async move {
124            match WgpuContext::new_web().await {
125                Ok(context) => {
126                    log::info!("WebGPU context initialized successfully");
127                    *wgpu_context.borrow_mut() = Some(context);
128                    on_finish_launching();
129                }
130                Err(err) => {
131                    log::error!("Failed to initialize WebGPU context: {err:#}");
132                    on_finish_launching();
133                }
134            }
135        });
136    }
137
138    fn quit(&self) {
139        log::warn!("WebPlatform::quit called, but quitting is not supported in the browser .");
140    }
141
142    fn restart(&self, _binary_path: Option<PathBuf>) {}
143
144    fn activate(&self, _ignoring_other_apps: bool) {}
145
146    fn hide(&self) {}
147
148    fn hide_other_apps(&self) {}
149
150    fn unhide_other_apps(&self) {}
151
152    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
153        vec![self.active_display.clone()]
154    }
155
156    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
157        Some(self.active_display.clone())
158    }
159
160    fn active_window(&self) -> Option<AnyWindowHandle> {
161        *self.active_window.borrow()
162    }
163
164    fn open_window(
165        &self,
166        handle: AnyWindowHandle,
167        params: WindowParams,
168    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
169        // Native popups are not implemented on the web yet. Rejecting lets callers fall back to
170        // gpui's in-window popovers.
171        if let WindowKind::AnchoredPopup(_) = params.kind {
172            return Err(PopupNotSupportedError.into());
173        }
174
175        let context_ref = self.wgpu_context.borrow();
176        let context = context_ref.as_ref().ok_or_else(|| {
177            anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?")
178        })?;
179
180        let window = WebWindow::new(handle, params, context, self.browser_window.clone())?;
181        *self.active_window.borrow_mut() = Some(handle);
182        Ok(Box::new(window))
183    }
184
185    fn window_appearance(&self) -> WindowAppearance {
186        let Ok(Some(media_query)) = self
187            .browser_window
188            .match_media("(prefers-color-scheme: dark)")
189        else {
190            return WindowAppearance::Light;
191        };
192        if media_query.matches() {
193            WindowAppearance::Dark
194        } else {
195            WindowAppearance::Light
196        }
197    }
198
199    fn open_url(&self, url: &str) {
200        if let Err(error) = self.browser_window.open_with_url(url) {
201            log::warn!("Failed to open URL '{url}': {error:?}");
202        }
203    }
204
205    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
206        self.callbacks.borrow_mut().open_urls = Some(callback);
207    }
208
209    fn register_url_scheme(&self, _url: &str) -> Task<Result<()>> {
210        Task::ready(Ok(()))
211    }
212
213    fn prompt_for_paths(
214        &self,
215        _options: PathPromptOptions,
216    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
217        let (tx, rx) = oneshot::channel();
218        tx.send(Err(anyhow::anyhow!(
219            "prompt_for_paths is not supported on the web"
220        )))
221        .ok();
222        rx
223    }
224
225    fn prompt_for_new_path(
226        &self,
227        _directory: &Path,
228        _suggested_name: Option<&str>,
229    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
230        let (sender, receiver) = oneshot::channel();
231        sender
232            .send(Err(anyhow::anyhow!(
233                "prompt_for_new_path is not supported on the web"
234            )))
235            .ok();
236        receiver
237    }
238
239    fn can_select_mixed_files_and_dirs(&self) -> bool {
240        false
241    }
242
243    fn reveal_path(&self, _path: &Path) {}
244
245    fn open_with_system(&self, _path: &Path) {}
246
247    fn on_quit(&self, callback: Box<dyn FnMut()>) {
248        self.callbacks.borrow_mut().quit = Some(callback);
249    }
250
251    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
252        self.callbacks.borrow_mut().reopen = Some(callback);
253    }
254
255    fn on_system_wake(&self, _callback: Box<dyn FnMut()>) {}
256
257    fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
258
259    fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {}
260
261    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
262        self.callbacks.borrow_mut().app_menu_action = Some(callback);
263    }
264
265    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
266        self.callbacks.borrow_mut().will_open_app_menu = Some(callback);
267    }
268
269    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
270        self.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
271    }
272
273    fn thermal_state(&self) -> ThermalState {
274        ThermalState::Nominal
275    }
276
277    fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>) {
278        self.callbacks.borrow_mut().thermal_state_change = Some(callback);
279    }
280
281    fn compositor_name(&self) -> &'static str {
282        "Web"
283    }
284
285    fn app_path(&self) -> Result<PathBuf> {
286        Err(anyhow::anyhow!("app_path is not available on the web"))
287    }
288
289    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
290        Err(anyhow::anyhow!(
291            "path_for_auxiliary_executable is not available on the web"
292        ))
293    }
294
295    fn set_cursor_style(&self, style: CursorStyle) {
296        let css_cursor = match style {
297            CursorStyle::Arrow => "default",
298            CursorStyle::IBeam => "text",
299            CursorStyle::Crosshair => "crosshair",
300            CursorStyle::ClosedHand => "grabbing",
301            CursorStyle::OpenHand => "grab",
302            CursorStyle::PointingHand => "pointer",
303            CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
304                "ew-resize"
305            }
306            CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
307                "ns-resize"
308            }
309            CursorStyle::ResizeUpLeftDownRight => "nesw-resize",
310            CursorStyle::ResizeUpRightDownLeft => "nwse-resize",
311            CursorStyle::ResizeColumn => "col-resize",
312            CursorStyle::ResizeRow => "row-resize",
313            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
314            CursorStyle::OperationNotAllowed => "not-allowed",
315            CursorStyle::DragLink => "alias",
316            CursorStyle::DragCopy => "copy",
317            CursorStyle::ContextualMenu => "context-menu",
318        };
319
320        self.last_cursor_css.set(css_cursor);
321        if self.cursor_visible.get() {
322            set_body_cursor(&self.browser_window, css_cursor);
323        }
324    }
325
326    fn hide_cursor_until_mouse_moves(&self) {
327        if !self.cursor_visible.replace(false) {
328            return;
329        }
330        set_body_cursor(&self.browser_window, "none");
331    }
332
333    fn is_cursor_visible(&self) -> bool {
334        self.cursor_visible.get()
335    }
336
337    fn should_auto_hide_scrollbars(&self) -> bool {
338        true
339    }
340
341    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
342        None
343    }
344
345    fn write_to_clipboard(&self, item: ClipboardItem) {
346        if let Some(text) = item.text()
347            && let Some(window) = web_sys::window()
348        {
349            // Fire-and-forget; called synchronously inside the user's input
350            // event, which satisfies the browser's user-activation requirement.
351            drop(window.navigator().clipboard().write_text(&text));
352        }
353    }
354
355    fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
356        Task::ready(Err(anyhow::anyhow!(
357            "credential storage is not available on the web"
358        )))
359    }
360
361    fn read_credentials(&self, _url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
362        Task::ready(Ok(None))
363    }
364
365    fn delete_credentials(&self, _url: &str) -> Task<Result<()>> {
366        Task::ready(Err(anyhow::anyhow!(
367            "credential storage is not available on the web"
368        )))
369    }
370
371    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
372        Box::new(WebKeyboardLayout)
373    }
374
375    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
376        Rc::new(DummyKeyboardMapper)
377    }
378
379    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
380        self.callbacks.borrow_mut().keyboard_layout_change = Some(callback);
381    }
382}
383
384struct EventListenerHandle {
385    target: web_sys::EventTarget,
386    event_name: &'static str,
387    closure: Closure<dyn FnMut(JsValue)>,
388}
389
390impl Drop for EventListenerHandle {
391    fn drop(&mut self) {
392        self.target
393            .remove_event_listener_with_callback(
394                self.event_name,
395                self.closure.as_ref().unchecked_ref(),
396            )
397            .ok();
398    }
399}
400
401fn cursor_restore_listeners(
402    browser_window: &web_sys::Window,
403    cursor_visible: Rc<Cell<bool>>,
404    last_cursor_css: Rc<Cell<&'static str>>,
405) -> Vec<EventListenerHandle> {
406    let mut handles = Vec::new();
407    let Some(document) = browser_window.document() else {
408        return handles;
409    };
410
411    let make_restore_handler = |browser_window: web_sys::Window| {
412        let cursor_visible = cursor_visible.clone();
413        let last_cursor_css = last_cursor_css.clone();
414        Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
415            if !cursor_visible.replace(true) {
416                set_body_cursor(&browser_window, last_cursor_css.get());
417            }
418        })
419    };
420
421    let mut add_listener = |target: &web_sys::EventTarget, event_name: &'static str| {
422        let closure = make_restore_handler(browser_window.clone());
423        target
424            .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
425            .ok();
426        handles.push(EventListenerHandle {
427            target: target.clone(),
428            event_name,
429            closure,
430        });
431    };
432
433    let document_target: &web_sys::EventTarget = document.as_ref();
434    let window_target: &web_sys::EventTarget = browser_window.as_ref();
435
436    add_listener(document_target, "mousemove");
437    add_listener(document_target, "mouseenter");
438    add_listener(window_target, "blur");
439    add_listener(document_target, "visibilitychange");
440
441    handles
442}
443
444fn set_body_cursor(browser_window: &web_sys::Window, css_cursor: &str) {
445    if let Some(document) = browser_window.document()
446        && let Some(body) = document.body()
447        && let Err(error) = body.style().set_property("cursor", css_cursor)
448    {
449        log::warn!("Failed to set cursor style: {error:?}");
450    }
451}
452
Served at tenant.openagents/omega Member data and write actions are omitted.