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}