1mod color_presentation;
2pub use color_presentation::{WindowColorFallback, WindowColorMode, WindowColorStatus};
3
4mod app_menu;
5mod keyboard;
6mod keystroke;
7mod platform_info;
8
9#[cfg(all(target_os = "linux", feature = "wayland"))]
10#[expect(missing_docs)]
11pub mod layer_shell;
12
13pub mod popup;
15
16#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
17mod threaded_dispatcher;
18
19#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
20mod test;
21
22#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
23mod visual_test;
24
25#[cfg(all(
26 feature = "screen-capture",
27 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
28))]
29pub mod scap_screen_capture;
30
31#[cfg(all(
32 any(target_os = "windows", target_os = "linux"),
33 feature = "screen-capture"
34))]
35pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
36#[cfg(not(feature = "screen-capture"))]
37pub(crate) type PlatformScreenCaptureFrame = ();
38#[cfg(all(target_os = "macos", feature = "screen-capture"))]
39pub(crate) type PlatformScreenCaptureFrame =
40 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
41
42use crate::{
43 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, Corners,
44 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
45 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
46 LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority,
47 RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
48 ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea,
49 hash, point, px, size,
50};
51#[cfg(any(target_os = "linux", target_os = "freebsd"))]
52use anyhow::bail;
53use anyhow::{Context as _, Result};
54use async_task::Runnable;
55use collections::FxHashMap;
56use futures::channel::oneshot;
57#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
58use image::RgbaImage;
59use image::codecs::gif::GifDecoder;
60use image::{AnimationDecoder as _, DynamicImage, Frame};
61use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
62use scheduler::Instant;
63pub use scheduler::RunnableMeta;
64use schemars::JsonSchema;
65use seahash::SeaHasher;
66use serde::{Deserialize, Serialize};
67use smallvec::SmallVec;
68use std::borrow::Cow;
69use std::collections::hash_map::Entry;
70use std::hash::{Hash, Hasher};
71use std::io::Cursor;
72use std::ops;
73use std::time::Duration;
74use std::{
75 ffi::OsString,
76 fmt::{self, Debug},
77 ops::Range,
78 path::{Path, PathBuf},
79 rc::Rc,
80 sync::Arc,
81};
82use strum::EnumIter;
83use uuid::Uuid;
84
85pub use app_menu::*;
86pub use keyboard::*;
87pub use keystroke::*;
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum WindowVisibility {
111 Visible,
114 Hidden,
117}
118
119impl WindowVisibility {
120 pub fn is_visible(self) -> bool {
122 self == Self::Visible
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130pub enum DisplayEvent {
131 Added(DisplayId),
133 Removed(DisplayId),
136 Changed {
138 id: DisplayId,
140 changes: DisplayChanges,
142 },
143}
144
145bitflags::bitflags! {
146 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148 pub struct DisplayChanges: u8 {
149 const BOUNDS = 1 << 0;
151 const REFRESH_INTERVAL = 1 << 1;
153 }
154}
155
156pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
159 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
160}
161
162#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
169pub enum ActivationPolicy {
170 #[default]
172 Regular,
173 Accessory,
175}
176
177#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
178pub(crate) use test::*;
179
180#[cfg(any(test, feature = "test-support"))]
181pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
182
183#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
184pub use threaded_dispatcher::ThreadedDispatcher;
185
186#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
187pub use visual_test::VisualTestPlatform;
188
189pub struct ActivityGuard {
191 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
192}
193
194impl ActivityGuard {
195 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
197 Self {
198 _release: gpui_util::defer(Box::new(release)),
199 }
200 }
201
202 pub fn noop() -> Self {
204 Self::new(|| {})
205 }
206}
207
208#[cfg(any(target_os = "linux", target_os = "freebsd"))]
212#[inline]
213pub fn guess_compositor() -> &'static str {
214 if std::env::var_os("ZED_HEADLESS").is_some() {
215 return "Headless";
216 }
217 GraphicalEnvironment::detect().guess_compositor()
218}
219
220#[cfg(any(target_os = "linux", target_os = "freebsd"))]
229#[derive(Clone, Debug, Default)]
230pub struct GraphicalEnvironment {
231 pub wayland_display: Option<OsString>,
233 pub x11_display: Option<OsString>,
235 pub xdg_runtime_dir: Option<OsString>,
237 pub activation_token: Option<String>,
243}
244
245#[cfg(target_os = "windows")]
251#[derive(Clone, Debug, Default)]
252pub struct GraphicalEnvironment {
253 pub session_id: Option<u32>,
256}
257
258#[cfg(target_os = "windows")]
259impl GraphicalEnvironment {
260 pub fn detect() -> Self {
262 let mut session_id = 0;
263 let result = unsafe {
265 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
266 windows::Win32::System::Threading::GetCurrentProcessId(),
267 &mut session_id,
268 )
269 };
270 Self {
271 session_id: result.is_ok().then_some(session_id),
272 }
273 }
274
275 pub fn apply_to(&self, _command: &mut std::process::Command) {}
278}
279
280#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
282#[derive(Clone, Debug, Default)]
283pub struct GraphicalEnvironment;
284
285#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
286impl GraphicalEnvironment {
287 pub fn detect() -> Self {
289 Self
290 }
291
292 pub fn apply_to(&self, _command: &mut std::process::Command) {}
294}
295
296#[derive(Clone, Debug)]
298pub enum WindowingRequest {
299 Headless,
301 Windowed(GraphicalEnvironment),
303}
304
305#[cfg(any(target_os = "linux", target_os = "freebsd"))]
306impl GraphicalEnvironment {
307 pub fn detect() -> Self {
312 Self {
313 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
314 x11_display: std::env::var_os("DISPLAY"),
315 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
316 activation_token: None,
317 }
318 }
319
320 pub fn apply_to(&self, command: &mut std::process::Command) {
324 for (name, value) in [
325 ("WAYLAND_DISPLAY", &self.wayland_display),
326 ("DISPLAY", &self.x11_display),
327 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
328 ] {
329 match value {
330 Some(value) => command.env(name, value),
331 None => command.env_remove(name),
332 };
333 }
334 }
335
336 pub fn guess_compositor(&self) -> &'static str {
340 let is_set =
341 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
342 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
343 "Wayland"
344 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
345 "X11"
346 } else {
347 "Headless"
348 }
349 }
350}
351
352#[cfg(any(target_os = "linux", target_os = "freebsd"))]
353bitflags::bitflags! {
354 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
356 pub struct WindowingModes: u8 {
357 const WAYLAND = 1 << 0;
359 const X11 = 1 << 1;
361 const HEADLESS = 1 << 2;
363 }
364}
365
366pub use platform_info::{OperatingSystem, PlatformInfo, PlatformTarget};
367
368#[expect(missing_docs)]
369pub trait Platform: 'static {
370 fn platform_info(&self) -> PlatformInfo;
372
373 fn background_executor(&self) -> BackgroundExecutor;
374 fn foreground_executor(&self) -> ForegroundExecutor;
375 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
376
377 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
378 fn quit(&self);
379 fn set_initial_windowing(&self, _request: WindowingRequest) {}
384 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
385 Task::ready(Err(anyhow::anyhow!(
386 "this platform cannot switch between headless and windowed modes"
387 )))
388 }
389 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
390 fn activate(&self, ignoring_other_apps: bool);
391 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
393 fn hide(&self);
394 fn hide_other_apps(&self);
395 fn unhide_other_apps(&self);
396
397 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
398 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
399 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
404 fn active_window(&self) -> Option<AnyWindowHandle>;
405 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
406 None
407 }
408
409 fn is_screen_capture_supported(&self) -> bool {
410 false
411 }
412
413 fn screen_capture_sources(
414 &self,
415 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
416 let (sources_tx, sources_rx) = oneshot::channel();
417 sources_tx
418 .send(Err(anyhow::anyhow!(
419 "gpui was compiled without the screen-capture feature"
420 )))
421 .ok();
422 sources_rx
423 }
424
425 fn open_window(
426 &self,
427 handle: AnyWindowHandle,
428 options: WindowParams,
429 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
430
431 fn window_appearance(&self) -> WindowAppearance;
433
434 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
443
444 fn button_layout(&self) -> Option<WindowButtonLayout> {
446 None
447 }
448
449 fn open_url(&self, url: &str);
450 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
451 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
452
453 fn prompt_for_paths(
454 &self,
455 options: PathPromptOptions,
456 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
457 fn prompt_for_new_path(
458 &self,
459 directory: &Path,
460 suggested_name: Option<&str>,
461 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
462 fn can_select_mixed_files_and_dirs(&self) -> bool;
463 fn reveal_path(&self, path: &Path);
464 fn open_with_system(&self, path: &Path);
465
466 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
467 fn on_reopen(&self, callback: Box<dyn FnMut()>);
468 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
470 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
472
473 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
483
484 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
489
490 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
494 None
495 }
496
497 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
498 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
499 None
500 }
501
502 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
503 fn perform_dock_menu_action(&self, _action: usize) {}
504 fn add_recent_document(&self, _path: &Path) {}
505 fn update_jump_list(
506 &self,
507 _menus: Vec<MenuItem>,
508 _entries: Vec<SmallVec<[PathBuf; 2]>>,
509 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
510 Task::ready(Vec::new())
511 }
512 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
513 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
514 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
515
516 fn thermal_state(&self) -> ThermalState;
517 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
518 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
519
520 fn power_mode(&self) -> PowerMode {
523 PowerMode::Unknown
524 }
525 fn on_power_mode_change(&self, _callback: Box<dyn FnMut()>) {}
527
528 fn set_app_identity(&self, identifier: &str, name: &str) {
535 _ = (identifier, name);
536 }
537
538 fn show_system_notification(&self, notification: SystemNotification) {
545 _ = notification;
546 }
547
548 fn dismiss_system_notification(&self, tag: &str) {
553 _ = tag;
554 }
555
556 fn on_system_notification_response(
562 &self,
563 callback: Box<dyn FnMut(SystemNotificationResponse)>,
564 ) {
565 _ = callback;
566 }
567
568 fn compositor_name(&self) -> &'static str {
569 ""
570 }
571 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
574 None
575 }
576 fn app_path(&self) -> Result<PathBuf>;
577 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
578
579 fn set_cursor_style(&self, style: CursorStyle);
580
581 fn hide_cursor_until_mouse_moves(&self);
584
585 fn is_cursor_visible(&self) -> bool;
587
588 fn should_auto_hide_scrollbars(&self) -> bool;
589
590 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
591 fn write_to_clipboard(&self, item: ClipboardItem);
592
593 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
601 Task::ready(Ok(self.read_from_clipboard()))
602 }
603
604 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
605 fn read_from_primary(&self) -> Option<ClipboardItem>;
606 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
607 fn write_to_primary(&self, item: ClipboardItem);
608
609 #[cfg(target_os = "macos")]
610 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
611 #[cfg(target_os = "macos")]
612 fn write_to_find_pasteboard(&self, item: ClipboardItem);
613
614 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
615
616 #[cfg(target_os = "macos")]
620 fn write_credentials_if_not_exists(
621 &self,
622 _url: &str,
623 _username: &str,
624 _password: &[u8],
625 ) -> Task<Result<bool>> {
626 Task::ready(Err(anyhow::anyhow!(
627 "this platform does not support atomic credential writes"
628 )))
629 }
630
631 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
632 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
633
634 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
635 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
636 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
637}
638
639pub trait PlatformDisplay: Debug {
641 fn id(&self) -> DisplayId;
643
644 fn uuid(&self) -> Result<Uuid>;
647
648 fn bounds(&self) -> Bounds<Pixels>;
650
651 fn refresh_interval(&self) -> Option<Duration>;
658
659 fn visible_bounds(&self) -> Bounds<Pixels> {
663 self.bounds()
664 }
665
666 fn default_bounds(&self) -> Bounds<Pixels> {
668 let bounds = self.bounds();
669 let center = bounds.center();
670 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
671
672 let offset = clipped_window_size / 2.0;
673 let origin = point(center.x - offset.width, center.y - offset.height);
674 Bounds::new(origin, clipped_window_size)
675 }
676}
677
678#[derive(Clone, Debug, PartialEq, Eq)]
681pub struct SystemNotification {
682 pub tag: SharedString,
686 pub title: SharedString,
688 pub body: SharedString,
690 pub actions: Vec<SystemNotificationAction>,
693}
694
695#[derive(Clone, Debug, PartialEq, Eq, Hash)]
697pub struct SystemNotificationAction {
698 pub id: SharedString,
701 pub label: SharedString,
703}
704
705#[derive(Clone, Debug, PartialEq, Eq)]
707pub struct SystemNotificationResponse {
708 pub tag: SharedString,
710 pub action_id: Option<SharedString>,
713}
714
715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
717pub enum PowerMode {
718 Unknown,
721 LowPower,
723 Balanced,
725 HighPower,
727}
728
729#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub enum ThermalState {
732 Nominal,
734 Fair,
736 Serious,
738 Critical,
740}
741
742#[derive(Clone)]
744pub struct SourceMetadata {
745 pub id: u64,
747 pub label: Option<SharedString>,
749 pub is_main: Option<bool>,
751 pub resolution: Size<DevicePixels>,
753}
754
755pub trait ScreenCaptureSource {
757 fn metadata(&self) -> Result<SourceMetadata>;
759
760 fn stream(
763 &self,
764 foreground_executor: &ForegroundExecutor,
765 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
766 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
767}
768
769pub trait ScreenCaptureStream {
771 fn metadata(&self) -> Result<SourceMetadata>;
773}
774
775pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
777
778#[derive(PartialEq, Eq, Hash, Copy, Clone)]
780pub struct DisplayId(pub(crate) u64);
781
782impl DisplayId {
783 pub fn new(id: u64) -> Self {
785 Self(id)
786 }
787}
788
789impl From<u64> for DisplayId {
790 fn from(id: u64) -> Self {
791 Self(id)
792 }
793}
794
795impl From<DisplayId> for u64 {
796 fn from(id: DisplayId) -> Self {
797 id.0
798 }
799}
800
801impl Debug for DisplayId {
802 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803 write!(f, "DisplayId({})", self.0)
804 }
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
809pub enum ResizeEdge {
810 Top,
812 TopRight,
814 Right,
816 BottomRight,
818 Bottom,
820 BottomLeft,
822 Left,
824 TopLeft,
826}
827
828#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
830pub enum WindowDecorations {
831 #[default]
832 Server,
834 Client,
836}
837
838#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
840pub enum Decorations {
841 #[default]
843 Server,
844 Client {
846 tiling: Tiling,
848 },
849}
850
851#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
853pub struct WindowControls {
854 pub fullscreen: bool,
856 pub maximize: bool,
858 pub minimize: bool,
860 pub window_menu: bool,
862}
863
864impl Default for WindowControls {
865 fn default() -> Self {
866 Self {
868 fullscreen: true,
869 maximize: true,
870 minimize: true,
871 window_menu: true,
872 }
873 }
874}
875
876#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
878pub enum WindowButton {
879 Minimize,
881 Maximize,
883 Close,
885}
886
887impl WindowButton {
888 pub fn id(&self) -> &'static str {
890 match self {
891 WindowButton::Minimize => "minimize",
892 WindowButton::Maximize => "maximize",
893 WindowButton::Close => "close",
894 }
895 }
896
897 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
898 fn index(&self) -> usize {
899 match self {
900 WindowButton::Minimize => 0,
901 WindowButton::Maximize => 1,
902 WindowButton::Close => 2,
903 }
904 }
905}
906
907pub const MAX_BUTTONS_PER_SIDE: usize = 3;
909
910#[derive(Debug, Clone, Copy, PartialEq, Eq)]
915pub struct WindowButtonLayout {
916 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
918 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
920}
921
922#[cfg(any(target_os = "linux", target_os = "freebsd"))]
923impl WindowButtonLayout {
924 pub fn linux_default() -> Self {
926 Self {
927 left: [None; MAX_BUTTONS_PER_SIDE],
928 right: [
929 Some(WindowButton::Minimize),
930 Some(WindowButton::Maximize),
931 Some(WindowButton::Close),
932 ],
933 }
934 }
935
936 pub fn parse(layout_string: &str) -> Result<Self> {
938 fn parse_side(
939 s: &str,
940 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
941 unrecognized: &mut Vec<String>,
942 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
943 let mut result = [None; MAX_BUTTONS_PER_SIDE];
944 let mut i = 0;
945 for name in s.split(',') {
946 let trimmed = name.trim();
947 if trimmed.is_empty() {
948 continue;
949 }
950 let button = match trimmed {
951 "minimize" => Some(WindowButton::Minimize),
952 "maximize" => Some(WindowButton::Maximize),
953 "close" => Some(WindowButton::Close),
954 other => {
955 unrecognized.push(other.to_string());
956 None
957 }
958 };
959 if let Some(button) = button {
960 if seen_buttons[button.index()] {
961 continue;
962 }
963 if let Some(slot) = result.get_mut(i) {
964 *slot = Some(button);
965 seen_buttons[button.index()] = true;
966 i += 1;
967 }
968 }
969 }
970 result
971 }
972
973 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
974 let mut unrecognized = Vec::new();
975 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
976 let layout = Self {
977 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
978 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
979 };
980
981 if !unrecognized.is_empty()
982 && layout.left.iter().all(Option::is_none)
983 && layout.right.iter().all(Option::is_none)
984 {
985 bail!(
986 "button layout string {:?} contains no valid buttons (unrecognized: {})",
987 layout_string,
988 unrecognized.join(", ")
989 );
990 }
991
992 Ok(layout)
993 }
994
995 #[cfg(test)]
997 pub fn format(&self) -> String {
998 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
999 buttons
1000 .iter()
1001 .flatten()
1002 .map(|button| match button {
1003 WindowButton::Minimize => "minimize",
1004 WindowButton::Maximize => "maximize",
1005 WindowButton::Close => "close",
1006 })
1007 .collect::<Vec<_>>()
1008 .join(",")
1009 }
1010
1011 format!("{}:{}", format_side(&self.left), format_side(&self.right))
1012 }
1013}
1014
1015#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1017pub struct Tiling {
1018 pub top: bool,
1020 pub left: bool,
1022 pub right: bool,
1024 pub bottom: bool,
1026}
1027
1028impl Tiling {
1029 pub fn tiled() -> Self {
1031 Self {
1032 top: true,
1033 left: true,
1034 right: true,
1035 bottom: true,
1036 }
1037 }
1038
1039 pub fn is_tiled(&self) -> bool {
1041 self.top || self.left || self.right || self.bottom
1042 }
1043}
1044
1045pub struct A11yCallbacks {
1047 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1049 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1051 pub deactivation: Box<dyn Fn() + Send + 'static>,
1053}
1054
1055#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1057pub enum FrameRequestSource {
1058 #[default]
1060 NativeCallback,
1061 LocalSchedule,
1063}
1064
1065#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1066#[expect(missing_docs)]
1067pub struct RequestFrameOptions {
1068 pub require_presentation: bool,
1070 pub force_render: bool,
1072 pub signal_at: Option<Instant>,
1078 pub signal_source: FrameRequestSource,
1080}
1081
1082pub struct PlatformFrameSignal {
1091 #[cfg(feature = "profiler")]
1092 origin: Instant,
1093 #[cfg(feature = "profiler")]
1094 first_signal: std::sync::atomic::AtomicU64,
1095}
1096
1097impl Default for PlatformFrameSignal {
1098 fn default() -> Self {
1099 Self::new()
1100 }
1101}
1102
1103impl PlatformFrameSignal {
1104 pub fn new() -> Self {
1106 Self {
1107 #[cfg(feature = "profiler")]
1108 origin: Instant::now(),
1109 #[cfg(feature = "profiler")]
1110 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1111 }
1112 }
1113
1114 #[inline]
1118 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1119 if cfg!(feature = "profiler") {
1120 Some(capture())
1121 } else {
1122 None
1123 }
1124 }
1125
1126 #[inline]
1128 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1129 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1130 #[cfg(feature = "profiler")]
1131 {
1132 let nanoseconds = at
1133 .saturating_duration_since(self.origin)
1134 .as_nanos()
1135 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1136 let source_bit = match source {
1137 FrameRequestSource::NativeCallback => 0,
1138 FrameRequestSource::LocalSchedule => 1,
1139 };
1140 self.first_signal.fetch_min(
1141 (nanoseconds << 1) | source_bit,
1142 std::sync::atomic::Ordering::Relaxed,
1143 );
1144 }
1145 }
1146
1147 #[inline]
1149 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1150 #[cfg(feature = "profiler")]
1151 {
1152 let signal = self
1153 .first_signal
1154 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1155 (signal != u64::MAX).then(|| {
1156 let source = match signal & 1 {
1157 0 => FrameRequestSource::NativeCallback,
1158 _ => FrameRequestSource::LocalSchedule,
1159 };
1160 (self.origin + Duration::from_nanos(signal >> 1), source)
1161 })
1162 }
1163 #[cfg(not(feature = "profiler"))]
1164 {
1165 None
1166 }
1167 }
1168}
1169
1170#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1183pub enum AppLifecyclePhase {
1184 Active,
1186 Inactive,
1188 Background,
1191 Foreground,
1193 Disconnected,
1197}
1198
1199#[derive(Debug, Clone, Default, PartialEq)]
1206pub struct WindowInsets {
1207 pub safe_area: Edges<Pixels>,
1212 pub ime: Edges<Pixels>,
1216 pub corner_radii: Corners<Pixels>,
1221}
1222
1223impl WindowInsets {
1224 pub fn effective(&self) -> Edges<Pixels> {
1226 Edges {
1227 top: self.safe_area.top.max(self.ime.top),
1228 right: self.safe_area.right.max(self.ime.right),
1229 bottom: self.safe_area.bottom.max(self.ime.bottom),
1230 left: self.safe_area.left.max(self.ime.left),
1231 }
1232 }
1233}
1234
1235#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1237pub enum TextInputStateChange {
1238 FocusGained,
1240 FocusLost,
1242 SelectionChanged,
1244 ContentChanged,
1246}
1247
1248#[expect(missing_docs)]
1249pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1250 fn set_color_mode(&mut self, mode: WindowColorMode) -> anyhow::Result<WindowColorStatus> {
1252 match mode {
1253 WindowColorMode::Default => Ok(self.color_status()),
1254 _ => anyhow::bail!("wide-gamut SDR presentation is unsupported"),
1255 }
1256 }
1257 fn color_status(&self) -> WindowColorStatus {
1259 WindowColorStatus::default()
1260 }
1261
1262 fn bounds(&self) -> Bounds<Pixels>;
1263 fn is_maximized(&self) -> bool;
1264 fn window_bounds(&self) -> WindowBounds;
1265 fn content_size(&self) -> Size<Pixels>;
1266 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1272 Bounds::new(Point::default(), self.content_size())
1273 }
1274 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1279 fn prepare_frame(&self) -> bool {
1284 false
1285 }
1286 fn resize(&mut self, size: Size<Pixels>);
1287 fn scale_factor(&self) -> f32;
1288 fn appearance(&self) -> WindowAppearance;
1289 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1290 fn mouse_position(&self) -> Point<Pixels>;
1291 fn modifiers(&self) -> Modifiers;
1292 fn capslock(&self) -> Capslock;
1293 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1294 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1295 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1300 fn prompt(
1301 &self,
1302 level: PromptLevel,
1303 msg: &str,
1304 detail: Option<&str>,
1305 answers: &[PromptButton],
1306 ) -> Option<oneshot::Receiver<usize>>;
1307 fn activate(&self);
1308 fn request_attention(&self) {}
1310 fn is_active(&self) -> bool;
1311 fn visibility(&self) -> WindowVisibility;
1314 fn is_hovered(&self) -> bool;
1315 fn background_appearance(&self) -> WindowBackgroundAppearance;
1316 fn set_title(&mut self, title: &str);
1317 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1318 fn minimize(&self);
1319 fn zoom(&self);
1320 fn toggle_fullscreen(&self);
1321 fn is_fullscreen(&self) -> bool;
1322 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1323 None
1324 }
1325 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1326 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1327 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1328 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1332 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1333 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1334 fn on_moved(&self, callback: Box<dyn FnMut()>);
1335 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1339 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1340 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1341 fn on_close(&self, callback: Box<dyn FnOnce()>);
1342 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1343 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1344 fn draw(&self, scene: &Scene);
1345 #[cfg(any(test, feature = "test-support"))]
1347 fn submit_scene_for_validation(&self, _scene: &Scene) -> anyhow::Result<()> {
1348 anyhow::bail!("native window ownership validation is unsupported on this backend")
1349 }
1350 fn schedule_frame(&self) {}
1351 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1352 fn is_subpixel_rendering_supported(&self) -> bool;
1353
1354 fn get_title(&self) -> String {
1356 String::new()
1357 }
1358 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1359 None
1360 }
1361 fn tab_bar_visible(&self) -> bool {
1362 false
1363 }
1364 fn set_edited(&mut self, _edited: bool) {}
1365 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1366 fn toggle_simple_fullscreen(&self) {}
1367 fn is_simple_fullscreen(&self) -> bool {
1368 false
1369 }
1370 #[cfg(target_os = "macos")]
1371 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1372 fn show_character_palette(&self) {}
1373 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1374 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1375 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1376 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1377 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1378 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1379 fn merge_all_windows(&self) {}
1380 fn move_tab_to_new_window(&self) {}
1381 fn toggle_window_tab_overview(&self) {}
1382 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1383
1384 fn native_window_state(&self) -> Option<Vec<u8>> {
1385 None
1386 }
1387 fn restore_native_window_state(&self, _state: &[u8]) {}
1388
1389 #[cfg(target_os = "windows")]
1390 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1391
1392 fn inner_window_bounds(&self) -> WindowBounds {
1394 self.window_bounds()
1395 }
1396 fn request_decorations(&self, _decorations: WindowDecorations) {}
1397 fn show_window_menu(&self, _position: Point<Pixels>) {}
1398 fn start_window_move(&self) {}
1399 fn can_start_external_drag(&self) -> bool {
1400 false
1401 }
1402 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1403 false
1404 }
1405 fn start_window_resize(&self, _edge: ResizeEdge) {}
1406 fn set_exclusive_zone(&self, _zone: Pixels) {}
1407 #[cfg(all(target_os = "linux", feature = "wayland"))]
1408 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1409 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1410 fn window_decorations(&self) -> Decorations {
1411 Decorations::Server
1412 }
1413 fn set_app_id(&mut self, _app_id: &str) {}
1414 fn map_window(&mut self) -> anyhow::Result<()> {
1415 Ok(())
1416 }
1417 fn window_controls(&self) -> WindowControls {
1418 WindowControls::default()
1419 }
1420 fn set_client_inset(&self, _inset: Pixels) {}
1421 fn gpu_specs(&self) -> Option<GpuSpecs>;
1422
1423 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1424
1425 fn insets(&self) -> WindowInsets {
1430 WindowInsets::default()
1431 }
1432
1433 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1439
1440 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1443
1444 fn set_back_enabled(&self, _enabled: bool) {}
1447
1448 fn show_soft_keyboard(&self) {}
1450
1451 fn hide_soft_keyboard(&self) {}
1453
1454 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1456
1457 fn play_system_bell(&self) {}
1458
1459 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1461
1462 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1464
1465 fn a11y_update_window_bounds(&self) {}
1467
1468 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1469 fn as_test(&mut self) -> Option<&mut TestWindow> {
1470 None
1471 }
1472
1473 #[cfg(any(test, feature = "test-support"))]
1476 fn render_to_linear_p3(&self, _scene: &Scene) -> Result<LinearP3Image> {
1477 anyhow::bail!("linear P3 readback is unsupported on this backend")
1478 }
1479
1480 #[cfg(any(test, feature = "test-support"))]
1484 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1485 anyhow::bail!("render_to_image not implemented for this platform")
1486 }
1487}
1488
1489#[cfg(any(test, feature = "test-support"))]
1493#[derive(Debug)]
1494pub struct LinearP3Image {
1495 pub size: Size<DevicePixels>,
1497 pub pixels: Vec<[u16; 4]>,
1499}
1500
1501#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1503pub trait PlatformHeadlessRenderer {
1504 fn render_scene_to_image(
1506 &mut self,
1507 scene: &Scene,
1508 size: Size<DevicePixels>,
1509 ) -> Result<RgbaImage>;
1510
1511 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1517
1518 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1520}
1521
1522#[doc(hidden)]
1525pub type RunnableVariant = Runnable<RunnableMeta>;
1526
1527#[doc(hidden)]
1528pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1529
1530#[doc(hidden)]
1531pub enum TasksIncluded {
1532 OnlyCompleted,
1533 CompletedAndRunning,
1534}
1535
1536#[doc(hidden)]
1539pub trait PlatformDispatcher: Send + Sync {
1540 fn is_main_thread(&self) -> bool;
1541 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1542 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1543 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1544
1545 fn dispatch_on_main_thread_when_idle(
1546 &self,
1547 runnable: RunnableVariant,
1548 timeout: Option<Duration>,
1549 ) {
1550 let _ = timeout;
1551 self.dispatch_on_main_thread(runnable, Priority::Low);
1552 }
1553
1554 fn idle_time_remaining(&self) -> Option<Duration> {
1555 None
1556 }
1557
1558 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1559
1560 fn now(&self) -> Instant {
1561 Instant::now()
1562 }
1563
1564 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1565 gpui_util::defer(Box::new(|| {}))
1566 }
1567
1568 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1569 ActivityGuard::noop()
1570 }
1571
1572 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1573 fn as_test(&self) -> Option<&TestDispatcher> {
1574 None
1575 }
1576
1577 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1580 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1581 None
1582 }
1583}
1584
1585#[expect(missing_docs)]
1586pub trait PlatformTextSystem: Send + Sync {
1587 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1588 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1590 fn all_font_names(&self) -> Vec<String>;
1592 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1594 fn font_capabilities(&self) -> crate::FontCapabilities {
1596 crate::FontCapabilities::default()
1597 }
1598 fn font_table(
1607 &self,
1608 _font_id: FontId,
1609 _tag: crate::FontTag,
1610 ) -> Result<crate::FontQuery<Option<Vec<u8>>>> {
1611 Ok(crate::FontQuery::Unsupported)
1612 }
1613 fn font_axes(
1616 &self,
1617 font_id: FontId,
1618 ) -> Result<crate::FontQuery<Vec<crate::VariationAxisInfo>>> {
1619 crate::text_system::font_inspection::query_axes(|tag| self.font_table(font_id, tag))
1620 }
1621 fn opentype_features(
1626 &self,
1627 font_id: FontId,
1628 ) -> Result<crate::FontQuery<Vec<crate::OpenTypeFeatureInfo>>> {
1629 crate::text_system::font_inspection::query_features(|tag| self.font_table(font_id, tag))
1630 }
1631 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1633 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1635 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1637 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1639 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1641 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1643 fn rasterize_glyph(
1645 &self,
1646 params: &RenderGlyphParams,
1647 raster_bounds: Bounds<DevicePixels>,
1648 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1649 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1651 fn shape_paragraph(
1653 &self,
1654 _request: crate::ParagraphRequest<'_>,
1655 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1656 anyhow::bail!("paragraph layout is unsupported by this text backend")
1657 }
1658 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1660 -> TextRenderingMode;
1661 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1663 0
1664 }
1665}
1666
1667#[expect(missing_docs)]
1668pub struct NoopTextSystem;
1669
1670#[expect(missing_docs)]
1671impl NoopTextSystem {
1672 #[allow(dead_code)]
1673 pub fn new() -> Self {
1674 Self
1675 }
1676}
1677
1678impl PlatformTextSystem for NoopTextSystem {
1679 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1680 Ok(())
1681 }
1682
1683 fn all_font_names(&self) -> Vec<String> {
1684 Vec::new()
1685 }
1686
1687 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1688 Ok(FontId(1))
1689 }
1690
1691 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1692 FontMetrics {
1693 units_per_em: 1000,
1694 ascent: 1025.0,
1695 descent: -275.0,
1696 line_gap: 0.0,
1697 underline_position: -95.0,
1698 underline_thickness: 60.0,
1699 cap_height: 698.0,
1700 x_height: 516.0,
1701 bounding_box: Bounds {
1702 origin: Point {
1703 x: -260.0,
1704 y: -245.0,
1705 },
1706 size: Size {
1707 width: 1501.0,
1708 height: 1364.0,
1709 },
1710 },
1711 }
1712 }
1713
1714 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1715 Ok(Bounds {
1716 origin: Point { x: 54.0, y: 0.0 },
1717 size: size(392.0, 528.0),
1718 })
1719 }
1720
1721 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1722 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1723 }
1724
1725 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1726 Some(GlyphId(ch.len_utf16() as u32))
1727 }
1728
1729 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1730 Ok(Default::default())
1731 }
1732
1733 fn rasterize_glyph(
1734 &self,
1735 _params: &RenderGlyphParams,
1736 raster_bounds: Bounds<DevicePixels>,
1737 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1738 Ok((raster_bounds.size, Vec::new()))
1739 }
1740
1741 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1742 let mut position = px(0.);
1743 let metrics = self.font_metrics(FontId(0));
1744 let em_width = font_size
1745 * self
1746 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1747 .unwrap()
1748 .width
1749 / metrics.units_per_em as f32;
1750 let mut glyphs = Vec::new();
1751 for (ix, c) in text.char_indices() {
1752 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1753 glyphs.push(ShapedGlyph {
1754 id: glyph,
1755 position: point(position, px(0.)),
1756 index: ix,
1757 is_emoji: glyph.0 == 2,
1758 });
1759 if glyph.0 == 2 {
1760 position += em_width * 2.0;
1761 } else {
1762 position += em_width;
1763 }
1764 } else {
1765 position += em_width
1766 }
1767 }
1768 let mut runs = Vec::default();
1769 if !glyphs.is_empty() {
1770 runs.push(ShapedRun {
1771 font_id: FontId(0),
1772 glyphs,
1773 });
1774 } else {
1775 position = px(0.);
1776 }
1777
1778 LineLayout {
1779 font_size,
1780 width: position,
1781 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1782 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1783 runs,
1784 len: text.len(),
1785 }
1786 }
1787
1788 fn recommended_rendering_mode(
1789 &self,
1790 _font_id: FontId,
1791 _font_size: Pixels,
1792 ) -> TextRenderingMode {
1793 TextRenderingMode::Grayscale
1794 }
1795}
1796
1797#[allow(dead_code)]
1802pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1803 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1804 [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], ];
1818
1819 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1820 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1821
1822 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1823 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1824
1825 [
1826 ratios[0] * NORM13,
1827 ratios[1] * NORM24,
1828 ratios[2] * NORM13,
1829 ratios[3] * NORM24,
1830 ]
1831}
1832
1833#[derive(PartialEq, Eq, Hash, Clone)]
1834#[expect(missing_docs)]
1835pub enum AtlasKey {
1836 Glyph(RenderGlyphParams),
1837 Svg(RenderSvgParams),
1838 Image(RenderImageParams),
1839}
1840
1841impl AtlasKey {
1842 pub fn texture_kind(&self) -> AtlasTextureKind {
1844 match self {
1845 AtlasKey::Glyph(params) => {
1846 if params.is_emoji {
1847 AtlasTextureKind::Polychrome
1848 } else if params.subpixel_rendering {
1849 AtlasTextureKind::Subpixel
1850 } else {
1851 AtlasTextureKind::Monochrome
1852 }
1853 }
1854 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1855 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1856 }
1857 }
1858}
1859
1860impl From<RenderGlyphParams> for AtlasKey {
1861 fn from(params: RenderGlyphParams) -> Self {
1862 Self::Glyph(params)
1863 }
1864}
1865
1866impl From<RenderSvgParams> for AtlasKey {
1867 fn from(params: RenderSvgParams) -> Self {
1868 Self::Svg(params)
1869 }
1870}
1871
1872impl From<RenderImageParams> for AtlasKey {
1873 fn from(params: RenderImageParams) -> Self {
1874 Self::Image(params)
1875 }
1876}
1877
1878#[expect(missing_docs)]
1879pub trait PlatformAtlas {
1880 fn get_or_insert_with<'a>(
1882 &self,
1883 key: AtlasKey,
1884 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1885 ) -> Result<Option<AtlasTile>>;
1886 fn remove(&self, key: &AtlasKey);
1887
1888 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1889 fn contains(&self, _key: &AtlasKey) -> bool {
1890 false
1891 }
1892}
1893
1894#[doc(hidden)]
1895pub trait AtlasBackend {
1896 fn insert(
1897 &mut self,
1898 kind: AtlasTextureKind,
1899 size: Size<DevicePixels>,
1900 bytes: &[u8],
1901 ) -> Result<AtlasTile>;
1902
1903 fn remove(&mut self, tile: AtlasTile);
1904}
1905
1906#[doc(hidden)]
1907pub struct AtlasState<Backend> {
1908 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1909 pub backend: Backend,
1910}
1911
1912impl<Backend> AtlasState<Backend> {
1913 pub fn new(backend: Backend) -> Self {
1914 Self {
1915 tiles_by_key: FxHashMap::default(),
1916 backend,
1917 }
1918 }
1919
1920 pub fn contains(&self, key: &AtlasKey) -> bool {
1921 self.tiles_by_key.contains_key(key)
1922 }
1923
1924 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1925 self.tiles_by_key.clear();
1926 reset_backend(&mut self.backend);
1927 }
1928}
1929
1930impl<Backend: Default> Default for AtlasState<Backend> {
1931 fn default() -> Self {
1932 Self::new(Backend::default())
1933 }
1934}
1935
1936impl<Backend: AtlasBackend> AtlasState<Backend> {
1937 pub fn get_or_insert_with<'a>(
1938 &mut self,
1939 key: AtlasKey,
1940 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1941 ) -> Result<Option<AtlasTile>> {
1942 match self.tiles_by_key.entry(key) {
1943 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1944 Entry::Vacant(entry) => {
1945 profiling::scope!("new tile");
1946 let Some((size, bytes)) = build()? else {
1947 return Ok(None);
1948 };
1949 let tile = self
1950 .backend
1951 .insert(entry.key().texture_kind(), size, &bytes)?;
1952 entry.insert(tile);
1953 Ok(Some(tile))
1954 }
1955 }
1956 }
1957
1958 pub fn remove(&mut self, key: &AtlasKey) {
1959 if let Some(tile) = self.tiles_by_key.remove(key) {
1960 self.backend.remove(tile);
1961 }
1962 }
1963}
1964
1965#[derive(Default)]
1969pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1970
1971#[doc(hidden)]
1972#[derive(Default)]
1973pub struct HeadlessAtlasBackend {
1974 next_id: u32,
1975}
1976
1977impl AtlasBackend for HeadlessAtlasBackend {
1978 fn insert(
1979 &mut self,
1980 kind: AtlasTextureKind,
1981 size: Size<DevicePixels>,
1982 _bytes: &[u8],
1983 ) -> Result<AtlasTile> {
1984 self.next_id += 1;
1985 let texture_id = self.next_id;
1986 self.next_id += 1;
1987 let tile_id = self.next_id;
1988 Ok(AtlasTile {
1989 texture_id: AtlasTextureId {
1990 index: texture_id,
1991 kind,
1992 },
1993 tile_id: TileId(tile_id),
1994 padding: 0,
1995 bounds: Bounds {
1996 origin: Point::default(),
1997 size,
1998 },
1999 })
2000 }
2001
2002 fn remove(&mut self, _tile: AtlasTile) {}
2003}
2004
2005impl PlatformAtlas for HeadlessAtlas {
2006 fn get_or_insert_with<'a>(
2007 &self,
2008 key: AtlasKey,
2009 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2010 ) -> Result<Option<AtlasTile>> {
2011 self.0.lock().get_or_insert_with(key, build)
2012 }
2013
2014 fn remove(&self, key: &AtlasKey) {
2015 self.0.lock().remove(key);
2016 }
2017
2018 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2019 fn contains(&self, key: &AtlasKey) -> bool {
2020 self.0.lock().contains(key)
2021 }
2022}
2023
2024#[doc(hidden)]
2025pub struct AtlasTextureList<T> {
2026 pub textures: Vec<Option<T>>,
2027 pub free_list: Vec<usize>,
2028}
2029
2030impl<T> Default for AtlasTextureList<T> {
2031 fn default() -> Self {
2032 Self {
2033 textures: Vec::default(),
2034 free_list: Vec::default(),
2035 }
2036 }
2037}
2038
2039impl<T> ops::Index<usize> for AtlasTextureList<T> {
2040 type Output = Option<T>;
2041
2042 fn index(&self, index: usize) -> &Self::Output {
2043 &self.textures[index]
2044 }
2045}
2046
2047impl<T> AtlasTextureList<T> {
2048 #[allow(unused)]
2049 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
2050 self.free_list.clear();
2051 self.textures.drain(..)
2052 }
2053
2054 #[allow(dead_code)]
2055 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
2056 self.textures.iter_mut().flatten()
2057 }
2058}
2059
2060#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2061#[repr(C)]
2062#[expect(missing_docs)]
2063pub struct AtlasTile {
2064 pub texture_id: AtlasTextureId,
2066 pub tile_id: TileId,
2068 pub padding: u32,
2070 pub bounds: Bounds<DevicePixels>,
2072}
2073
2074#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2075#[repr(C)]
2076#[expect(missing_docs)]
2077pub struct AtlasTextureId {
2078 pub index: u32,
2081 pub kind: AtlasTextureKind,
2083}
2084
2085#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2086#[repr(C)]
2087#[cfg_attr(
2088 all(
2089 any(target_os = "linux", target_os = "freebsd"),
2090 not(any(feature = "x11", feature = "wayland"))
2091 ),
2092 allow(dead_code)
2093)]
2094#[expect(missing_docs)]
2095pub enum AtlasTextureKind {
2096 Monochrome = 0,
2097 Polychrome = 1,
2098 Subpixel = 2,
2099}
2100
2101#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2102#[repr(C)]
2103#[expect(missing_docs)]
2104pub struct TileId(pub u32);
2105
2106impl From<etagere::AllocId> for TileId {
2107 fn from(id: etagere::AllocId) -> Self {
2108 Self(id.serialize())
2109 }
2110}
2111
2112impl From<TileId> for etagere::AllocId {
2113 fn from(id: TileId) -> Self {
2114 Self::deserialize(id.0)
2115 }
2116}
2117
2118#[expect(missing_docs)]
2119pub struct PlatformInputHandler {
2120 cx: AsyncWindowContext,
2121 handler: Box<dyn InputHandler>,
2122 focus_id: Option<crate::FocusId>,
2123}
2124
2125#[expect(missing_docs)]
2126#[cfg_attr(
2127 all(
2128 any(target_os = "linux", target_os = "freebsd"),
2129 not(any(feature = "x11", feature = "wayland"))
2130 ),
2131 allow(dead_code)
2132)]
2133impl PlatformInputHandler {
2134 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2135 Self {
2136 cx,
2137 handler,
2138 focus_id: None,
2139 }
2140 }
2141
2142 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2143 self.focus_id = Some(focus_id);
2144 self
2145 }
2146
2147 pub fn focus_id(&self) -> Option<crate::FocusId> {
2152 self.focus_id
2153 }
2154
2155 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2156 self.cx
2157 .update(|window, cx| {
2158 self.handler
2159 .selected_text_range(ignore_disabled_input, window, cx)
2160 })
2161 .ok()
2162 .flatten()
2163 }
2164
2165 #[cfg_attr(target_os = "windows", allow(dead_code))]
2166 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2167 self.cx
2168 .update(|window, cx| self.handler.marked_text_range(window, cx))
2169 .ok()
2170 .flatten()
2171 }
2172
2173 #[cfg_attr(
2174 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2175 allow(dead_code)
2176 )]
2177 pub fn text_for_range(
2178 &mut self,
2179 range_utf16: Range<usize>,
2180 adjusted: &mut Option<Range<usize>>,
2181 ) -> Option<String> {
2182 self.cx
2183 .update(|window, cx| {
2184 self.handler
2185 .text_for_range(range_utf16, adjusted, window, cx)
2186 })
2187 .ok()
2188 .flatten()
2189 }
2190
2191 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2192 self.cx
2193 .update(|window, cx| {
2194 self.handler
2195 .replace_text_in_range(replacement_range, text, window, cx);
2196 })
2197 .ok();
2198 }
2199
2200 pub fn replace_and_mark_text_in_range(
2201 &mut self,
2202 range_utf16: Option<Range<usize>>,
2203 new_text: &str,
2204 new_selected_range: Option<Range<usize>>,
2205 ) {
2206 self.cx
2207 .update(|window, cx| {
2208 self.handler.replace_and_mark_text_in_range(
2209 range_utf16,
2210 new_text,
2211 new_selected_range,
2212 window,
2213 cx,
2214 )
2215 })
2216 .ok();
2217 }
2218
2219 #[cfg_attr(target_os = "windows", allow(dead_code))]
2220 pub fn unmark_text(&mut self) {
2221 self.cx
2222 .update(|window, cx| self.handler.unmark_text(window, cx))
2223 .ok();
2224 }
2225
2226 pub fn paste(&mut self, item: ClipboardItem) {
2227 self.cx
2228 .update(|window, cx| self.handler.paste(item, window, cx))
2229 .ok();
2230 }
2231
2232 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2233 self.cx
2234 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2235 .ok()
2236 .flatten()
2237 }
2238
2239 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2240 self.cx
2241 .update(|window, cx| {
2242 self.handler
2243 .selection_bounds_for_range(range_utf16, window, cx)
2244 })
2245 .unwrap_or_default()
2246 }
2247
2248 #[allow(dead_code)]
2249 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2250 self.handler.apple_press_and_hold_enabled()
2251 }
2252
2253 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2254 self.handler.replace_text_in_range(None, input, window, cx);
2255 }
2256
2257 pub fn compute_ime_candidate_bounds(
2258 marked_range: Option<Range<usize>>,
2259 selection: &UTF16Selection,
2260 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2261 ) -> Option<Bounds<Pixels>> {
2262 if let Some(marked_range) = marked_range {
2263 let mut line_start = marked_range.start;
2265
2266 let caret = selection.range.end;
2270 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2271 for i in (marked_range.start..caret).rev() {
2272 if let Some(b) = bounds_for_range(i..i) {
2273 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2274 line_start = i + 1;
2275 break;
2276 }
2277 }
2278 }
2279 }
2280 bounds_for_range(line_start..line_start)
2281 } else {
2282 let offset = if selection.reversed {
2284 selection.range.start
2285 } else {
2286 selection.range.end
2287 };
2288 bounds_for_range(offset..offset)
2289 }
2290 }
2291
2292 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2293 let marked_range = self.handler.marked_text_range(window, cx);
2294 let selection = self.handler.selected_text_range(true, window, cx)?;
2295 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2296 self.handler.bounds_for_range(range, window, cx)
2297 })
2298 }
2299
2300 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2301 let marked_range = self.marked_text_range();
2302 let selection = self.selected_text_range(true)?;
2303 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2304 self.bounds_for_range(range)
2305 })
2306 }
2307
2308 #[allow(unused)]
2309 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2310 self.cx
2311 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2312 .ok()
2313 .flatten()
2314 }
2315
2316 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2318 self.cx
2319 .update(|window, cx| {
2320 self.handler
2321 .set_selected_text_range(range_utf16, window, cx)
2322 })
2323 .ok();
2324 }
2325
2326 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2328 self.cx
2329 .update(|window, cx| self.handler.element_bounds(window, cx))
2330 .ok()
2331 .flatten()
2332 }
2333
2334 pub fn text_length_utf16(&mut self) -> Option<usize> {
2336 self.cx
2337 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2338 .ok()
2339 .flatten()
2340 }
2341
2342 #[allow(dead_code)]
2343 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2344 self.handler.accepts_text_input(window, cx)
2345 }
2346
2347 #[allow(dead_code)]
2348 pub fn query_accepts_text_input(&mut self) -> bool {
2349 self.cx
2350 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2351 .unwrap_or(true)
2352 }
2353
2354 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2359 let focus_id = self.focus_id;
2360 self.cx
2361 .update(|window, cx| {
2362 focus_id.is_some()
2363 && window.focus == focus_id
2364 && self.handler.accepts_text_input(window, cx)
2365 })
2366 .unwrap_or(false)
2367 }
2368
2369 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2375 self.cx
2376 .update(|window, cx| {
2377 !window.has_pending_keystrokes()
2379 && self.handler.prefers_ime_for_printable_keys(window, cx)
2380 })
2381 .unwrap_or(false)
2382 }
2383
2384 pub fn text_input_configuration(
2386 &mut self,
2387 window: &mut Window,
2388 cx: &mut App,
2389 ) -> TextInputConfiguration {
2390 self.handler.text_input_configuration(window, cx)
2391 }
2392
2393 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2395 self.cx
2396 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2397 .ok()
2398 .flatten()
2399 }
2400}
2401
2402#[derive(Debug)]
2405pub struct UTF16Selection {
2406 pub range: Range<usize>,
2409 pub reversed: bool,
2412}
2413
2414pub trait InputHandler: 'static {
2419 fn selected_text_range(
2424 &mut self,
2425 ignore_disabled_input: bool,
2426 window: &mut Window,
2427 cx: &mut App,
2428 ) -> Option<UTF16Selection>;
2429
2430 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2435
2436 fn text_for_range(
2441 &mut self,
2442 range_utf16: Range<usize>,
2443 adjusted_range: &mut Option<Range<usize>>,
2444 window: &mut Window,
2445 cx: &mut App,
2446 ) -> Option<String>;
2447
2448 fn replace_text_in_range(
2453 &mut self,
2454 replacement_range: Option<Range<usize>>,
2455 text: &str,
2456 window: &mut Window,
2457 cx: &mut App,
2458 );
2459
2460 fn replace_and_mark_text_in_range(
2467 &mut self,
2468 range_utf16: Option<Range<usize>>,
2469 new_text: &str,
2470 new_selected_range: Option<Range<usize>>,
2471 window: &mut Window,
2472 cx: &mut App,
2473 );
2474
2475 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2478
2479 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2486 if let Some(text) = item.text() {
2487 self.replace_text_in_range(None, &text, window, cx);
2488 }
2489 }
2490
2491 fn bounds_for_range(
2496 &mut self,
2497 range_utf16: Range<usize>,
2498 window: &mut Window,
2499 cx: &mut App,
2500 ) -> Option<Bounds<Pixels>>;
2501
2502 fn selection_bounds_for_range(
2509 &mut self,
2510 _range_utf16: Range<usize>,
2511 _window: &mut Window,
2512 _cx: &mut App,
2513 ) -> Vec<Bounds<Pixels>> {
2514 Vec::new()
2515 }
2516
2517 fn character_index_for_point(
2521 &mut self,
2522 point: Point<Pixels>,
2523 window: &mut Window,
2524 cx: &mut App,
2525 ) -> Option<usize>;
2526
2527 fn set_selected_text_range(
2537 &mut self,
2538 _range_utf16: Range<usize>,
2539 _window: &mut Window,
2540 _cx: &mut App,
2541 ) {
2542 }
2543
2544 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2551 None
2552 }
2553
2554 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2556 None
2557 }
2558
2559 #[allow(dead_code)]
2564 fn apple_press_and_hold_enabled(&mut self) -> bool {
2565 true
2566 }
2567
2568 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2570 true
2571 }
2572
2573 fn text_input_editable_range(
2584 &mut self,
2585 _window: &mut Window,
2586 _cx: &mut App,
2587 ) -> Option<Range<usize>> {
2588 None
2589 }
2590
2591 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2600 false
2601 }
2602
2603 fn text_input_configuration(
2609 &mut self,
2610 _window: &mut Window,
2611 _cx: &mut App,
2612 ) -> TextInputConfiguration {
2613 TextInputConfiguration::default()
2614 }
2615}
2616
2617#[derive(Clone, Debug, Default, PartialEq, Eq)]
2627pub struct TextInputConfiguration {
2628 pub autocorrect: bool,
2630 pub autocapitalize: Autocapitalize,
2632 pub suggestions: bool,
2634 pub input_action: TextInputAction,
2636}
2637
2638#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2640pub enum Autocapitalize {
2641 #[default]
2643 None,
2644 Words,
2646 Sentences,
2648 Characters,
2650}
2651
2652#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2662pub enum TextInputAction {
2663 #[default]
2665 Unspecified,
2666 Enter,
2668 Done,
2670 Go,
2672 Next,
2674 Previous,
2676 Search,
2678 Send,
2680}
2681
2682#[derive(Debug)]
2684pub struct WindowOptions {
2685 pub window_bounds: Option<WindowBounds>,
2689
2690 pub titlebar: Option<TitlebarOptions>,
2692
2693 pub focus: bool,
2695
2696 pub show: bool,
2698
2699 pub kind: WindowKind,
2701
2702 pub is_movable: bool,
2706
2707 pub app_owns_titlebar_drag: bool,
2719
2720 pub inactive_frame_interval: Option<Duration>,
2724
2725 pub is_resizable: bool,
2727
2728 pub is_minimizable: bool,
2730
2731 pub display_id: Option<DisplayId>,
2734
2735 pub window_background: WindowBackgroundAppearance,
2737
2738 pub app_id: Option<String>,
2740
2741 pub window_min_size: Option<Size<Pixels>>,
2743
2744 pub window_decorations: Option<WindowDecorations>,
2747
2748 pub icon: Option<Arc<image::RgbaImage>>,
2750
2751 pub tabbing_identifier: Option<String>,
2753
2754 pub use_display_p3: bool,
2759}
2760
2761#[derive(Debug)]
2763#[cfg_attr(
2764 all(
2765 any(target_os = "linux", target_os = "freebsd"),
2766 not(any(feature = "x11", feature = "wayland"))
2767 ),
2768 allow(dead_code)
2769)]
2770#[allow(missing_docs)]
2771pub struct WindowParams {
2772 pub bounds: Bounds<Pixels>,
2773
2774 #[cfg_attr(feature = "wayland", allow(dead_code))]
2776 pub titlebar: Option<TitlebarOptions>,
2777
2778 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2780 pub kind: WindowKind,
2781
2782 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2784 pub is_movable: bool,
2785
2786 #[cfg_attr(
2788 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2789 allow(dead_code)
2790 )]
2791 pub app_owns_titlebar_drag: bool,
2792
2793 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2795 pub is_resizable: bool,
2796
2797 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2799 pub is_minimizable: bool,
2800
2801 #[cfg_attr(
2802 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2803 allow(dead_code)
2804 )]
2805 pub focus: bool,
2806
2807 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2808 pub show: bool,
2809
2810 #[cfg_attr(feature = "wayland", allow(dead_code))]
2812 pub icon: Option<Arc<image::RgbaImage>>,
2813
2814 #[cfg_attr(feature = "wayland", allow(dead_code))]
2815 pub display_id: Option<DisplayId>,
2816
2817 #[cfg_attr(feature = "wayland", allow(dead_code))]
2818 pub app_id: Option<String>,
2819
2820 pub window_min_size: Option<Size<Pixels>>,
2821
2822 #[cfg(target_os = "macos")]
2823 pub tabbing_identifier: Option<String>,
2824
2825 #[cfg(target_os = "macos")]
2826 pub use_display_p3: bool,
2827}
2828
2829#[derive(Debug, Copy, Clone, PartialEq)]
2831pub enum WindowBounds {
2832 Windowed(Bounds<Pixels>),
2834 Maximized(Bounds<Pixels>),
2837 Fullscreen(Bounds<Pixels>),
2840}
2841
2842impl Default for WindowBounds {
2843 fn default() -> Self {
2844 WindowBounds::Windowed(Bounds::default())
2845 }
2846}
2847
2848impl WindowBounds {
2849 pub fn get_bounds(&self) -> Bounds<Pixels> {
2851 match self {
2852 WindowBounds::Windowed(bounds) => *bounds,
2853 WindowBounds::Maximized(bounds) => *bounds,
2854 WindowBounds::Fullscreen(bounds) => *bounds,
2855 }
2856 }
2857
2858 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2860 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2861 }
2862}
2863
2864impl Default for WindowOptions {
2865 fn default() -> Self {
2866 Self {
2867 window_bounds: None,
2868 titlebar: Some(TitlebarOptions {
2869 title: Default::default(),
2870 appears_transparent: Default::default(),
2871 traffic_light_position: Default::default(),
2872 }),
2873 focus: true,
2874 show: true,
2875 kind: WindowKind::Normal,
2876 is_movable: true,
2877 app_owns_titlebar_drag: false,
2878 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2879 is_resizable: true,
2880 is_minimizable: true,
2881 display_id: None,
2882 window_background: WindowBackgroundAppearance::default(),
2883 icon: None,
2884 app_id: None,
2885 window_min_size: None,
2886 window_decorations: None,
2887 tabbing_identifier: None,
2888 use_display_p3: false,
2889 }
2890 }
2891}
2892
2893#[derive(Debug, Default)]
2895pub struct TitlebarOptions {
2896 pub title: Option<SharedString>,
2898
2899 pub appears_transparent: bool,
2902
2903 pub traffic_light_position: Option<Point<Pixels>>,
2905}
2906
2907#[derive(Clone, Debug, PartialEq, Eq)]
2909pub enum WindowKind {
2910 Normal,
2912
2913 PopUp,
2916
2917 AnchoredPopup(popup::PopupOptions),
2924
2925 Floating,
2927
2928 #[cfg(all(target_os = "linux", feature = "wayland"))]
2931 LayerShell(layer_shell::LayerShellOptions),
2932
2933 Dialog,
2936}
2937
2938#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2943pub enum WindowAppearance {
2944 #[default]
2948 Light,
2949
2950 VibrantLight,
2954
2955 Dark,
2959
2960 VibrantDark,
2964}
2965
2966#[derive(Copy, Clone, Debug, Default, PartialEq)]
2969pub enum WindowBackgroundAppearance {
2970 #[default]
2978 Opaque,
2979 Transparent,
2981 Blurred,
2985 MicaBackdrop,
2987 MicaAltBackdrop,
2989}
2990
2991#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2993pub enum TextRenderingMode {
2994 #[default]
2996 PlatformDefault,
2997 Subpixel,
2999 Grayscale,
3001}
3002
3003#[derive(Clone, Debug)]
3005pub struct PathPromptOptions {
3006 pub files: bool,
3008 pub directories: bool,
3010 pub multiple: bool,
3012 pub prompt: Option<SharedString>,
3014}
3015
3016#[derive(Copy, Clone, Debug, PartialEq)]
3018pub enum PromptLevel {
3019 Info,
3021
3022 Warning,
3024
3025 Critical,
3027}
3028
3029#[derive(Clone, Debug, PartialEq)]
3031pub enum PromptButton {
3032 Ok(SharedString),
3034 Cancel(SharedString),
3036 Other(SharedString),
3038}
3039
3040impl PromptButton {
3041 pub fn new(label: impl Into<SharedString>) -> Self {
3043 PromptButton::Other(label.into())
3044 }
3045
3046 pub fn ok(label: impl Into<SharedString>) -> Self {
3048 PromptButton::Ok(label.into())
3049 }
3050
3051 pub fn cancel(label: impl Into<SharedString>) -> Self {
3053 PromptButton::Cancel(label.into())
3054 }
3055
3056 #[allow(dead_code)]
3058 pub fn is_cancel(&self) -> bool {
3059 matches!(self, PromptButton::Cancel(_))
3060 }
3061
3062 pub fn label(&self) -> &SharedString {
3064 match self {
3065 PromptButton::Ok(label) => label,
3066 PromptButton::Cancel(label) => label,
3067 PromptButton::Other(label) => label,
3068 }
3069 }
3070}
3071
3072impl From<&str> for PromptButton {
3073 fn from(value: &str) -> Self {
3074 match value.to_lowercase().as_str() {
3075 "ok" => PromptButton::Ok("OK".into()),
3076 "cancel" => PromptButton::Cancel("Cancel".into()),
3077 _ => PromptButton::Other(SharedString::from(value.to_owned())),
3078 }
3079 }
3080}
3081
3082#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3084pub enum CursorStyle {
3085 #[default]
3087 Arrow,
3088
3089 IBeam,
3092
3093 Crosshair,
3096
3097 ClosedHand,
3100
3101 OpenHand,
3104
3105 PointingHand,
3108
3109 ResizeLeft,
3112
3113 ResizeRight,
3116
3117 ResizeLeftRight,
3120
3121 ResizeUp,
3124
3125 ResizeDown,
3128
3129 ResizeUpDown,
3132
3133 ResizeUpLeftDownRight,
3136
3137 ResizeUpRightDownLeft,
3140
3141 ResizeColumn,
3144
3145 ResizeRow,
3148
3149 IBeamCursorForVerticalLayout,
3152
3153 OperationNotAllowed,
3156
3157 DragLink,
3160
3161 DragCopy,
3164
3165 ContextualMenu,
3168}
3169
3170#[derive(Clone, Debug, Eq, PartialEq)]
3172pub struct ClipboardItem {
3173 pub entries: Vec<ClipboardEntry>,
3175}
3176
3177#[derive(Clone, Debug, PartialEq, Eq)]
3182pub enum ClipboardReadError {
3183 Unavailable,
3187 Denied(String),
3190 UnsupportedContent,
3193}
3194
3195impl std::fmt::Display for ClipboardReadError {
3196 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3197 match self {
3198 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3199 Self::Denied(message) => {
3200 write!(formatter, "clipboard access was denied: {message}")
3201 }
3202 Self::UnsupportedContent => {
3203 formatter.write_str("the clipboard contents are unsupported")
3204 }
3205 }
3206 }
3207}
3208
3209impl std::error::Error for ClipboardReadError {}
3210
3211#[derive(Clone, Debug, Eq, PartialEq)]
3213pub enum ClipboardEntry {
3214 String(ClipboardString),
3216 Image(Image),
3218 ExternalPaths(crate::ExternalPaths),
3220}
3221
3222impl ClipboardItem {
3223 pub fn new_string(text: String) -> Self {
3225 Self {
3226 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3227 }
3228 }
3229
3230 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3232 Self {
3233 entries: vec![ClipboardEntry::String(ClipboardString {
3234 text,
3235 metadata: Some(metadata),
3236 })],
3237 }
3238 }
3239
3240 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3242 Self {
3243 entries: vec![ClipboardEntry::String(
3244 ClipboardString::new(text).with_json_metadata(metadata),
3245 )],
3246 }
3247 }
3248
3249 pub fn new_image(image: &Image) -> Self {
3251 Self {
3252 entries: vec![ClipboardEntry::Image(image.clone())],
3253 }
3254 }
3255
3256 pub fn text(&self) -> Option<String> {
3259 let mut answer = String::new();
3260
3261 for entry in self.entries.iter() {
3262 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3263 answer.push_str(text);
3264 }
3265 }
3266
3267 if answer.is_empty() {
3268 for entry in self.entries.iter() {
3269 if let ClipboardEntry::ExternalPaths(paths) = entry {
3270 for path in &paths.0 {
3271 use std::fmt::Write as _;
3272 _ = write!(answer, "{}", path.display());
3273 }
3274 }
3275 }
3276 }
3277
3278 if !answer.is_empty() {
3279 Some(answer)
3280 } else {
3281 None
3282 }
3283 }
3284
3285 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3287 pub fn metadata(&self) -> Option<&String> {
3288 match self.entries().first() {
3289 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3290 clipboard_string.metadata.as_ref()
3291 }
3292 _ => None,
3293 }
3294 }
3295
3296 pub fn entries(&self) -> &[ClipboardEntry] {
3298 &self.entries
3299 }
3300
3301 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3303 self.entries.into_iter()
3304 }
3305}
3306
3307impl From<ClipboardString> for ClipboardEntry {
3308 fn from(value: ClipboardString) -> Self {
3309 Self::String(value)
3310 }
3311}
3312
3313impl From<String> for ClipboardEntry {
3314 fn from(value: String) -> Self {
3315 Self::from(ClipboardString::from(value))
3316 }
3317}
3318
3319impl From<Image> for ClipboardEntry {
3320 fn from(value: Image) -> Self {
3321 Self::Image(value)
3322 }
3323}
3324
3325impl From<ClipboardEntry> for ClipboardItem {
3326 fn from(value: ClipboardEntry) -> Self {
3327 Self {
3328 entries: vec![value],
3329 }
3330 }
3331}
3332
3333impl From<String> for ClipboardItem {
3334 fn from(value: String) -> Self {
3335 Self::from(ClipboardEntry::from(value))
3336 }
3337}
3338
3339impl From<Image> for ClipboardItem {
3340 fn from(value: Image) -> Self {
3341 Self::from(ClipboardEntry::from(value))
3342 }
3343}
3344
3345#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3347pub enum ImageFormat {
3348 Png,
3353 Jpeg,
3355 Webp,
3357 Gif,
3359 Svg,
3361 Bmp,
3363 Tiff,
3365 Ico,
3367 Pnm,
3369}
3370
3371impl ImageFormat {
3372 pub const fn mime_type(self) -> &'static str {
3374 match self {
3375 ImageFormat::Png => "image/png",
3376 ImageFormat::Jpeg => "image/jpeg",
3377 ImageFormat::Webp => "image/webp",
3378 ImageFormat::Gif => "image/gif",
3379 ImageFormat::Svg => "image/svg+xml",
3380 ImageFormat::Bmp => "image/bmp",
3381 ImageFormat::Tiff => "image/tiff",
3382 ImageFormat::Ico => "image/ico",
3383 ImageFormat::Pnm => "image/x-portable-anymap",
3384 }
3385 }
3386
3387 pub const fn extension(self) -> &'static str {
3389 match self {
3390 ImageFormat::Png => "png",
3391 ImageFormat::Jpeg => "jpg",
3392 ImageFormat::Webp => "webp",
3393 ImageFormat::Gif => "gif",
3394 ImageFormat::Svg => "svg",
3395 ImageFormat::Bmp => "bmp",
3396 ImageFormat::Tiff => "tiff",
3397 ImageFormat::Ico => "ico",
3398 ImageFormat::Pnm => "pnm",
3399 }
3400 }
3401
3402 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3404 use strum::IntoEnumIterator;
3405 Self::iter()
3406 .find(|format| format.mime_type() == mime_type)
3407 .or_else(|| Self::from_mime_type_alias(mime_type))
3408 }
3409
3410 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3414 match mime_type {
3415 "image/jpg" => Some(Self::Jpeg),
3416 "image/tif" => Some(Self::Tiff),
3417 _ => None,
3418 }
3419 }
3420}
3421
3422#[derive(Clone, Debug, PartialEq, Eq)]
3424pub struct Image {
3425 pub format: ImageFormat,
3427 pub bytes: Vec<u8>,
3429 pub id: u64,
3431}
3432
3433pub(crate) fn decode_static_image(
3434 bytes: &[u8],
3435 format: image::ImageFormat,
3436) -> Result<SmallVec<[Frame; 1]>> {
3437 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3438 .into_decoder()
3439 .context("creating image decoder")?;
3440 decode_static_image_from_decoder(decoder)
3441}
3442
3443pub(crate) fn decode_static_image_from_decoder(
3444 mut decoder: impl image::ImageDecoder,
3445) -> Result<SmallVec<[Frame; 1]>> {
3446 let orientation = decoder
3447 .orientation()
3448 .context("reading decoder's orientation")?;
3449 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3450 image.apply_orientation(orientation);
3451
3452 let mut data = image.into_rgba8();
3453 for pixel in data.chunks_exact_mut(4) {
3454 pixel.swap(0, 2);
3455 }
3456
3457 Ok(SmallVec::from_elem(Frame::new(data), 1))
3458}
3459
3460impl Hash for Image {
3461 fn hash<H: Hasher>(&self, state: &mut H) {
3462 state.write_u64(self.id);
3463 }
3464}
3465
3466impl Image {
3467 pub fn empty() -> Self {
3469 Self::from_bytes(ImageFormat::Png, Vec::new())
3470 }
3471
3472 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3474 Self {
3475 id: hash(&bytes),
3476 format,
3477 bytes,
3478 }
3479 }
3480
3481 pub fn id(&self) -> u64 {
3483 self.id
3484 }
3485
3486 pub fn use_render_image(
3488 self: Arc<Self>,
3489 window: &mut Window,
3490 cx: &mut App,
3491 ) -> Option<Arc<RenderImage>> {
3492 ImageSource::Image(self)
3493 .use_data(None, window, cx)
3494 .and_then(|result| result.ok())
3495 }
3496
3497 pub fn get_render_image(
3499 self: Arc<Self>,
3500 window: &mut Window,
3501 cx: &mut App,
3502 ) -> Option<Arc<RenderImage>> {
3503 ImageSource::Image(self)
3504 .get_data(None, window, cx)
3505 .and_then(|result| result.ok())
3506 }
3507
3508 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3510 ImageSource::Image(self).remove_asset(cx);
3511 }
3512
3513 #[cfg(any(test, feature = "test-support"))]
3516 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3517 ImageSource::Image(self.clone()).is_asset_cached(cx)
3518 }
3519
3520 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3522 let frames = match self.format {
3523 ImageFormat::Gif => {
3524 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3525 let mut frames = SmallVec::new();
3526
3527 for frame in decoder.into_frames() {
3528 match frame {
3529 Ok(mut frame) => {
3530 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3532 pixel.swap(0, 2);
3533 }
3534 frames.push(frame);
3535 }
3536 Err(err) => {
3537 log::debug!("Skipping GIF frame due to decode error: {err}");
3538 }
3539 }
3540 }
3541
3542 if frames.is_empty() {
3543 anyhow::bail!("GIF could not be decoded: all frames failed");
3544 }
3545
3546 frames
3547 }
3548 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3549 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3550 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3551 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3552 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3553 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3554 ImageFormat::Svg => {
3555 return svg_renderer
3556 .render_single_frame(&self.bytes, 1.0)
3557 .map_err(Into::into);
3558 }
3559 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3560 };
3561
3562 Ok(Arc::new(RenderImage::new(frames)))
3563 }
3564
3565 pub fn format(&self) -> ImageFormat {
3567 self.format
3568 }
3569
3570 pub fn bytes(&self) -> &[u8] {
3572 self.bytes.as_slice()
3573 }
3574}
3575
3576#[derive(Clone, Debug, Eq, PartialEq)]
3578pub struct ClipboardString {
3579 pub text: String,
3581 pub metadata: Option<String>,
3583}
3584
3585impl ClipboardString {
3586 pub fn new(text: String) -> Self {
3588 Self {
3589 text,
3590 metadata: None,
3591 }
3592 }
3593
3594 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3597 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3598 self
3599 }
3600
3601 pub fn text(&self) -> &String {
3603 &self.text
3604 }
3605
3606 pub fn into_text(self) -> String {
3608 self.text
3609 }
3610
3611 pub fn metadata_json<T>(&self) -> Option<T>
3613 where
3614 T: for<'a> Deserialize<'a>,
3615 {
3616 self.metadata
3617 .as_ref()
3618 .and_then(|m| serde_json::from_str(m).ok())
3619 }
3620
3621 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3622 pub fn text_hash(text: &str) -> u64 {
3624 let mut hasher = SeaHasher::new();
3625 text.hash(&mut hasher);
3626 hasher.finish()
3627 }
3628}
3629
3630impl From<String> for ClipboardString {
3631 fn from(value: String) -> Self {
3632 Self {
3633 text: value,
3634 metadata: None,
3635 }
3636 }
3637}
3638
3639#[cfg(test)]
3640mod image_tests {
3641 use super::*;
3642 use std::sync::Arc;
3643
3644 #[test]
3645 fn test_image_to_image_data_applies_exif_orientation() {
3646 let image = Image::from_bytes(
3647 ImageFormat::Jpeg,
3648 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3649 );
3650
3651 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3652
3653 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3654
3655 let bytes = render_image.as_bytes(0).unwrap();
3656 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3657 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3658 }
3659
3660 #[test]
3661 fn test_svg_image_to_image_data_converts_to_bgra() {
3662 let image = Image::from_bytes(
3663 ImageFormat::Svg,
3664 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3665<rect width="1" height="1" fill="#38BDF8"/>
3666</svg>"##
3667 .to_vec(),
3668 );
3669
3670 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3671 let bytes = render_image.as_bytes(0).unwrap();
3672
3673 for pixel in bytes.chunks_exact(4) {
3674 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3675 }
3676 }
3677}
3678
3679#[cfg(test)]
3680mod atlas_tests {
3681 use super::*;
3682
3683 const TILE_SIZE: Size<DevicePixels> = Size {
3684 width: DevicePixels(1),
3685 height: DevicePixels(1),
3686 };
3687
3688 #[derive(Default)]
3689 struct RecordingAtlasBackend {
3690 insert_calls: u32,
3691 fail_next_insert: bool,
3692 removed_tiles: Vec<AtlasTile>,
3693 }
3694
3695 impl AtlasBackend for RecordingAtlasBackend {
3696 fn insert(
3697 &mut self,
3698 kind: AtlasTextureKind,
3699 size: Size<DevicePixels>,
3700 _bytes: &[u8],
3701 ) -> Result<AtlasTile> {
3702 self.insert_calls += 1;
3703 if std::mem::take(&mut self.fail_next_insert) {
3704 anyhow::bail!("backend failed");
3705 }
3706 Ok(AtlasTile {
3707 texture_id: AtlasTextureId { index: 0, kind },
3708 tile_id: TileId(self.insert_calls),
3709 padding: 0,
3710 bounds: Bounds {
3711 origin: Point::default(),
3712 size,
3713 },
3714 })
3715 }
3716
3717 fn remove(&mut self, tile: AtlasTile) {
3718 self.removed_tiles.push(tile);
3719 }
3720 }
3721
3722 fn image_key(image_id: usize) -> AtlasKey {
3723 AtlasKey::Image(RenderImageParams {
3724 image_id: crate::ImageId(image_id),
3725 frame_index: 0,
3726 })
3727 }
3728
3729 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3730 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3731 }
3732
3733 #[test]
3734 fn only_successful_inserts_are_cached() -> Result<()> {
3735 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3736 let key = image_key(1);
3737
3738 assert_eq!(
3739 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3740 None
3741 );
3742 state
3743 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3744 .expect_err("builder error should propagate");
3745 assert!(!state.contains(&key));
3746 assert_eq!(state.backend.insert_calls, 0);
3747
3748 state.backend.fail_next_insert = true;
3749 state
3750 .get_or_insert_with(key.clone(), &mut build_tile)
3751 .expect_err("backend error should propagate");
3752 assert!(!state.contains(&key));
3753 assert_eq!(state.backend.insert_calls, 1);
3754
3755 let tile = state
3756 .get_or_insert_with(key.clone(), &mut build_tile)?
3757 .context("builder should produce a tile")?;
3758 assert_eq!(tile.texture_id.kind, key.texture_kind());
3759 assert_eq!(
3760 state.get_or_insert_with(key.clone(), &mut || {
3761 anyhow::bail!("cache hit must not call the builder")
3762 })?,
3763 Some(tile)
3764 );
3765 assert!(state.contains(&key));
3766 assert_eq!(state.backend.insert_calls, 2);
3767 Ok(())
3768 }
3769
3770 #[test]
3771 fn remove_and_clear_invalidate_keys() -> Result<()> {
3772 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3773 let key = image_key(1);
3774 let other_key = image_key(2);
3775 let tile = state
3776 .get_or_insert_with(key.clone(), &mut build_tile)?
3777 .context("builder should produce a tile")?;
3778 state
3779 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3780 .context("builder should produce another tile")?;
3781
3782 state.remove(&key);
3783 state.remove(&key);
3784 assert!(!state.contains(&key));
3785 assert!(state.contains(&other_key));
3786 assert_eq!(state.backend.removed_tiles, vec![tile]);
3787
3788 let mut reset_calls = 0;
3789 state.clear(|_| reset_calls += 1);
3790 assert_eq!(reset_calls, 1);
3791 assert!(!state.contains(&other_key));
3792 assert_eq!(state.backend.removed_tiles, vec![tile]);
3793 Ok(())
3794 }
3795}
3796
3797#[cfg(test)]
3798mod frame_signal_tests {
3799 use super::*;
3800
3801 #[cfg(feature = "profiler")]
3802 #[test]
3803 fn coalesced_signals_retain_the_earliest_until_drained() {
3804 for (first_source, later_source) in [
3805 (
3806 FrameRequestSource::NativeCallback,
3807 FrameRequestSource::LocalSchedule,
3808 ),
3809 (
3810 FrameRequestSource::LocalSchedule,
3811 FrameRequestSource::NativeCallback,
3812 ),
3813 ] {
3814 let signal = PlatformFrameSignal::new();
3815 let first =
3816 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3817 assert_eq!(signal.take(), None);
3818 signal.record(first + Duration::from_millis(16), later_source);
3819 signal.record(first, first_source);
3820 signal.record(first + Duration::from_millis(32), later_source);
3821 assert_eq!(signal.take(), Some((first, first_source)));
3822 assert_eq!(signal.take(), None);
3823 let next = first + Duration::from_millis(48);
3824 signal.record(next, later_source);
3825 assert_eq!(signal.take(), Some((next, later_source)));
3826 }
3827 }
3828
3829 #[cfg(not(feature = "profiler"))]
3830 #[test]
3831 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3832 let captured = PlatformFrameSignal::capture(|| {
3833 panic!("profiler-disabled builds must not evaluate the capture closure");
3834 });
3835 assert_eq!(captured, None);
3836 let signal = PlatformFrameSignal::new();
3837 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3838 assert_eq!(signal.take(), None);
3839 }
3840}
3841
3842#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3843mod tests {
3844 use super::*;
3845 use std::collections::HashSet;
3846
3847 #[test]
3848 fn test_refresh_interval_from_hz() {
3849 assert_eq!(
3850 refresh_interval_from_hz(50.0),
3851 Some(Duration::from_millis(20))
3852 );
3853 assert_eq!(
3854 refresh_interval_from_hz(120.0),
3855 Some(Duration::from_secs(1) / 120)
3856 );
3857 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3858 assert_eq!(refresh_interval_from_hz(unknown), None);
3859 }
3860 }
3861
3862 #[test]
3863 fn test_window_button_layout_parse_standard() {
3864 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3865 assert_eq!(
3866 layout.left,
3867 [
3868 Some(WindowButton::Close),
3869 Some(WindowButton::Minimize),
3870 None
3871 ]
3872 );
3873 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3874 }
3875
3876 #[test]
3877 fn test_window_button_layout_parse_right_only() {
3878 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3879 assert_eq!(layout.left, [None, None, None]);
3880 assert_eq!(
3881 layout.right,
3882 [
3883 Some(WindowButton::Minimize),
3884 Some(WindowButton::Maximize),
3885 Some(WindowButton::Close)
3886 ]
3887 );
3888 }
3889
3890 #[test]
3891 fn test_window_button_layout_parse_left_only() {
3892 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3893 assert_eq!(
3894 layout.left,
3895 [
3896 Some(WindowButton::Close),
3897 Some(WindowButton::Minimize),
3898 Some(WindowButton::Maximize)
3899 ]
3900 );
3901 assert_eq!(layout.right, [None, None, None]);
3902 }
3903
3904 #[test]
3905 fn test_window_button_layout_parse_with_whitespace() {
3906 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3907 assert_eq!(
3908 layout.left,
3909 [
3910 Some(WindowButton::Close),
3911 Some(WindowButton::Minimize),
3912 None
3913 ]
3914 );
3915 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3916 }
3917
3918 #[test]
3919 fn test_window_button_layout_parse_empty() {
3920 let layout = WindowButtonLayout::parse("").unwrap();
3921 assert_eq!(layout.left, [None, None, None]);
3922 assert_eq!(layout.right, [None, None, None]);
3923 }
3924
3925 #[test]
3926 fn test_window_button_layout_parse_intentionally_empty() {
3927 let layout = WindowButtonLayout::parse(":").unwrap();
3928 assert_eq!(layout.left, [None, None, None]);
3929 assert_eq!(layout.right, [None, None, None]);
3930 }
3931
3932 #[test]
3933 fn test_window_button_layout_parse_invalid_buttons() {
3934 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3935 assert_eq!(
3936 layout.left,
3937 [
3938 Some(WindowButton::Close),
3939 Some(WindowButton::Minimize),
3940 None
3941 ]
3942 );
3943 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3944 }
3945
3946 #[test]
3947 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3948 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3949 assert_eq!(
3950 layout.right,
3951 [
3952 Some(WindowButton::Close),
3953 Some(WindowButton::Minimize),
3954 None
3955 ]
3956 );
3957 assert_eq!(layout.format(), ":close,minimize");
3958 }
3959
3960 #[test]
3961 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3962 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3963 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3964 assert_eq!(
3965 layout.right,
3966 [
3967 Some(WindowButton::Maximize),
3968 Some(WindowButton::Minimize),
3969 None
3970 ]
3971 );
3972
3973 let button_ids: Vec<_> = layout
3974 .left
3975 .iter()
3976 .chain(layout.right.iter())
3977 .flatten()
3978 .map(WindowButton::id)
3979 .collect();
3980 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3981 assert_eq!(unique_button_ids.len(), button_ids.len());
3982 assert_eq!(layout.format(), "close:maximize,minimize");
3983 }
3984
3985 #[test]
3986 fn test_window_button_layout_parse_gnome_style() {
3987 let layout = WindowButtonLayout::parse("close").unwrap();
3988 assert_eq!(layout.left, [None, None, None]);
3989 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3990 }
3991
3992 #[test]
3993 fn test_window_button_layout_parse_elementary_style() {
3994 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3995 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3996 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3997 }
3998
3999 #[test]
4000 fn test_window_button_layout_round_trip() {
4001 let cases = [
4002 "close:minimize,maximize",
4003 "minimize,maximize,close:",
4004 ":close",
4005 "close:",
4006 "close:maximize",
4007 ":",
4008 ];
4009
4010 for case in cases {
4011 let layout = WindowButtonLayout::parse(case).unwrap();
4012 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
4013 }
4014 }
4015
4016 #[test]
4017 fn test_window_button_layout_linux_default() {
4018 let layout = WindowButtonLayout::linux_default();
4019 assert_eq!(layout.left, [None, None, None]);
4020 assert_eq!(
4021 layout.right,
4022 [
4023 Some(WindowButton::Minimize),
4024 Some(WindowButton::Maximize),
4025 Some(WindowButton::Close)
4026 ]
4027 );
4028
4029 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
4030 assert_eq!(round_tripped, layout);
4031 }
4032
4033 #[test]
4034 fn test_window_button_layout_parse_all_invalid() {
4035 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
4036 }
4037}