gpui_platform/platform.rs
1//! The [`Platform`] trait implemented by each platform backend.
2//!
3//! `gpui` drives an application through a `dyn Platform`; backends
4//! (`gpui_macos`, `gpui_linux`, `gpui_windows`, `gpui_web`) implement it
5//! without depending on the framework itself.
6
7use crate::{
8 ActivityGuard, AppLifecyclePhase, BackgroundExecutor, ClipboardItem, ClipboardReadError,
9 CursorStyle, ForegroundExecutor, GpuAdapterInfo, GpuPreference, MenuCommandId,
10 PathPromptOptions, PlatformDisplay, PlatformGestures, PlatformKeyboardLayout,
11 PlatformKeyboardMapper, PlatformMenu, PlatformMenuItem, PlatformWindow, ScreenCaptureSource,
12 SystemNotification, SystemNotificationResponse, Task, ThermalState, WindowAppearance,
13 WindowButtonLayout, WindowId, WindowParams,
14};
15use anyhow::Result;
16use futures::channel::oneshot;
17use gpui_engine::{DmaBufCapabilities, PlatformTextSystem};
18use smallvec::SmallVec;
19use std::{
20 ffi::OsString,
21 path::{Path, PathBuf},
22 rc::Rc,
23 sync::Arc,
24};
25
26// TODO(jk): return an enum instead of a string
27/// Return which compositor we're guessing we'll use.
28/// Does not attempt to connect to the given compositor.
29#[cfg(any(target_os = "linux", target_os = "freebsd"))]
30#[inline]
31pub fn guess_compositor() -> &'static str {
32 if std::env::var_os("ZED_HEADLESS").is_some() {
33 return "Headless";
34 }
35 GraphicalEnvironment::detect().guess_compositor()
36}
37
38/// The graphical session to connect to: the variables that locate its display server, as some
39/// process sees them.
40///
41/// A long-running process can outlive the graphical session it was started in, so a platform
42/// that attaches to a display server later can be given a fresher environment than its own.
43/// While connected, programs the platform launches (for example to open a URL) get these
44/// variables instead of the ones this process started with. Apply
45/// [`App::graphical_environment`] to the programs an app launches, for the same reason.
46#[cfg(any(target_os = "linux", target_os = "freebsd"))]
47#[derive(Clone, Debug, Default)]
48pub struct GraphicalEnvironment {
49 /// `WAYLAND_DISPLAY`: a socket name relative to `xdg_runtime_dir`, or an absolute path.
50 pub wayland_display: Option<OsString>,
51 /// `DISPLAY`: the X11 display name.
52 pub x11_display: Option<OsString>,
53 /// `XDG_RUNTIME_DIR`: the directory containing Wayland sockets.
54 pub xdg_runtime_dir: Option<OsString>,
55 /// `XDG_ACTIVATION_TOKEN`: lets the first window take focus on Wayland.
56 ///
57 /// A platform takes this process's token from its environment when it's created, and uses
58 /// it if it starts windowed. Pass one here to switch to windowed mode later, for example
59 /// the token of the process that asked for a window.
60 pub activation_token: Option<String>,
61}
62
63/// The graphical session to connect to: the Windows session whose desktop windows appear on.
64///
65/// A process can only show windows in its own session, so switching to windowed mode fails if
66/// this names another one, for example when a process started over SSH is asked to show a
67/// window on the desktop.
68#[cfg(target_os = "windows")]
69#[derive(Clone, Debug, Default)]
70pub struct GraphicalEnvironment {
71 /// The session ID, as `ProcessIdToSessionId` reports it. `None` means this process's own
72 /// session.
73 pub session_id: Option<u32>,
74}
75
76#[cfg(target_os = "windows")]
77impl GraphicalEnvironment {
78 /// Returns the environment of this process's session.
79 pub fn detect() -> Self {
80 let mut session_id = 0;
81 // SAFETY: `session_id` is a valid pointer for the call's duration.
82 let result = unsafe {
83 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
84 windows::Win32::System::Threading::GetCurrentProcessId(),
85 &mut session_id,
86 )
87 };
88 Self {
89 session_id: result.is_ok().then_some(session_id),
90 }
91 }
92
93 /// Does nothing on this platform: programs inherit the session of the process that
94 /// starts them.
95 pub fn apply_to(&self, _command: &mut std::process::Command) {}
96}
97
98/// The graphical session to connect to. Carries nothing yet on this platform.
99#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
100#[derive(Clone, Debug, Default)]
101pub struct GraphicalEnvironment;
102
103#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
104impl GraphicalEnvironment {
105 /// Returns the environment of this process's graphical session.
106 pub fn detect() -> Self {
107 Self
108 }
109
110 /// Does nothing on this platform.
111 pub fn apply_to(&self, _command: &mut std::process::Command) {}
112}
113
114/// A display mode for [`App::request_windowing`] to switch to.
115#[derive(Clone, Debug)]
116pub enum WindowingRequest {
117 /// No display server. Windows lay out and handle input but draw nothing.
118 Headless,
119 /// Connected to the display server that the environment names.
120 Windowed(GraphicalEnvironment),
121}
122
123#[cfg(any(target_os = "linux", target_os = "freebsd"))]
124impl GraphicalEnvironment {
125 /// Reads the display variables from this process's environment.
126 ///
127 /// This only reads environment variables: whether they name a reachable display server is
128 /// checked when a platform connects. Leaves `activation_token` unset: see its documentation.
129 pub fn detect() -> Self {
130 Self {
131 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
132 x11_display: std::env::var_os("DISPLAY"),
133 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
134 activation_token: None,
135 }
136 }
137
138 /// Sets this environment's display variables on `command`, and removes the ones it doesn't
139 /// set, so the program connects to this graphical session rather than the one this process
140 /// started in. Leaves `XDG_ACTIVATION_TOKEN` alone.
141 pub fn apply_to(&self, command: &mut std::process::Command) {
142 for (name, value) in [
143 ("WAYLAND_DISPLAY", &self.wayland_display),
144 ("DISPLAY", &self.x11_display),
145 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
146 ] {
147 match value {
148 Some(value) => command.env(name, value),
149 None => command.env_remove(name),
150 };
151 }
152 }
153
154 /// Returns the compositor this environment selects: Wayland, then X11, then headless.
155 ///
156 /// Does not attempt to connect to the compositor.
157 pub fn guess_compositor(&self) -> &'static str {
158 let is_set =
159 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
160 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
161 "Wayland"
162 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
163 "X11"
164 } else {
165 "Headless"
166 }
167 }
168}
169
170#[cfg(any(target_os = "linux", target_os = "freebsd"))]
171bitflags::bitflags! {
172 /// The windowing modes a platform may start in or switch to.
173 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
174 pub struct WindowingModes: u8 {
175 /// Connected to a Wayland compositor.
176 const WAYLAND = 1 << 0;
177 /// Connected to an X server.
178 const X11 = 1 << 1;
179 /// No display server. Windows lay out and handle input but draw nothing.
180 const HEADLESS = 1 << 2;
181 }
182}
183
184/// Controls whether the application participates in the system's foreground UI.
185///
186/// Only has an effect on macOS; other platforms ignore this setting. There,
187/// [`App::request_windowing`] sets it: `Accessory` while headless and `Regular` while windowed.
188/// Set it directly with [`App::set_activation_policy`] for the one case that doesn't cover: an
189/// accessory app that shows windows, such as a menu bar utility.
190#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
191pub enum ActivationPolicy {
192 /// Participate in foreground application UI, such as the Dock and menu bar on macOS.
193 #[default]
194 Regular,
195 /// Run without foreground application UI while retaining the ability to open windows.
196 Accessory,
197}
198
199#[expect(missing_docs)]
200pub trait Platform: 'static {
201 fn background_executor(&self) -> BackgroundExecutor;
202 fn foreground_executor(&self) -> ForegroundExecutor;
203 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
204
205 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
206 fn quit(&self);
207 /// Switches a capable platform between headless and windowed modes. See
208 /// [`App::request_windowing`].
209 /// Sets the windowing mode the platform starts in. Called before `run`. See
210 /// [`Application::with_windowing`].
211 fn set_initial_windowing(&self, _request: WindowingRequest) {}
212 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
213 Task::ready(Err(anyhow::anyhow!(
214 "this platform cannot switch between headless and windowed modes"
215 )))
216 }
217 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
218 fn activate(&self, ignoring_other_apps: bool);
219 /// Sets the initial or current activation policy. Has no effect outside macOS.
220 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
221 fn hide(&self);
222 fn hide_other_apps(&self);
223 fn unhide_other_apps(&self);
224
225 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
226 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
227 /// Registers the callback invoked when displays may have been added or
228 /// removed, or a display's properties may have changed. GPUI rereads
229 /// [`Self::displays`] to find out what changed, so calls may be spurious.
230 /// The callback runs on the main thread outside of any window update.
231 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
232 fn active_window(&self) -> Option<WindowId>;
233 fn window_stack(&self) -> Option<Vec<WindowId>> {
234 None
235 }
236
237 fn is_screen_capture_supported(&self) -> bool {
238 false
239 }
240
241 fn screen_capture_sources(
242 &self,
243 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
244 let (sources_tx, sources_rx) = oneshot::channel();
245 sources_tx
246 .send(Err(anyhow::anyhow!(
247 "gpui was compiled without the screen-capture feature"
248 )))
249 .ok();
250 sources_rx
251 }
252
253 fn open_window(
254 &self,
255 handle: WindowId,
256 options: WindowParams,
257 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
258
259 /// Returns the appearance of the application's windows.
260 fn window_appearance(&self) -> WindowAppearance;
261
262 /// Overrides the appearance (light/dark) applied to the app's windows, independent
263 /// of the OS-wide setting. Pass `None` to clear the override and follow the system
264 /// again. The override is reflected by [`Platform::window_appearance`].
265 ///
266 /// Currently only implemented on macOS, where it sets `NSApplication.appearance` so
267 /// the native window chrome (the window border and titlebar) of every window matches
268 /// a dark app theme even when the system is in light mode (or vice versa). A no-op on
269 /// other platforms.
270 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
271
272 /// States which GPU the application prefers the renderer to run on.
273 ///
274 /// The device is process-wide and chosen before the first window, so this has to be set before
275 /// the application runs. A backend that cannot choose ignores it. See decision `0008` in the
276 /// project record, `decisions/0008-gpu-preference-and-enumeration.md`.
277 fn set_gpu_preference(&self, _preference: GpuPreference) {}
278
279 /// The adapters the application could render on.
280 ///
281 /// Enumeration happens without creating a device, so this is safe to call before
282 /// [`run`](Self::run) and is what a preferences panel reads; it is also the list a
283 /// [`GpuPreference::Custom`] selector chooses from. The order is the platform's; a caller that
284 /// wants a particular one states a preference rather than indexing into this list.
285 ///
286 /// A backend that cannot enumerate returns **one** synthesized entry rather than an empty list:
287 /// the identity contract is that every reported adapter has a permanent part and a session, and
288 /// a single-GPU platform should still answer "which GPU" with "the GPU".
289 fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
290 vec![GpuAdapterInfo::single()]
291 }
292
293 /// What the renderer can import as a dma-buf surface, once it has a device.
294 ///
295 /// Answered on the device the renderer already made — no device is created — and `None` before
296 /// the first window or on a platform whose surfaces are not dma-bufs. A producer uses it to
297 /// choose a layout rather than discover a refusal at import time.
298 fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
299 None
300 }
301
302 /// The adapter the renderer actually chose, once it has chosen one.
303 ///
304 /// The device is process-wide and selected before the first window, so the answer is `None`
305 /// until a window has created one; after that it is the adapter every window draws through. A
306 /// platform that cannot report it returns `None`.
307 fn active_gpu(&self) -> Option<GpuAdapterInfo> {
308 None
309 }
310
311 /// Returns the window button layout configuration when supported.
312 fn button_layout(&self) -> Option<WindowButtonLayout> {
313 None
314 }
315
316 fn open_url(&self, url: &str);
317 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
318 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
319
320 fn prompt_for_paths(
321 &self,
322 options: PathPromptOptions,
323 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
324 fn prompt_for_new_path(
325 &self,
326 directory: &Path,
327 suggested_name: Option<&str>,
328 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
329 fn can_select_mixed_files_and_dirs(&self) -> bool;
330 fn reveal_path(&self, path: &Path);
331 fn open_with_system(&self, path: &Path);
332
333 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
334 fn on_reopen(&self, callback: Box<dyn FnMut()>);
335 /// Registers the callback invoked when the system is about to sleep.
336 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
337 /// Registers the callback invoked when the system resumes from sleep.
338 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
339
340 // Mobile platform methods. On mobile the OS owns the application
341 // lifecycle: apps are backgrounded, foregrounded, and killed at the
342 // system's discretion, and must react rather than decide.
343
344 /// Registers a callback invoked whenever the application's lifecycle
345 /// phase changes. See [`AppLifecyclePhase`] for the phase vocabulary and
346 /// its mapping onto iOS and Android.
347 ///
348 /// Desktop platforms never invoke this.
349 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
350
351 /// Registers a callback invoked when the OS signals memory pressure
352 /// (iOS `didReceiveMemoryWarning`, Android `onTrimMemory`).
353 ///
354 /// Desktop platforms never invoke this.
355 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
356
357 /// The platform's gesture recognition services, if it provides any
358 /// beyond gpui's portable recognizers. See
359 /// [`PlatformGestures`](crate::PlatformGestures).
360 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
361 None
362 }
363
364 fn set_menus(&self, menus: Vec<PlatformMenu>);
365
366 fn set_dock_menu(&self, menu: Vec<PlatformMenuItem>);
367 fn perform_dock_menu_action(&self, _action: usize) {}
368 fn add_recent_document(&self, _path: &Path) {}
369 fn update_jump_list(
370 &self,
371 _menus: Vec<PlatformMenuItem>,
372 _entries: Vec<SmallVec<[PathBuf; 2]>>,
373 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
374 Task::ready(Vec::new())
375 }
376 fn on_app_menu_action(&self, callback: Box<dyn FnMut(MenuCommandId)>);
377 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
378 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(MenuCommandId) -> bool>);
379
380 fn thermal_state(&self) -> ThermalState;
381 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
382 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
383
384 /// Sets the application's process-wide identity and user-visible name.
385 ///
386 /// The identifier is used for platform identity mechanisms such as the
387 /// Windows AppUserModelID. The name is used wherever the operating system
388 /// presents the application to the user. Call this once, early in startup,
389 /// before opening windows or posting notifications.
390 fn set_app_identity(&self, identifier: &str, name: &str) {
391 _ = (identifier, name);
392 }
393
394 /// Posts a notification to the operating system's notification center.
395 ///
396 /// Posting a notification whose [`SystemNotification::tag`] matches an
397 /// earlier one replaces that notification where the platform supports it.
398 /// No-op on platforms without notification support, or when delivery is
399 /// unavailable (e.g. authorization was denied).
400 fn show_system_notification(&self, notification: SystemNotification) {
401 _ = notification;
402 }
403
404 /// Removes the delivered or pending notification with this tag.
405 ///
406 /// Best-effort: some platforms cannot retract a notification once shown,
407 /// in which case it ages out of the notification center on its own.
408 fn dismiss_system_notification(&self, tag: &str) {
409 _ = tag;
410 }
411
412 /// Registers the callback invoked when the user activates a system
413 /// notification, either by clicking its body or one of its action
414 /// buttons.
415 ///
416 /// Implementations must invoke the callback on the main thread.
417 fn on_system_notification_response(
418 &self,
419 callback: Box<dyn FnMut(SystemNotificationResponse)>,
420 ) {
421 _ = callback;
422 }
423
424 fn compositor_name(&self) -> &'static str {
425 ""
426 }
427 /// The environment of the display server a platform that can switch windowing modes is
428 /// connected to. See [`App::graphical_environment`].
429 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
430 None
431 }
432 fn app_path(&self) -> Result<PathBuf>;
433 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
434
435 fn set_cursor_style(&self, style: CursorStyle);
436
437 /// Hides the mouse cursor until the user moves the mouse over one of
438 /// this application's windows.
439 fn hide_cursor_until_mouse_moves(&self);
440
441 /// Returns whether the mouse cursor is currently visible.
442 fn is_cursor_visible(&self) -> bool;
443
444 fn should_auto_hide_scrollbars(&self) -> bool;
445
446 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
447 fn write_to_clipboard(&self, item: ClipboardItem);
448
449 /// Reads the clipboard, resolving once its contents are available.
450 ///
451 /// Most platforms read synchronously and return a ready task. Platforms
452 /// whose clipboard access is inherently asynchronous and permission-gated
453 /// (e.g. the browser's async clipboard API) override this method; on those
454 /// platforms [`Platform::read_from_clipboard`] cannot return the clipboard
455 /// contents, so callers that can await should prefer this method.
456 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
457 Task::ready(Ok(self.read_from_clipboard()))
458 }
459
460 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
461 fn read_from_primary(&self) -> Option<ClipboardItem>;
462 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
463 fn write_to_primary(&self, item: ClipboardItem);
464
465 #[cfg(target_os = "macos")]
466 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
467 #[cfg(target_os = "macos")]
468 fn write_to_find_pasteboard(&self, item: ClipboardItem);
469
470 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
471 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
472 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
473
474 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
475 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
476 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
477}