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}