Skip to repository content452 lines · 15.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:56:36.742Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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, 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