gpui/platform.rs
1mod app_menu;
2mod keyboard;
3mod keystroke;
4
5#[cfg(all(target_os = "linux", feature = "wayland"))]
6#[expect(missing_docs)]
7pub mod layer_shell;
8
9/// Types for configuring parent-anchored popup windows such as menus, dropdowns and tooltips.
10pub mod popup;
11
12#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
13mod threaded_dispatcher;
14
15#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
16mod test;
17
18#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
19mod visual_test;
20
21#[cfg(all(feature = "screen-capture", target_os = "windows"))]
22pub mod screen_capture;
23#[cfg(target_os = "windows")]
24mod windows_screen_capture;
25#[cfg(target_os = "windows")]
26pub use windows_screen_capture::WindowsScreenCaptureFrame;
27
28#[cfg(all(target_os = "windows", feature = "screen-capture"))]
29pub(crate) type PlatformScreenCaptureFrame = WindowsScreenCaptureFrame;
30#[cfg(not(feature = "screen-capture"))]
31pub(crate) type PlatformScreenCaptureFrame = ();
32#[cfg(all(target_os = "macos", feature = "screen-capture"))]
33pub(crate) type PlatformScreenCaptureFrame =
34 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
35#[cfg(all(
36 feature = "screen-capture",
37 not(any(target_os = "macos", target_os = "windows"))
38))]
39// Screen capture currently has native frame representations only on macOS and Windows. Keep the
40// cross-platform API well-formed for enabled-but-unsupported targets; source enumeration simply
41// yields no platform sources there.
42pub(crate) type PlatformScreenCaptureFrame = ();
43
44use crate::util::FluentBuilder;
45use crate::{
46 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
47 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
48 FontId, FontMetrics, ForegroundExecutor, GlyphAtlasEntry, GlyphId, GpuSpecs, ImageSource,
49 InlineLayout, InlineLayoutRequest, Keymap, LineLayout, Pixels, PlatformGestures, PlatformInput,
50 Point, PreparedRasterStyle, Priority, RasterStyleRequest, RasterizedGlyph,
51 RasterizedGlyphFormat, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
52 Scene, SharedString, Size, SvgRenderer, SystemWindowTab, Task, TextLayoutRequest,
53 ValidatedRasterizedGlyph, Window, WindowControlArea, hash, point, px,
54};
55#[cfg(any(target_os = "linux", target_os = "freebsd"))]
56use anyhow::bail;
57use anyhow::{Context as _, Result};
58use async_task::Runnable;
59use collections::FxHashMap;
60use futures::channel::oneshot;
61#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
62use image::RgbaImage;
63use image::codecs::gif::GifDecoder;
64use image::{AnimationDecoder as _, DynamicImage, Frame};
65use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
66use scheduler::Instant;
67pub use scheduler::{LocalTaskScope, RunnableMeta};
68use schemars::JsonSchema;
69use seahash::SeaHasher;
70use serde::{Deserialize, Serialize};
71use smallvec::SmallVec;
72use std::borrow::Cow;
73use std::collections::hash_map::Entry;
74use std::hash::{Hash, Hasher};
75use std::io::Cursor;
76use std::ops;
77use std::time::Duration;
78use std::{
79 ffi::OsString,
80 fmt::{self, Debug},
81 ops::Range,
82 path::{Path, PathBuf},
83 rc::Rc,
84 sync::Arc,
85};
86use strum::EnumIter;
87use uuid::Uuid;
88
89pub use app_menu::*;
90pub use keyboard::*;
91pub use keystroke::*;
92
93/// Whether the platform is presenting a window's frames.
94///
95/// This is about presentation, not the window's shown/hidden state: a shown
96/// window that is fully covered by other windows, minimized, on another
97/// Space or virtual desktop, or on a display that is asleep is `Hidden`. A
98/// window only partly covered by other windows is `Visible`.
99///
100/// Each platform reports from a single source, and what that source can see
101/// differs:
102///
103/// * macOS: `NSWindow.occlusionState`. Covers all of the cases above.
104/// * Windows: `WS_VISIBLE` and the minimized state. Windows keeps compositing
105/// covered windows for thumbnails and Alt-Tab and offers no occlusion
106/// notification, so a fully covered window stays `Visible`. Display sleep is
107/// not reported either.
108/// * Wayland: the `xdg_toplevel` `suspended` state (xdg-shell v6). Compositors
109/// that don't support it never report `Hidden`.
110/// * X11: mapped state plus `VisibilityNotify`. Compositing window managers
111/// generally never report a window as fully obscured, so covering is only
112/// detected without compositing.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum WindowVisibility {
115 /// At least part of the window is being presented; frames drawn for it
116 /// will be shown.
117 Visible,
118 /// No part of the window is being presented. The platform will not
119 /// request frames for it until it becomes visible again.
120 Hidden,
121}
122
123impl WindowVisibility {
124 /// Whether frames drawn for the window will be shown.
125 pub fn is_visible(self) -> bool {
126 self == Self::Visible
127 }
128}
129
130/// A change to the connected displays, reported through
131/// [`App::observe_displays`](crate::App::observe_displays).
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133#[non_exhaustive]
134pub enum DisplayEvent {
135 /// A display was connected.
136 Added(DisplayId),
137 /// A display was disconnected. Windows on it are moved to another
138 /// display by the platform.
139 Removed(DisplayId),
140 /// A connected display's properties changed.
141 Changed {
142 /// The display that changed.
143 id: DisplayId,
144 /// Which of its properties changed.
145 changes: DisplayChanges,
146 },
147}
148
149bitflags::bitflags! {
150 /// The properties of a display that a [`DisplayEvent::Changed`] reports.
151 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
152 pub struct DisplayChanges: u8 {
153 /// [`PlatformDisplay::bounds`] changed.
154 const BOUNDS = 1 << 0;
155 /// [`PlatformDisplay::refresh_interval`] changed.
156 const REFRESH_INTERVAL = 1 << 1;
157 }
158}
159
160/// Converts a refresh rate in hertz to the time between refreshes, rejecting
161/// the zero, negative, and non-finite rates platforms use to mean "unknown".
162pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
163 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
164}
165
166/// Controls whether the application participates in the system's foreground UI.
167///
168/// Only has an effect on macOS; other platforms ignore this setting. There,
169/// [`App::request_windowing`] sets it: `Accessory` while headless and `Regular` while windowed.
170/// Set it directly with [`App::set_activation_policy`] for the one case that doesn't cover: an
171/// accessory app that shows windows, such as a menu bar utility.
172#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
173pub enum ActivationPolicy {
174 /// Participate in foreground application UI, such as the Dock and menu bar on macOS.
175 #[default]
176 Regular,
177 /// Run without foreground application UI while retaining the ability to open windows.
178 Accessory,
179 /// Run in the background without windows; the app cannot be activated.
180 Prohibited,
181}
182
183#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
184pub(crate) use test::*;
185
186#[cfg(any(test, feature = "test-support"))]
187pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
188
189#[cfg(any(test, feature = "test-support"))]
190pub use tests::TestTextSystem;
191
192#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
193pub use threaded_dispatcher::ThreadedDispatcher;
194
195#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
196pub use visual_test::VisualTestPlatform;
197
198/// Keeps an operating system activity, such as an idle sleep inhibitor, alive until dropped.
199pub struct ActivityGuard {
200 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
201}
202
203impl ActivityGuard {
204 /// Runs `release` when the guard is dropped.
205 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
206 Self {
207 _release: gpui_util::defer(Box::new(release)),
208 }
209 }
210
211 /// A guard for platforms without a corresponding activity.
212 pub fn noop() -> Self {
213 Self::new(|| {})
214 }
215}
216
217// TODO(jk): return an enum instead of a string
218/// Return which compositor we're guessing we'll use.
219/// Does not attempt to connect to the given compositor.
220#[cfg(any(target_os = "linux", target_os = "freebsd"))]
221#[inline]
222pub fn guess_compositor() -> &'static str {
223 if std::env::var_os("ZED_HEADLESS").is_some() {
224 return "Headless";
225 }
226 GraphicalEnvironment::detect().guess_compositor()
227}
228
229/// The graphical session to connect to: the variables that locate its display server, as some
230/// process sees them.
231///
232/// A long-running process can outlive the graphical session it was started in, so a platform
233/// that attaches to a display server later can be given a fresher environment than its own.
234/// While connected, programs the platform launches (for example to open a URL) get these
235/// variables instead of the ones this process started with. Apply
236/// [`App::graphical_environment`] to the programs an app launches, for the same reason.
237#[cfg(any(target_os = "linux", target_os = "freebsd"))]
238#[derive(Clone, Debug, Default)]
239pub struct GraphicalEnvironment {
240 /// `WAYLAND_DISPLAY`: a socket name relative to `xdg_runtime_dir`, or an absolute path.
241 pub wayland_display: Option<OsString>,
242 /// `DISPLAY`: the X11 display name.
243 pub x11_display: Option<OsString>,
244 /// `XDG_RUNTIME_DIR`: the directory containing Wayland sockets.
245 pub xdg_runtime_dir: Option<OsString>,
246 /// `XDG_ACTIVATION_TOKEN`: lets the first window take focus on Wayland.
247 ///
248 /// A platform takes this process's token from its environment when it's created, and uses
249 /// it if it starts windowed. Pass one here to switch to windowed mode later, for example
250 /// the token of the process that asked for a window.
251 pub activation_token: Option<String>,
252}
253
254/// The graphical session to connect to: the Windows session whose desktop windows appear on.
255///
256/// A process can only show windows in its own session, so switching to windowed mode fails if
257/// this names another one, for example when a process started over SSH is asked to show a
258/// window on the desktop.
259#[cfg(target_os = "windows")]
260#[derive(Clone, Debug, Default)]
261pub struct GraphicalEnvironment {
262 /// The session ID, as `ProcessIdToSessionId` reports it. `None` means this process's own
263 /// session.
264 pub session_id: Option<u32>,
265}
266
267#[cfg(target_os = "windows")]
268impl GraphicalEnvironment {
269 /// Returns the environment of this process's session.
270 pub fn detect() -> Self {
271 let mut session_id = 0;
272 // SAFETY: `session_id` is a valid pointer for the call's duration.
273 let result = unsafe {
274 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
275 windows::Win32::System::Threading::GetCurrentProcessId(),
276 &mut session_id,
277 )
278 };
279 Self {
280 session_id: result.is_ok().then_some(session_id),
281 }
282 }
283
284 /// Does nothing on this platform: programs inherit the session of the process that
285 /// starts them.
286 pub fn apply_to(&self, _command: &mut std::process::Command) {}
287}
288
289/// The graphical session to connect to. Carries nothing yet on this platform.
290#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
291#[derive(Clone, Debug, Default)]
292pub struct GraphicalEnvironment;
293
294#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
295impl GraphicalEnvironment {
296 /// Returns the environment of this process's graphical session.
297 pub fn detect() -> Self {
298 Self
299 }
300
301 /// Does nothing on this platform.
302 pub fn apply_to(&self, _command: &mut std::process::Command) {}
303}
304
305/// A display mode for [`App::request_windowing`] to switch to.
306#[derive(Clone, Debug)]
307pub enum WindowingRequest {
308 /// No display server. Windows lay out and handle input but draw nothing.
309 Headless,
310 /// Connected to the display server that the environment names.
311 Windowed(GraphicalEnvironment),
312}
313
314#[cfg(any(target_os = "linux", target_os = "freebsd"))]
315impl GraphicalEnvironment {
316 /// Reads the display variables from this process's environment.
317 ///
318 /// This only reads environment variables: whether they name a reachable display server is
319 /// checked when a platform connects. Leaves `activation_token` unset: see its documentation.
320 pub fn detect() -> Self {
321 Self {
322 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
323 x11_display: std::env::var_os("DISPLAY"),
324 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
325 activation_token: None,
326 }
327 }
328
329 /// Sets this environment's display variables on `command`, and removes the ones it doesn't
330 /// set, so the program connects to this graphical session rather than the one this process
331 /// started in. Leaves `XDG_ACTIVATION_TOKEN` alone.
332 pub fn apply_to(&self, command: &mut std::process::Command) {
333 for (name, value) in [
334 ("WAYLAND_DISPLAY", &self.wayland_display),
335 ("DISPLAY", &self.x11_display),
336 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
337 ] {
338 match value {
339 Some(value) => command.env(name, value),
340 None => command.env_remove(name),
341 };
342 }
343 }
344
345 /// Returns the compositor this environment selects: Wayland, then X11, then headless.
346 ///
347 /// Does not attempt to connect to the compositor.
348 pub fn guess_compositor(&self) -> &'static str {
349 let is_set =
350 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
351 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
352 "Wayland"
353 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
354 "X11"
355 } else {
356 "Headless"
357 }
358 }
359}
360
361#[cfg(any(target_os = "linux", target_os = "freebsd"))]
362bitflags::bitflags! {
363 /// The windowing modes a platform may start in or switch to.
364 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
365 pub struct WindowingModes: u8 {
366 /// Connected to a Wayland compositor.
367 const WAYLAND = 1 << 0;
368 /// Connected to an X server.
369 const X11 = 1 << 1;
370 /// No display server. Windows lay out and handle input but draw nothing.
371 const HEADLESS = 1 << 2;
372 }
373}
374
375/// The activation policy for a macOS application.
376#[deprecated(note = "renamed to `ActivationPolicy`")]
377pub type MacActivationPolicy = ActivationPolicy;
378
379/// The operating system or browser that GPUI runs on, as seen by the [`App`].
380///
381/// Each backend crate implements this trait once: `gpui_macos`, `gpui_windows`,
382/// `gpui_linux` and `gpui_web`. Application code doesn't call most of these methods
383/// directly. It calls the [`App`] methods that wrap them. Use this trait to
384/// implement a new backend, or to reach a platform service that [`App`] doesn't
385/// expose.
386///
387/// Methods without a default body are required. Methods with a default body are
388/// optional platform services: the default supplies the behavior for a backend
389/// that does not provide the service. The [platforms](crate::_documentation::platforms)
390/// table lists the backends that override each default.
391pub trait Platform: 'static {
392 /// Returns the executor for work that runs off the main thread.
393 fn background_executor(&self) -> BackgroundExecutor;
394 /// Returns the executor that runs tasks on the main thread, the thread that owns windows.
395 fn foreground_executor(&self) -> ForegroundExecutor;
396 /// Returns the platform's text system, which shapes and rasterizes glyphs.
397 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
398 /// Starts the event loop and calls `on_finish_launching` once the application is ready.
399 ///
400 /// This method blocks until the application exits. Create windows inside
401 /// `on_finish_launching`.
402 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
403 /// Asks the application to quit.
404 ///
405 /// On macOS the request is processed asynchronously, so the event loop keeps
406 /// running briefly. The web backend only logs a warning, because a page can't quit
407 /// itself.
408 fn quit(&self);
409 /// Sets the windowing mode the platform starts in. Called before run. See
410 /// Application::with_windowing.
411 fn set_initial_windowing(&self, _request: WindowingRequest) {}
412 /// Requests a switch between headless and windowed modes while the application is running.
413 ///
414 /// The returned task resolves when the platform has applied the change. Platforms that do
415 /// not support switching return an error from the default implementation.
416 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
417 Task::ready(Err(anyhow::anyhow!(
418 "this platform cannot switch between headless and windowed modes"
419 )))
420 }
421 /// Restarts the application by launching a new process and quitting this one.
422 ///
423 /// `binary_path` defaults to the current executable when `None`, and `arguments`
424 /// are passed to the new process. The web backend does nothing.
425 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
426 /// Brings the application to the front.
427 ///
428 /// When `ignoring_other_apps` is `true`, the application takes focus even if
429 /// another application is active. The web backend ignores the call.
430 fn activate(&self, ignoring_other_apps: bool);
431 /// Sets the initial or current activation policy. Has no effect outside macOS.
432 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
433 /// Hides all of the application's windows. macOS hides them. Windows does nothing,
434 /// and Linux logs a message and ignores the call.
435 fn hide(&self);
436 /// Hides all other running applications. macOS does this. Linux and the web do nothing.
437 /// Windows panics, because its implementation is `unimplemented!()`.
438 fn hide_other_apps(&self);
439 /// Shows the applications hidden by [`Platform::hide_other_apps`]. macOS does this.
440 /// Linux and the web do nothing.
441 fn unhide_other_apps(&self);
442
443 /// Returns every display currently connected, in no particular order.
444 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
445 /// Returns the display that new windows open on by default, or `None` if there is none.
446 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
447 /// Registers the callback invoked when displays may have been added or
448 /// removed, or a display's properties may have changed. GPUI rereads
449 /// [`Self::displays`] to find out what changed, so calls may be spurious.
450 /// The callback runs on the main thread outside of any window update.
451 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
452 /// Returns the handle of the window that currently has focus, if any.
453 fn active_window(&self) -> Option<AnyWindowHandle>;
454 /// Returns the open windows from front to back, or `None` if the platform doesn't track stacking order.
455 ///
456 /// Implemented on macOS and Linux. The default returns `None`.
457 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
458 None
459 }
460
461 /// Returns `true` if this platform can capture the screen.
462 ///
463 /// Returns `true` on macOS 12.3 or later and on Windows. Other platforms return
464 /// `false`.
465 fn is_screen_capture_supported(&self) -> bool {
466 false
467 }
468
469 /// Lists the displays and windows that can be captured.
470 ///
471 /// The result arrives on the returned channel once enumeration finishes. The
472 /// default sends an error, so backends must override it to list sources.
473 fn screen_capture_sources(
474 &self,
475 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
476 let (sources_tx, sources_rx) = oneshot::channel();
477 sources_tx
478 .send(Err(anyhow::anyhow!(
479 "gpui was compiled without the screen-capture feature"
480 )))
481 .ok();
482 sources_rx
483 }
484
485 /// Creates a native window for `handle` with the given `options`.
486 ///
487 /// [`App::open_window`] calls this. The returned [`PlatformWindow`] represents the
488 /// native window. Errors are returned for unsupported options, such as a second
489 /// window on the web.
490 fn open_window(
491 &self,
492 handle: AnyWindowHandle,
493 options: WindowParams,
494 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
495
496 /// Returns the appearance of the application's windows.
497 fn window_appearance(&self) -> WindowAppearance;
498
499 /// Overrides the appearance (light/dark) applied to the app's windows, independent
500 /// of the OS-wide setting. Pass `None` to clear the override and follow the system
501 /// again. The override is reflected by [`Platform::window_appearance`].
502 ///
503 /// Only macOS implements this, by setting `NSApplication.appearance`. This keeps
504 /// the native window chrome matching a dark app theme when the system is light, or
505 /// the reverse. Other platforms ignore the call.
506 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
507
508 /// Returns the order of window control buttons, or `None` if the platform
509 /// doesn't use the layout. Only Linux returns a value. macOS, Windows and the web return `None`.
510 fn button_layout(&self) -> Option<WindowButtonLayout> {
511 None
512 }
513
514 /// Opens `url` in the default handler for its scheme, such as the browser for `https`.
515 fn open_url(&self, url: &str);
516 /// Registers the callback that receives URLs the operating system asks this application to open.
517 ///
518 /// Registering replaces any earlier callback. The web backend stores the callback
519 /// but never calls it.
520 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
521 /// Registers this application as the handler for `url`'s scheme, such as `myapp://`.
522 ///
523 /// The task resolves once registration finishes. The web backend resolves it at once and does nothing.
524 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
525
526 /// Shows a file or folder picker and resolves to the chosen paths.
527 ///
528 /// The receiver yields `Ok(None)` when the user cancels. The options control whether
529 /// files, directories or several items can be picked. The web backend doesn't
530 /// support this, so the receiver resolves to an error.
531 fn prompt_for_paths(
532 &self,
533 options: PathPromptOptions,
534 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
535 /// Shows a save dialog in `directory` and resolves to the chosen path.
536 ///
537 /// `suggested_name` pre-fills the file name. The receiver yields `Ok(None)` when
538 /// the user cancels. The web backend doesn't support this.
539 fn prompt_for_new_path(
540 &self,
541 directory: &Path,
542 suggested_name: Option<&str>,
543 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
544 /// Returns whether a single file picker can select both files and directories.
545 ///
546 /// Only macOS returns `true`. Other platforms return `false` and need separate
547 /// pickers for files and directories.
548 fn can_select_mixed_files_and_dirs(&self) -> bool;
549 /// Shows `path` selected in the file manager, such as Finder or Explorer.
550 fn reveal_path(&self, path: &Path);
551 /// Opens `path` with the application the system associates with its file type.
552 fn open_with_system(&self, path: &Path);
553
554 /// Registers the callback the platform runs when the application is asked to quit.
555 ///
556 /// The callback returns `true` when shutdown has completed, and `false` when it
557 /// couldn't shut down synchronously. macOS calls it while terminating and ignores
558 /// the return value.
559 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
560 /// Registers the callback run when the user activates the running application again, such as clicking its Dock icon on macOS.
561 fn on_reopen(&self, callback: Box<dyn FnMut()>);
562 /// Registers the callback invoked when the system is about to sleep.
563 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
564 /// Registers the callback run after the system wakes from sleep.
565 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
566
567 // Mobile platform methods. On mobile the OS owns the application
568 // lifecycle: apps are backgrounded, foregrounded, and killed at the
569 // system's discretion, and must react rather than decide.
570
571 /// Registers a callback invoked whenever the application's lifecycle
572 /// phase changes. See [`AppLifecyclePhase`] for the phase vocabulary and
573 /// its mapping onto iOS and Android.
574 ///
575 /// Desktop platforms never invoke this.
576 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
577
578 /// Registers a callback invoked when the OS signals memory pressure
579 /// (iOS `didReceiveMemoryWarning`, Android `onTrimMemory`).
580 ///
581 /// Desktop platforms never invoke this.
582 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
583
584 /// Returns the platform's gesture recognizers, or `None` to use GPUI's portable ones.
585 /// See [`PlatformGestures`].
586 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
587 None
588 }
589
590 /// Replaces the application menu with `menus`, and uses `keymap` to show each item's shortcut.
591 ///
592 /// Windows stores the menus but doesn't draw them. Linux and the web don't show a
593 /// menu bar.
594 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
595 /// Returns the menus last set with [`Platform::set_menus`], or `None` if the platform doesn't keep them.
596 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
597 None
598 }
599
600 /// Sets the menu shown when the user right-clicks the app's Dock icon (macOS) or taskbar entry (Windows, as a jump list).
601 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
602 /// Runs the dock menu item at index `_action`. Windows implements this.
603 fn perform_dock_menu_action(&self, _action: usize) {}
604 /// Adds `path` to the system's recent documents list. Only macOS implements this.
605 fn add_recent_document(&self, _path: &Path) {}
606 /// Updates the Windows jump list with `_menus` and the recent-file `_entries`, and returns the entries that were kept.
607 ///
608 /// Other backends return an empty list.
609 fn update_jump_list(
610 &self,
611 _menus: Vec<MenuItem>,
612 _entries: Vec<SmallVec<[PathBuf; 2]>>,
613 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
614 Task::ready(Vec::new())
615 }
616 /// Registers the callback that receives actions chosen from the application menu.
617 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
618 /// Registers the callback run just before a menu opens, so the app can update its menu items.
619 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
620 /// Registers the callback that decides whether a menu item is enabled. Return `true` to enable the action.
621 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
622
623 /// Returns the device's current thermal state. See [`ThermalState`].
624 fn thermal_state(&self) -> ThermalState;
625 /// Registers the callback run when [`Platform::thermal_state`] changes. Read the new state from the platform inside the callback.
626 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
627 /// Requests that the operating system keep the device awake while the returned guard lives.
628 ///
629 /// The reason explains the activity to the platform and may be shown in system UI. Dropping
630 /// the ActivityGuard releases the request. The task resolves with the guard or an error if
631 /// the platform cannot create the activity request.
632 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
633
634 /// Sets the application's identifier and user-visible name.
635 ///
636 /// Windows sets `identifier` as the AppUserModelID, unless the process already has
637 /// package identity. Linux stores `name`. macOS and the web ignore both values.
638 ///
639 /// Call it once early in startup, before opening windows or posting notifications.
640 fn set_app_identity(&self, identifier: &str, name: &str) {
641 _ = (identifier, name);
642 }
643
644 /// Posts `notification` to the operating system's notification center.
645 ///
646 /// Posting a notification whose [`SystemNotification::tag`] matches an
647 /// earlier one replaces that notification where the platform supports it.
648 /// No-op on platforms without notification support, or when delivery is
649 /// unavailable (e.g. authorization was denied).
650 fn show_system_notification(&self, notification: SystemNotification) {
651 _ = notification;
652 }
653
654 /// Removes the delivered or pending notification with this tag.
655 ///
656 /// Best-effort: some platforms cannot retract a notification once shown,
657 /// in which case it ages out of the notification center on its own.
658 fn dismiss_system_notification(&self, tag: &str) {
659 _ = tag;
660 }
661
662 /// Registers the callback invoked when the user activates a system
663 /// notification, either by clicking its body or one of its action
664 /// buttons.
665 ///
666 /// Implementations must invoke the callback on the main thread.
667 fn on_system_notification_response(
668 &self,
669 callback: Box<dyn FnMut(SystemNotificationResponse)>,
670 ) {
671 _ = callback;
672 }
673
674 /// Returns the name of the display server or compositor, such as `"Wayland"`, `"X11"` or `"Headless"`.
675 ///
676 /// Returns an empty string by default.
677 fn compositor_name(&self) -> &'static str {
678 ""
679 }
680 /// The environment of the display server a platform that can switch windowing modes is
681 /// connected to. See [`App::graphical_environment`].
682 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
683 None
684 }
685 /// Returns the path of the running application's executable.
686 fn app_path(&self) -> Result<PathBuf>;
687 /// Returns the path of a helper executable named `name` that ships alongside the application.
688 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
689
690 /// Shows the mouse cursor `style` over this application's windows.
691 fn set_cursor_style(&self, style: CursorStyle);
692
693 /// Hides the mouse cursor until the user moves the mouse over one of
694 /// this application's windows.
695 fn hide_cursor_until_mouse_moves(&self);
696
697 /// Returns whether the mouse cursor is currently visible.
698 fn is_cursor_visible(&self) -> bool;
699
700 /// Returns `true` when scrollbars should hide until the user scrolls. macOS follows
701 /// the overlay-scroller setting, Windows and Linux read their UI settings, and the
702 /// web returns `true`.
703 fn should_auto_hide_scrollbars(&self) -> bool;
704
705 /// Returns the clipboard's contents synchronously, or `None` if it's empty.
706 ///
707 /// The web backend can't read the clipboard synchronously and always returns
708 /// `None`. Use [`Platform::read_from_clipboard_async`] there.
709 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
710 /// Replaces the clipboard's contents with `item`.
711 fn write_to_clipboard(&self, item: ClipboardItem);
712
713 /// Reads the clipboard, resolving once its contents are available.
714 ///
715 /// Most platforms read synchronously and return a ready task. A platform with
716 /// inherently asynchronous or permission-gated access, such as the browser's
717 /// Async Clipboard API, overrides this method. On those platforms,
718 /// [`Platform::read_from_clipboard`] cannot return the contents; callers that
719 /// can await should use this method.
720 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
721 Task::ready(Ok(self.read_from_clipboard()))
722 }
723
724 /// Returns the contents of the X11 or Wayland primary selection, the text selected with the mouse.
725 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
726 fn read_from_primary(&self) -> Option<ClipboardItem>;
727 /// Sets the primary selection to `item`. The selection is the text highlighted with the mouse, pasted with middle-click.
728 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
729 fn write_to_primary(&self, item: ClipboardItem);
730
731 /// Returns the contents of the macOS find pasteboard, which the find bar shares across apps.
732 #[cfg(target_os = "macos")]
733 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
734 /// Sets the macOS find pasteboard to `item`.
735 #[cfg(target_os = "macos")]
736 fn write_to_find_pasteboard(&self, item: ClipboardItem);
737
738 /// Stores `username` and `password` for `url` in the system credential store.
739 ///
740 /// The task resolves once the credentials are saved. Use [`Platform::read_credentials`] to read them back.
741 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
742 /// Reads the username and password stored for `url`, resolving to `None` if there are none.
743 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
744 /// Deletes the credentials stored for `url`.
745 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
746
747 /// Returns the current keyboard layout.
748 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
749 /// Returns the mapper that converts key codes to the characters the current layout produces.
750 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
751 /// Registers the callback run when the keyboard layout changes.
752 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
753
754 /// Registers extra GPU device requirements, such as features and limits, before the first window is opened.
755 ///
756 /// The value must be a `gpui_wgpu::WgpuDeviceRequirements`. macOS and Linux use
757 /// it. Other backends ignore it.
758 fn set_gpu_requirements(&self, _requirements: Box<dyn std::any::Any>) {}
759
760 /// Sets the label applied to credentials stored in the system keyring.
761 /// Only Linux/FreeBSD use this label.
762 fn set_keyring_label(&self, _label: SharedString) {}
763
764 /// Returns `true` if [`Platform::play_haptic_feedback`] has an effect on this platform.
765 ///
766 /// Only macOS returns `true`, for trackpads with Force Touch.
767 fn supports_haptic_feedback(&self) -> bool {
768 false
769 }
770
771 /// Plays haptic feedback of the given style.
772 ///
773 /// No-op on platforms that don't support haptic feedback.
774 fn play_haptic_feedback(&self, _style: HapticFeedbackStyle) {}
775}
776
777/// A display, such as a monitor or a laptop screen, as the platform reports it.
778///
779/// Obtain one from [`App::displays`] or [`App::primary_display`]. Its bounds are
780/// in logical pixels.
781pub trait PlatformDisplay: Debug {
782 /// Returns the display's id for the current session. To persist a display across restarts, use [`PlatformDisplay::uuid`].
783 fn id(&self) -> DisplayId;
784
785 /// Returns a stable identifier for this display that survives restarts.
786 ///
787 /// Returns an error if the platform can't provide one.
788 fn uuid(&self) -> Result<Uuid>;
789
790 /// Returns the display's full bounds in logical pixels, including areas such as the taskbar.
791 fn bounds(&self) -> Bounds<Pixels>;
792
793 /// The time between the display's refreshes. Variable refresh rate
794 /// displays report their maximum rate. `None` when the platform doesn't
795 /// report it.
796 ///
797 /// May query the operating system; GPUI caches it per display, see
798 /// [`Window::refresh_interval`](crate::Window::refresh_interval).
799 fn refresh_interval(&self) -> Option<Duration>;
800
801 /// Returns the usable bounds, excluding the menu bar, Dock and taskbar.
802 ///
803 /// Defaults to [`PlatformDisplay::bounds`] when a backend doesn't override it.
804 fn visible_bounds(&self) -> Bounds<Pixels> {
805 self.bounds()
806 }
807
808 /// Returns the bounds a new window opens at on this display: the default window size, centered and clipped to the display.
809 fn default_bounds(&self) -> Bounds<Pixels> {
810 let bounds = self.bounds();
811 let center = bounds.center();
812 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
813
814 let offset = clipped_window_size / 2.0;
815 let origin = point(center.x - offset.width, center.y - offset.height);
816 Bounds::new(origin, clipped_window_size)
817 }
818}
819
820/// A notification for the operating system's notification center, rather than
821/// in-app UI.
822///
823/// Post one with [`App::show_system_notification`]. Responses arrive as a
824/// [`SystemNotificationResponse`] with the same `tag`.
825#[derive(Clone, Debug, PartialEq, Eq)]
826pub struct SystemNotification {
827 /// Stable identity for the notification. Posting a new notification with
828 /// the same tag replaces the previous one where the platform supports it,
829 /// and responses carry the tag back to the application.
830 pub tag: SharedString,
831 /// The notification's headline.
832 pub title: SharedString,
833 /// Additional text displayed below the title.
834 pub body: SharedString,
835 /// Buttons shown with the notification. Platforms may show fewer than given.
836 pub actions: Vec<SystemNotificationAction>,
837}
838
839/// A button on a [`SystemNotification`].
840#[derive(Clone, Debug, PartialEq, Eq, Hash)]
841pub struct SystemNotificationAction {
842 /// Identifies the button in a [`SystemNotificationResponse::action_id`].
843 pub id: SharedString,
844 /// The text shown on the button.
845 pub label: SharedString,
846}
847
848/// The user's response to a [`SystemNotification`].
849#[derive(Clone, Debug, PartialEq, Eq)]
850pub struct SystemNotificationResponse {
851 /// The [`SystemNotification::tag`] of the notification the user activated.
852 pub tag: SharedString,
853 /// The [`SystemNotificationAction::id`] of the button clicked, or `None` when the body was clicked.
854 pub action_id: Option<SharedString>,
855}
856
857/// The device's thermal pressure, from least to most constrained.
858///
859/// Read it with [`App::thermal_state`]. When it changes, the callback from
860/// [`App::on_thermal_state_change`] runs. Reduce discretionary work, such as
861/// animation frames or background indexing, as the state rises.
862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub enum ThermalState {
864 /// The system has no thermal constraints.
865 Nominal,
866 /// The system is slightly constrained. Reduce discretionary work.
867 Fair,
868 /// The system is moderately constrained. Reduce CPU and GPU intensive work.
869 Serious,
870 /// The system is critically constrained. Minimize resource use.
871 Critical,
872}
873
874/// A kind of haptic feedback to play with [`App::play_haptic_feedback`].
875///
876/// The styles map to `NSHapticFeedbackPattern` on macOS. Other platforms do nothing.
877#[derive(Debug, Clone, Copy, PartialEq, Eq)]
878pub enum HapticFeedbackStyle {
879 /// A generic tap, for most interactions.
880 Generic,
881 /// A sharp snap, for alignment guides, detents and snapping.
882 Alignment,
883 /// A distinct click, for slider steps, toggles and discrete state changes.
884 LevelChange,
885}
886
887/// Describes a screen or window that can be captured.
888///
889/// Returned by [`ScreenCaptureSource::metadata`] and [`ScreenCaptureStream::metadata`].
890#[derive(Clone)]
891pub struct SourceMetadata {
892 /// An opaque identifier for the source. It is stable only within one session.
893 pub id: u64,
894 /// A name to show in a picker, or `None` when the platform gives none.
895 pub label: Option<SharedString>,
896 /// Whether this is the main display, or `None` when unknown.
897 pub is_main: Option<bool>,
898 /// The size of the captured video, in device pixels.
899 pub resolution: Size<DevicePixels>,
900}
901
902/// A screen or window that can be captured.
903///
904/// Get sources from [`Platform::screen_capture_sources`]. Call
905/// [`ScreenCaptureSource::stream`] to start receiving frames.
906pub trait ScreenCaptureSource {
907 /// Returns a description of this source.
908 fn metadata(&self) -> Result<SourceMetadata>;
909
910 /// Starts capturing this source and calls `frame_callback` with each frame.
911 ///
912 /// The returned receiver resolves to the running [`ScreenCaptureStream`], or to an
913 /// error if capture can't start. The platform chooses the thread that runs
914 /// `frame_callback`, so the callback must be `Send`. Drop the stream to stop capture.
915 fn stream(
916 &self,
917 foreground_executor: &ForegroundExecutor,
918 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
919 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
920}
921
922/// A running capture of a [`ScreenCaptureSource`].
923pub trait ScreenCaptureStream {
924 /// Returns a description of the captured source.
925 fn metadata(&self) -> Result<SourceMetadata>;
926}
927
928/// One frame of captured video.
929///
930/// The wrapped value is platform-specific. On macOS it is a `CVImageBuffer`, and on
931/// Windows it is a `WindowsScreenCaptureFrame`. Without the `screen-capture`
932/// feature it is `()`.
933pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
934
935/// An identifier for a display that is valid for the current session.
936#[derive(PartialEq, Eq, Hash, Copy, Clone)]
937pub struct DisplayId(pub(crate) u64);
938
939impl DisplayId {
940 /// Creates a `DisplayId` from a raw platform display identifier.
941 pub fn new(id: u64) -> Self {
942 Self(id)
943 }
944}
945
946impl From<u64> for DisplayId {
947 fn from(id: u64) -> Self {
948 Self(id)
949 }
950}
951
952impl From<DisplayId> for u64 {
953 fn from(id: DisplayId) -> Self {
954 id.0
955 }
956}
957
958impl Debug for DisplayId {
959 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960 write!(f, "DisplayId({})", self.0)
961 }
962}
963
964/// The edge or corner of a window that a user drags to resize it.
965#[derive(Debug, Clone, Copy, PartialEq, Eq)]
966pub enum ResizeEdge {
967 /// The top edge
968 Top,
969 /// The top right corner
970 TopRight,
971 /// The right edge
972 Right,
973 /// The bottom right corner
974 BottomRight,
975 /// The bottom edge
976 Bottom,
977 /// The bottom left corner
978 BottomLeft,
979 /// The left edge
980 Left,
981 /// The top left corner
982 TopLeft,
983}
984
985impl From<ResizeEdge> for CursorStyle {
986 fn from(edge: ResizeEdge) -> Self {
987 match edge {
988 ResizeEdge::Top => Self::ResizeUp,
989 ResizeEdge::Right => Self::ResizeRight,
990 ResizeEdge::Bottom => Self::ResizeDown,
991 ResizeEdge::Left => Self::ResizeLeft,
992 ResizeEdge::TopLeft => Self::ResizeUpLeft,
993 ResizeEdge::TopRight => Self::ResizeUpRight,
994 ResizeEdge::BottomLeft => Self::ResizeDownLeft,
995 ResizeEdge::BottomRight => Self::ResizeDownRight,
996 }
997 }
998}
999
1000/// Whether a window draws its own titlebar and borders, or the window server does.
1001///
1002/// Set this with [`WindowOptions::window_decorations`]. Linux's Wayland and X11
1003/// backends honor it. The other backends ignore it.
1004#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1005pub enum WindowDecorations {
1006 /// The window server draws the titlebar and borders.
1007 #[default]
1008 Server,
1009 /// GPUI draws the titlebar and borders.
1010 Client,
1011}
1012
1013/// The decorations a window is currently using.
1014///
1015/// Read it with [`Window::window_decorations`] to decide whether to draw resize handles or
1016/// window controls yourself.
1017#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1018pub enum Decorations {
1019 /// The window server draws the decorations.
1020 #[default]
1021 Server,
1022 /// GPUI draws the decorations. `tiling` says which edges are snapped to the screen.
1023 Client {
1024 /// Which edges of the window are tiled.
1025 tiling: Tiling,
1026 },
1027}
1028
1029/// The window controls a platform supports.
1030///
1031/// The default assumes that every control is available.
1032#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1033pub struct WindowControls {
1034 /// Whether the window can enter fullscreen.
1035 pub fullscreen: bool,
1036 /// Whether the window can be maximized.
1037 pub maximize: bool,
1038 /// Whether the window can be minimized.
1039 pub minimize: bool,
1040 /// Whether the window has a window menu.
1041 pub window_menu: bool,
1042}
1043
1044impl Default for WindowControls {
1045 fn default() -> Self {
1046 // Assume that we can do anything, unless told otherwise
1047 Self {
1048 fullscreen: true,
1049 maximize: true,
1050 minimize: true,
1051 window_menu: true,
1052 }
1053 }
1054}
1055
1056/// A window control button, used in [`WindowButtonLayout`].
1057#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1058pub enum WindowButton {
1059 /// The minimize button
1060 Minimize,
1061 /// The maximize button
1062 Maximize,
1063 /// The close button
1064 Close,
1065}
1066
1067impl WindowButton {
1068 /// Returns a stable element id for rendering this button.
1069 ///
1070 /// Use the id to find the button's element in a titlebar. The ids are `"minimize"`,
1071 /// `"maximize"` and `"close"`.
1072 pub fn id(&self) -> &'static str {
1073 match self {
1074 WindowButton::Minimize => "minimize",
1075 WindowButton::Maximize => "maximize",
1076 WindowButton::Close => "close",
1077 }
1078 }
1079
1080 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1081 fn index(&self) -> usize {
1082 match self {
1083 WindowButton::Minimize => 0,
1084 WindowButton::Maximize => 1,
1085 WindowButton::Close => 2,
1086 }
1087 }
1088}
1089
1090/// The largest number of [`WindowButton`]s on one side of the titlebar.
1091pub const MAX_BUTTONS_PER_SIDE: usize = 3;
1092
1093/// Which [`WindowButton`]s appear on each side of the titlebar, in order.
1094///
1095/// On Linux, this is read from the desktop environment's configuration
1096/// (the `button-layout` key of `org.gnome.desktop.wm.preferences`) via `WindowButtonLayout::parse`.
1097/// Empty slots are `None`.
1098#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1099pub struct WindowButtonLayout {
1100 /// The buttons on the left side of the titlebar, from the edge inwards.
1101 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
1102 /// The buttons on the right side of the titlebar, from the edge inwards.
1103 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
1104}
1105
1106#[cfg(any(target_os = "linux", target_os = "freebsd"))]
1107impl WindowButtonLayout {
1108 /// Returns GPUI's fallback layout for Linux titlebars: minimize, maximize and close on the right.
1109 pub fn linux_default() -> Self {
1110 Self {
1111 left: [None; MAX_BUTTONS_PER_SIDE],
1112 right: [
1113 Some(WindowButton::Minimize),
1114 Some(WindowButton::Maximize),
1115 Some(WindowButton::Close),
1116 ],
1117 }
1118 }
1119
1120 /// Parses a GNOME-style `button-layout` string such as `"close,minimize:maximize"`.
1121 ///
1122 /// The text before the `:` is the left side and the text after it is the right side.
1123 /// Names are `minimize`, `maximize` and `close`. Unknown names and repeated buttons
1124 /// are skipped, and each side keeps at most [`MAX_BUTTONS_PER_SIDE`] buttons.
1125 ///
1126 /// # Errors
1127 ///
1128 /// Returns an error when the string names no valid buttons and at least one name is unknown.
1129 pub fn parse(layout_string: &str) -> Result<Self> {
1130 fn parse_side(
1131 s: &str,
1132 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
1133 unrecognized: &mut Vec<String>,
1134 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
1135 let mut result = [None; MAX_BUTTONS_PER_SIDE];
1136 let mut i = 0;
1137 for name in s.split(',') {
1138 let trimmed = name.trim();
1139 if trimmed.is_empty() {
1140 continue;
1141 }
1142 let button = match trimmed {
1143 "minimize" => Some(WindowButton::Minimize),
1144 "maximize" => Some(WindowButton::Maximize),
1145 "close" => Some(WindowButton::Close),
1146 other => {
1147 unrecognized.push(other.to_string());
1148 None
1149 }
1150 };
1151 if let Some(button) = button {
1152 if seen_buttons[button.index()] {
1153 continue;
1154 }
1155 if let Some(slot) = result.get_mut(i) {
1156 *slot = Some(button);
1157 seen_buttons[button.index()] = true;
1158 i += 1;
1159 }
1160 }
1161 }
1162 result
1163 }
1164
1165 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
1166 let mut unrecognized = Vec::new();
1167 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
1168 let layout = Self {
1169 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
1170 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
1171 };
1172
1173 if !unrecognized.is_empty()
1174 && layout.left.iter().all(Option::is_none)
1175 && layout.right.iter().all(Option::is_none)
1176 {
1177 bail!(
1178 "button layout string {:?} contains no valid buttons (unrecognized: {})",
1179 layout_string,
1180 unrecognized.join(", ")
1181 );
1182 }
1183
1184 Ok(layout)
1185 }
1186
1187 /// Formats the layout back into a GNOME-style `button-layout` string.
1188 #[cfg(test)]
1189 pub fn format(&self) -> String {
1190 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
1191 buttons
1192 .iter()
1193 .flatten()
1194 .map(|button| match button {
1195 WindowButton::Minimize => "minimize",
1196 WindowButton::Maximize => "maximize",
1197 WindowButton::Close => "close",
1198 })
1199 .collect::<Vec<_>>()
1200 .join(",")
1201 }
1202
1203 format!("{}:{}", format_side(&self.left), format_side(&self.right))
1204 }
1205}
1206
1207/// Which edges of a window are tiled against the screen edge.
1208#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1209pub struct Tiling {
1210 /// Whether the top edge is tiled.
1211 pub top: bool,
1212 /// Whether the left edge is tiled.
1213 pub left: bool,
1214 /// Whether the right edge is tiled.
1215 pub right: bool,
1216 /// Whether the bottom edge is tiled.
1217 pub bottom: bool,
1218}
1219
1220impl Tiling {
1221 /// Returns a [`Tiling`] with every edge tiled.
1222 pub fn tiled() -> Self {
1223 Self {
1224 top: true,
1225 left: true,
1226 right: true,
1227 bottom: true,
1228 }
1229 }
1230
1231 /// Returns `true` if any edge is tiled.
1232 pub fn is_tiled(&self) -> bool {
1233 self.top || self.left || self.right || self.bottom
1234 }
1235}
1236
1237/// The callbacks an accessibility adapter uses to talk to the application.
1238///
1239/// The platform's accessibility adapter calls these when a screen reader connects,
1240/// disconnects, or asks for an action. Register them with
1241/// [`PlatformWindow::a11y_init`].
1242pub struct A11yCallbacks {
1243 /// Returns the accessibility tree to send when a screen reader connects.
1244 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1245 /// Handles an action, such as focusing or activating an element, that a screen reader requested.
1246 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1247 /// Runs when the screen reader disconnects.
1248 pub deactivation: Box<dyn Fn() + Send + 'static>,
1249}
1250
1251/// The source of a platform frame request's timestamp.
1252#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1253pub enum FrameRequestSource {
1254 /// An OS callback or compositor-paced frame request.
1255 #[default]
1256 NativeCallback,
1257 /// A refresh timer, retry, queued wakeup, or fallback sleep paced by the app.
1258 LocalSchedule,
1259}
1260
1261/// Options for a frame the platform asks GPUI to draw. The platform passes them to
1262/// the callback registered with [`PlatformWindow::on_request_frame`].
1263#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1264pub struct RequestFrameOptions {
1265 /// Whether the frame must be presented to the screen, not only drawn.
1266 pub require_presentation: bool,
1267 /// Whether to redraw everything, ignoring cached rendering state.
1268 pub force_render: bool,
1269 /// When the platform first requested this frame, before main-thread dispatch.
1270 ///
1271 /// `None` means the captured request time is unavailable.
1272 /// Coalesced requests carry their first request time, not their delivery time.
1273 /// Built-in backends collect timestamps only with the `profiler` feature.
1274 pub signal_at: Option<Instant>,
1275 /// Distinguishes native callbacks from local scheduling requests.
1276 pub signal_source: FrameRequestSource,
1277}
1278
1279/// Preserves the first platform frame request time across coalesced notifications.
1280///
1281/// Producers may record from a platform thread without waiting for the UI thread.
1282/// The consumer drains the timestamp before dispatching the request on the UI thread.
1283/// The timestamp offset and source share one atomic word so coalescing cannot
1284/// mix a request's time with another request's source. The low bit encodes the
1285/// source; the remaining bits encode nanoseconds from `origin`. `u64::MAX` is empty.
1286/// Without the `profiler` feature, this accumulator has no timestamp storage.
1287pub struct PlatformFrameSignal {
1288 #[cfg(feature = "profiler")]
1289 origin: Instant,
1290 #[cfg(feature = "profiler")]
1291 first_signal: std::sync::atomic::AtomicU64,
1292}
1293
1294impl Default for PlatformFrameSignal {
1295 fn default() -> Self {
1296 Self::new()
1297 }
1298}
1299
1300impl PlatformFrameSignal {
1301 /// Creates an empty signal accumulator.
1302 pub fn new() -> Self {
1303 Self {
1304 #[cfg(feature = "profiler")]
1305 origin: Instant::now(),
1306 #[cfg(feature = "profiler")]
1307 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1308 }
1309 }
1310
1311 /// Captures a platform request timestamp only when profiling is enabled.
1312 ///
1313 /// The closure is not invoked in profiler-disabled builds.
1314 #[inline]
1315 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1316 if cfg!(feature = "profiler") {
1317 Some(capture())
1318 } else {
1319 None
1320 }
1321 }
1322
1323 /// Records a platform frame request, retaining the first undrained timestamp.
1324 #[inline]
1325 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1326 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1327 #[cfg(feature = "profiler")]
1328 {
1329 let nanoseconds = at
1330 .saturating_duration_since(self.origin)
1331 .as_nanos()
1332 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1333 let source_bit = match source {
1334 FrameRequestSource::NativeCallback => 0,
1335 FrameRequestSource::LocalSchedule => 1,
1336 };
1337 self.first_signal.fetch_min(
1338 (nanoseconds << 1) | source_bit,
1339 std::sync::atomic::Ordering::Relaxed,
1340 );
1341 }
1342 }
1343
1344 /// Drains the first platform frame request time and source, leaving the accumulator empty.
1345 #[inline]
1346 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1347 #[cfg(feature = "profiler")]
1348 {
1349 let signal = self
1350 .first_signal
1351 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1352 (signal != u64::MAX).then(|| {
1353 let source = match signal & 1 {
1354 0 => FrameRequestSource::NativeCallback,
1355 _ => FrameRequestSource::LocalSchedule,
1356 };
1357 (self.origin + Duration::from_nanos(signal >> 1), source)
1358 })
1359 }
1360 #[cfg(not(feature = "profiler"))]
1361 {
1362 None
1363 }
1364 }
1365}
1366
1367/// The application's lifecycle phase, as owned and reported by a mobile OS.
1368///
1369/// `Inactive` means visible but not receiving input (a system dialog on
1370/// top), while `Background` means not visible at all, with process death
1371/// possible at any time thereafter.
1372///
1373/// | Phase | iOS | Android |
1374/// |--------------|------------------------------|--------------|
1375/// | `Active` | `didBecomeActive` | `onResume` |
1376/// | `Inactive` | `willResignActive` | `onPause` |
1377/// | `Background` | `didEnterBackground` | `onStop` |
1378/// | `Foreground` | `willEnterForeground` | `onStart` |
1379#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1380pub enum AppLifecyclePhase {
1381 /// Foreground and receiving input.
1382 Active,
1383 /// Foreground (visible) but not receiving input.
1384 Inactive,
1385 /// Not visible. The GPU surface may be destroyed while backgrounded and
1386 /// the process may be killed without further notice.
1387 Background,
1388 /// Becoming visible again, before input is restored.
1389 Foreground,
1390 /// The native scene has disconnected. Its windows may reconnect later.
1391 /// This is not a process-termination notification or a guaranteed final
1392 /// opportunity to save state.
1393 Disconnected,
1394}
1395
1396/// Regions of a window that are obscured or reserved by the system.
1397///
1398/// Mobile applications often share space in their window with system-specific
1399/// geometry, from keyboards to camera notches. In GPUI, all this is abstracted
1400/// into a single "inset" which should be overlaid on the window's bounds.
1401/// It is up to the application develop to determine how to handle these cases.
1402#[derive(Debug, Clone, Default, PartialEq)]
1403pub struct WindowInsets {
1404 /// Regions covered by system UI or hardware: status bar, display
1405 /// cutouts/notch, home indicator, navigation bars.
1406 /// (iOS: `safeAreaInsets`. Android: `WindowInsets` of types
1407 /// `systemBars() | displayCutout()`.)
1408 pub safe_area: Edges<Pixels>,
1409 /// The region covered by the keyboard, when present.
1410 /// (iOS: derived from `keyboardWillShow`/frame-change notifications.
1411 /// Android: `WindowInsets.Type.ime()`.)
1412 pub ime: Edges<Pixels>,
1413}
1414
1415impl WindowInsets {
1416 /// Returns, on each side, the larger of the system inset and the keyboard inset.
1417 ///
1418 /// Content should stay clear of this area.
1419 pub fn effective(&self) -> Edges<Pixels> {
1420 Edges {
1421 top: self.safe_area.top.max(self.ime.top),
1422 right: self.safe_area.right.max(self.ime.right),
1423 bottom: self.safe_area.bottom.max(self.ime.bottom),
1424 left: self.safe_area.left.max(self.ime.left),
1425 }
1426 }
1427}
1428
1429/// A change in the state of the focused text input.
1430#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1431pub enum TextInputStateChange {
1432 /// The window changed from having no active text input to having one.
1433 FocusGained,
1434 /// The window no longer has an active text input.
1435 FocusLost,
1436 /// The selection or caret moved
1437 SelectionChanged,
1438 /// The document content changed outside of platform-initiated edits.
1439 ContentChanged,
1440}
1441
1442/// The native window that GPUI draws into, implemented by each backend.
1443///
1444/// [`App::open_window`] creates one through [`Platform::open_window`] for each
1445/// [`Window`]. Application code doesn't call these methods directly. [`Window`] and
1446/// the [`Platform`] wrap them. Each backend implements the trait for its windowing
1447/// system. Methods with a default body are optional, and the default is the behavior
1448/// for a platform that lacks the feature.
1449///
1450/// Methods that a platform doesn't support are commented with the platform that
1451/// does.
1452pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1453 /// Returns the window's frame in screen coordinates, in logical pixels.
1454 fn bounds(&self) -> Bounds<Pixels>;
1455 /// Returns whether the window is maximized.
1456 fn is_maximized(&self) -> bool;
1457 /// Returns the window's state, and the bounds to restore when it leaves that state.
1458 fn window_bounds(&self) -> WindowBounds;
1459 /// Returns the drawable size, excluding the titlebar, in logical pixels.
1460 fn content_size(&self) -> Size<Pixels>;
1461 /// Returns the visible viewport in logical pixels relative to the content origin.
1462 ///
1463 /// This may be smaller or offset when a keyboard or zoom obscures content;
1464 /// it must not change the full layout size returned by `content_size`.
1465 /// Implementations should return a frame snapshot, not query platform layout here.
1466 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1467 Bounds::new(Point::default(), self.content_size())
1468 }
1469 /// Registers a callback when visible geometry may have changed.
1470 ///
1471 /// This requests a frame; backends can sample the new viewport and safe-area
1472 /// geometry in `prepare_frame` rather than updating it inside the callback.
1473 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1474 /// Samples platform geometry before a draw, returning whether view caches must be invalidated.
1475 ///
1476 /// Geometry getters must remain consistent throughout the ensuing draw.
1477 /// Do not invoke callbacks here: GPUI is already updating this window.
1478 fn prepare_frame(&self) -> bool {
1479 false
1480 }
1481 /// Resizes the window's content area to `size`, in logical pixels.
1482 fn resize(&mut self, size: Size<Pixels>);
1483 /// Returns the number of device pixels per logical pixel.
1484 fn scale_factor(&self) -> f32;
1485 /// Returns the window's light or dark appearance.
1486 fn appearance(&self) -> WindowAppearance;
1487 /// Returns the display the window is on, or `None` if the platform can't tell.
1488 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1489 /// Returns the mouse position in window coordinates, in logical pixels.
1490 fn mouse_position(&self) -> Point<Pixels>;
1491 /// Returns the modifier keys currently held down.
1492 fn modifiers(&self) -> Modifiers;
1493 /// Returns the state of the Caps Lock key.
1494 fn capslock(&self) -> Capslock;
1495 /// Installs the handler that receives text input for the focused element.
1496 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1497 /// Removes and returns the installed input handler.
1498 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1499 /// Apply the focused text region's [`TextInputConfiguration`] to the
1500 /// platform's text input session (e.g. attributes of the hidden editable
1501 /// element on web). Called only when the configuration changes, because
1502 /// reconfiguring a live input session can restart the IME connection.
1503 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1504 /// Shows a native alert or prompt with `answers` as its buttons.
1505 ///
1506 /// The returned receiver yields the index in `answers` of the button the user
1507 /// chose. Returns `None` when the platform can't show a prompt. The web backend
1508 /// always returns `None`.
1509 fn prompt(
1510 &self,
1511 level: PromptLevel,
1512 msg: &str,
1513 detail: Option<&str>,
1514 answers: &[PromptButton],
1515 ) -> Option<oneshot::Receiver<usize>>;
1516 /// Request activation, optionally using an externally supplied token.
1517 /// Returns whether a request was sent, not whether focus was granted.
1518 fn activate(&self, token: Option<&str>) -> bool;
1519 /// Requests that the operating system draw attention to this window.
1520 fn request_attention(&self) {}
1521 /// Returns whether this window has keyboard focus.
1522 fn is_active(&self) -> bool;
1523 /// The current [`WindowVisibility`]. Read once when the window is created;
1524 /// afterwards changes arrive through [`Self::on_visibility_change`].
1525 fn visibility(&self) -> WindowVisibility;
1526 /// Returns whether the mouse is over this window.
1527 fn is_hovered(&self) -> bool;
1528 /// Returns how the window background is drawn.
1529 fn background_appearance(&self) -> WindowBackgroundAppearance;
1530 /// Sets the window title.
1531 fn set_title(&mut self, title: &str);
1532 /// Sets how the window background is drawn: opaque, transparent or blurred.
1533 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1534 /// Show or hide the window without explicitly requesting focus.
1535 ///
1536 /// The default implementation does nothing for platforms that do not support
1537 /// changing window visibility at runtime.
1538 fn set_visible(&self, _visible: bool) {}
1539 /// Minimizes the window. The web backend logs a warning and does nothing.
1540 fn minimize(&self);
1541 /// Zooms the window with the platform's zoom behavior. The web backend logs a
1542 /// warning and does nothing.
1543 fn zoom(&self);
1544 /// Enters fullscreen, or leaves it if the window is already fullscreen.
1545 fn toggle_fullscreen(&self);
1546 /// Returns whether the window is fullscreen.
1547 fn is_fullscreen(&self) -> bool;
1548 /// Returns a function that wakes the event loop to draw a frame, or `None` if the
1549 /// platform doesn't provide one.
1550 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1551 None
1552 }
1553 /// Registers the callback that draws a frame when the platform asks for one.
1554 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1555 /// Registers the callback that receives input events. Its return value tells the
1556 /// platform whether GPUI handled the event.
1557 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1558 /// Registers the callback run when the window gains or loses focus. It receives `true` when the window is active.
1559 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1560 /// Registers the callback invoked when [`Self::visibility`] changes. Only
1561 /// transitions are reported; the callback runs on the main thread outside
1562 /// of any window update.
1563 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1564 /// Registers the callback run when the mouse enters or leaves the window. It receives `true` on enter.
1565 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1566 /// Registers the callback run when the content size or scale factor changes. It
1567 /// receives the new size and the scale factor.
1568 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1569 /// Registers the callback run when the window moves.
1570 fn on_moved(&self, callback: Box<dyn FnMut()>);
1571 /// Registers the callback invoked when the window moves to another
1572 /// display. Calls may be spurious. The callback runs on the main thread
1573 /// outside of any window update.
1574 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1575 /// Registers the callback run when the user asks to close the window. Return `false` to keep it open.
1576 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1577 /// Registers the callback the platform calls to find out which titlebar control, if any, the mouse is over.
1578 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1579 /// Registers the callback run once the window has closed.
1580 fn on_close(&self, callback: Box<dyn FnOnce()>);
1581 /// Registers the callback run when the window's appearance changes.
1582 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1583 /// Registers the callback run when the titlebar button layout changes. The default does nothing.
1584 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1585 /// Draws `scene` into the window. GPUI calls this once per frame.
1586 fn draw(&self, scene: &Scene);
1587 /// Asks the platform to request another frame soon. The default does nothing.
1588 fn schedule_frame(&self) {}
1589 /// Returns the atlas that stores the glyph and image textures for this window.
1590 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1591 /// Returns whether the renderer supports subpixel (LCD) text antialiasing. macOS returns `false`.
1592 fn is_subpixel_rendering_supported(&self) -> bool;
1593
1594 // macOS specific methods
1595 /// Returns the window title. macOS and the Linux headless window override this. The default returns an empty string.
1596 fn get_title(&self) -> String {
1597 String::new()
1598 }
1599 /// Returns the windows in this window's native tab group, or `None` if the window isn't tabbed. macOS only.
1600 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1601 None
1602 }
1603 /// Returns whether the native tab bar is visible. macOS only.
1604 fn tab_bar_visible(&self) -> bool {
1605 false
1606 }
1607 /// Marks the window's document as edited or unedited. macOS only.
1608 fn set_edited(&mut self, _edited: bool) {}
1609 /// Sets the document path that the titlebar shows for the window. macOS only.
1610 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1611 /// Toggles simple fullscreen, which fills the screen without creating a new Space. macOS only.
1612 fn toggle_simple_fullscreen(&self) {}
1613 /// Returns whether the window is in simple fullscreen. macOS only.
1614 fn is_simple_fullscreen(&self) -> bool {
1615 false
1616 }
1617 /// Moves the traffic-light buttons to `position`, in logical pixels. macOS only.
1618 #[cfg(target_os = "macos")]
1619 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1620 /// Show the platform character palette.
1621 ///
1622 /// The default implementation logs a warning when the backend does not
1623 /// support showing a character palette.
1624 fn show_character_palette(&self) {
1625 log::warn!("show_character_palette is not implemented by this platform backend");
1626 }
1627 /// Runs the titlebar double-click action. macOS only, and the default does nothing.
1628 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1629 /// Registers the callback for the Move Tab to New Window action. macOS only.
1630 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1631 /// Registers the callback for the Merge All Windows action. macOS only.
1632 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1633 /// Registers the callback for selecting the previous tab. macOS only.
1634 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1635 /// Registers the callback for selecting the next tab. macOS only.
1636 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1637 /// Registers the callback for showing or hiding the tab bar. macOS only.
1638 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1639 /// Merges all windows into one tab group. macOS only.
1640 fn merge_all_windows(&self) {}
1641 /// Moves this window's tab into a new window. macOS only.
1642 fn move_tab_to_new_window(&self) {}
1643 /// Shows or hides the tab overview. macOS only.
1644 fn toggle_window_tab_overview(&self) {}
1645 /// Sets the identifier that groups windows into one native tab group. macOS only.
1646 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1647
1648 /// Captures opaque, platform-specific state for restoring this native window later.
1649 ///
1650 /// Returns None when the backend has no restorable state. The bytes are only meaningful to
1651 /// the backend that produced them; the default implementation returns None.
1652 fn native_window_state(&self) -> Option<Vec<u8>> {
1653 None
1654 }
1655 /// Restores state previously returned by PlatformWindow::native_window_state.
1656 ///
1657 /// The default implementation does nothing on platforms without native state restoration.
1658 fn restore_native_window_state(&self, _state: &[u8]) {}
1659
1660 /// Returns the window's Win32 `HWND`. Windows only.
1661 #[cfg(target_os = "windows")]
1662 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1663
1664 // Linux specific methods
1665 /// Returns the window's state and bounds, excluding decorations. Defaults to [`PlatformWindow::window_bounds`].
1666 fn inner_window_bounds(&self) -> WindowBounds {
1667 self.window_bounds()
1668 }
1669 /// Asks for server-side or client-side decorations. Linux only.
1670 fn request_decorations(&self, _decorations: WindowDecorations) {}
1671 /// Shows the window menu at `position`, in logical pixels. Linux only.
1672 fn show_window_menu(&self, _position: Point<Pixels>) {}
1673 /// Starts an interactive move, as if the user dragged the titlebar. Implemented on macOS, Windows and Linux; the web backend does nothing.
1674 fn start_window_move(&self) {}
1675 /// Returns whether this window can start a drag to another application.
1676 fn can_start_external_drag(&self) -> bool {
1677 false
1678 }
1679 /// Starts a drag of `payload` to another application. Returns `true` if the drag started.
1680 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1681 false
1682 }
1683 /// Starts an interactive resize from `edge`. Linux only.
1684 fn start_window_resize(&self, _edge: ResizeEdge) {}
1685 /// Reserves `zone` logical pixels at the screen edge for a layer-shell surface. Wayland only.
1686 fn set_exclusive_zone(&self, _zone: Pixels) {}
1687 /// Sets the screen edge that a layer-shell exclusive zone uses. Wayland only.
1688 #[cfg(all(target_os = "linux", feature = "wayland"))]
1689 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1690 /// Limits input to `region`, in logical pixels. `None` lets the whole window take input. Linux only.
1691 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1692 /// Returns the decorations the window is using. The default is [`Decorations::Server`].
1693 fn window_decorations(&self) -> Decorations {
1694 Decorations::Server
1695 }
1696 /// Sets the application id that the window manager uses to group windows and find the desktop entry. Linux only.
1697 fn set_app_id(&mut self, _app_id: &str) {}
1698 /// Maps the window so the platform shows it. The default does nothing and returns `Ok(())`.
1699 fn map_window(&mut self) -> anyhow::Result<()> {
1700 Ok(())
1701 }
1702 /// Returns which window controls the platform supports.
1703 fn window_controls(&self) -> WindowControls {
1704 WindowControls::default()
1705 }
1706 /// Sets the width of the client-side shadow that surrounds the window, in logical pixels. Linux only.
1707 fn set_client_inset(&self, _inset: Pixels) {}
1708 /// Returns the GPU's name and driver details, or `None` if they're unavailable.
1709 fn gpu_specs(&self) -> Option<GpuSpecs>;
1710
1711 /// Returns the GPU context for this window's renderer.
1712 /// The returned `Box` contains `(Arc<wgpu::Device>, Arc<wgpu::Queue>)`.
1713 #[cfg(any(
1714 target_os = "linux",
1715 target_os = "freebsd",
1716 target_os = "windows",
1717 target_os = "macos"
1718 ))]
1719 fn gpu_context(&self) -> Option<Box<dyn std::any::Any>> {
1720 None
1721 }
1722
1723 /// Returns typed backend-specific GPU context information for custom
1724 /// controls. The value is intentionally type-erased in this crate so the
1725 /// core UI crate does not depend on a rendering backend.
1726 #[cfg(any(
1727 target_family = "wasm",
1728 target_os = "linux",
1729 target_os = "freebsd",
1730 target_os = "macos"
1731 ))]
1732 fn gpu_context_info(&self) -> Option<Box<dyn std::any::Any>> {
1733 None
1734 }
1735
1736 /// Whether this window's GPU device has been lost (the platform renderer
1737 /// recovers it on a subsequent draw). `None` when the backend cannot
1738 /// know. Safe to call mid-recovery, unlike `gpu_context`. Embedders that
1739 /// captured the device from `gpu_context` should stop submitting while
1740 /// this is `Some(true)` and re-acquire the device once it reads
1741 /// `Some(false)` again.
1742 #[cfg(any(
1743 target_os = "linux",
1744 target_os = "freebsd",
1745 target_os = "windows",
1746 target_os = "macos"
1747 ))]
1748 fn gpu_device_lost(&self) -> Option<bool> {
1749 None
1750 }
1751
1752 /// Moves the input method's candidate window next to `bounds`, in window coordinates.
1753 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1754
1755 // Mobile platform methods.
1756
1757 /// The regions of this window currently obscured or reserved by the
1758 /// system. Zero on platforms without such regions.
1759 fn insets(&self) -> WindowInsets {
1760 WindowInsets::default()
1761 }
1762
1763 /// Registers a callback invoked whenever [`Self::insets`] change.
1764 ///
1765 /// Contract: fires continuously during animated transitions (Android
1766 /// `WindowInsetsAnimation` progress; on iOS the platform interpolates
1767 /// the keyboard animation curve on frame ticks) and is exact at rest.
1768 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1769
1770 /// Sets the handler for the system back action (Android back
1771 /// button/gesture; no source on iOS or desktop).
1772 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1773
1774 /// Declares whether the application would currently handle the system
1775 /// back action (e.g. navigation depth > 0).
1776 fn set_back_enabled(&self, _enabled: bool) {}
1777
1778 /// Requests that the soft keyboard be shown. Mobile only.
1779 fn show_soft_keyboard(&self) {}
1780
1781 /// Requests that the soft keyboard be hidden. Mobile only.
1782 fn hide_soft_keyboard(&self) {}
1783
1784 /// Tells the platform that the focused text input changed.
1785 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1786
1787 /// Plays the system alert sound.
1788 fn play_system_bell(&self) {}
1789
1790 /// Initialize the accessibility adapter with callbacks.
1791 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1792
1793 /// Provide a TreeUpdate to the accessibility adapter.
1794 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1795
1796 /// Inform the adapter of updated window bounds.
1797 fn a11y_update_window_bounds(&self) {}
1798
1799 /// Returns this window as a `TestWindow`, if it is one. Only the test platform's windows are.
1800 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1801 fn as_test(&mut self) -> Option<&mut TestWindow> {
1802 None
1803 }
1804
1805 /// Renders the given scene to a texture and returns the pixel data as an RGBA image.
1806 /// This does not present the frame to screen - useful for visual testing where we want
1807 /// to capture what would be rendered without displaying it or requiring the window to be visible.
1808 #[cfg(any(test, feature = "test-support"))]
1809 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1810 anyhow::bail!("render_to_image not implemented for this platform")
1811 }
1812}
1813
1814/// Renders scenes offscreen for windows that have no display, such as in tests and benchmarks.
1815#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1816pub trait PlatformHeadlessRenderer {
1817 /// Renders `scene` at `size` and returns the pixels as an RGBA image.
1818 fn render_scene_to_image(
1819 &mut self,
1820 scene: &Scene,
1821 size: Size<DevicePixels>,
1822 ) -> Result<RgbaImage>;
1823
1824 /// Render a scene to an offscreen target without reading the result back.
1825 ///
1826 /// This is the headless analogue of presenting a frame: it performs the
1827 /// same CPU-side scene encoding and GPU submission as drawing to a real
1828 /// window, but doesn't block on GPU completion or copy pixels back.
1829 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1830
1831 /// Returns the sprite atlas used by this renderer.
1832 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1833}
1834
1835/// Type alias for runnables with metadata.
1836/// Previously an enum with a single variant, now simplified to a direct type alias.
1837#[doc(hidden)]
1838pub type RunnableVariant = Runnable<RunnableMeta>;
1839
1840#[doc(hidden)]
1841pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1842
1843#[doc(hidden)]
1844pub enum TasksIncluded {
1845 OnlyCompleted,
1846 CompletedAndRunning,
1847}
1848
1849/// This type is public so that our test macro can generate and use it, but it should not
1850/// be considered part of our public API.
1851#[doc(hidden)]
1852pub trait PlatformDispatcher: Send + Sync {
1853 fn is_main_thread(&self) -> bool;
1854 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1855 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1856 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1857
1858 fn dispatch_on_main_thread_when_idle(
1859 &self,
1860 runnable: RunnableVariant,
1861 timeout: Option<Duration>,
1862 ) {
1863 let _ = timeout;
1864 self.dispatch_on_main_thread(runnable, Priority::Low);
1865 }
1866
1867 /// Creates an owner-thread local-task scope for platforms that retire event loops.
1868 #[doc(hidden)]
1869 fn create_local_task_scope(&self) -> Option<Arc<LocalTaskScope>> {
1870 None
1871 }
1872
1873 /// Dispatches a scoped runnable through the normal owner-thread queue.
1874 #[doc(hidden)]
1875 fn dispatch_local_task(&self, _scope: Arc<LocalTaskScope>, runnable: RunnableVariant) {
1876 self.dispatch_on_main_thread(runnable, Priority::default());
1877 }
1878
1879 /// Dispatches a scoped runnable through the owner's idle queue.
1880 #[doc(hidden)]
1881 fn dispatch_local_task_when_idle(
1882 &self,
1883 _scope: Arc<LocalTaskScope>,
1884 runnable: RunnableVariant,
1885 timeout: Option<Duration>,
1886 ) {
1887 self.dispatch_on_main_thread_when_idle(runnable, timeout);
1888 }
1889
1890 /// Enqueues a scoped runnable for owner-thread destruction without polling.
1891 #[doc(hidden)]
1892 fn dispatch_local_task_cancellation(
1893 &self,
1894 _scope: Arc<LocalTaskScope>,
1895 runnable: RunnableVariant,
1896 ) {
1897 self.dispatch_on_main_thread(runnable, Priority::High);
1898 }
1899
1900 /// Begins retiring a scope after switching its queue to cancellation mode.
1901 #[doc(hidden)]
1902 fn begin_local_task_retirement(&self, scope: &Arc<LocalTaskScope>) {
1903 scope.begin_retirement();
1904 }
1905
1906 /// Drops one batch of queued cancellation runnables on the owner thread.
1907 #[doc(hidden)]
1908 fn drain_local_task_retirements(&self, _scope: &Arc<LocalTaskScope>) {}
1909
1910 /// Finishes retirement only after all scoped queues and tasks are empty.
1911 #[doc(hidden)]
1912 fn finish_local_task_retirement(&self, scope: &Arc<LocalTaskScope>) -> bool {
1913 scope.seal_if_quiescent()
1914 }
1915
1916 fn idle_time_remaining(&self) -> Option<Duration> {
1917 None
1918 }
1919
1920 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1921
1922 fn now(&self) -> Instant {
1923 Instant::now()
1924 }
1925
1926 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1927 gpui_util::defer(Box::new(|| {}))
1928 }
1929
1930 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1931 ActivityGuard::noop()
1932 }
1933
1934 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1935 fn as_test(&self) -> Option<&TestDispatcher> {
1936 None
1937 }
1938
1939 // This cfg must match the `threaded_dispatcher` module's, which implements
1940 // this method whenever it compiles.
1941 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1942 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1943 None
1944 }
1945}
1946
1947/// Loads fonts and lays out text for one platform. Each backend implements this trait.
1948///
1949/// Application code uses [`TextSystem`](crate::TextSystem), which calls these methods and caches the
1950/// results. Glyph and layout methods run on whatever thread the caller uses, so
1951/// implementations must be `Send + Sync`.
1952pub trait PlatformTextSystem: Send + Sync {
1953 /// Adds fonts from their file bytes, such as TTF or OTF data, so layout can find them by family name.
1954 ///
1955 /// Returns an error if any of the bytes can't be parsed as a font.
1956 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1957 /// Returns the names of every font family available to layout.
1958 fn all_font_names(&self) -> Vec<String>;
1959 /// Returns a number that changes whenever the font collection changes, so caches can detect stale entries.
1960 ///
1961 /// The default returns `0`, which means the collection never changes.
1962 fn font_generation(&self) -> u64 {
1963 0
1964 }
1965 /// Returns the id of the font that best matches `descriptor`.
1966 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1967 /// Loads the caches that shaping needs for `font_ids`, so the first layout with them is fast.
1968 /// The default does nothing.
1969 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1970 /// Returns the metrics, such as ascent, descent and line gap, for `font_id`.
1971 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1972 /// Returns the glyph's bounding box, relative to its origin on the baseline.
1973 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1974 /// Returns the distance the pen moves after drawing this glyph.
1975 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1976 /// Returns the glyph that `font_id` uses for `ch`, or `None` if the font has no glyph for it.
1977 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1978 /// Rasterizes a glyph, including its atlas placement and pixels.
1979 fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<RasterizedGlyph>;
1980 /// Returns the raster style to use for `request`, normalized so that cached glyph
1981 /// pixels can be shared. The default uses the requested mode unchanged.
1982 fn prepare_raster_style(&self, request: RasterStyleRequest) -> PreparedRasterStyle {
1983 PreparedRasterStyle::independent(request.requested_mode)
1984 }
1985 /// Lays out one complete text document, including hard breaks and optional wrapping.
1986 fn layout_text(&self, request: TextLayoutRequest<'_>) -> LineLayout;
1987 /// Lays out one complete text document that contains atomic element boxes, which
1988 /// keep their size and sit on the text line.
1989 fn layout_inline(&self, request: InlineLayoutRequest<'_>) -> InlineLayout;
1990 /// Returns the text rendering mode the platform recommends for `font_id` at `font_size`.
1991 ///
1992 /// The default returns [`TextRenderingMode::Subpixel`].
1993 fn recommended_rendering_mode(
1994 &self,
1995 _font_id: FontId,
1996 _font_size: Pixels,
1997 ) -> TextRenderingMode {
1998 TextRenderingMode::Subpixel
1999 }
2000}
2001
2002#[cfg(any(test, feature = "test-support"))]
2003mod tests {
2004 use crate::{
2005 Bounds, CaretAffinity, CaretMovement, CaretPosition, Font, FontId, FontMetrics, GlyphId,
2006 InlineLayout, InlineLayoutRequest, InlineVisualLine, LineLayout, PaintFragment, PaintStyle,
2007 Pixels, PlatformTextLayout, PlatformTextSystem, Point, PositionedInlineBox,
2008 RasterizedGlyph, RasterizedGlyphFormat, RenderGlyphParams, ResolvedDirection, ShapedGlyph,
2009 Size, TextAlign, TextBoundary, TextDirection, TextLayoutRequest, TextMovement,
2010 TextRenderingMode, TextSelectionKind, VisualDirection, VisualLine, align_inline_boxes,
2011 point, px, size,
2012 };
2013 use anyhow::Result;
2014 use std::{borrow::Cow, ops::Range, sync::Arc};
2015
2016 #[expect(missing_docs)]
2017 pub struct TestTextSystem;
2018
2019 #[derive(Debug)]
2020 /// A deterministic test layout with exactly one visual and hard line.
2021 struct TestPlatformTextLayout {
2022 /// Source text.
2023 text: String,
2024 /// Ordered (byte index, x position) caret stops.
2025 stops: Vec<(usize, Pixels)>,
2026 /// Size of the single visual line.
2027 size: Size<Pixels>,
2028 }
2029
2030 impl TestPlatformTextLayout {
2031 fn caret_stop(&self, byte_offset: usize) -> (usize, Pixels) {
2032 let stop_index = self
2033 .stops
2034 .partition_point(|(index, _position)| *index <= byte_offset)
2035 .saturating_sub(1);
2036
2037 self.stops[stop_index]
2038 }
2039 }
2040
2041 impl PlatformTextLayout for TestPlatformTextLayout {
2042 fn len(&self) -> usize {
2043 self.stops.last().map_or(0, |(index, _)| *index)
2044 }
2045
2046 fn line_count(&self) -> usize {
2047 // This test layout deliberately reports its single visual line.
2048 1
2049 }
2050
2051 fn size(&self) -> Size<Pixels> {
2052 self.size
2053 }
2054
2055 fn byte_index_from_pixel_point(
2056 &self,
2057 pixel_point: Point<Pixels>,
2058 line_height: Pixels,
2059 ) -> Result<usize, usize> {
2060 let closest = self
2061 .caret_from_pixel_point(pixel_point, line_height)
2062 .unwrap_or_else(|caret| caret)
2063 .index;
2064 self.stops
2065 .windows(2)
2066 .find_map(|stops| {
2067 let [(start_index, start_x), (_end_index, end_x)] = stops else {
2068 return None;
2069 };
2070
2071 (pixel_point.x >= *start_x && pixel_point.x < *end_x)
2072 .then_some(Ok(*start_index))
2073 })
2074 .unwrap_or(Err(closest))
2075 }
2076
2077 fn caret_from_pixel_point(
2078 &self,
2079 pixel_point: Point<Pixels>,
2080 line_height: Pixels,
2081 ) -> Result<CaretPosition, CaretPosition> {
2082 let index = self
2083 .stops
2084 .iter()
2085 .min_by(|(_, left), (_, right)| {
2086 (f32::from(*left) - f32::from(pixel_point.x))
2087 .abs()
2088 .total_cmp(&(f32::from(*right) - f32::from(pixel_point.x)).abs())
2089 })
2090 .map_or(0, |(index, _)| *index);
2091 let caret = self.normalized_caret(CaretPosition {
2092 index,
2093 affinity: CaretAffinity::Downstream,
2094 });
2095
2096 if pixel_point.y >= Pixels::ZERO
2097 && pixel_point.y < line_height
2098 && pixel_point.x >= Pixels::ZERO
2099 && pixel_point.x < self.size.width
2100 {
2101 Ok(caret)
2102 } else {
2103 Err(caret)
2104 }
2105 }
2106
2107 fn caret_bounds(
2108 &self,
2109 caret: CaretPosition,
2110 line_height: Pixels,
2111 ) -> Option<Bounds<Pixels>> {
2112 let caret = self.normalized_caret(caret);
2113 let (_index, position) = self.caret_stop(caret.index);
2114
2115 Some(Bounds::new(
2116 point(position, Pixels::ZERO),
2117 size(Pixels::ZERO, line_height),
2118 ))
2119 }
2120
2121 fn normalized_caret(&self, caret: CaretPosition) -> CaretPosition {
2122 let (index, _position) = self.caret_stop(caret.index);
2123 let affinity = if index == self.len() && index != 0 {
2124 CaretAffinity::Upstream
2125 } else {
2126 caret.affinity
2127 };
2128
2129 CaretPosition { index, affinity }
2130 }
2131
2132 fn adjacent_visual_caret(
2133 &self,
2134 caret: CaretPosition,
2135 direction: VisualDirection,
2136 ) -> Option<CaretPosition> {
2137 let caret = self.normalized_caret(caret);
2138 let position = self
2139 .stops
2140 .iter()
2141 .position(|(index, _)| *index == caret.index)?;
2142 let position = match direction {
2143 VisualDirection::Left => position.checked_sub(1)?,
2144 VisualDirection::Right => position.checked_add(1)?,
2145 };
2146
2147 let index = self.stops.get(position)?.0;
2148 Some(self.normalized_caret(CaretPosition {
2149 index,
2150 affinity: CaretAffinity::Downstream,
2151 }))
2152 }
2153
2154 fn selection_bounds(
2155 &self,
2156 byte_range: Range<usize>,
2157 line_height: Pixels,
2158 ) -> Vec<Bounds<Pixels>> {
2159 if byte_range.is_empty() {
2160 return Vec::new();
2161 }
2162
2163 let (_start_index, start) = self.caret_stop(byte_range.start);
2164 let (_end_index, end) = self.caret_stop(byte_range.end);
2165
2166 // The platform contract returns one bound per visual line. Since this test
2167 // layout has exactly one visual line, every non-empty selection has one bound.
2168 vec![Bounds::from_corners(
2169 point(start.min(end), Pixels::ZERO),
2170 point(start.max(end), line_height),
2171 )]
2172 }
2173
2174 fn logical_cluster_before(&self, caret: CaretPosition) -> Option<Range<usize>> {
2175 self.stops
2176 .windows(2)
2177 .rev()
2178 .find(|stops| stops[1].0 <= caret.index)
2179 .map(|stops| stops[0].0..stops[1].0)
2180 }
2181
2182 fn logical_cluster_after(&self, caret: CaretPosition) -> Option<Range<usize>> {
2183 self.stops
2184 .windows(2)
2185 .find(|stops| stops[0].0 >= caret.index)
2186 .map(|stops| stops[0].0..stops[1].0)
2187 }
2188
2189 fn caret_movement(
2190 &self,
2191 caret: CaretPosition,
2192 movement: TextMovement,
2193 vertical_navigation_x: Option<Pixels>,
2194 ) -> CaretMovement {
2195 use TextBoundary::*;
2196 use TextDirection::*;
2197
2198 let index = match (movement.direction, movement.boundary) {
2199 (Left, Cluster) => {
2200 self.adjacent_visual_caret(caret, VisualDirection::Left)
2201 .unwrap_or(caret)
2202 .index
2203 }
2204 (Right, Cluster) => {
2205 self.adjacent_visual_caret(caret, VisualDirection::Right)
2206 .unwrap_or(caret)
2207 .index
2208 }
2209 (Left, Word) => {
2210 let prefix = &self.text[..caret.index.min(self.text.len())];
2211 let trimmed = prefix.trim_end_matches(char::is_whitespace);
2212 trimmed.rfind(char::is_whitespace).map_or(0, |index| {
2213 index + trimmed[index..].chars().next().unwrap().len_utf8()
2214 })
2215 }
2216 (Right, Word) => {
2217 let start = caret.index.min(self.text.len());
2218 let suffix = &self.text[start..];
2219 let word_end = suffix.find(char::is_whitespace).unwrap_or(suffix.len());
2220 let rest = &suffix[word_end..];
2221 start
2222 + word_end
2223 + rest
2224 .find(|character: char| !character.is_whitespace())
2225 .unwrap_or(rest.len())
2226 }
2227 // This test layout has one visual and hard line. Without wrapping or
2228 // additional hard lines, these movements resolve to the document endpoints.
2229 (Up | Start, VisualLine) | (Start, HardLine | Document) => 0,
2230 (Down | End, VisualLine) | (End, HardLine | Document) => self.len(),
2231 _ => caret.index,
2232 };
2233
2234 let vertical_navigation_x =
2235 matches!(movement.direction, TextDirection::Up | TextDirection::Down).then(|| {
2236 vertical_navigation_x.unwrap_or_else(|| {
2237 self.caret_bounds(caret, self.size.height)
2238 .map_or(Pixels::ZERO, |bounds| bounds.origin.x)
2239 })
2240 });
2241
2242 CaretMovement {
2243 result: self.normalized_caret(CaretPosition {
2244 index,
2245 affinity: CaretAffinity::Downstream,
2246 }),
2247 vertical_navigation_x,
2248 }
2249 }
2250
2251 fn selection_from_pixel_point(
2252 &self,
2253 pixel_point: Point<Pixels>,
2254 line_height: Pixels,
2255 kind: TextSelectionKind,
2256 ) -> Range<usize> {
2257 if !matches!(kind, TextSelectionKind::Word) {
2258 return 0..self.len();
2259 }
2260
2261 let index = self
2262 .caret_from_pixel_point(pixel_point, line_height)
2263 .unwrap_or_else(|caret| caret)
2264 .index
2265 .min(self.text.len());
2266 let start = self.text[..index]
2267 .rfind(char::is_whitespace)
2268 .map_or(0, |offset| {
2269 offset + self.text[offset..].chars().next().unwrap().len_utf8()
2270 });
2271 let end = self.text[index..]
2272 .find(char::is_whitespace)
2273 .map_or(self.text.len(), |offset| index + offset);
2274 start..end
2275 }
2276 }
2277
2278 #[expect(missing_docs)]
2279 impl TestTextSystem {
2280 #[allow(dead_code)]
2281 pub fn new() -> Self {
2282 Self
2283 }
2284
2285 fn em_width(&self, font_size: Pixels) -> Pixels {
2286 let metrics = self.font_metrics(FontId(0));
2287 let advance = self
2288 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
2289 .unwrap();
2290
2291 font_size * advance.width / metrics.units_per_em as f32
2292 }
2293 }
2294
2295 fn position_test_inline_boxes(
2296 request: InlineLayoutRequest<'_>,
2297 em_width: Pixels,
2298 baseline: Pixels,
2299 ) -> Vec<PositionedInlineBox> {
2300 let mut preceding_width = Pixels::ZERO;
2301 request
2302 .boxes
2303 .iter()
2304 .map(|inline_box| {
2305 let text_width = em_width
2306 * request.text[..inline_box.index]
2307 .chars()
2308 .map(|character| character.len_utf16() as f32)
2309 .sum::<f32>();
2310 let positioned = PositionedInlineBox {
2311 id: inline_box.id,
2312 line_index: 0,
2313 bounds: Bounds::new(
2314 point(
2315 text_width + preceding_width,
2316 baseline - inline_box.size.height,
2317 ),
2318 inline_box.size,
2319 ),
2320 };
2321
2322 preceding_width += inline_box.size.width;
2323 positioned
2324 })
2325 .collect()
2326 }
2327
2328 fn add_test_inline_box_advances(layout: &mut LineLayout, request: InlineLayoutRequest<'_>) {
2329 for fragment in &mut layout.paint_fragments {
2330 // This layout owns its freshly built glyphs. If they become shared,
2331 // make_mut copies the slice before changing their positions.
2332 for (glyph, (index, _)) in Arc::make_mut(&mut fragment.glyphs)
2333 .iter_mut()
2334 .zip(request.text.char_indices())
2335 {
2336 glyph.position.x += request
2337 .boxes
2338 .iter()
2339 .filter(|inline_box| inline_box.index <= index)
2340 .map(|inline_box| inline_box.size.width)
2341 .sum::<Pixels>();
2342 }
2343 }
2344
2345 let box_width = request
2346 .boxes
2347 .iter()
2348 .map(|inline_box| inline_box.size.width)
2349 .sum::<Pixels>();
2350 for fragment in &mut layout.paint_fragments {
2351 fragment.x_range.end += box_width;
2352 }
2353
2354 layout.width += box_width;
2355
2356 if let Some(line) = layout.visual_lines.first_mut() {
2357 line.advance_width += box_width;
2358 }
2359 }
2360
2361 impl PlatformTextSystem for TestTextSystem {
2362 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
2363 Ok(())
2364 }
2365
2366 fn all_font_names(&self) -> Vec<String> {
2367 Vec::new()
2368 }
2369
2370 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
2371 Ok(FontId(1))
2372 }
2373
2374 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
2375 FontMetrics {
2376 units_per_em: 1000,
2377 ascent: 1025.0,
2378 descent: -275.0,
2379 line_gap: 0.0,
2380 underline_position: -95.0,
2381 underline_thickness: 60.0,
2382 cap_height: 698.0,
2383 x_height: 516.0,
2384 bounding_box: Bounds {
2385 origin: Point {
2386 x: -260.0,
2387 y: -245.0,
2388 },
2389 size: Size {
2390 width: 1501.0,
2391 height: 1364.0,
2392 },
2393 },
2394 }
2395 }
2396
2397 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
2398 Ok(Bounds {
2399 origin: Point { x: 54.0, y: 0.0 },
2400 size: size(392.0, 528.0),
2401 })
2402 }
2403
2404 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
2405 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
2406 }
2407
2408 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
2409 Some(GlyphId(ch.len_utf16() as u32))
2410 }
2411
2412 fn rasterize_glyph(&self, _params: &RenderGlyphParams) -> Result<RasterizedGlyph> {
2413 Ok(RasterizedGlyph {
2414 bounds: Default::default(),
2415 size: Default::default(),
2416 format: RasterizedGlyphFormat::AlphaMask,
2417 pixels: Vec::new(),
2418 })
2419 }
2420
2421 fn layout_text(&self, request: TextLayoutRequest<'_>) -> LineLayout {
2422 let text = request.text;
2423 let font_size = request.font_size;
2424 let shaping_runs = request.runs;
2425 let mut position = px(0.);
2426 let metrics = self.font_metrics(FontId(0));
2427 let em_width = self.em_width(font_size);
2428 let mut glyphs = Vec::new();
2429 let mut stops = vec![(0, Pixels::ZERO)];
2430
2431 for (idx, character) in text.char_indices() {
2432 let glyph_id = GlyphId(character.len_utf16() as u32);
2433 glyphs.push(ShapedGlyph {
2434 id: glyph_id,
2435 position: point(position, Pixels::ZERO),
2436 is_emoji: glyph_id.0 == 2,
2437 });
2438
2439 position += em_width * glyph_id.0 as f32;
2440 stops.push((idx + character.len_utf8(), position));
2441 }
2442
2443 let mut tracking = px(0.);
2444 let mut tracking_covered = 0usize;
2445 for run in shaping_runs {
2446 let start = tracking_covered.min(text.len());
2447 let end = start.saturating_add(run.len).min(text.len()).max(start);
2448 let slice = text.get(start..end).unwrap_or("");
2449 let n = slice.chars().count();
2450 if n > 1 {
2451 if let Some(spacing) = run.letter_spacing {
2452 tracking += spacing * (n - 1) as f32;
2453 }
2454 }
2455 tracking_covered = end;
2456 }
2457
2458 let direction = match request.options.direction {
2459 crate::ParagraphDirection::LeftToRight => ResolvedDirection::LeftToRight,
2460 crate::ParagraphDirection::RightToLeft => ResolvedDirection::RightToLeft,
2461 crate::ParagraphDirection::Auto => {
2462 ResolvedDirection::from_first_strong(text).unwrap_or_default()
2463 }
2464 };
2465 let advance = position + tracking;
2466 let alignment_width = request.options.alignment_width.unwrap_or(advance);
2467 let offset = match request.options.text_align {
2468 TextAlign::Start if direction.is_rtl() => alignment_width - advance,
2469 TextAlign::Start | TextAlign::Left => Pixels::ZERO,
2470 TextAlign::Center => (alignment_width - advance) / 2.0,
2471 TextAlign::End if direction.is_rtl() => Pixels::ZERO,
2472 TextAlign::End | TextAlign::Right => alignment_width - advance,
2473 };
2474
2475 for (_idx, position) in &mut stops {
2476 *position += offset;
2477 }
2478
2479 let visual_lines = [VisualLine {
2480 text_range: 0..text.len(),
2481 paint_fragment_range: 0..usize::from(!glyphs.is_empty()),
2482 advance_width: advance,
2483 offset,
2484 direction,
2485 }]
2486 .into_iter()
2487 .collect();
2488 let paint_fragments = (!glyphs.is_empty())
2489 .then(|| PaintFragment {
2490 source_run: 0,
2491 font_id: FontId(0),
2492 font_size,
2493 glyphs: glyphs.into(),
2494 x_range: Pixels::ZERO..advance,
2495 style: shaping_runs
2496 .first()
2497 .map_or_else(PaintStyle::default, PaintStyle::from),
2498 underline_offset: Some(font_size * 0.1),
2499 strikethrough_offset: Some(-font_size * 0.3),
2500 })
2501 .into_iter()
2502 .collect();
2503 LineLayout {
2504 font_size,
2505 width: advance,
2506 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
2507 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
2508 visual_lines,
2509 paint_fragments,
2510 len: text.len(),
2511 platform_layout: Arc::new(TestPlatformTextLayout {
2512 text: text.to_owned(),
2513 stops,
2514 size: size(advance, font_size),
2515 }),
2516 }
2517 }
2518
2519 fn layout_inline(&self, request: InlineLayoutRequest<'_>) -> InlineLayout {
2520 let mut layout = self.layout_text(TextLayoutRequest {
2521 text: request.text,
2522 font_size: request.font_size,
2523 runs: request.runs,
2524 options: request.options,
2525 });
2526
2527 let em_width = self.em_width(request.font_size);
2528 let baseline = request
2529 .boxes
2530 .iter()
2531 .map(|inline_box| inline_box.size.height)
2532 .fold(request.line_height, Pixels::max);
2533 let positioned_boxes = position_test_inline_boxes(request, em_width, baseline);
2534 add_test_inline_box_advances(&mut layout, request);
2535 let line_width = request
2536 .options
2537 .wrap_width
2538 .unwrap_or(Pixels::MAX)
2539 .min(layout.width);
2540 let line_offset = layout.visual_lines[0].offset;
2541 let mut inline = InlineLayout {
2542 size: size(layout.width, baseline),
2543 layout: Arc::new(layout),
2544 lines: [InlineVisualLine {
2545 origin: point(line_offset, Pixels::ZERO),
2546 size: size(line_width, baseline),
2547 baseline,
2548 }]
2549 .into_iter()
2550 .collect(),
2551 boxes: positioned_boxes,
2552 alignment_offset: Pixels::ZERO,
2553 };
2554
2555 align_inline_boxes(
2556 &mut inline.lines,
2557 &mut inline.boxes,
2558 &mut inline.size,
2559 request.boxes,
2560 &[request.text_metrics],
2561 &[],
2562 request.text_metrics,
2563 request.line_height,
2564 );
2565 inline
2566 }
2567
2568 fn recommended_rendering_mode(
2569 &self,
2570 _font_id: FontId,
2571 _font_size: Pixels,
2572 ) -> TextRenderingMode {
2573 TextRenderingMode::Grayscale
2574 }
2575 }
2576}
2577
2578// Adapted from https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp
2579// Copyright (c) Microsoft Corporation.
2580// Licensed under the MIT license.
2581/// Returns the gamma correction ratios for subpixel text at `gamma`.
2582///
2583/// `gamma` is rounded to one decimal place and clamped to the range `1.0` to `2.2`.
2584#[allow(dead_code)]
2585pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
2586 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
2587 [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0
2588 [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1
2589 [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2
2590 [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3
2591 [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4
2592 [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5
2593 [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6
2594 [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7
2595 [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8
2596 [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9
2597 [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0
2598 [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1
2599 [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2
2600 ];
2601
2602 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
2603 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
2604
2605 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
2606 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
2607
2608 [
2609 ratios[0] * NORM13,
2610 ratios[1] * NORM24,
2611 ratios[2] * NORM13,
2612 ratios[3] * NORM24,
2613 ]
2614}
2615
2616/// Identifies one entry in a [`PlatformAtlas`]: a glyph, an SVG or a raster image.
2617#[derive(PartialEq, Eq, Hash, Clone)]
2618pub enum AtlasKey {
2619 /// A rasterized glyph, with the format it was rasterized in.
2620 Glyph {
2621 /// The glyph and how it is rendered.
2622 params: RenderGlyphParams,
2623 /// The pixel format it was rasterized in.
2624 format: RasterizedGlyphFormat,
2625 },
2626 /// An SVG rendered at specific parameters.
2627 Svg(RenderSvgParams),
2628 /// A raster image, identified by its render parameters.
2629 Image(RenderImageParams),
2630}
2631
2632impl AtlasKey {
2633 /// Returns the texture kind for this atlas key.
2634 pub fn texture_kind(&self) -> AtlasTextureKind {
2635 match self {
2636 AtlasKey::Glyph { format, .. } => match format {
2637 RasterizedGlyphFormat::AlphaMask => AtlasTextureKind::Monochrome,
2638 RasterizedGlyphFormat::BgraSubpixelMask => AtlasTextureKind::Subpixel,
2639 RasterizedGlyphFormat::BgraColor => AtlasTextureKind::Polychrome,
2640 },
2641 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
2642 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
2643 }
2644 }
2645}
2646
2647impl From<(RenderGlyphParams, RasterizedGlyphFormat)> for AtlasKey {
2648 fn from((params, format): (RenderGlyphParams, RasterizedGlyphFormat)) -> Self {
2649 Self::Glyph { params, format }
2650 }
2651}
2652
2653impl From<RenderSvgParams> for AtlasKey {
2654 fn from(params: RenderSvgParams) -> Self {
2655 Self::Svg(params)
2656 }
2657}
2658
2659impl From<RenderImageParams> for AtlasKey {
2660 fn from(params: RenderImageParams) -> Self {
2661 Self::Image(params)
2662 }
2663}
2664
2665#[doc(hidden)]
2666#[derive(Default)]
2667pub struct GlyphAtlasCache {
2668 entries: FxHashMap<RenderGlyphParams, GlyphAtlasEntry>,
2669}
2670
2671impl GlyphAtlasCache {
2672 pub fn get(&self, params: &RenderGlyphParams) -> Option<GlyphAtlasEntry> {
2673 self.entries.get(params).copied()
2674 }
2675
2676 pub fn insert(
2677 &mut self,
2678 params: &RenderGlyphParams,
2679 glyph: &RasterizedGlyph,
2680 tile: Option<AtlasTile>,
2681 ) -> GlyphAtlasEntry {
2682 let entry = GlyphAtlasEntry {
2683 tile,
2684 bounds: glyph.bounds,
2685 format: glyph.format,
2686 };
2687 self.entries.insert(params.clone(), entry);
2688
2689 entry
2690 }
2691
2692 pub fn remove(&mut self, key: &AtlasKey) {
2693 let AtlasKey::Glyph { params, format } = key else {
2694 return;
2695 };
2696
2697 if self
2698 .entries
2699 .get(params)
2700 .is_some_and(|entry| entry.format == *format)
2701 {
2702 self.entries.remove(params);
2703 }
2704 }
2705
2706 pub fn clear(&mut self) {
2707 self.entries.clear();
2708 }
2709}
2710
2711/// A texture atlas that packs glyphs, SVGs and images into shared GPU textures.
2712///
2713/// Each window's renderer owns one atlas. Methods that insert tiles build the pixels
2714/// only on a cache miss, so callers don't pay for rasterizing a tile they already
2715/// have.
2716pub trait PlatformAtlas {
2717 /// Returns the tile for `key`, calling `build` on a cache miss to produce the pixels.
2718 ///
2719 /// `build` returns `None` when there is nothing to draw, and then this returns
2720 /// `Ok(None)`. Returns an error if the pixels fail validation or the atlas is full.
2721 /// The builder runs with the atlas locked and must not re-enter the same atlas.
2722 fn get_or_insert_with<'a>(
2723 &self,
2724 key: AtlasKey,
2725 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2726 ) -> Result<Option<AtlasTile>>;
2727 /// Returns the atlas entry for the glyph `params`, calling `build` on a cache miss.
2728 ///
2729 /// `build` returns a [`ValidatedRasterizedGlyph`], which is inserted into the atlas.
2730 fn get_or_insert_glyph_with(
2731 &self,
2732 params: &RenderGlyphParams,
2733 build: &mut dyn FnMut() -> Result<ValidatedRasterizedGlyph>,
2734 ) -> Result<GlyphAtlasEntry>;
2735 /// Removes the tile for `key`, freeing its space in the atlas.
2736 fn remove(&self, key: &AtlasKey);
2737
2738 /// Returns `true` if the atlas has a tile for `key`. Atlases that don't track this return `false`.
2739 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2740 fn contains(&self, _key: &AtlasKey) -> bool {
2741 false
2742 }
2743}
2744
2745#[doc(hidden)]
2746pub trait AtlasBackend {
2747 fn insert(
2748 &mut self,
2749 kind: AtlasTextureKind,
2750 size: Size<DevicePixels>,
2751 bytes: &[u8],
2752 ) -> Result<AtlasTile>;
2753
2754 fn remove(&mut self, tile: AtlasTile);
2755}
2756
2757#[doc(hidden)]
2758pub struct AtlasState<Backend> {
2759 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
2760 pub backend: Backend,
2761}
2762
2763impl<Backend> AtlasState<Backend> {
2764 pub fn new(backend: Backend) -> Self {
2765 Self {
2766 tiles_by_key: FxHashMap::default(),
2767 backend,
2768 }
2769 }
2770
2771 pub fn contains(&self, key: &AtlasKey) -> bool {
2772 self.tiles_by_key.contains_key(key)
2773 }
2774
2775 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
2776 self.tiles_by_key.clear();
2777 reset_backend(&mut self.backend);
2778 }
2779}
2780
2781impl<Backend: Default> Default for AtlasState<Backend> {
2782 fn default() -> Self {
2783 Self::new(Backend::default())
2784 }
2785}
2786
2787impl<Backend: AtlasBackend> AtlasState<Backend> {
2788 pub fn get_or_insert_with<'a>(
2789 &mut self,
2790 key: AtlasKey,
2791 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2792 ) -> Result<Option<AtlasTile>> {
2793 match self.tiles_by_key.entry(key) {
2794 Entry::Occupied(entry) => Ok(Some(*entry.get())),
2795 Entry::Vacant(entry) => {
2796 profiling::scope!("new tile");
2797 let Some((size, bytes)) = build()? else {
2798 return Ok(None);
2799 };
2800 let tile = self
2801 .backend
2802 .insert(entry.key().texture_kind(), size, &bytes)?;
2803 entry.insert(tile);
2804 Ok(Some(tile))
2805 }
2806 }
2807 }
2808
2809 pub fn remove(&mut self, key: &AtlasKey) {
2810 if let Some(tile) = self.tiles_by_key.remove(key) {
2811 self.backend.remove(tile);
2812 }
2813 }
2814}
2815
2816/// A sprite atlas for windows without a GPU. It hands out uniquely identified
2817/// tiles without uploading any pixels, so glyph, SVG, and image painting can
2818/// run to completion in tests and headless platforms.
2819#[derive(Default)]
2820pub struct HeadlessAtlas(parking_lot::Mutex<HeadlessAtlasState>);
2821
2822#[derive(Default)]
2823struct HeadlessAtlasState {
2824 atlas: AtlasState<HeadlessAtlasBackend>,
2825 glyph_cache: GlyphAtlasCache,
2826}
2827
2828#[doc(hidden)]
2829#[derive(Default)]
2830pub struct HeadlessAtlasBackend {
2831 next_id: u32,
2832}
2833
2834impl AtlasBackend for HeadlessAtlasBackend {
2835 fn insert(
2836 &mut self,
2837 kind: AtlasTextureKind,
2838 size: Size<DevicePixels>,
2839 _bytes: &[u8],
2840 ) -> Result<AtlasTile> {
2841 self.next_id += 1;
2842 let texture_id = self.next_id;
2843 self.next_id += 1;
2844 let tile_id = self.next_id;
2845 Ok(AtlasTile {
2846 texture_id: AtlasTextureId {
2847 index: texture_id,
2848 kind,
2849 },
2850 tile_id: TileId(tile_id),
2851 padding: 0,
2852 bounds: Bounds {
2853 origin: Point::default(),
2854 size,
2855 },
2856 })
2857 }
2858
2859 fn remove(&mut self, _tile: AtlasTile) {}
2860}
2861
2862impl PlatformAtlas for HeadlessAtlas {
2863 fn get_or_insert_with<'a>(
2864 &self,
2865 key: AtlasKey,
2866 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2867 ) -> Result<Option<AtlasTile>> {
2868 self.0.lock().atlas.get_or_insert_with(key, build)
2869 }
2870
2871 fn get_or_insert_glyph_with(
2872 &self,
2873 params: &RenderGlyphParams,
2874 build: &mut dyn FnMut() -> Result<ValidatedRasterizedGlyph>,
2875 ) -> Result<GlyphAtlasEntry> {
2876 let mut state = self.0.lock();
2877 if let Some(entry) = state.glyph_cache.get(params) {
2878 return Ok(entry);
2879 }
2880
2881 let glyph = build()?;
2882 let tile = if glyph.size == Size::default() {
2883 None
2884 } else {
2885 let key = AtlasKey::from((params.clone(), glyph.format));
2886 let mut pixels = || Ok(Some((glyph.size, Cow::Borrowed(glyph.pixels.as_slice()))));
2887 state.atlas.get_or_insert_with(key, &mut pixels)?
2888 };
2889 Ok(state.glyph_cache.insert(params, &glyph, tile))
2890 }
2891
2892 fn remove(&self, key: &AtlasKey) {
2893 let mut state = self.0.lock();
2894 state.glyph_cache.remove(key);
2895 state.atlas.remove(key);
2896 }
2897
2898 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2899 fn contains(&self, key: &AtlasKey) -> bool {
2900 self.0.lock().atlas.contains(key)
2901 }
2902}
2903
2904#[doc(hidden)]
2905pub struct AtlasTextureList<T> {
2906 pub textures: Vec<Option<T>>,
2907 pub free_list: Vec<usize>,
2908}
2909
2910impl<T> Default for AtlasTextureList<T> {
2911 fn default() -> Self {
2912 Self {
2913 textures: Vec::default(),
2914 free_list: Vec::default(),
2915 }
2916 }
2917}
2918
2919impl<T> ops::Index<usize> for AtlasTextureList<T> {
2920 type Output = Option<T>;
2921
2922 fn index(&self, index: usize) -> &Self::Output {
2923 &self.textures[index]
2924 }
2925}
2926
2927impl<T> AtlasTextureList<T> {
2928 #[allow(unused)]
2929 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
2930 self.free_list.clear();
2931 self.textures.drain(..)
2932 }
2933
2934 #[allow(dead_code)]
2935 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
2936 self.textures.iter_mut().flatten()
2937 }
2938}
2939
2940/// Where a glyph, SVG or image sits in an atlas texture.
2941#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2942#[repr(C)]
2943pub struct AtlasTile {
2944 /// The texture this tile belongs to.
2945 pub texture_id: AtlasTextureId,
2946 /// The unique ID of this tile within its texture.
2947 pub tile_id: TileId,
2948 /// Padding around the tile content in pixels.
2949 pub padding: u32,
2950 /// The bounds of this tile within the texture.
2951 pub bounds: Bounds<DevicePixels>,
2952}
2953
2954/// Identifies one texture in an atlas.
2955#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2956#[repr(C)]
2957pub struct AtlasTextureId {
2958 // We use u32 instead of usize for Metal Shader Language compatibility
2959 /// The index of this texture in the atlas.
2960 pub index: u32,
2961 /// The kind of content stored in this texture.
2962 pub kind: AtlasTextureKind,
2963}
2964
2965#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2966#[repr(C)]
2967#[cfg_attr(
2968 all(
2969 any(target_os = "linux", target_os = "freebsd"),
2970 not(any(feature = "x11", feature = "wayland"))
2971 ),
2972 allow(dead_code)
2973)]
2974/// The kind of pixels a texture in an atlas stores.
2975pub enum AtlasTextureKind {
2976 /// One coverage byte per pixel, used for alpha-masked glyphs and SVGs.
2977 Monochrome = 0,
2978 /// Four bytes per pixel, used for full-color images and emoji.
2979 Polychrome = 1,
2980 /// Four bytes per pixel, used for subpixel (LCD) antialiased glyphs.
2981 Subpixel = 2,
2982}
2983
2984impl AtlasTextureKind {
2985 /// Validates a tightly packed bitmap before an atlas allocates or caches its tile.
2986 /// Monochrome tiles contain one coverage byte; color and LCD tiles contain four bytes.
2987 pub fn validate_upload(self, size: Size<DevicePixels>, bytes: &[u8]) -> Result<()> {
2988 anyhow::ensure!(
2989 size.width.0 > 0 && size.height.0 > 0,
2990 "{self:?} atlas upload requires positive dimensions, got {size:?}"
2991 );
2992 let channels = match self {
2993 Self::Monochrome => 1,
2994 Self::Polychrome | Self::Subpixel => 4,
2995 };
2996 let expected = (size.width.0 as usize)
2997 .checked_mul(size.height.0 as usize)
2998 .and_then(|pixels| pixels.checked_mul(channels))
2999 .ok_or_else(|| anyhow::anyhow!("atlas upload byte count overflow for {size:?}"))?;
3000 anyhow::ensure!(
3001 bytes.len() == expected,
3002 "{self:?} atlas upload for {size:?} requires {expected} bytes, got {}",
3003 bytes.len()
3004 );
3005 Ok(())
3006 }
3007}
3008
3009/// Identifies a tile within its atlas texture.
3010#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
3011#[repr(C)]
3012pub struct TileId(pub u32);
3013
3014impl From<etagere::AllocId> for TileId {
3015 fn from(id: etagere::AllocId) -> Self {
3016 Self(id.serialize())
3017 }
3018}
3019
3020impl From<TileId> for etagere::AllocId {
3021 fn from(id: TileId) -> Self {
3022 Self::deserialize(id.0)
3023 }
3024}
3025
3026/// Connects the platform's input method (IME) to the focused [`InputHandler`].
3027///
3028/// GPUI creates one for the focused element while it's focused. Each method
3029/// runs the matching [`InputHandler`] method inside an update of the window. If the
3030/// window has closed, the methods return `None` or do nothing.
3031pub struct PlatformInputHandler {
3032 cx: AsyncWindowContext,
3033 handler: Box<dyn InputHandler>,
3034 focus_id: Option<crate::FocusId>,
3035}
3036
3037#[cfg_attr(
3038 all(
3039 any(target_os = "linux", target_os = "freebsd"),
3040 not(any(feature = "x11", feature = "wayland"))
3041 ),
3042 allow(dead_code)
3043)]
3044impl PlatformInputHandler {
3045 /// Wraps `handler` so the platform can call it through the window context `cx`.
3046 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
3047 Self {
3048 cx,
3049 handler,
3050 focus_id: None,
3051 }
3052 }
3053
3054 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
3055 self.focus_id = Some(focus_id);
3056 self
3057 }
3058
3059 /// Identifies the input field registered through [`Window::handle_input`].
3060 ///
3061 /// This identity survives adapter replacement during repaint. Backends can
3062 /// use it to detect a field change without restarting the keyboard session.
3063 pub fn focus_id(&self) -> Option<crate::FocusId> {
3064 self.focus_id
3065 }
3066
3067 /// Returns the selection, in UTF-16 units, or `None` if the handler has no selection.
3068 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
3069 self.cx
3070 .update(|window, cx| {
3071 self.handler
3072 .selected_text_range(ignore_disabled_input, window, cx)
3073 })
3074 .ok()
3075 .flatten()
3076 }
3077
3078 /// Returns the range of composing (marked) text in UTF-16 units, or `None` when no text is being composed.
3079 #[cfg_attr(target_os = "windows", allow(dead_code))]
3080 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
3081 self.cx
3082 .update(|window, cx| self.handler.marked_text_range(window, cx))
3083 .ok()
3084 .flatten()
3085 }
3086
3087 #[cfg_attr(
3088 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
3089 allow(dead_code)
3090 )]
3091 /// Returns the document text in `range_utf16`, or `None` if the handler has none.
3092 ///
3093 /// `adjusted` receives the range the handler actually used, which may differ from the request.
3094 pub fn text_for_range(
3095 &mut self,
3096 range_utf16: Range<usize>,
3097 adjusted: &mut Option<Range<usize>>,
3098 ) -> Option<String> {
3099 self.cx
3100 .update(|window, cx| {
3101 self.handler
3102 .text_for_range(range_utf16, adjusted, window, cx)
3103 })
3104 .ok()
3105 .flatten()
3106 }
3107
3108 /// Replaces `replacement_range`, in UTF-16 units, with `text`. `None` replaces the selection.
3109 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
3110 self.cx
3111 .update(|window, cx| {
3112 self.handler
3113 .replace_text_in_range(replacement_range, text, window, cx);
3114 })
3115 .ok();
3116 }
3117
3118 /// Replaces `range_utf16` with `new_text` and marks the new text as composing, as an
3119 /// IME does while the user types. `new_selected_range` sets the selection inside the composing text.
3120 pub fn replace_and_mark_text_in_range(
3121 &mut self,
3122 range_utf16: Option<Range<usize>>,
3123 new_text: &str,
3124 new_selected_range: Option<Range<usize>>,
3125 ) {
3126 self.cx
3127 .update(|window, cx| {
3128 self.handler.replace_and_mark_text_in_range(
3129 range_utf16,
3130 new_text,
3131 new_selected_range,
3132 window,
3133 cx,
3134 )
3135 })
3136 .ok();
3137 }
3138
3139 /// Ends the composition and keeps the marked text.
3140 #[cfg_attr(target_os = "windows", allow(dead_code))]
3141 pub fn unmark_text(&mut self) {
3142 self.cx
3143 .update(|window, cx| self.handler.unmark_text(window, cx))
3144 .ok();
3145 }
3146
3147 /// Pastes `item` at the selection through [`InputHandler::paste`].
3148 pub fn paste(&mut self, item: ClipboardItem) {
3149 self.cx
3150 .update(|window, cx| self.handler.paste(item, window, cx))
3151 .ok();
3152 }
3153
3154 /// Returns the screen bounds of `range_utf16`, in logical pixels. The platform uses these to place the IME candidate window.
3155 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
3156 self.cx
3157 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
3158 .ok()
3159 .flatten()
3160 }
3161
3162 /// Returns selection rectangles for a range of UTF-16 code units in document order.
3163 ///
3164 /// The platform uses these logical-pixel bounds to position selection UI. A backend may
3165 /// return an empty vector when selection geometry is unavailable; IME candidate placement
3166 /// can still use bounds_for_range. If the window has closed, this also returns an empty vector.
3167 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
3168 self.cx
3169 .update(|window, cx| {
3170 self.handler
3171 .selection_bounds_for_range(range_utf16, window, cx)
3172 })
3173 .unwrap_or_default()
3174 }
3175
3176 /// Returns whether macOS press-and-hold should stay enabled for this handler.
3177 #[allow(dead_code)]
3178 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
3179 self.handler.apple_press_and_hold_enabled()
3180 }
3181
3182 /// Replaces the selection with `input`, using the handler's [`InputHandler::replace_text_in_range`].
3183 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
3184 self.handler.replace_text_in_range(None, input, window, cx);
3185 }
3186
3187 /// Returns where the IME candidate window should appear.
3188 ///
3189 /// While text is composing, this is the start of the composing text's visual line,
3190 /// found by walking back from the caret until the line changes. Otherwise it's the
3191 /// caret position. `bounds_for_range` returns the bounds of each position it's asked for.
3192 pub fn compute_ime_candidate_bounds(
3193 marked_range: Option<Range<usize>>,
3194 selection: &UTF16Selection,
3195 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
3196 ) -> Option<Bounds<Pixels>> {
3197 // The active end of the selection, which is `start` when it is reversed.
3198 let caret = if selection.reversed {
3199 selection.range.start
3200 } else {
3201 selection.range.end
3202 };
3203
3204 if let Some(marked_range) = marked_range {
3205 // Default to the start of the marked (composing) range.
3206 let mut line_start = marked_range.start;
3207
3208 // Walk backward from the caret looking for a line break. A change in
3209 // the Y coordinate means we crossed into the previous visual line, so
3210 // the line start is one position after the break point.
3211 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
3212 for i in (marked_range.start..caret).rev() {
3213 if let Some(b) = bounds_for_range(i..i) {
3214 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
3215 line_start = i + 1;
3216 break;
3217 }
3218 }
3219 }
3220 }
3221 bounds_for_range(line_start..line_start)
3222 } else {
3223 // No active composition — use the selection endpoint.
3224 bounds_for_range(caret..caret)
3225 }
3226 }
3227
3228 /// Returns the IME candidate position for the current selection.
3229 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
3230 let marked_range = self.handler.marked_text_range(window, cx);
3231 let selection = self.handler.selected_text_range(true, window, cx)?;
3232 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
3233 self.handler.bounds_for_range(range, window, cx)
3234 })
3235 }
3236
3237 /// Returns the IME candidate position for the current selection, through the window context.
3238 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
3239 let marked_range = self.marked_text_range();
3240 let selection = self.selected_text_range(true)?;
3241 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
3242 self.bounds_for_range(range)
3243 })
3244 }
3245
3246 /// Returns the UTF-16 index of the character at `point`, in window coordinates, or `None` if there is none.
3247 #[allow(unused)]
3248 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
3249 self.cx
3250 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
3251 .ok()
3252 .flatten()
3253 }
3254
3255 /// See [`InputHandler::set_selected_text_range`].
3256 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
3257 self.cx
3258 .update(|window, cx| {
3259 self.handler
3260 .set_selected_text_range(range_utf16, window, cx)
3261 })
3262 .ok();
3263 }
3264
3265 /// See [`InputHandler::element_bounds`].
3266 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
3267 self.cx
3268 .update(|window, cx| self.handler.element_bounds(window, cx))
3269 .ok()
3270 .flatten()
3271 }
3272
3273 /// See [`InputHandler::text_length_utf16`].
3274 pub fn text_length_utf16(&mut self) -> Option<usize> {
3275 self.cx
3276 .update(|window, cx| self.handler.text_length_utf16(window, cx))
3277 .ok()
3278 .flatten()
3279 }
3280
3281 /// Returns whether the handler accepts text input right now.
3282 #[allow(dead_code)]
3283 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
3284 self.handler.accepts_text_input(window, cx)
3285 }
3286
3287 /// Returns whether the handler accepts text input. Returns `true` if the window is gone.
3288 #[allow(dead_code)]
3289 pub fn query_accepts_text_input(&mut self) -> bool {
3290 self.cx
3291 .update(|window, cx| self.handler.accepts_text_input(window, cx))
3292 .unwrap_or(true)
3293 }
3294
3295 /// Checks eligibility against the current focus, including before the next frame is drawn.
3296 ///
3297 /// Unlike the legacy acceptance query, this fails closed when the window cannot
3298 /// be updated and requires registration through `Window::handle_input`.
3299 pub fn query_accepts_focused_text_input(&mut self) -> bool {
3300 let focus_id = self.focus_id;
3301 self.cx
3302 .update(|window, cx| {
3303 focus_id.is_some()
3304 && window.focus == focus_id
3305 && self.handler.accepts_text_input(window, cx)
3306 })
3307 .unwrap_or(false)
3308 }
3309
3310 /// See [`InputHandler::prefers_ime_for_printable_keys`].
3311 ///
3312 /// This is not a pure delegation to the handler: while a multi-stroke binding is pending this
3313 /// returns `false` regardless of the handler's preference, because the next printable key may
3314 /// complete a binding whose prefix already bypassed the IME.
3315 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
3316 self.cx
3317 .update(|window, cx| {
3318 // The next printable key may complete a chord whose prefix bypassed the IME.
3319 !window.has_pending_keystrokes()
3320 && self.handler.prefers_ime_for_printable_keys(window, cx)
3321 })
3322 .unwrap_or(false)
3323 }
3324
3325 /// Returns the text input configuration the handler requests.
3326 pub fn text_input_configuration(
3327 &mut self,
3328 window: &mut Window,
3329 cx: &mut App,
3330 ) -> TextInputConfiguration {
3331 self.handler.text_input_configuration(window, cx)
3332 }
3333
3334 /// See [`InputHandler::text_input_editable_range`].
3335 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
3336 self.cx
3337 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
3338 .ok()
3339 .flatten()
3340 }
3341}
3342
3343/// A struct representing a selection in a text buffer, in UTF16 characters.
3344/// This is different from a range because the head may be before the tail.
3345#[derive(Debug)]
3346pub struct UTF16Selection {
3347 /// The range of text in the document this selection corresponds to
3348 /// in UTF16 characters.
3349 pub range: Range<usize>,
3350 /// Whether the head of this selection is at the start (true), or end (false)
3351 /// of the range
3352 pub reversed: bool,
3353}
3354
3355/// Receives text from the platform's input method (IME) on behalf of a focused element.
3356///
3357/// Implement this on the element that accepts text, and return it from
3358/// [`Window::handle_input`]. The methods mirror the macOS `NSTextInputClient`
3359/// protocol. All ranges are in UTF-16 code units, because that is what platforms
3360/// use, not bytes or characters.
3361///
3362/// Methods without a default body must be implemented. The others have defaults, so
3363/// override only what your text needs. See [text input](crate::_documentation::guides::text_input).
3364pub trait InputHandler: 'static {
3365 /// Get the range of the user's currently selected text, if any
3366 /// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
3367 ///
3368 /// Return value is in terms of UTF-16 characters, from 0 to the length of the document
3369 fn selected_text_range(
3370 &mut self,
3371 ignore_disabled_input: bool,
3372 window: &mut Window,
3373 cx: &mut App,
3374 ) -> Option<UTF16Selection>;
3375
3376 /// Get the range of the currently marked text, if any
3377 /// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
3378 ///
3379 /// Return value is in terms of UTF-16 characters, from 0 to the length of the document
3380 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
3381
3382 /// Get the text for the given document range in UTF-16 characters
3383 /// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
3384 ///
3385 /// range_utf16 is in terms of UTF-16 characters
3386 fn text_for_range(
3387 &mut self,
3388 range_utf16: Range<usize>,
3389 adjusted_range: &mut Option<Range<usize>>,
3390 window: &mut Window,
3391 cx: &mut App,
3392 ) -> Option<String>;
3393
3394 /// Replace the text in the given document range with the given text
3395 /// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
3396 ///
3397 /// replacement_range is in terms of UTF-16 characters
3398 fn replace_text_in_range(
3399 &mut self,
3400 replacement_range: Option<Range<usize>>,
3401 text: &str,
3402 window: &mut Window,
3403 cx: &mut App,
3404 );
3405
3406 /// Replace the text in the given document range with the given text,
3407 /// and mark the given text as part of an IME 'composing' state
3408 /// Corresponds to [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext)
3409 ///
3410 /// range_utf16 is in terms of UTF-16 characters
3411 /// new_selected_range is in terms of UTF-16 characters
3412 fn replace_and_mark_text_in_range(
3413 &mut self,
3414 range_utf16: Option<Range<usize>>,
3415 new_text: &str,
3416 new_selected_range: Option<Range<usize>>,
3417 window: &mut Window,
3418 cx: &mut App,
3419 );
3420
3421 /// Remove the IME 'composing' state from the document
3422 /// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
3423 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
3424
3425 /// Insert a platform-initiated paste at the current selection.
3426 ///
3427 /// Platforms that deliver paste as an input event rather than through an
3428 /// application-defined action (e.g. the DOM `paste` event on web) call
3429 /// this with the full clipboard contents. The default implementation
3430 /// inserts only the plain-text portion of the item.
3431 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
3432 if let Some(text) = item.text() {
3433 self.replace_text_in_range(None, &text, window, cx);
3434 }
3435 }
3436
3437 /// Get the bounds of the given document range in window coordinates.
3438 /// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
3439 ///
3440 /// This is used for positioning the IME candidate window
3441 fn bounds_for_range(
3442 &mut self,
3443 range_utf16: Range<usize>,
3444 window: &mut Window,
3445 cx: &mut App,
3446 ) -> Option<Bounds<Pixels>>;
3447
3448 /// Returns selection rectangles in document order, in window coordinates.
3449 ///
3450 /// Return line or shaped-run fragments, not one rectangle per character. The
3451 /// first and last rectangles contain the range's start and end respectively.
3452 /// Empty means native selection geometry is unavailable; caret/IME geometry can
3453 /// still be supplied by `bounds_for_range`.
3454 fn selection_bounds_for_range(
3455 &mut self,
3456 _range_utf16: Range<usize>,
3457 _window: &mut Window,
3458 _cx: &mut App,
3459 ) -> Vec<Bounds<Pixels>> {
3460 Vec::new()
3461 }
3462
3463 /// Get the character offset for the given point in terms of UTF16 characters
3464 ///
3465 /// Corresponds to [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:))
3466 fn character_index_for_point(
3467 &mut self,
3468 point: Point<Pixels>,
3469 window: &mut Window,
3470 cx: &mut App,
3471 ) -> Option<usize>;
3472
3473 /// Set the range of the user's currently selected text.
3474 ///
3475 /// This is the reverse data-flow direction from [`Self::selected_text_range`]:
3476 /// platforms call it when the system text machinery moves the selection on the
3477 /// application's behalf — e.g. the user drags a system selection handle or
3478 /// invokes Select All from system UI (iOS `UITextInput setSelectedTextRange:`,
3479 /// Android `InputConnection.setSelection`).
3480 ///
3481 /// range_utf16 is in terms of UTF-16 characters, from 0 to the length of the document
3482 fn set_selected_text_range(
3483 &mut self,
3484 _range_utf16: Range<usize>,
3485 _window: &mut Window,
3486 _cx: &mut App,
3487 ) {
3488 }
3489
3490 /// Get the bounds of the focused text element in window coordinates, if known.
3491 ///
3492 /// This is the pull counterpart to the [`PlatformWindow::update_ime_position`]
3493 /// push: mobile platforms ask for the focused element's geometry when they
3494 /// need it (e.g. to frame system text-interaction UI overlaid on the focused
3495 /// element).
3496 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
3497 None
3498 }
3499
3500 /// Get the length of the document in UTF-16 characters, if known.
3501 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
3502 None
3503 }
3504
3505 /// Allows a given input context to opt into getting raw key repeats instead of
3506 /// sending these to the platform.
3507 /// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults
3508 /// (which is how iTerm does it) but it doesn't seem to work for me.
3509 #[allow(dead_code)]
3510 fn apple_press_and_hold_enabled(&mut self) -> bool {
3511 true
3512 }
3513
3514 /// Returns whether this handler is accepting text input to be inserted.
3515 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
3516 true
3517 }
3518
3519 /// The contiguous range of text, in UTF-16 code units, that platform text
3520 /// input may read and edit around the current selection.
3521 ///
3522 /// Platforms that mirror document text into an IME-editable buffer clamp
3523 /// the mirrored window to this range, so multi-step IME edit gestures
3524 /// (word deletion, autocorrect rewrites, suggestion picks) cannot reach
3525 /// content outside it. The range should contain the current selection;
3526 /// when it cannot (a selection spanning a region boundary), platforms
3527 /// degrade the mirrored IME context rather than widening the range.
3528 /// `None` places no bound.
3529 fn text_input_editable_range(
3530 &mut self,
3531 _window: &mut Window,
3532 _cx: &mut App,
3533 ) -> Option<Range<usize>> {
3534 None
3535 }
3536
3537 /// Returns whether printable keys should be routed to the IME before keybinding
3538 /// matching when a non-ASCII input source (e.g. Japanese, Korean, Chinese IME)
3539 /// is active. This prevents multi-stroke keybindings like `jj` from intercepting
3540 /// keys that the IME should compose.
3541 ///
3542 /// Defaults to `false`. The editor overrides this based on whether it expects
3543 /// character input (e.g. Vim insert mode returns `true`, normal mode returns `false`).
3544 /// The terminal keeps the default `false` so that raw keys reach the terminal process.
3545 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
3546 false
3547 }
3548
3549 /// Get this handler's preferences for platform text assistance.
3550 ///
3551 /// GPUI re-queries this every frame and forwards it to the platform window
3552 /// only when it changes, so implementations must be cheap and may vary the
3553 /// result with application state (e.g. with the cursor's position).
3554 fn text_input_configuration(
3555 &mut self,
3556 _window: &mut Window,
3557 _cx: &mut App,
3558 ) -> TextInputConfiguration {
3559 TextInputConfiguration::default()
3560 }
3561}
3562
3563/// Platform text-assistance preferences for the focused text region.
3564///
3565/// Returned by [`InputHandler::text_input_configuration`] and forwarded to the
3566/// platform whenever it changes; the platform maps the fields onto its native
3567/// input-session attributes (on web, DOM attributes of the hidden editable
3568/// element such as `autocorrect` and `enterkeyhint`).
3569///
3570/// The default disables all text assistance and requests no particular action
3571/// key presentation.
3572#[derive(Clone, Debug, Default, PartialEq, Eq)]
3573pub struct TextInputConfiguration {
3574 /// Whether the platform may automatically correct entered text.
3575 pub autocorrect: bool,
3576 /// How software keyboards automatically capitalize entered text.
3577 pub autocapitalize: Autocapitalize,
3578 /// Whether software keyboards may offer word suggestions and spellcheck.
3579 pub suggestions: bool,
3580 /// The action advertised on a software keyboard's confirm ("enter") key.
3581 pub input_action: TextInputAction,
3582}
3583
3584/// Automatic capitalization applied by software keyboards.
3585#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3586pub enum Autocapitalize {
3587 /// No automatic capitalization.
3588 #[default]
3589 None,
3590 /// Capitalize the first letter of each word.
3591 Words,
3592 /// Capitalize the first letter of each sentence.
3593 Sentences,
3594 /// Capitalize every letter.
3595 Characters,
3596}
3597
3598/// The action a software keyboard advertises on its confirm ("enter") key.
3599///
3600/// This affects only how the key is presented (icon or label); pressing it is
3601/// still delivered as ordinary input.
3602///
3603/// The variants are the HTML `enterkeyhint` attribute's value set
3604/// (<https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-enterkeyhint-attribute>),
3605/// which also maps onto Android's `IME_ACTION_*` constants and iOS's
3606/// `UIReturnKeyType`; [`TextInputAction::Unspecified`] means "emit no hint".
3607#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3608pub enum TextInputAction {
3609 /// Let the platform choose its default presentation.
3610 #[default]
3611 Unspecified,
3612 /// Inserting a line break.
3613 Enter,
3614 /// Committing the field's value.
3615 Done,
3616 /// Navigating to the typed target.
3617 Go,
3618 /// Moving to the next field.
3619 Next,
3620 /// Moving to the previous field.
3621 Previous,
3622 /// Executing a search.
3623 Search,
3624 /// Sending a message.
3625 Send,
3626}
3627
3628/// Options for creating a window, passed to [`App::open_window`].
3629///
3630/// Start from [`WindowOptions::default`] (or [`WindowOptions::new`], which is the same)
3631/// and chain setters for the fields you want to change. Each setter takes the value
3632/// itself, or an `Option` to clear the field. The defaults give a focused, shown,
3633/// resizable, movable normal window with a titlebar, at the platform's default size
3634/// and position.
3635///
3636/// Not every platform reads every field. The web backend uses [`WindowOptions::kind`],
3637/// [`WindowOptions::show`], and `web_window_background`. The page's canvas is
3638/// the window, so bounds, the titlebar, the minimum size and the resize options are ignored.
3639/// Each field's docs say which
3640/// platforms use it. See [platforms](crate::_documentation::platforms).
3641///
3642/// # Examples
3643///
3644/// Start from the defaults and change only what you need:
3645///
3646/// ```
3647/// use gpui::{Bounds, WindowBounds, WindowOptions, point, px, size};
3648///
3649/// let bounds = Bounds::new(point(px(100.), px(100.)), size(px(800.), px(600.)));
3650/// let options = WindowOptions::default()
3651/// .window_bounds(WindowBounds::Windowed(bounds))
3652/// .is_resizable(false)
3653/// .titlebar(None);
3654///
3655/// assert_eq!(options.window_bounds, Some(WindowBounds::Windowed(bounds)));
3656/// assert!(!options.is_resizable);
3657/// assert!(options.titlebar.is_none());
3658/// assert!(options.focus);
3659/// ```
3660///
3661/// Create a window that doesn't take focus and opens hidden, so you can show it later:
3662///
3663/// ```
3664/// use gpui::WindowOptions;
3665///
3666/// let options = WindowOptions::new().focus(false).show(false);
3667///
3668/// assert!(!options.focus);
3669/// assert!(!options.show);
3670/// ```
3671#[derive(Debug, derive_setters::Setters)]
3672#[setters(into)]
3673pub struct WindowOptions {
3674 /// The window's state and bounds in screen coordinates, in logical pixels.
3675 ///
3676 /// `None` inherits the bounds from the platform, and `Some` opens the window in the
3677 /// given state. A `Windowed` window opens at the rectangle, and `Maximized` and
3678 /// `Fullscreen` restore to the rectangle when left. Ignored on the web.
3679 pub window_bounds: Option<WindowBounds>,
3680
3681 /// The titlebar, or `None` for a window without one. Ignored on the web.
3682 ///
3683 /// The default is [`TitlebarOptions::default`], which shows the platform's titlebar with no title.
3684 pub titlebar: Option<TitlebarOptions>,
3685
3686 /// Whether the window takes keyboard focus when it opens. Defaults to `true`.
3687 pub focus: bool,
3688
3689 /// Whether the window appears when it opens. Set it to `false` to show the window later
3690 /// with [`PlatformWindow::set_visible`].
3691 pub show: bool,
3692
3693 /// The kind of window to create. Defaults to [`WindowKind::Normal`]. On the web, only `Normal` works.
3694 pub kind: WindowKind,
3695
3696 /// Whether the user can drag the window. Defaults to `true`. macOS and Windows use this
3697 /// value. Linux and the web ignore it. Programmatic moves still work.
3698 pub is_movable: bool,
3699
3700 /// Whether the application owns dragging of the (custom) titlebar, rather than
3701 /// AppKit. Only has an effect on macOS.
3702 ///
3703 /// Set this to `true` for windows that draw their own titlebar and move the window
3704 /// themselves via [`Window::start_window_move`]. It marks the whole content view as
3705 /// app-owned titlebar content, so AppKit neither drags the window from the titlebar
3706 /// nor delays titlebar clicks while disambiguating double-clicks (a delay first
3707 /// observed on macOS 27). It is independent of `is_movable`, so such windows stay
3708 /// user-movable (via their own drag) and keep the Window-menu tiling items enabled.
3709 ///
3710 /// Leave this `false` for windows that rely on AppKit's native titlebar dragging.
3711 pub app_owns_titlebar_drag: bool,
3712
3713 /// The minimum time between animation frames while the window is inactive.
3714 ///
3715 /// Defaults to about 30 frames per second. Set `None` to draw inactive windows at the
3716 /// full frame rate.
3717 pub inactive_frame_interval: Option<Duration>,
3718
3719 /// Whether the user can resize the window. Defaults to `true`. Ignored on the web.
3720 pub is_resizable: bool,
3721
3722 /// Whether the user can minimize the window. Defaults to `true`. Ignored on the web.
3723 pub is_minimizable: bool,
3724
3725 /// The display to open the window on, or `None` for the primary display. Ignored on the web.
3726 pub display_id: Option<DisplayId>,
3727
3728 /// The background appearance of a macOS window.
3729 #[cfg(target_os = "macos")]
3730 pub macos_window_background: MacosWindowBackground,
3731
3732 /// The background appearance of a Windows window.
3733 #[cfg(target_os = "windows")]
3734 pub windows_window_background: WindowsWindowBackground,
3735
3736 /// The background appearance of a Linux window.
3737 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
3738 pub linux_window_background: LinuxWindowBackground,
3739
3740 /// The background appearance of a web window.
3741 #[cfg(target_family = "wasm")]
3742 pub web_window_background: WebWindowBackground,
3743
3744 /// The application id that desktop environments use to group windows and find the desktop entry.
3745 ///
3746 /// Linux only. Other platforms ignore it.
3747 pub app_id: Option<String>,
3748
3749 /// The smallest size the user can resize the window to, in logical pixels. `None` sets no minimum.
3750 /// Ignored on the web.
3751 pub window_min_size: Option<Size<Pixels>>,
3752
3753 /// Whether the window draws its own decorations (client-side) or the window server does (server-side).
3754 ///
3755 /// `None` uses the server's default. Linux's Wayland and X11 backends honor it, and
3756 /// the other backends ignore it. The platform may ignore requests it can't satisfy.
3757 pub window_decorations: Option<WindowDecorations>,
3758
3759 /// The window's icon, from an RGBA image. Linux only, and Wayland needs the
3760 /// `xdg-toplevel-icon` protocol to show it.
3761 pub icon: Option<Arc<image::RgbaImage>>,
3762
3763 /// The name of the native tab group. Windows with the same identifier open as tabs of one window.
3764 /// macOS only.
3765 pub tabbing_identifier: Option<String>,
3766
3767 /// Whether to render the window using the Display P3 color space on macOS.
3768 pub use_display_p3: bool,
3769}
3770
3771impl FluentBuilder for WindowOptions {}
3772
3773/// The variables that can be configured when creating a new window
3774#[derive(Debug)]
3775#[cfg_attr(
3776 all(
3777 any(target_os = "linux", target_os = "freebsd"),
3778 not(any(feature = "x11", feature = "wayland"))
3779 ),
3780 allow(dead_code)
3781)]
3782/// The platform-level options for one window, passed to [`Platform::open_window`].
3783///
3784/// [`WindowOptions`] is the public form. GPUI resolves its fields into these parameters,
3785/// with `bounds` already computed.
3786#[allow(missing_docs)]
3787pub struct WindowParams {
3788 /// The window's initial bounds, in logical pixels.
3789 pub bounds: Bounds<Pixels>,
3790
3791 /// The titlebar configuration of the window
3792 #[cfg_attr(feature = "wayland", allow(dead_code))]
3793 pub titlebar: Option<TitlebarOptions>,
3794
3795 /// The kind of window to create
3796 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3797 pub kind: WindowKind,
3798
3799 /// Whether the window should be movable by the user
3800 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3801 pub is_movable: bool,
3802
3803 /// Whether the application owns dragging of the (custom) titlebar (macOS only)
3804 #[cfg_attr(
3805 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
3806 allow(dead_code)
3807 )]
3808 /// Whether the application owns dragging of the custom titlebar. macOS only. See [`WindowOptions::app_owns_titlebar_drag`].
3809 pub app_owns_titlebar_drag: bool,
3810
3811 /// Whether the window should be resizable by the user
3812 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3813 pub is_resizable: bool,
3814
3815 /// Whether the window should be minimized by the user
3816 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3817 pub is_minimizable: bool,
3818
3819 #[cfg_attr(
3820 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
3821 allow(dead_code)
3822 )]
3823 /// Whether the window takes keyboard focus when it opens.
3824 pub focus: bool,
3825
3826 /// Whether the window appears when it opens.
3827 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3828 pub show: bool,
3829
3830 /// An image to set as the window icon (X11 and Wayland only)
3831 /// Wayland requires the xdg-toplevel-icon protocol to be available
3832 #[cfg_attr(feature = "wayland", allow(dead_code))]
3833 pub icon: Option<Arc<image::RgbaImage>>,
3834
3835 /// The display to open the window on, or `None` for the primary display.
3836 #[cfg_attr(feature = "wayland", allow(dead_code))]
3837 pub display_id: Option<DisplayId>,
3838
3839 /// The application id for desktop environments to group windows. Linux only.
3840 #[cfg_attr(feature = "wayland", allow(dead_code))]
3841 pub app_id: Option<String>,
3842
3843 /// The smallest size the user can resize the window to, in logical pixels.
3844 pub window_min_size: Option<Size<Pixels>>,
3845
3846 /// The name of the native tab group. macOS only.
3847 #[cfg(target_os = "macos")]
3848 pub tabbing_identifier: Option<String>,
3849
3850 #[cfg(target_os = "macos")]
3851 pub use_display_p3: bool,
3852}
3853
3854/// The state a window opens in, with the bounds it uses for that state.
3855///
3856/// Set it with [`WindowOptions::window_bounds`]. The bounds are in screen coordinates.
3857#[derive(Debug, Copy, Clone, PartialEq)]
3858pub enum WindowBounds {
3859 /// Indicates that the window should open in a windowed state with the given bounds.
3860 Windowed(Bounds<Pixels>),
3861 /// Indicates that the window should open in a maximized state.
3862 /// The bounds provided here represent the restore size of the window.
3863 Maximized(Bounds<Pixels>),
3864 /// Indicates that the window should open in fullscreen mode.
3865 /// The bounds provided here represent the restore size of the window.
3866 Fullscreen(Bounds<Pixels>),
3867}
3868
3869impl Default for WindowBounds {
3870 fn default() -> Self {
3871 WindowBounds::Windowed(Bounds::default())
3872 }
3873}
3874
3875impl WindowBounds {
3876 /// Returns the bounds, whatever the state. For a maximized or fullscreen window these are the restore bounds.
3877 pub fn get_bounds(&self) -> Bounds<Pixels> {
3878 match self {
3879 WindowBounds::Windowed(bounds) => *bounds,
3880 WindowBounds::Maximized(bounds) => *bounds,
3881 WindowBounds::Fullscreen(bounds) => *bounds,
3882 }
3883 }
3884
3885 /// Returns `WindowBounds::Windowed` with `size`, centered on the primary display.
3886 ///
3887 /// Use [`Bounds::centered`] to center on another display.
3888 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
3889 WindowBounds::Windowed(Bounds::centered(None, size, cx))
3890 }
3891}
3892
3893impl Default for WindowOptions {
3894 fn default() -> Self {
3895 Self {
3896 window_bounds: None,
3897 titlebar: Some(TitlebarOptions {
3898 title: Default::default(),
3899 appears_transparent: Default::default(),
3900 traffic_light_position: Default::default(),
3901 }),
3902 focus: true,
3903 show: true,
3904 kind: WindowKind::Normal,
3905 is_movable: true,
3906 app_owns_titlebar_drag: false,
3907 inactive_frame_interval: Some(Duration::from_micros(33_333)),
3908 is_resizable: true,
3909 is_minimizable: true,
3910 display_id: None,
3911 #[cfg(target_os = "macos")]
3912 macos_window_background: MacosWindowBackground::default(),
3913 #[cfg(target_os = "windows")]
3914 windows_window_background: WindowsWindowBackground::default(),
3915 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
3916 linux_window_background: LinuxWindowBackground::default(),
3917 #[cfg(target_family = "wasm")]
3918 web_window_background: WebWindowBackground::default(),
3919 icon: None,
3920 app_id: None,
3921 window_min_size: None,
3922 window_decorations: None,
3923 tabbing_identifier: None,
3924 use_display_p3: false,
3925 }
3926 }
3927}
3928
3929impl WindowOptions {
3930 /// Returns window options with the platform's defaults: a focused, shown,
3931 /// resizable, minimizable, movable normal window with a titlebar.
3932 pub fn new() -> Self {
3933 Self::default()
3934 }
3935
3936 /// Returns the window's background appearance for the current platform,
3937 /// erased into the renderer-facing [`WindowBackgroundAppearance`].
3938 pub fn background_appearance(&self) -> WindowBackgroundAppearance {
3939 #[cfg(target_os = "macos")]
3940 {
3941 self.macos_window_background.into()
3942 }
3943 #[cfg(target_os = "windows")]
3944 {
3945 self.windows_window_background.into()
3946 }
3947 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
3948 {
3949 self.linux_window_background.into()
3950 }
3951 #[cfg(target_family = "wasm")]
3952 {
3953 self.web_window_background.into()
3954 }
3955 #[cfg(not(any(
3956 target_os = "macos",
3957 target_os = "windows",
3958 any(target_os = "linux", target_os = "freebsd"),
3959 target_family = "wasm",
3960 )))]
3961 {
3962 WindowBackgroundAppearance::default()
3963 }
3964 }
3965}
3966
3967/// Options for a window's titlebar.
3968///
3969/// Chain setters on [`TitlebarOptions::default`] and pass the result to
3970/// [`WindowOptions::titlebar`]. Pass `None` to clear the title or traffic light position.
3971///
3972/// ```
3973/// use gpui::{SharedString, TitlebarOptions, point, px};
3974///
3975/// let titlebar = TitlebarOptions::default()
3976/// .title(SharedString::from("My app"))
3977/// .appears_transparent(true)
3978/// .traffic_light_position(point(px(16.0), px(16.0)));
3979///
3980/// assert_eq!(titlebar.title, Some(SharedString::from("My app")));
3981/// assert!(titlebar.appears_transparent);
3982/// ```
3983#[derive(Debug, Default, derive_setters::Setters)]
3984#[setters(into)]
3985pub struct TitlebarOptions {
3986 /// The window's initial title. The web backend ignores it.
3987 pub title: Option<SharedString>,
3988
3989 /// Whether to hide the system titlebar so you can draw your own. macOS and Windows only.
3990 /// On Linux, use [`WindowOptions::window_decorations`] instead.
3991 pub appears_transparent: bool,
3992
3993 /// The position of the macOS traffic-light buttons, in logical pixels. macOS only.
3994 pub traffic_light_position: Option<Point<Pixels>>,
3995}
3996
3997impl FluentBuilder for TitlebarOptions {}
3998
3999/// The kind of window to create, set with [`WindowOptions::kind`].
4000///
4001/// On the web, only `Normal` works. The other kinds return an error from
4002/// [`App::open_window`].
4003#[derive(Clone, Debug, PartialEq, Eq)]
4004pub enum WindowKind {
4005 /// A normal application window
4006 Normal,
4007
4008 /// A window that appears above all other windows, usually used for alerts or popups.
4009 /// Use it sparingly.
4010 PopUp,
4011
4012 /// A parent-anchored, platform-native popup window for menus, comboboxes, context menus and
4013 /// tooltips. Unlike [`WindowKind::PopUp`], it is positioned relative to a parent window.
4014 ///
4015 /// The popup's size comes from [`WindowOptions::window_bounds`], whose origin is ignored.
4016 /// See [`popup::PopupOptions`] for the placement options. Platforms without a native
4017 /// implementation reject it with [`popup::PopupNotSupportedError`].
4018 AnchoredPopup(popup::PopupOptions),
4019
4020 /// A floating window that appears on top of its parent window.
4021 Floating,
4022
4023 /// A Wayland LayerShell window, used to draw overlays or backgrounds for applications such as
4024 /// docks, notifications or wallpapers.
4025 #[cfg(all(target_os = "linux", feature = "wayland"))]
4026 LayerShell(layer_shell::LayerShellOptions),
4027
4028 /// A window that appears on top of its parent window and blocks interaction with it
4029 /// until the modal window is closed.
4030 Dialog,
4031}
4032
4033/// The light or dark appearance of a window, as the operating system defines it.
4034///
4035/// Read it with [`Window::appearance`]. [`Platform::set_window_appearance`] can override the system's setting on macOS.
4036#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
4037pub enum WindowAppearance {
4038 /// A light appearance.
4039 ///
4040 /// On macOS, this corresponds to the `aqua` appearance.
4041 #[default]
4042 Light,
4043
4044 /// A light appearance with vibrant colors.
4045 ///
4046 /// On macOS, this corresponds to the `NSAppearanceNameVibrantLight` appearance.
4047 VibrantLight,
4048
4049 /// A dark appearance.
4050 ///
4051 /// On macOS, this corresponds to the `darkAqua` appearance.
4052 Dark,
4053
4054 /// A dark appearance with vibrant colors.
4055 ///
4056 /// On macOS, this corresponds to the `NSAppearanceNameVibrantDark` appearance.
4057 VibrantDark,
4058}
4059
4060/// How a window's background is drawn when there is no content or the content is transparent.
4061///
4062/// The variants a platform supports depend on the target. The Mica backdrop materials
4063/// are supported only on Windows 11, and are ignored elsewhere. Set the appearance
4064/// with the `macos_window_background`, `windows_window_background`,
4065/// `linux_window_background` or `web_window_background` field of [`WindowOptions`].
4066#[derive(Copy, Clone, Debug, Default, PartialEq)]
4067pub enum WindowBackgroundAppearance {
4068 /// Opaque.
4069 ///
4070 /// This lets the window manager know that content behind this
4071 /// window does not need to be drawn.
4072 ///
4073 /// Actual color depends on the system and themes should define a fully
4074 /// opaque background color instead.
4075 #[default]
4076 Opaque,
4077 /// Plain alpha transparency.
4078 Transparent,
4079 /// Transparency, but the contents behind the window are blurred.
4080 ///
4081 /// Not always supported.
4082 Blurred,
4083 /// The Mica backdrop material, supported on Windows 11.
4084 MicaBackdrop,
4085 /// The Mica Alt backdrop material, supported on Windows 11.
4086 MicaAltBackdrop,
4087}
4088
4089impl WindowBackgroundAppearance {
4090 /// Whether the window's background hides everything behind the window,
4091 /// letting the platform skip compositing the content it covers.
4092 pub fn is_opaque(&self) -> bool {
4093 matches!(self, Self::Opaque)
4094 }
4095
4096 /// Whether the window's background lets content behind the window show
4097 /// through, either directly, blurred, or via a system backdrop material.
4098 pub fn is_transparent(&self) -> bool {
4099 !self.is_opaque()
4100 }
4101}
4102
4103#[cfg(test)]
4104mod window_background_appearance_tests {
4105 use super::WindowBackgroundAppearance;
4106
4107 #[test]
4108 fn only_opaque_hides_the_content_behind_the_window() {
4109 assert!(WindowBackgroundAppearance::Opaque.is_opaque());
4110 assert!(!WindowBackgroundAppearance::Opaque.is_transparent());
4111
4112 for appearance in [
4113 WindowBackgroundAppearance::Transparent,
4114 WindowBackgroundAppearance::Blurred,
4115 WindowBackgroundAppearance::MicaBackdrop,
4116 WindowBackgroundAppearance::MicaAltBackdrop,
4117 ] {
4118 assert!(appearance.is_transparent());
4119 assert!(!appearance.is_opaque());
4120 }
4121 }
4122}
4123
4124/// The background appearance of a macOS window, set through
4125/// [`WindowOptions::macos_window_background`].
4126#[cfg(target_os = "macos")]
4127#[derive(Copy, Clone, Debug, Default, PartialEq)]
4128pub enum MacosWindowBackground {
4129 /// Hides everything behind the window. Themes should define a fully
4130 /// opaque background color instead of relying on the system's.
4131 #[default]
4132 Opaque,
4133 /// Plain alpha transparency.
4134 Transparent,
4135 /// Transparency with the contents behind the window blurred, via the
4136 /// system's vibrancy materials.
4137 Blurred,
4138}
4139
4140/// The background appearance of a Windows window, set through
4141/// [`WindowOptions::windows_window_background`].
4142#[cfg(target_os = "windows")]
4143#[derive(Copy, Clone, Debug, Default, PartialEq)]
4144pub enum WindowsWindowBackground {
4145 /// Hides everything behind the window. Themes should define a fully
4146 /// opaque background color instead of relying on the system's.
4147 #[default]
4148 Opaque,
4149 /// Plain alpha transparency.
4150 Transparent,
4151 /// Transparency with the contents behind the window blurred.
4152 Blurred,
4153 /// The Mica backdrop material, supported on Windows 11.
4154 MicaBackdrop,
4155 /// The Mica Alt backdrop material, supported on Windows 11.
4156 MicaAltBackdrop,
4157}
4158
4159/// The background appearance of a Linux window, set through
4160/// [`WindowOptions::linux_window_background`]. Whether blurring is honored
4161/// depends on the compositor; X11 treats it as plain transparency.
4162#[cfg(any(target_os = "linux", target_os = "freebsd"))]
4163#[derive(Copy, Clone, Debug, Default, PartialEq)]
4164pub enum LinuxWindowBackground {
4165 /// Hides everything behind the window. Themes should define a fully
4166 /// opaque background color instead of relying on the system's.
4167 #[default]
4168 Opaque,
4169 /// Plain alpha transparency.
4170 Transparent,
4171 /// Transparency with the contents behind the window blurred, when the
4172 /// compositor supports it.
4173 Blurred,
4174}
4175
4176/// The background appearance of a web window, set through
4177/// `web_window_background`. Web windows are always opaque at
4178/// the platform level.
4179#[cfg(target_family = "wasm")]
4180#[derive(Copy, Clone, Debug, Default, PartialEq)]
4181pub enum WebWindowBackground {
4182 /// The window background is always opaque.
4183 #[default]
4184 Opaque,
4185}
4186
4187#[cfg(target_os = "macos")]
4188impl From<MacosWindowBackground> for WindowBackgroundAppearance {
4189 fn from(background: MacosWindowBackground) -> Self {
4190 match background {
4191 MacosWindowBackground::Opaque => Self::Opaque,
4192 MacosWindowBackground::Transparent => Self::Transparent,
4193 MacosWindowBackground::Blurred => Self::Blurred,
4194 }
4195 }
4196}
4197
4198#[cfg(target_os = "windows")]
4199impl From<WindowsWindowBackground> for WindowBackgroundAppearance {
4200 fn from(background: WindowsWindowBackground) -> Self {
4201 match background {
4202 WindowsWindowBackground::Opaque => Self::Opaque,
4203 WindowsWindowBackground::Transparent => Self::Transparent,
4204 WindowsWindowBackground::Blurred => Self::Blurred,
4205 WindowsWindowBackground::MicaBackdrop => Self::MicaBackdrop,
4206 WindowsWindowBackground::MicaAltBackdrop => Self::MicaAltBackdrop,
4207 }
4208 }
4209}
4210
4211#[cfg(any(target_os = "linux", target_os = "freebsd"))]
4212impl From<LinuxWindowBackground> for WindowBackgroundAppearance {
4213 fn from(background: LinuxWindowBackground) -> Self {
4214 match background {
4215 LinuxWindowBackground::Opaque => Self::Opaque,
4216 LinuxWindowBackground::Transparent => Self::Transparent,
4217 LinuxWindowBackground::Blurred => Self::Blurred,
4218 }
4219 }
4220}
4221
4222#[cfg(target_family = "wasm")]
4223impl From<WebWindowBackground> for WindowBackgroundAppearance {
4224 fn from(background: WebWindowBackground) -> Self {
4225 match background {
4226 WebWindowBackground::Opaque => Self::Opaque,
4227 }
4228 }
4229}
4230
4231#[cfg(test)]
4232mod platform_window_background_tests {
4233 use super::WindowBackgroundAppearance;
4234
4235 #[cfg(target_os = "macos")]
4236 #[test]
4237 fn macos_backgrounds_map_to_appearances() {
4238 use super::MacosWindowBackground;
4239 assert_eq!(
4240 WindowBackgroundAppearance::from(MacosWindowBackground::Opaque),
4241 WindowBackgroundAppearance::Opaque
4242 );
4243 assert_eq!(
4244 WindowBackgroundAppearance::from(MacosWindowBackground::Blurred),
4245 WindowBackgroundAppearance::Blurred
4246 );
4247 }
4248
4249 #[cfg(target_os = "windows")]
4250 #[test]
4251 fn windows_backgrounds_map_to_appearances() {
4252 use super::WindowsWindowBackground;
4253 assert_eq!(
4254 WindowBackgroundAppearance::from(WindowsWindowBackground::MicaAltBackdrop),
4255 WindowBackgroundAppearance::MicaAltBackdrop
4256 );
4257 assert_eq!(
4258 WindowBackgroundAppearance::from(WindowsWindowBackground::default()),
4259 WindowBackgroundAppearance::Opaque
4260 );
4261 }
4262
4263 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
4264 #[test]
4265 fn linux_backgrounds_map_to_appearances() {
4266 use super::LinuxWindowBackground;
4267 assert_eq!(
4268 WindowBackgroundAppearance::from(LinuxWindowBackground::Transparent),
4269 WindowBackgroundAppearance::Transparent
4270 );
4271 }
4272
4273 #[cfg(target_family = "wasm")]
4274 #[test]
4275 fn web_backgrounds_map_to_appearances() {
4276 use super::WebWindowBackground;
4277 assert_eq!(
4278 WindowBackgroundAppearance::from(WebWindowBackground::Opaque),
4279 WindowBackgroundAppearance::Opaque
4280 );
4281 }
4282}
4283
4284/// The text rendering mode for glyphs.
4285///
4286/// [`PlatformTextSystem::recommended_rendering_mode`] returns the platform's choice.
4287#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
4288pub enum TextRenderingMode {
4289 /// Use the platform's default text rendering mode.
4290 #[default]
4291 PlatformDefault,
4292 /// Use subpixel (ClearType-style) text rendering.
4293 Subpixel,
4294 /// Use grayscale text rendering.
4295 Grayscale,
4296}
4297
4298/// Options for [`Platform::prompt_for_paths`], which shows a file or folder picker.
4299#[derive(Clone, Debug)]
4300pub struct PathPromptOptions {
4301 /// Whether the user can pick files.
4302 pub files: bool,
4303 /// Whether the user can pick directories.
4304 pub directories: bool,
4305 /// Whether the user can pick more than one item.
4306 pub multiple: bool,
4307 /// Text that explains the request in the picker, on platforms that display it.
4308 pub prompt: Option<SharedString>,
4309}
4310
4311/// How serious a prompt is. The platform uses it to choose the alert's icon or style.
4312#[derive(Copy, Clone, Debug, PartialEq)]
4313pub enum PromptLevel {
4314 /// A prompt that is shown when the user should be notified of something
4315 Info,
4316
4317 /// A prompt that is shown when the user needs to be warned of a potential problem
4318 Warning,
4319
4320 /// A prompt that is shown when a critical problem has occurred
4321 Critical,
4322}
4323
4324/// A button on a prompt shown with [`PlatformWindow::prompt`].
4325///
4326/// The variant tells the platform the button's role, such as the default or cancel action.
4327/// Use [`PromptButton::from`] to get a role from a label: `"ok"` and `"cancel"`,
4328/// in any case, become `Ok` and `Cancel`, and any other text becomes `Other`.
4329///
4330/// ```
4331/// use gpui::PromptButton;
4332///
4333/// assert_eq!(PromptButton::from("ok"), PromptButton::Ok("OK".into()));
4334/// assert_eq!(PromptButton::from("Delete"), PromptButton::Other("Delete".into()));
4335/// ```
4336#[derive(Clone, Debug, PartialEq)]
4337pub enum PromptButton {
4338 /// Ok button
4339 Ok(SharedString),
4340 /// Cancel button
4341 Cancel(SharedString),
4342 /// Other button
4343 Other(SharedString),
4344}
4345
4346impl PromptButton {
4347 /// Creates a button with `label` and no special role.
4348 pub fn new(label: impl Into<SharedString>) -> Self {
4349 PromptButton::Other(label.into())
4350 }
4351
4352 /// Creates a button with the [`PromptButton::Ok`] role.
4353 pub fn ok(label: impl Into<SharedString>) -> Self {
4354 PromptButton::Ok(label.into())
4355 }
4356
4357 /// Creates a button with the [`PromptButton::Cancel`] role.
4358 pub fn cancel(label: impl Into<SharedString>) -> Self {
4359 PromptButton::Cancel(label.into())
4360 }
4361
4362 /// Returns `true` if this is the cancel button.
4363 #[allow(dead_code)]
4364 pub fn is_cancel(&self) -> bool {
4365 matches!(self, PromptButton::Cancel(_))
4366 }
4367
4368 /// Returns the button's label.
4369 pub fn label(&self) -> &SharedString {
4370 match self {
4371 PromptButton::Ok(label) => label,
4372 PromptButton::Cancel(label) => label,
4373 PromptButton::Other(label) => label,
4374 }
4375 }
4376}
4377
4378impl From<&str> for PromptButton {
4379 fn from(value: &str) -> Self {
4380 match value.to_lowercase().as_str() {
4381 "ok" => PromptButton::Ok("OK".into()),
4382 "cancel" => PromptButton::Cancel("Cancel".into()),
4383 _ => PromptButton::Other(SharedString::from(value.to_owned())),
4384 }
4385 }
4386}
4387
4388/// The shape of the mouse cursor. Each variant names the CSS cursor it maps to on the web.
4389#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
4390pub enum CursorStyle {
4391 /// The default cursor, corresponds to the CSS cursor value `default`
4392 #[default]
4393 Arrow,
4394
4395 /// A text input cursor
4396 /// corresponds to the CSS cursor value `text`
4397 IBeam,
4398
4399 /// A crosshair cursor
4400 /// corresponds to the CSS cursor value `crosshair`
4401 Crosshair,
4402
4403 /// A closed hand cursor
4404 /// corresponds to the CSS cursor value `grabbing`
4405 ClosedHand,
4406
4407 /// An open hand cursor
4408 /// corresponds to the CSS cursor value `grab`
4409 OpenHand,
4410
4411 /// A pointing hand cursor
4412 /// corresponds to the CSS cursor value `pointer`
4413 PointingHand,
4414
4415 /// A resize left cursor
4416 /// corresponds to the CSS cursor value `ew-resize`
4417 ResizeLeft,
4418
4419 /// A resize right cursor
4420 /// corresponds to the CSS cursor value `ew-resize`
4421 ResizeRight,
4422
4423 /// A resize cursor to the left and right
4424 /// corresponds to the CSS cursor value `ew-resize`
4425 ResizeLeftRight,
4426
4427 /// A resize up cursor
4428 /// corresponds to the CSS cursor value `ns-resize`
4429 ResizeUp,
4430
4431 /// A resize down cursor
4432 /// corresponds to the CSS cursor value `ns-resize`
4433 ResizeDown,
4434
4435 /// A resize cursor directing up and down
4436 /// corresponds to the CSS cursor value `ns-resize`
4437 ResizeUpDown,
4438
4439 /// A cursor for the upper-left resize edge (`nw-resize`).
4440 ResizeUpLeft,
4441
4442 /// A cursor for the upper-right resize edge (`ne-resize`).
4443 ResizeUpRight,
4444
4445 /// A cursor for the lower-left resize edge (`sw-resize`).
4446 ResizeDownLeft,
4447
4448 /// A cursor for the lower-right resize edge (`se-resize`).
4449 ResizeDownRight,
4450
4451 /// A resize cursor directing up-left and down-right
4452 /// corresponds to the CSS cursor value `nwse-resize`
4453 ResizeUpLeftDownRight,
4454
4455 /// A resize cursor directing up-right and down-left
4456 /// corresponds to the CSS cursor value `nesw-resize`
4457 ResizeUpRightDownLeft,
4458
4459 /// A cursor indicating that the item/column can be resized horizontally.
4460 /// corresponds to the CSS cursor value `col-resize`
4461 ResizeColumn,
4462
4463 /// A cursor indicating that the item/row can be resized vertically.
4464 /// corresponds to the CSS cursor value `row-resize`
4465 ResizeRow,
4466
4467 /// A text input cursor for vertical layout
4468 /// corresponds to the CSS cursor value `vertical-text`
4469 IBeamCursorForVerticalLayout,
4470
4471 /// A cursor indicating that the operation is not allowed
4472 /// corresponds to the CSS cursor value `not-allowed`
4473 OperationNotAllowed,
4474
4475 /// A cursor indicating that the operation will result in a link
4476 /// corresponds to the CSS cursor value `alias`
4477 DragLink,
4478
4479 /// A cursor indicating that the operation will result in a copy
4480 /// corresponds to the CSS cursor value `copy`
4481 DragCopy,
4482
4483 /// A cursor indicating that the operation will result in a context menu
4484 /// corresponds to the CSS cursor value `context-menu`
4485 ContextualMenu,
4486}
4487
4488/// Something to put on the clipboard, or something read from it.
4489///
4490/// An item holds one or more [`ClipboardEntry`] values, such as text, an image or file
4491/// paths. Use [`App::write_to_clipboard`] to copy an item, and
4492/// [`App::read_from_clipboard`] to read one.
4493///
4494/// # Examples
4495///
4496/// ```
4497/// use gpui::{ClipboardEntry, ClipboardItem};
4498///
4499/// let item = ClipboardItem::new_string("hello".to_string());
4500/// assert_eq!(item.text().as_deref(), Some("hello"));
4501/// assert_eq!(item.metadata(), None);
4502///
4503/// let item = ClipboardItem::new_string_with_json_metadata("1, 2, 3".to_string(), vec![1, 2, 3]);
4504/// assert_eq!(item.metadata().map(String::as_str), Some("[1,2,3]"));
4505///
4506/// let ClipboardEntry::String(entry) = &item.entries()[0] else {
4507/// panic!("expected a string entry");
4508/// };
4509/// assert_eq!(entry.metadata_json::<Vec<i32>>(), Some(vec![1, 2, 3]));
4510/// ```
4511#[derive(Clone, Debug, Eq, PartialEq)]
4512pub struct ClipboardItem {
4513 /// The entries in this clipboard item.
4514 pub entries: Vec<ClipboardEntry>,
4515}
4516
4517/// An error from [`Platform::read_from_clipboard_async`]. Each variant calls for different guidance to the user.
4518#[derive(Clone, Debug, PartialEq, Eq)]
4519pub enum ClipboardReadError {
4520 /// The platform clipboard is not available in this context, e.g. the
4521 /// browser does not expose the async clipboard API or the page is not a
4522 /// secure context.
4523 Unavailable,
4524 /// The platform refused access, e.g. the user declined the browser's
4525 /// clipboard permission prompt or paste confirmation.
4526 Denied(String),
4527 /// The clipboard contents could not be converted into a
4528 /// [`ClipboardItem`].
4529 UnsupportedContent,
4530}
4531
4532impl std::fmt::Display for ClipboardReadError {
4533 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4534 match self {
4535 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
4536 Self::Denied(message) => {
4537 write!(formatter, "clipboard access was denied: {message}")
4538 }
4539 Self::UnsupportedContent => {
4540 formatter.write_str("the clipboard contents are unsupported")
4541 }
4542 }
4543 }
4544}
4545
4546impl std::error::Error for ClipboardReadError {}
4547
4548/// One part of a [`ClipboardItem`]: a string, an image or a list of file paths.
4549#[derive(Clone, Debug, Eq, PartialEq)]
4550pub enum ClipboardEntry {
4551 /// A string entry
4552 String(ClipboardString),
4553 /// An image entry
4554 Image(Image),
4555 /// A file entry
4556 ExternalPaths(crate::ExternalPaths),
4557}
4558
4559impl ClipboardItem {
4560 /// Creates an item holding `text`, with no metadata.
4561 pub fn new_string(text: String) -> Self {
4562 Self {
4563 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
4564 }
4565 }
4566
4567 /// Creates an item holding `text` and a raw `metadata` string.
4568 ///
4569 /// Use [`ClipboardItem::new_string_with_json_metadata`] to store structured data.
4570 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
4571 Self {
4572 entries: vec![ClipboardEntry::String(ClipboardString {
4573 text,
4574 metadata: Some(metadata),
4575 })],
4576 }
4577 }
4578
4579 /// Creates an item holding `text` and `metadata`, serialized as JSON.
4580 ///
4581 /// Read the metadata back with [`ClipboardString::metadata_json`].
4582 ///
4583 /// # Panics
4584 ///
4585 /// Panics if `metadata` can't be serialized to JSON, such as a map with non-string keys.
4586 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
4587 Self {
4588 entries: vec![ClipboardEntry::String(
4589 ClipboardString::new(text).with_json_metadata(metadata),
4590 )],
4591 }
4592 }
4593
4594 /// Creates an item holding a copy of `image`.
4595 pub fn new_image(image: &Image) -> Self {
4596 Self {
4597 entries: vec![ClipboardEntry::Image(image.clone())],
4598 }
4599 }
4600
4601 /// Returns the text of all string entries joined together, or `None` if the item has no text.
4602 ///
4603 /// If the string entries contain no text, the file paths of any [`ClipboardEntry::ExternalPaths`]
4604 /// entries are joined with no separator instead. The result is `None` if there are no paths either.
4605 pub fn text(&self) -> Option<String> {
4606 let mut answer = String::new();
4607
4608 for entry in self.entries.iter() {
4609 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
4610 answer.push_str(text);
4611 }
4612 }
4613
4614 if answer.is_empty() {
4615 for entry in self.entries.iter() {
4616 if let ClipboardEntry::ExternalPaths(paths) = entry {
4617 for path in &paths.0 {
4618 use std::fmt::Write as _;
4619 _ = write!(answer, "{}", path.display());
4620 }
4621 }
4622 }
4623 }
4624
4625 if !answer.is_empty() {
4626 Some(answer)
4627 } else {
4628 None
4629 }
4630 }
4631
4632 /// Returns the metadata of the item's string entry, or `None`.
4633 ///
4634 /// Returns `None` unless the item has exactly one entry and that entry is a string.
4635 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
4636 pub fn metadata(&self) -> Option<&String> {
4637 match self.entries().first() {
4638 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
4639 clipboard_string.metadata.as_ref()
4640 }
4641 _ => None,
4642 }
4643 }
4644
4645 /// Returns the item's entries.
4646 pub fn entries(&self) -> &[ClipboardEntry] {
4647 &self.entries
4648 }
4649
4650 /// Consumes the item and returns its entries.
4651 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
4652 self.entries.into_iter()
4653 }
4654}
4655
4656impl From<ClipboardString> for ClipboardEntry {
4657 fn from(value: ClipboardString) -> Self {
4658 Self::String(value)
4659 }
4660}
4661
4662impl From<String> for ClipboardEntry {
4663 fn from(value: String) -> Self {
4664 Self::from(ClipboardString::from(value))
4665 }
4666}
4667
4668impl From<Image> for ClipboardEntry {
4669 fn from(value: Image) -> Self {
4670 Self::Image(value)
4671 }
4672}
4673
4674impl From<ClipboardEntry> for ClipboardItem {
4675 fn from(value: ClipboardEntry) -> Self {
4676 Self {
4677 entries: vec![value],
4678 }
4679 }
4680}
4681
4682impl From<String> for ClipboardItem {
4683 fn from(value: String) -> Self {
4684 Self::from(ClipboardEntry::from(value))
4685 }
4686}
4687
4688impl From<Image> for ClipboardItem {
4689 fn from(value: Image) -> Self {
4690 Self::from(ClipboardEntry::from(value))
4691 }
4692}
4693
4694/// An image format GPUI recognizes in clipboard data and in [`Image`].
4695#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
4696pub enum ImageFormat {
4697 // Sorted from most to least likely to be pasted into an editor,
4698 // which matters when we iterate through them trying to see if
4699 // clipboard content matches them.
4700 /// .png
4701 Png,
4702 /// .jpeg or .jpg
4703 Jpeg,
4704 /// .webp
4705 Webp,
4706 /// .gif
4707 Gif,
4708 /// .svg
4709 Svg,
4710 /// .bmp
4711 Bmp,
4712 /// .tif or .tiff
4713 Tiff,
4714 /// .ico
4715 Ico,
4716 /// Netpbm image formats (.pbm, .ppm, .pgm).
4717 Pnm,
4718}
4719
4720impl ImageFormat {
4721 /// Returns the canonical MIME type, such as `image/png`.
4722 pub const fn mime_type(self) -> &'static str {
4723 match self {
4724 ImageFormat::Png => "image/png",
4725 ImageFormat::Jpeg => "image/jpeg",
4726 ImageFormat::Webp => "image/webp",
4727 ImageFormat::Gif => "image/gif",
4728 ImageFormat::Svg => "image/svg+xml",
4729 ImageFormat::Bmp => "image/bmp",
4730 ImageFormat::Tiff => "image/tiff",
4731 ImageFormat::Ico => "image/ico",
4732 ImageFormat::Pnm => "image/x-portable-anymap",
4733 }
4734 }
4735
4736 /// Returns the file extension for this image format (without leading dot).
4737 pub const fn extension(self) -> &'static str {
4738 match self {
4739 ImageFormat::Png => "png",
4740 ImageFormat::Jpeg => "jpg",
4741 ImageFormat::Webp => "webp",
4742 ImageFormat::Gif => "gif",
4743 ImageFormat::Svg => "svg",
4744 ImageFormat::Bmp => "bmp",
4745 ImageFormat::Tiff => "tiff",
4746 ImageFormat::Ico => "ico",
4747 ImageFormat::Pnm => "pnm",
4748 }
4749 }
4750
4751 /// Returns the format for `mime_type`, or `None` if the type is unknown.
4752 ///
4753 /// The aliases `image/jpg` and `image/tif` map to [`ImageFormat::Jpeg`] and [`ImageFormat::Tiff`].
4754 ///
4755 /// ```
4756 /// use gpui::ImageFormat;
4757 ///
4758 /// assert_eq!(ImageFormat::from_mime_type("image/jpg"), Some(ImageFormat::Jpeg));
4759 /// assert_eq!(ImageFormat::Png.extension(), "png");
4760 /// assert_eq!(ImageFormat::from_mime_type("text/plain"), None);
4761 /// ```
4762 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
4763 use strum::IntoEnumIterator;
4764 Self::iter()
4765 .find(|format| format.mime_type() == mime_type)
4766 .or_else(|| Self::from_mime_type_alias(mime_type))
4767 }
4768
4769 /// Non-canonical mime types that some producers use in the wild.
4770 /// Unlike `mime_type()` which returns the single canonical form,
4771 /// these are legacy or shortened variants we still need to recognize.
4772 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
4773 match mime_type {
4774 "image/jpg" => Some(Self::Jpeg),
4775 "image/tif" => Some(Self::Tiff),
4776 _ => None,
4777 }
4778 }
4779}
4780
4781/// Encoded image data and the format it is encoded in.
4782///
4783/// Use [`Image::from_bytes`] to create one. GPUI decodes it for display through
4784/// [`Image::to_image_data`] or the asset APIs.
4785#[derive(Clone, Debug, PartialEq, Eq)]
4786pub struct Image {
4787 /// The format the bytes are encoded in, such as PNG.
4788 pub format: ImageFormat,
4789 /// The encoded image bytes.
4790 pub bytes: Vec<u8>,
4791 /// A key for the image in GPUI's asset cache. [`Image::from_bytes`] sets it to a hash of `bytes`.
4792 pub id: u64,
4793}
4794
4795pub(crate) fn decode_static_image(
4796 bytes: &[u8],
4797 format: image::ImageFormat,
4798) -> Result<SmallVec<[Frame; 1]>> {
4799 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
4800 .into_decoder()
4801 .context("creating image decoder")?;
4802 decode_static_image_from_decoder(decoder)
4803}
4804
4805pub(crate) fn decode_static_image_from_decoder(
4806 mut decoder: impl image::ImageDecoder,
4807) -> Result<SmallVec<[Frame; 1]>> {
4808 let orientation = decoder
4809 .orientation()
4810 .context("reading decoder's orientation")?;
4811 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
4812 image.apply_orientation(orientation);
4813
4814 let mut data = image.into_rgba8();
4815 for pixel in data.chunks_exact_mut(4) {
4816 pixel.swap(0, 2);
4817 }
4818
4819 Ok(SmallVec::from_elem(Frame::new(data), 1))
4820}
4821
4822impl Hash for Image {
4823 fn hash<H: Hasher>(&self, state: &mut H) {
4824 state.write_u64(self.id);
4825 }
4826}
4827
4828impl Image {
4829 /// Returns a PNG image with no bytes. It can't be decoded.
4830 pub fn empty() -> Self {
4831 Self::from_bytes(ImageFormat::Png, Vec::new())
4832 }
4833
4834 /// Creates an image from `format` and encoded `bytes`.
4835 ///
4836 /// The [`Image::id`] is a hash of `bytes` alone, so two images with the same bytes but different formats share an id.
4837 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
4838 Self {
4839 id: hash(&bytes),
4840 format,
4841 bytes,
4842 }
4843 }
4844
4845 /// Returns the image's cache key. [`Image::from_bytes`] sets it from a hash of the bytes.
4846 pub fn id(&self) -> u64 {
4847 self.id
4848 }
4849
4850 /// Returns the decoded image, starting the load if it isn't cached yet.
4851 ///
4852 /// Returns `None` while the image is loading, or if it can't be decoded. Call it from a render method, so the window redraws when the load finishes.
4853 pub fn use_render_image(
4854 self: Arc<Self>,
4855 window: &mut Window,
4856 cx: &mut App,
4857 ) -> Option<Arc<RenderImage>> {
4858 ImageSource::Image(self)
4859 .use_data(None, window, cx)
4860 .and_then(|result| result.ok())
4861 }
4862
4863 /// Returns the decoded image if it's already cached. Doesn't start a load.
4864 pub fn get_render_image(
4865 self: Arc<Self>,
4866 window: &mut Window,
4867 cx: &mut App,
4868 ) -> Option<Arc<RenderImage>> {
4869 ImageSource::Image(self)
4870 .get_data(None, window, cx)
4871 .and_then(|result| result.ok())
4872 }
4873
4874 /// Removes the decoded image from GPUI's asset cache.
4875 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
4876 ImageSource::Image(self).remove_asset(cx);
4877 }
4878
4879 /// Check whether this image is present in GPUI's asset cache (loading or
4880 /// loaded), without fetching it.
4881 #[cfg(any(test, feature = "test-support"))]
4882 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
4883 ImageSource::Image(self.clone()).is_asset_cached(cx)
4884 }
4885
4886 /// Decodes the image into a [`RenderImage`] that you can paint.
4887 ///
4888 /// SVG images are rendered at scale `1.0`.
4889 ///
4890 /// # Errors
4891 ///
4892 /// Returns an error if the bytes can't be decoded. For a GIF, frames that fail to
4893 /// decode are skipped and logged, and an error is returned only when every frame fails.
4894 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
4895 let frames = match self.format {
4896 ImageFormat::Gif => {
4897 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
4898 let mut frames = SmallVec::new();
4899
4900 for frame in decoder.into_frames() {
4901 match frame {
4902 Ok(mut frame) => {
4903 // Convert from RGBA to BGRA.
4904 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
4905 pixel.swap(0, 2);
4906 }
4907 frames.push(frame);
4908 }
4909 Err(err) => {
4910 log::debug!("Skipping GIF frame due to decode error: {err}");
4911 }
4912 }
4913 }
4914
4915 if frames.is_empty() {
4916 anyhow::bail!("GIF could not be decoded: all frames failed");
4917 }
4918
4919 frames
4920 }
4921 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
4922 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
4923 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
4924 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
4925 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
4926 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
4927 ImageFormat::Svg => {
4928 return svg_renderer
4929 .render_single_frame(&self.bytes, 1.0)
4930 .map_err(Into::into);
4931 }
4932 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
4933 };
4934
4935 Ok(Arc::new(RenderImage::new(frames)))
4936 }
4937
4938 /// Returns the image's format.
4939 pub fn format(&self) -> ImageFormat {
4940 self.format
4941 }
4942
4943 /// Returns the encoded image bytes.
4944 pub fn bytes(&self) -> &[u8] {
4945 self.bytes.as_slice()
4946 }
4947}
4948
4949/// A string on the clipboard, with optional metadata.
4950///
4951/// Metadata lets an application record more about copied text, such as where it came
4952/// from. The metadata is stored with the text and read back with [`ClipboardString::metadata_json`].
4953#[derive(Clone, Debug, Eq, PartialEq)]
4954pub struct ClipboardString {
4955 /// The text content.
4956 pub text: String,
4957 /// Optional metadata associated with this clipboard string.
4958 pub metadata: Option<String>,
4959}
4960
4961impl ClipboardString {
4962 /// Creates a clipboard string with `text` and no metadata.
4963 pub fn new(text: String) -> Self {
4964 Self {
4965 text,
4966 metadata: None,
4967 }
4968 }
4969
4970 /// Sets the metadata to `metadata`, serialized as JSON. Returns the updated string.
4971 ///
4972 /// # Panics
4973 ///
4974 /// Panics if `metadata` can't be serialized to JSON.
4975 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
4976 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
4977 self
4978 }
4979
4980 /// Returns the text.
4981 pub fn text(&self) -> &String {
4982 &self.text
4983 }
4984
4985 /// Consumes the string and returns its text.
4986 pub fn into_text(self) -> String {
4987 self.text
4988 }
4989
4990 /// Returns the metadata deserialized as `T`.
4991 ///
4992 /// Returns `None` if there is no metadata or it doesn't deserialize as `T`.
4993 pub fn metadata_json<T>(&self) -> Option<T>
4994 where
4995 T: for<'a> Deserialize<'a>,
4996 {
4997 self.metadata
4998 .as_ref()
4999 .and_then(|m| serde_json::from_str(m).ok())
5000 }
5001
5002 /// Returns a hash of `text`. Use it to detect whether the clipboard text has changed.
5003 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
5004 pub fn text_hash(text: &str) -> u64 {
5005 let mut hasher = SeaHasher::new();
5006 text.hash(&mut hasher);
5007 hasher.finish()
5008 }
5009}
5010
5011impl From<String> for ClipboardString {
5012 fn from(value: String) -> Self {
5013 Self {
5014 text: value,
5015 metadata: None,
5016 }
5017 }
5018}
5019
5020#[cfg(test)]
5021mod ime_candidate_bounds_tests {
5022 use super::*;
5023 use crate::size;
5024
5025 // Offsets 0..3 sit on the first visual line and the rest wrap onto the second.
5026 fn bounds_at(offset: usize) -> Bounds<Pixels> {
5027 let line = if offset < 3 { 0. } else { 20. };
5028 Bounds::new(point(px(offset as f32), px(line)), size(px(1.), px(20.)))
5029 }
5030
5031 fn candidate_bounds(
5032 marked_range: Option<Range<usize>>,
5033 selection: UTF16Selection,
5034 ) -> Option<Bounds<Pixels>> {
5035 PlatformInputHandler::compute_ime_candidate_bounds(marked_range, &selection, |range| {
5036 Some(bounds_at(range.start))
5037 })
5038 }
5039
5040 #[test]
5041 fn composition_follows_the_active_end_of_a_reversed_selection() {
5042 // The caret is at offset 2 on the first line, so the candidate window
5043 // stays at the start of the composition instead of the wrapped line.
5044 let bounds = candidate_bounds(
5045 Some(0..8),
5046 UTF16Selection {
5047 range: 2..7,
5048 reversed: true,
5049 },
5050 );
5051 assert_eq!(bounds, Some(bounds_at(0)));
5052 }
5053
5054 #[test]
5055 fn composition_uses_the_start_of_the_caret_line() {
5056 let bounds = candidate_bounds(
5057 Some(0..8),
5058 UTF16Selection {
5059 range: 2..7,
5060 reversed: false,
5061 },
5062 );
5063 assert_eq!(bounds, Some(bounds_at(3)));
5064 }
5065
5066 #[test]
5067 fn without_composition_uses_the_active_end() {
5068 let bounds = candidate_bounds(
5069 None,
5070 UTF16Selection {
5071 range: 2..7,
5072 reversed: true,
5073 },
5074 );
5075 assert_eq!(bounds, Some(bounds_at(2)));
5076 }
5077}
5078
5079#[cfg(test)]
5080mod image_tests {
5081 use crate::{AssetRegistry, size};
5082
5083 use super::*;
5084 use std::sync::Arc;
5085
5086 #[test]
5087 fn test_image_to_image_data_applies_exif_orientation() {
5088 let image = Image::from_bytes(
5089 ImageFormat::Jpeg,
5090 include_bytes!("../tests/fixtures/exif-orientation-rotate-180.jpg").to_vec(),
5091 );
5092
5093 let render_image = image
5094 .to_image_data(SvgRenderer::new(Arc::new(AssetRegistry::default())))
5095 .unwrap();
5096
5097 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
5098
5099 let bytes = render_image.as_bytes(0).unwrap();
5100 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
5101 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
5102 }
5103
5104 #[test]
5105 fn test_svg_image_to_image_data_converts_to_bgra() {
5106 let image = Image::from_bytes(
5107 ImageFormat::Svg,
5108 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
5109<rect width="1" height="1" fill="#38BDF8"/>
5110</svg>"##
5111 .to_vec(),
5112 );
5113
5114 let render_image = image
5115 .to_image_data(SvgRenderer::new(Arc::new(AssetRegistry::default())))
5116 .unwrap();
5117 let bytes = render_image.as_bytes(0).unwrap();
5118
5119 for pixel in bytes.chunks_exact(4) {
5120 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
5121 }
5122 }
5123}
5124
5125#[cfg(test)]
5126mod atlas_tests {
5127 use super::*;
5128
5129 const TILE_SIZE: Size<DevicePixels> = Size {
5130 width: DevicePixels(1),
5131 height: DevicePixels(1),
5132 };
5133
5134 #[derive(Default)]
5135 struct RecordingAtlasBackend {
5136 insert_calls: u32,
5137 fail_next_insert: bool,
5138 removed_tiles: Vec<AtlasTile>,
5139 }
5140
5141 impl AtlasBackend for RecordingAtlasBackend {
5142 fn insert(
5143 &mut self,
5144 kind: AtlasTextureKind,
5145 size: Size<DevicePixels>,
5146 _bytes: &[u8],
5147 ) -> Result<AtlasTile> {
5148 self.insert_calls += 1;
5149 if std::mem::take(&mut self.fail_next_insert) {
5150 anyhow::bail!("backend failed");
5151 }
5152 Ok(AtlasTile {
5153 texture_id: AtlasTextureId { index: 0, kind },
5154 tile_id: TileId(self.insert_calls),
5155 padding: 0,
5156 bounds: Bounds {
5157 origin: Point::default(),
5158 size,
5159 },
5160 })
5161 }
5162
5163 fn remove(&mut self, tile: AtlasTile) {
5164 self.removed_tiles.push(tile);
5165 }
5166 }
5167
5168 fn image_key(image_id: usize) -> AtlasKey {
5169 AtlasKey::Image(RenderImageParams {
5170 image_id: crate::ImageId(image_id),
5171 frame_index: 0,
5172 })
5173 }
5174
5175 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
5176 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
5177 }
5178
5179 #[test]
5180 fn only_successful_inserts_are_cached() -> Result<()> {
5181 let mut state = AtlasState::new(RecordingAtlasBackend::default());
5182 let key = image_key(1);
5183
5184 assert_eq!(
5185 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
5186 None
5187 );
5188 state
5189 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
5190 .expect_err("builder error should propagate");
5191 assert!(!state.contains(&key));
5192 assert_eq!(state.backend.insert_calls, 0);
5193
5194 state.backend.fail_next_insert = true;
5195 state
5196 .get_or_insert_with(key.clone(), &mut build_tile)
5197 .expect_err("backend error should propagate");
5198 assert!(!state.contains(&key));
5199 assert_eq!(state.backend.insert_calls, 1);
5200
5201 let tile = state
5202 .get_or_insert_with(key.clone(), &mut build_tile)?
5203 .context("builder should produce a tile")?;
5204 assert_eq!(tile.texture_id.kind, key.texture_kind());
5205 assert_eq!(
5206 state.get_or_insert_with(key.clone(), &mut || {
5207 anyhow::bail!("cache hit must not call the builder")
5208 })?,
5209 Some(tile)
5210 );
5211 assert!(state.contains(&key));
5212 assert_eq!(state.backend.insert_calls, 2);
5213 Ok(())
5214 }
5215
5216 #[test]
5217 fn remove_and_clear_invalidate_keys() -> Result<()> {
5218 let mut state = AtlasState::new(RecordingAtlasBackend::default());
5219 let key = image_key(1);
5220 let other_key = image_key(2);
5221 let tile = state
5222 .get_or_insert_with(key.clone(), &mut build_tile)?
5223 .context("builder should produce a tile")?;
5224 state
5225 .get_or_insert_with(other_key.clone(), &mut build_tile)?
5226 .context("builder should produce another tile")?;
5227
5228 state.remove(&key);
5229 state.remove(&key);
5230 assert!(!state.contains(&key));
5231 assert!(state.contains(&other_key));
5232 assert_eq!(state.backend.removed_tiles, vec![tile]);
5233
5234 let mut reset_calls = 0;
5235 state.clear(|_| reset_calls += 1);
5236 assert_eq!(reset_calls, 1);
5237 assert!(!state.contains(&other_key));
5238 assert_eq!(state.backend.removed_tiles, vec![tile]);
5239 Ok(())
5240 }
5241}
5242
5243#[cfg(test)]
5244mod frame_signal_tests {
5245 use super::*;
5246
5247 #[cfg(feature = "profiler")]
5248 #[test]
5249 fn coalesced_signals_retain_the_earliest_until_drained() {
5250 for (first_source, later_source) in [
5251 (
5252 FrameRequestSource::NativeCallback,
5253 FrameRequestSource::LocalSchedule,
5254 ),
5255 (
5256 FrameRequestSource::LocalSchedule,
5257 FrameRequestSource::NativeCallback,
5258 ),
5259 ] {
5260 let signal = PlatformFrameSignal::new();
5261 let first =
5262 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
5263 assert_eq!(signal.take(), None);
5264 signal.record(first + Duration::from_millis(16), later_source);
5265 signal.record(first, first_source);
5266 signal.record(first + Duration::from_millis(32), later_source);
5267 assert_eq!(signal.take(), Some((first, first_source)));
5268 assert_eq!(signal.take(), None);
5269 let next = first + Duration::from_millis(48);
5270 signal.record(next, later_source);
5271 assert_eq!(signal.take(), Some((next, later_source)));
5272 }
5273 }
5274
5275 #[cfg(not(feature = "profiler"))]
5276 #[test]
5277 fn disabled_profiling_does_not_capture_or_store_timestamps() {
5278 let captured = PlatformFrameSignal::capture(|| {
5279 panic!("profiler-disabled builds must not evaluate the capture closure");
5280 });
5281 assert_eq!(captured, None);
5282 let signal = PlatformFrameSignal::new();
5283 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
5284 assert_eq!(signal.take(), None);
5285 }
5286}
5287
5288#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
5289mod window_button_layout_tests {
5290 use super::*;
5291 use std::collections::HashSet;
5292
5293 #[test]
5294 fn test_refresh_interval_from_hz() {
5295 assert_eq!(
5296 refresh_interval_from_hz(50.0),
5297 Some(Duration::from_millis(20))
5298 );
5299 assert_eq!(
5300 refresh_interval_from_hz(120.0),
5301 Some(Duration::from_secs(1) / 120)
5302 );
5303 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
5304 assert_eq!(refresh_interval_from_hz(unknown), None);
5305 }
5306 }
5307
5308 #[test]
5309 fn test_window_button_layout_parse_standard() {
5310 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
5311 assert_eq!(
5312 layout.left,
5313 [
5314 Some(WindowButton::Close),
5315 Some(WindowButton::Minimize),
5316 None
5317 ]
5318 );
5319 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
5320 }
5321
5322 #[test]
5323 fn test_window_button_layout_parse_right_only() {
5324 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
5325 assert_eq!(layout.left, [None, None, None]);
5326 assert_eq!(
5327 layout.right,
5328 [
5329 Some(WindowButton::Minimize),
5330 Some(WindowButton::Maximize),
5331 Some(WindowButton::Close)
5332 ]
5333 );
5334 }
5335
5336 #[test]
5337 fn test_window_button_layout_parse_left_only() {
5338 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
5339 assert_eq!(
5340 layout.left,
5341 [
5342 Some(WindowButton::Close),
5343 Some(WindowButton::Minimize),
5344 Some(WindowButton::Maximize)
5345 ]
5346 );
5347 assert_eq!(layout.right, [None, None, None]);
5348 }
5349
5350 #[test]
5351 fn test_window_button_layout_parse_with_whitespace() {
5352 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
5353 assert_eq!(
5354 layout.left,
5355 [
5356 Some(WindowButton::Close),
5357 Some(WindowButton::Minimize),
5358 None
5359 ]
5360 );
5361 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
5362 }
5363
5364 #[test]
5365 fn test_window_button_layout_parse_empty() {
5366 let layout = WindowButtonLayout::parse("").unwrap();
5367 assert_eq!(layout.left, [None, None, None]);
5368 assert_eq!(layout.right, [None, None, None]);
5369 }
5370
5371 #[test]
5372 fn test_window_button_layout_parse_intentionally_empty() {
5373 let layout = WindowButtonLayout::parse(":").unwrap();
5374 assert_eq!(layout.left, [None, None, None]);
5375 assert_eq!(layout.right, [None, None, None]);
5376 }
5377
5378 #[test]
5379 fn test_window_button_layout_parse_invalid_buttons() {
5380 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
5381 assert_eq!(
5382 layout.left,
5383 [
5384 Some(WindowButton::Close),
5385 Some(WindowButton::Minimize),
5386 None
5387 ]
5388 );
5389 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
5390 }
5391
5392 #[test]
5393 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
5394 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
5395 assert_eq!(
5396 layout.right,
5397 [
5398 Some(WindowButton::Close),
5399 Some(WindowButton::Minimize),
5400 None
5401 ]
5402 );
5403 assert_eq!(layout.format(), ":close,minimize");
5404 }
5405
5406 #[test]
5407 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
5408 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
5409 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
5410 assert_eq!(
5411 layout.right,
5412 [
5413 Some(WindowButton::Maximize),
5414 Some(WindowButton::Minimize),
5415 None
5416 ]
5417 );
5418
5419 let button_ids: Vec<_> = layout
5420 .left
5421 .iter()
5422 .chain(layout.right.iter())
5423 .flatten()
5424 .map(WindowButton::id)
5425 .collect();
5426 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
5427 assert_eq!(unique_button_ids.len(), button_ids.len());
5428 assert_eq!(layout.format(), "close:maximize,minimize");
5429 }
5430
5431 #[test]
5432 fn test_window_button_layout_parse_gnome_style() {
5433 let layout = WindowButtonLayout::parse("close").unwrap();
5434 assert_eq!(layout.left, [None, None, None]);
5435 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
5436 }
5437
5438 #[test]
5439 fn test_window_button_layout_parse_elementary_style() {
5440 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
5441 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
5442 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
5443 }
5444
5445 #[test]
5446 fn test_window_button_layout_round_trip() {
5447 let cases = [
5448 "close:minimize,maximize",
5449 "minimize,maximize,close:",
5450 ":close",
5451 "close:",
5452 "close:maximize",
5453 ":",
5454 ];
5455
5456 for case in cases {
5457 let layout = WindowButtonLayout::parse(case).unwrap();
5458 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
5459 }
5460 }
5461
5462 #[test]
5463 fn test_window_button_layout_linux_default() {
5464 let layout = WindowButtonLayout::linux_default();
5465 assert_eq!(layout.left, [None, None, None]);
5466 assert_eq!(
5467 layout.right,
5468 [
5469 Some(WindowButton::Minimize),
5470 Some(WindowButton::Maximize),
5471 Some(WindowButton::Close)
5472 ]
5473 );
5474
5475 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
5476 assert_eq!(round_tripped, layout);
5477 }
5478
5479 #[test]
5480 fn test_window_button_layout_parse_all_invalid() {
5481 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
5482 }
5483}