1mod app_menu;
2mod keyboard;
3mod keystroke;
4mod platform_info;
5
6#[cfg(all(target_os = "linux", feature = "wayland"))]
7#[expect(missing_docs)]
8pub mod layer_shell;
9
10pub mod popup;
12
13#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
14mod threaded_dispatcher;
15
16#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
17mod test;
18
19#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
20mod visual_test;
21
22#[cfg(all(
23 feature = "screen-capture",
24 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
25))]
26pub mod scap_screen_capture;
27
28#[cfg(all(
29 any(target_os = "windows", target_os = "linux"),
30 feature = "screen-capture"
31))]
32pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
33#[cfg(not(feature = "screen-capture"))]
34pub(crate) type PlatformScreenCaptureFrame = ();
35#[cfg(all(target_os = "macos", feature = "screen-capture"))]
36pub(crate) type PlatformScreenCaptureFrame =
37 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
38
39use crate::{
40 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, Corners,
41 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
42 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
43 LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority,
44 RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
45 ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea,
46 hash, point, px, size,
47};
48#[cfg(any(target_os = "linux", target_os = "freebsd"))]
49use anyhow::bail;
50use anyhow::{Context as _, Result};
51use async_task::Runnable;
52use collections::FxHashMap;
53use futures::channel::oneshot;
54#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
55use image::RgbaImage;
56use image::codecs::gif::GifDecoder;
57use image::{AnimationDecoder as _, DynamicImage, Frame};
58use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
59use scheduler::Instant;
60pub use scheduler::RunnableMeta;
61use schemars::JsonSchema;
62use seahash::SeaHasher;
63use serde::{Deserialize, Serialize};
64use smallvec::SmallVec;
65use std::borrow::Cow;
66use std::collections::hash_map::Entry;
67use std::hash::{Hash, Hasher};
68use std::io::Cursor;
69use std::ops;
70use std::time::Duration;
71use std::{
72 ffi::OsString,
73 fmt::{self, Debug},
74 ops::Range,
75 path::{Path, PathBuf},
76 rc::Rc,
77 sync::Arc,
78};
79use strum::EnumIter;
80use uuid::Uuid;
81
82pub use app_menu::*;
83pub use keyboard::*;
84pub use keystroke::*;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum WindowVisibility {
108 Visible,
111 Hidden,
114}
115
116impl WindowVisibility {
117 pub fn is_visible(self) -> bool {
119 self == Self::Visible
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126#[non_exhaustive]
127pub enum DisplayEvent {
128 Added(DisplayId),
130 Removed(DisplayId),
133 Changed {
135 id: DisplayId,
137 changes: DisplayChanges,
139 },
140}
141
142bitflags::bitflags! {
143 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145 pub struct DisplayChanges: u8 {
146 const BOUNDS = 1 << 0;
148 const REFRESH_INTERVAL = 1 << 1;
150 }
151}
152
153pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
156 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
157}
158
159#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
166pub enum ActivationPolicy {
167 #[default]
169 Regular,
170 Accessory,
172}
173
174#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
175pub(crate) use test::*;
176
177#[cfg(any(test, feature = "test-support"))]
178pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
179
180#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
181pub use threaded_dispatcher::ThreadedDispatcher;
182
183#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
184pub use visual_test::VisualTestPlatform;
185
186pub struct ActivityGuard {
188 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
189}
190
191impl ActivityGuard {
192 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
194 Self {
195 _release: gpui_util::defer(Box::new(release)),
196 }
197 }
198
199 pub fn noop() -> Self {
201 Self::new(|| {})
202 }
203}
204
205#[cfg(any(target_os = "linux", target_os = "freebsd"))]
209#[inline]
210pub fn guess_compositor() -> &'static str {
211 if std::env::var_os("ZED_HEADLESS").is_some() {
212 return "Headless";
213 }
214 GraphicalEnvironment::detect().guess_compositor()
215}
216
217#[cfg(any(target_os = "linux", target_os = "freebsd"))]
226#[derive(Clone, Debug, Default)]
227pub struct GraphicalEnvironment {
228 pub wayland_display: Option<OsString>,
230 pub x11_display: Option<OsString>,
232 pub xdg_runtime_dir: Option<OsString>,
234 pub activation_token: Option<String>,
240}
241
242#[cfg(target_os = "windows")]
248#[derive(Clone, Debug, Default)]
249pub struct GraphicalEnvironment {
250 pub session_id: Option<u32>,
253}
254
255#[cfg(target_os = "windows")]
256impl GraphicalEnvironment {
257 pub fn detect() -> Self {
259 let mut session_id = 0;
260 let result = unsafe {
262 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
263 windows::Win32::System::Threading::GetCurrentProcessId(),
264 &mut session_id,
265 )
266 };
267 Self {
268 session_id: result.is_ok().then_some(session_id),
269 }
270 }
271
272 pub fn apply_to(&self, _command: &mut std::process::Command) {}
275}
276
277#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
279#[derive(Clone, Debug, Default)]
280pub struct GraphicalEnvironment;
281
282#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
283impl GraphicalEnvironment {
284 pub fn detect() -> Self {
286 Self
287 }
288
289 pub fn apply_to(&self, _command: &mut std::process::Command) {}
291}
292
293#[derive(Clone, Debug)]
295pub enum WindowingRequest {
296 Headless,
298 Windowed(GraphicalEnvironment),
300}
301
302#[cfg(any(target_os = "linux", target_os = "freebsd"))]
303impl GraphicalEnvironment {
304 pub fn detect() -> Self {
309 Self {
310 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
311 x11_display: std::env::var_os("DISPLAY"),
312 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
313 activation_token: None,
314 }
315 }
316
317 pub fn apply_to(&self, command: &mut std::process::Command) {
321 for (name, value) in [
322 ("WAYLAND_DISPLAY", &self.wayland_display),
323 ("DISPLAY", &self.x11_display),
324 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
325 ] {
326 match value {
327 Some(value) => command.env(name, value),
328 None => command.env_remove(name),
329 };
330 }
331 }
332
333 pub fn guess_compositor(&self) -> &'static str {
337 let is_set =
338 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
339 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
340 "Wayland"
341 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
342 "X11"
343 } else {
344 "Headless"
345 }
346 }
347}
348
349#[cfg(any(target_os = "linux", target_os = "freebsd"))]
350bitflags::bitflags! {
351 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
353 pub struct WindowingModes: u8 {
354 const WAYLAND = 1 << 0;
356 const X11 = 1 << 1;
358 const HEADLESS = 1 << 2;
360 }
361}
362
363pub use platform_info::{OperatingSystem, PlatformInfo, PlatformTarget};
364
365#[expect(missing_docs)]
366pub trait Platform: 'static {
367 fn platform_info(&self) -> PlatformInfo;
369
370 fn background_executor(&self) -> BackgroundExecutor;
371 fn foreground_executor(&self) -> ForegroundExecutor;
372 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
373
374 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
375 fn quit(&self);
376 fn set_initial_windowing(&self, _request: WindowingRequest) {}
381 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
382 Task::ready(Err(anyhow::anyhow!(
383 "this platform cannot switch between headless and windowed modes"
384 )))
385 }
386 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
387 fn activate(&self, ignoring_other_apps: bool);
388 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
390 fn hide(&self);
391 fn hide_other_apps(&self);
392 fn unhide_other_apps(&self);
393
394 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
395 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
396 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
401 fn active_window(&self) -> Option<AnyWindowHandle>;
402 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
403 None
404 }
405
406 fn is_screen_capture_supported(&self) -> bool {
407 false
408 }
409
410 fn screen_capture_sources(
411 &self,
412 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
413 let (sources_tx, sources_rx) = oneshot::channel();
414 sources_tx
415 .send(Err(anyhow::anyhow!(
416 "gpui was compiled without the screen-capture feature"
417 )))
418 .ok();
419 sources_rx
420 }
421
422 fn open_window(
423 &self,
424 handle: AnyWindowHandle,
425 options: WindowParams,
426 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
427
428 fn window_appearance(&self) -> WindowAppearance;
430
431 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
440
441 fn button_layout(&self) -> Option<WindowButtonLayout> {
443 None
444 }
445
446 fn open_url(&self, url: &str);
447 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
448 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
449
450 fn prompt_for_paths(
451 &self,
452 options: PathPromptOptions,
453 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
454 fn prompt_for_new_path(
455 &self,
456 directory: &Path,
457 suggested_name: Option<&str>,
458 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
459 fn can_select_mixed_files_and_dirs(&self) -> bool;
460 fn reveal_path(&self, path: &Path);
461 fn open_with_system(&self, path: &Path);
462
463 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
464 fn on_reopen(&self, callback: Box<dyn FnMut()>);
465 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
467 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
469
470 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
480
481 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
486
487 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
491 None
492 }
493
494 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
495 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
496 None
497 }
498
499 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
500 fn perform_dock_menu_action(&self, _action: usize) {}
501 fn add_recent_document(&self, _path: &Path) {}
502 fn update_jump_list(
503 &self,
504 _menus: Vec<MenuItem>,
505 _entries: Vec<SmallVec<[PathBuf; 2]>>,
506 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
507 Task::ready(Vec::new())
508 }
509 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
510 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
511 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
512
513 fn thermal_state(&self) -> ThermalState;
514 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
515 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
516
517 fn power_mode(&self) -> PowerMode {
520 PowerMode::Unknown
521 }
522 fn on_power_mode_change(&self, _callback: Box<dyn FnMut()>) {}
524
525 fn set_app_identity(&self, identifier: &str, name: &str) {
532 _ = (identifier, name);
533 }
534
535 fn show_system_notification(&self, notification: SystemNotification) {
542 _ = notification;
543 }
544
545 fn dismiss_system_notification(&self, tag: &str) {
550 _ = tag;
551 }
552
553 fn on_system_notification_response(
559 &self,
560 callback: Box<dyn FnMut(SystemNotificationResponse)>,
561 ) {
562 _ = callback;
563 }
564
565 fn compositor_name(&self) -> &'static str {
566 ""
567 }
568 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
571 None
572 }
573 fn app_path(&self) -> Result<PathBuf>;
574 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
575
576 fn set_cursor_style(&self, style: CursorStyle);
577
578 fn hide_cursor_until_mouse_moves(&self);
581
582 fn is_cursor_visible(&self) -> bool;
584
585 fn should_auto_hide_scrollbars(&self) -> bool;
586
587 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
588 fn write_to_clipboard(&self, item: ClipboardItem);
589
590 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
598 Task::ready(Ok(self.read_from_clipboard()))
599 }
600
601 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
602 fn read_from_primary(&self) -> Option<ClipboardItem>;
603 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
604 fn write_to_primary(&self, item: ClipboardItem);
605
606 #[cfg(target_os = "macos")]
607 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
608 #[cfg(target_os = "macos")]
609 fn write_to_find_pasteboard(&self, item: ClipboardItem);
610
611 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
612
613 #[cfg(target_os = "macos")]
617 fn write_credentials_if_not_exists(
618 &self,
619 _url: &str,
620 _username: &str,
621 _password: &[u8],
622 ) -> Task<Result<bool>> {
623 Task::ready(Err(anyhow::anyhow!(
624 "this platform does not support atomic credential writes"
625 )))
626 }
627
628 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
629 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
630
631 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
632 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
633 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
634}
635
636pub trait PlatformDisplay: Debug {
638 fn id(&self) -> DisplayId;
640
641 fn uuid(&self) -> Result<Uuid>;
644
645 fn bounds(&self) -> Bounds<Pixels>;
647
648 fn refresh_interval(&self) -> Option<Duration>;
655
656 fn visible_bounds(&self) -> Bounds<Pixels> {
660 self.bounds()
661 }
662
663 fn default_bounds(&self) -> Bounds<Pixels> {
665 let bounds = self.bounds();
666 let center = bounds.center();
667 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
668
669 let offset = clipped_window_size / 2.0;
670 let origin = point(center.x - offset.width, center.y - offset.height);
671 Bounds::new(origin, clipped_window_size)
672 }
673}
674
675#[derive(Clone, Debug, PartialEq, Eq)]
678pub struct SystemNotification {
679 pub tag: SharedString,
683 pub title: SharedString,
685 pub body: SharedString,
687 pub actions: Vec<SystemNotificationAction>,
690}
691
692#[derive(Clone, Debug, PartialEq, Eq, Hash)]
694pub struct SystemNotificationAction {
695 pub id: SharedString,
698 pub label: SharedString,
700}
701
702#[derive(Clone, Debug, PartialEq, Eq)]
704pub struct SystemNotificationResponse {
705 pub tag: SharedString,
707 pub action_id: Option<SharedString>,
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
714pub enum PowerMode {
715 Unknown,
718 LowPower,
720 Balanced,
722 HighPower,
724}
725
726#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub enum ThermalState {
729 Nominal,
731 Fair,
733 Serious,
735 Critical,
737}
738
739#[derive(Clone)]
741pub struct SourceMetadata {
742 pub id: u64,
744 pub label: Option<SharedString>,
746 pub is_main: Option<bool>,
748 pub resolution: Size<DevicePixels>,
750}
751
752pub trait ScreenCaptureSource {
754 fn metadata(&self) -> Result<SourceMetadata>;
756
757 fn stream(
760 &self,
761 foreground_executor: &ForegroundExecutor,
762 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
763 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
764}
765
766pub trait ScreenCaptureStream {
768 fn metadata(&self) -> Result<SourceMetadata>;
770}
771
772pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
774
775#[derive(PartialEq, Eq, Hash, Copy, Clone)]
777pub struct DisplayId(pub(crate) u64);
778
779impl DisplayId {
780 pub fn new(id: u64) -> Self {
782 Self(id)
783 }
784}
785
786impl From<u64> for DisplayId {
787 fn from(id: u64) -> Self {
788 Self(id)
789 }
790}
791
792impl From<DisplayId> for u64 {
793 fn from(id: DisplayId) -> Self {
794 id.0
795 }
796}
797
798impl Debug for DisplayId {
799 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
800 write!(f, "DisplayId({})", self.0)
801 }
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum ResizeEdge {
807 Top,
809 TopRight,
811 Right,
813 BottomRight,
815 Bottom,
817 BottomLeft,
819 Left,
821 TopLeft,
823}
824
825#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
827pub enum WindowDecorations {
828 #[default]
829 Server,
831 Client,
833}
834
835#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
837pub enum Decorations {
838 #[default]
840 Server,
841 Client {
843 tiling: Tiling,
845 },
846}
847
848#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
850pub struct WindowControls {
851 pub fullscreen: bool,
853 pub maximize: bool,
855 pub minimize: bool,
857 pub window_menu: bool,
859}
860
861impl Default for WindowControls {
862 fn default() -> Self {
863 Self {
865 fullscreen: true,
866 maximize: true,
867 minimize: true,
868 window_menu: true,
869 }
870 }
871}
872
873#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
875pub enum WindowButton {
876 Minimize,
878 Maximize,
880 Close,
882}
883
884impl WindowButton {
885 pub fn id(&self) -> &'static str {
887 match self {
888 WindowButton::Minimize => "minimize",
889 WindowButton::Maximize => "maximize",
890 WindowButton::Close => "close",
891 }
892 }
893
894 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
895 fn index(&self) -> usize {
896 match self {
897 WindowButton::Minimize => 0,
898 WindowButton::Maximize => 1,
899 WindowButton::Close => 2,
900 }
901 }
902}
903
904pub const MAX_BUTTONS_PER_SIDE: usize = 3;
906
907#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912pub struct WindowButtonLayout {
913 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
915 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
917}
918
919#[cfg(any(target_os = "linux", target_os = "freebsd"))]
920impl WindowButtonLayout {
921 pub fn linux_default() -> Self {
923 Self {
924 left: [None; MAX_BUTTONS_PER_SIDE],
925 right: [
926 Some(WindowButton::Minimize),
927 Some(WindowButton::Maximize),
928 Some(WindowButton::Close),
929 ],
930 }
931 }
932
933 pub fn parse(layout_string: &str) -> Result<Self> {
935 fn parse_side(
936 s: &str,
937 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
938 unrecognized: &mut Vec<String>,
939 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
940 let mut result = [None; MAX_BUTTONS_PER_SIDE];
941 let mut i = 0;
942 for name in s.split(',') {
943 let trimmed = name.trim();
944 if trimmed.is_empty() {
945 continue;
946 }
947 let button = match trimmed {
948 "minimize" => Some(WindowButton::Minimize),
949 "maximize" => Some(WindowButton::Maximize),
950 "close" => Some(WindowButton::Close),
951 other => {
952 unrecognized.push(other.to_string());
953 None
954 }
955 };
956 if let Some(button) = button {
957 if seen_buttons[button.index()] {
958 continue;
959 }
960 if let Some(slot) = result.get_mut(i) {
961 *slot = Some(button);
962 seen_buttons[button.index()] = true;
963 i += 1;
964 }
965 }
966 }
967 result
968 }
969
970 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
971 let mut unrecognized = Vec::new();
972 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
973 let layout = Self {
974 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
975 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
976 };
977
978 if !unrecognized.is_empty()
979 && layout.left.iter().all(Option::is_none)
980 && layout.right.iter().all(Option::is_none)
981 {
982 bail!(
983 "button layout string {:?} contains no valid buttons (unrecognized: {})",
984 layout_string,
985 unrecognized.join(", ")
986 );
987 }
988
989 Ok(layout)
990 }
991
992 #[cfg(test)]
994 pub fn format(&self) -> String {
995 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
996 buttons
997 .iter()
998 .flatten()
999 .map(|button| match button {
1000 WindowButton::Minimize => "minimize",
1001 WindowButton::Maximize => "maximize",
1002 WindowButton::Close => "close",
1003 })
1004 .collect::<Vec<_>>()
1005 .join(",")
1006 }
1007
1008 format!("{}:{}", format_side(&self.left), format_side(&self.right))
1009 }
1010}
1011
1012#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1014pub struct Tiling {
1015 pub top: bool,
1017 pub left: bool,
1019 pub right: bool,
1021 pub bottom: bool,
1023}
1024
1025impl Tiling {
1026 pub fn tiled() -> Self {
1028 Self {
1029 top: true,
1030 left: true,
1031 right: true,
1032 bottom: true,
1033 }
1034 }
1035
1036 pub fn is_tiled(&self) -> bool {
1038 self.top || self.left || self.right || self.bottom
1039 }
1040}
1041
1042pub struct A11yCallbacks {
1044 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1046 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1048 pub deactivation: Box<dyn Fn() + Send + 'static>,
1050}
1051
1052#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1054pub enum FrameRequestSource {
1055 #[default]
1057 NativeCallback,
1058 LocalSchedule,
1060}
1061
1062#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1063#[expect(missing_docs)]
1064pub struct RequestFrameOptions {
1065 pub require_presentation: bool,
1067 pub force_render: bool,
1069 pub signal_at: Option<Instant>,
1075 pub signal_source: FrameRequestSource,
1077}
1078
1079pub struct PlatformFrameSignal {
1088 #[cfg(feature = "profiler")]
1089 origin: Instant,
1090 #[cfg(feature = "profiler")]
1091 first_signal: std::sync::atomic::AtomicU64,
1092}
1093
1094impl Default for PlatformFrameSignal {
1095 fn default() -> Self {
1096 Self::new()
1097 }
1098}
1099
1100impl PlatformFrameSignal {
1101 pub fn new() -> Self {
1103 Self {
1104 #[cfg(feature = "profiler")]
1105 origin: Instant::now(),
1106 #[cfg(feature = "profiler")]
1107 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1108 }
1109 }
1110
1111 #[inline]
1115 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1116 if cfg!(feature = "profiler") {
1117 Some(capture())
1118 } else {
1119 None
1120 }
1121 }
1122
1123 #[inline]
1125 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1126 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1127 #[cfg(feature = "profiler")]
1128 {
1129 let nanoseconds = at
1130 .saturating_duration_since(self.origin)
1131 .as_nanos()
1132 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1133 let source_bit = match source {
1134 FrameRequestSource::NativeCallback => 0,
1135 FrameRequestSource::LocalSchedule => 1,
1136 };
1137 self.first_signal.fetch_min(
1138 (nanoseconds << 1) | source_bit,
1139 std::sync::atomic::Ordering::Relaxed,
1140 );
1141 }
1142 }
1143
1144 #[inline]
1146 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1147 #[cfg(feature = "profiler")]
1148 {
1149 let signal = self
1150 .first_signal
1151 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1152 (signal != u64::MAX).then(|| {
1153 let source = match signal & 1 {
1154 0 => FrameRequestSource::NativeCallback,
1155 _ => FrameRequestSource::LocalSchedule,
1156 };
1157 (self.origin + Duration::from_nanos(signal >> 1), source)
1158 })
1159 }
1160 #[cfg(not(feature = "profiler"))]
1161 {
1162 None
1163 }
1164 }
1165}
1166
1167#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1180pub enum AppLifecyclePhase {
1181 Active,
1183 Inactive,
1185 Background,
1188 Foreground,
1190 Disconnected,
1194}
1195
1196#[derive(Debug, Clone, Default, PartialEq)]
1203pub struct WindowInsets {
1204 pub safe_area: Edges<Pixels>,
1209 pub ime: Edges<Pixels>,
1213 pub corner_radii: Corners<Pixels>,
1218}
1219
1220impl WindowInsets {
1221 pub fn effective(&self) -> Edges<Pixels> {
1223 Edges {
1224 top: self.safe_area.top.max(self.ime.top),
1225 right: self.safe_area.right.max(self.ime.right),
1226 bottom: self.safe_area.bottom.max(self.ime.bottom),
1227 left: self.safe_area.left.max(self.ime.left),
1228 }
1229 }
1230}
1231
1232#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1234pub enum TextInputStateChange {
1235 FocusGained,
1237 FocusLost,
1239 SelectionChanged,
1241 ContentChanged,
1243}
1244
1245#[expect(missing_docs)]
1246pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1247 fn bounds(&self) -> Bounds<Pixels>;
1248 fn is_maximized(&self) -> bool;
1249 fn window_bounds(&self) -> WindowBounds;
1250 fn content_size(&self) -> Size<Pixels>;
1251 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1257 Bounds::new(Point::default(), self.content_size())
1258 }
1259 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1264 fn prepare_frame(&self) -> bool {
1269 false
1270 }
1271 fn resize(&mut self, size: Size<Pixels>);
1272 fn scale_factor(&self) -> f32;
1273 fn appearance(&self) -> WindowAppearance;
1274 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1275 fn mouse_position(&self) -> Point<Pixels>;
1276 fn modifiers(&self) -> Modifiers;
1277 fn capslock(&self) -> Capslock;
1278 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1279 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1280 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1285 fn prompt(
1286 &self,
1287 level: PromptLevel,
1288 msg: &str,
1289 detail: Option<&str>,
1290 answers: &[PromptButton],
1291 ) -> Option<oneshot::Receiver<usize>>;
1292 fn activate(&self);
1293 fn request_attention(&self) {}
1295 fn is_active(&self) -> bool;
1296 fn visibility(&self) -> WindowVisibility;
1299 fn is_hovered(&self) -> bool;
1300 fn background_appearance(&self) -> WindowBackgroundAppearance;
1301 fn set_title(&mut self, title: &str);
1302 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1303 fn minimize(&self);
1304 fn zoom(&self);
1305 fn toggle_fullscreen(&self);
1306 fn is_fullscreen(&self) -> bool;
1307 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1308 None
1309 }
1310 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1311 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1312 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1313 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1317 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1318 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1319 fn on_moved(&self, callback: Box<dyn FnMut()>);
1320 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1324 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1325 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1326 fn on_close(&self, callback: Box<dyn FnOnce()>);
1327 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1328 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1329 fn draw(&self, scene: &Scene);
1330 fn schedule_frame(&self) {}
1331 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1332 fn is_subpixel_rendering_supported(&self) -> bool;
1333
1334 fn get_title(&self) -> String {
1336 String::new()
1337 }
1338 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1339 None
1340 }
1341 fn tab_bar_visible(&self) -> bool {
1342 false
1343 }
1344 fn set_edited(&mut self, _edited: bool) {}
1345 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1346 fn toggle_simple_fullscreen(&self) {}
1347 fn is_simple_fullscreen(&self) -> bool {
1348 false
1349 }
1350 #[cfg(target_os = "macos")]
1351 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1352 fn show_character_palette(&self) {}
1353 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1354 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1355 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1356 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1357 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1358 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1359 fn merge_all_windows(&self) {}
1360 fn move_tab_to_new_window(&self) {}
1361 fn toggle_window_tab_overview(&self) {}
1362 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1363
1364 fn native_window_state(&self) -> Option<Vec<u8>> {
1365 None
1366 }
1367 fn restore_native_window_state(&self, _state: &[u8]) {}
1368
1369 #[cfg(target_os = "windows")]
1370 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1371
1372 fn inner_window_bounds(&self) -> WindowBounds {
1374 self.window_bounds()
1375 }
1376 fn request_decorations(&self, _decorations: WindowDecorations) {}
1377 fn show_window_menu(&self, _position: Point<Pixels>) {}
1378 fn start_window_move(&self) {}
1379 fn can_start_external_drag(&self) -> bool {
1380 false
1381 }
1382 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1383 false
1384 }
1385 fn start_window_resize(&self, _edge: ResizeEdge) {}
1386 fn set_exclusive_zone(&self, _zone: Pixels) {}
1387 #[cfg(all(target_os = "linux", feature = "wayland"))]
1388 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1389 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1390 fn window_decorations(&self) -> Decorations {
1391 Decorations::Server
1392 }
1393 fn set_app_id(&mut self, _app_id: &str) {}
1394 fn map_window(&mut self) -> anyhow::Result<()> {
1395 Ok(())
1396 }
1397 fn window_controls(&self) -> WindowControls {
1398 WindowControls::default()
1399 }
1400 fn set_client_inset(&self, _inset: Pixels) {}
1401 fn set_client_corner_radii(&self, _radii: Corners<Pixels>) {}
1402 fn gpu_specs(&self) -> Option<GpuSpecs>;
1403
1404 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1405
1406 fn insets(&self) -> WindowInsets {
1411 WindowInsets::default()
1412 }
1413
1414 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1420
1421 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1424
1425 fn set_back_enabled(&self, _enabled: bool) {}
1428
1429 fn show_soft_keyboard(&self) {}
1431
1432 fn hide_soft_keyboard(&self) {}
1434
1435 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1437
1438 fn play_system_bell(&self) {}
1439
1440 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1442
1443 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1445
1446 fn a11y_update_window_bounds(&self) {}
1448
1449 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1450 fn as_test(&mut self) -> Option<&mut TestWindow> {
1451 None
1452 }
1453
1454 #[cfg(any(test, feature = "test-support"))]
1458 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1459 anyhow::bail!("render_to_image not implemented for this platform")
1460 }
1461}
1462
1463#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1465pub trait PlatformHeadlessRenderer {
1466 fn render_scene_to_image(
1468 &mut self,
1469 scene: &Scene,
1470 size: Size<DevicePixels>,
1471 ) -> Result<RgbaImage>;
1472
1473 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1479
1480 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1482}
1483
1484#[doc(hidden)]
1487pub type RunnableVariant = Runnable<RunnableMeta>;
1488
1489#[doc(hidden)]
1490pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1491
1492#[doc(hidden)]
1493pub enum TasksIncluded {
1494 OnlyCompleted,
1495 CompletedAndRunning,
1496}
1497
1498#[doc(hidden)]
1501pub trait PlatformDispatcher: Send + Sync {
1502 fn is_main_thread(&self) -> bool;
1503 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1504 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1505 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1506
1507 fn dispatch_on_main_thread_when_idle(
1508 &self,
1509 runnable: RunnableVariant,
1510 timeout: Option<Duration>,
1511 ) {
1512 let _ = timeout;
1513 self.dispatch_on_main_thread(runnable, Priority::Low);
1514 }
1515
1516 fn idle_time_remaining(&self) -> Option<Duration> {
1517 None
1518 }
1519
1520 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1521
1522 fn now(&self) -> Instant {
1523 Instant::now()
1524 }
1525
1526 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1527 gpui_util::defer(Box::new(|| {}))
1528 }
1529
1530 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1531 ActivityGuard::noop()
1532 }
1533
1534 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1535 fn as_test(&self) -> Option<&TestDispatcher> {
1536 None
1537 }
1538
1539 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1542 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1543 None
1544 }
1545}
1546
1547#[expect(missing_docs)]
1548pub trait PlatformTextSystem: Send + Sync {
1549 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1550 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1552 fn all_font_names(&self) -> Vec<String>;
1554 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1556 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1558 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1560 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1562 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1564 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1566 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1568 fn rasterize_glyph(
1570 &self,
1571 params: &RenderGlyphParams,
1572 raster_bounds: Bounds<DevicePixels>,
1573 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1574 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1576 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1578 -> TextRenderingMode;
1579 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1581 0
1582 }
1583}
1584
1585#[expect(missing_docs)]
1586pub struct NoopTextSystem;
1587
1588#[expect(missing_docs)]
1589impl NoopTextSystem {
1590 #[allow(dead_code)]
1591 pub fn new() -> Self {
1592 Self
1593 }
1594}
1595
1596impl PlatformTextSystem for NoopTextSystem {
1597 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1598 Ok(())
1599 }
1600
1601 fn all_font_names(&self) -> Vec<String> {
1602 Vec::new()
1603 }
1604
1605 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1606 Ok(FontId(1))
1607 }
1608
1609 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1610 FontMetrics {
1611 units_per_em: 1000,
1612 ascent: 1025.0,
1613 descent: -275.0,
1614 line_gap: 0.0,
1615 underline_position: -95.0,
1616 underline_thickness: 60.0,
1617 cap_height: 698.0,
1618 x_height: 516.0,
1619 bounding_box: Bounds {
1620 origin: Point {
1621 x: -260.0,
1622 y: -245.0,
1623 },
1624 size: Size {
1625 width: 1501.0,
1626 height: 1364.0,
1627 },
1628 },
1629 }
1630 }
1631
1632 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1633 Ok(Bounds {
1634 origin: Point { x: 54.0, y: 0.0 },
1635 size: size(392.0, 528.0),
1636 })
1637 }
1638
1639 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1640 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1641 }
1642
1643 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1644 Some(GlyphId(ch.len_utf16() as u32))
1645 }
1646
1647 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1648 Ok(Default::default())
1649 }
1650
1651 fn rasterize_glyph(
1652 &self,
1653 _params: &RenderGlyphParams,
1654 raster_bounds: Bounds<DevicePixels>,
1655 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1656 Ok((raster_bounds.size, Vec::new()))
1657 }
1658
1659 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1660 let mut position = px(0.);
1661 let metrics = self.font_metrics(FontId(0));
1662 let em_width = font_size
1663 * self
1664 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1665 .unwrap()
1666 .width
1667 / metrics.units_per_em as f32;
1668 let mut glyphs = Vec::new();
1669 for (ix, c) in text.char_indices() {
1670 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1671 glyphs.push(ShapedGlyph {
1672 id: glyph,
1673 position: point(position, px(0.)),
1674 index: ix,
1675 is_emoji: glyph.0 == 2,
1676 });
1677 if glyph.0 == 2 {
1678 position += em_width * 2.0;
1679 } else {
1680 position += em_width;
1681 }
1682 } else {
1683 position += em_width
1684 }
1685 }
1686 let mut runs = Vec::default();
1687 if !glyphs.is_empty() {
1688 runs.push(ShapedRun {
1689 font_id: FontId(0),
1690 glyphs,
1691 });
1692 } else {
1693 position = px(0.);
1694 }
1695
1696 LineLayout {
1697 font_size,
1698 width: position,
1699 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1700 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1701 runs,
1702 len: text.len(),
1703 }
1704 }
1705
1706 fn recommended_rendering_mode(
1707 &self,
1708 _font_id: FontId,
1709 _font_size: Pixels,
1710 ) -> TextRenderingMode {
1711 TextRenderingMode::Grayscale
1712 }
1713}
1714
1715#[allow(dead_code)]
1720pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1721 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1722 [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], ];
1736
1737 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1738 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1739
1740 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1741 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1742
1743 [
1744 ratios[0] * NORM13,
1745 ratios[1] * NORM24,
1746 ratios[2] * NORM13,
1747 ratios[3] * NORM24,
1748 ]
1749}
1750
1751#[derive(PartialEq, Eq, Hash, Clone)]
1752#[expect(missing_docs)]
1753pub enum AtlasKey {
1754 Glyph(RenderGlyphParams),
1755 Svg(RenderSvgParams),
1756 Image(RenderImageParams),
1757}
1758
1759impl AtlasKey {
1760 pub fn texture_kind(&self) -> AtlasTextureKind {
1762 match self {
1763 AtlasKey::Glyph(params) => {
1764 if params.is_emoji {
1765 AtlasTextureKind::Polychrome
1766 } else if params.subpixel_rendering {
1767 AtlasTextureKind::Subpixel
1768 } else {
1769 AtlasTextureKind::Monochrome
1770 }
1771 }
1772 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1773 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1774 }
1775 }
1776}
1777
1778impl From<RenderGlyphParams> for AtlasKey {
1779 fn from(params: RenderGlyphParams) -> Self {
1780 Self::Glyph(params)
1781 }
1782}
1783
1784impl From<RenderSvgParams> for AtlasKey {
1785 fn from(params: RenderSvgParams) -> Self {
1786 Self::Svg(params)
1787 }
1788}
1789
1790impl From<RenderImageParams> for AtlasKey {
1791 fn from(params: RenderImageParams) -> Self {
1792 Self::Image(params)
1793 }
1794}
1795
1796#[expect(missing_docs)]
1797pub trait PlatformAtlas {
1798 fn get_or_insert_with<'a>(
1800 &self,
1801 key: AtlasKey,
1802 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1803 ) -> Result<Option<AtlasTile>>;
1804 fn remove(&self, key: &AtlasKey);
1805
1806 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1807 fn contains(&self, _key: &AtlasKey) -> bool {
1808 false
1809 }
1810}
1811
1812#[doc(hidden)]
1813pub trait AtlasBackend {
1814 fn insert(
1815 &mut self,
1816 kind: AtlasTextureKind,
1817 size: Size<DevicePixels>,
1818 bytes: &[u8],
1819 ) -> Result<AtlasTile>;
1820
1821 fn remove(&mut self, tile: AtlasTile);
1822}
1823
1824#[doc(hidden)]
1825pub struct AtlasState<Backend> {
1826 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1827 pub backend: Backend,
1828}
1829
1830impl<Backend> AtlasState<Backend> {
1831 pub fn new(backend: Backend) -> Self {
1832 Self {
1833 tiles_by_key: FxHashMap::default(),
1834 backend,
1835 }
1836 }
1837
1838 pub fn contains(&self, key: &AtlasKey) -> bool {
1839 self.tiles_by_key.contains_key(key)
1840 }
1841
1842 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1843 self.tiles_by_key.clear();
1844 reset_backend(&mut self.backend);
1845 }
1846}
1847
1848impl<Backend: Default> Default for AtlasState<Backend> {
1849 fn default() -> Self {
1850 Self::new(Backend::default())
1851 }
1852}
1853
1854impl<Backend: AtlasBackend> AtlasState<Backend> {
1855 pub fn get_or_insert_with<'a>(
1856 &mut self,
1857 key: AtlasKey,
1858 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1859 ) -> Result<Option<AtlasTile>> {
1860 match self.tiles_by_key.entry(key) {
1861 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1862 Entry::Vacant(entry) => {
1863 profiling::scope!("new tile");
1864 let Some((size, bytes)) = build()? else {
1865 return Ok(None);
1866 };
1867 let tile = self
1868 .backend
1869 .insert(entry.key().texture_kind(), size, &bytes)?;
1870 entry.insert(tile);
1871 Ok(Some(tile))
1872 }
1873 }
1874 }
1875
1876 pub fn remove(&mut self, key: &AtlasKey) {
1877 if let Some(tile) = self.tiles_by_key.remove(key) {
1878 self.backend.remove(tile);
1879 }
1880 }
1881}
1882
1883#[derive(Default)]
1887pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1888
1889#[doc(hidden)]
1890#[derive(Default)]
1891pub struct HeadlessAtlasBackend {
1892 next_id: u32,
1893}
1894
1895impl AtlasBackend for HeadlessAtlasBackend {
1896 fn insert(
1897 &mut self,
1898 kind: AtlasTextureKind,
1899 size: Size<DevicePixels>,
1900 _bytes: &[u8],
1901 ) -> Result<AtlasTile> {
1902 self.next_id += 1;
1903 let texture_id = self.next_id;
1904 self.next_id += 1;
1905 let tile_id = self.next_id;
1906 Ok(AtlasTile {
1907 texture_id: AtlasTextureId {
1908 index: texture_id,
1909 kind,
1910 },
1911 tile_id: TileId(tile_id),
1912 padding: 0,
1913 bounds: Bounds {
1914 origin: Point::default(),
1915 size,
1916 },
1917 })
1918 }
1919
1920 fn remove(&mut self, _tile: AtlasTile) {}
1921}
1922
1923impl PlatformAtlas for HeadlessAtlas {
1924 fn get_or_insert_with<'a>(
1925 &self,
1926 key: AtlasKey,
1927 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1928 ) -> Result<Option<AtlasTile>> {
1929 self.0.lock().get_or_insert_with(key, build)
1930 }
1931
1932 fn remove(&self, key: &AtlasKey) {
1933 self.0.lock().remove(key);
1934 }
1935
1936 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1937 fn contains(&self, key: &AtlasKey) -> bool {
1938 self.0.lock().contains(key)
1939 }
1940}
1941
1942#[doc(hidden)]
1943pub struct AtlasTextureList<T> {
1944 pub textures: Vec<Option<T>>,
1945 pub free_list: Vec<usize>,
1946}
1947
1948impl<T> Default for AtlasTextureList<T> {
1949 fn default() -> Self {
1950 Self {
1951 textures: Vec::default(),
1952 free_list: Vec::default(),
1953 }
1954 }
1955}
1956
1957impl<T> ops::Index<usize> for AtlasTextureList<T> {
1958 type Output = Option<T>;
1959
1960 fn index(&self, index: usize) -> &Self::Output {
1961 &self.textures[index]
1962 }
1963}
1964
1965impl<T> AtlasTextureList<T> {
1966 #[allow(unused)]
1967 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1968 self.free_list.clear();
1969 self.textures.drain(..)
1970 }
1971
1972 #[allow(dead_code)]
1973 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1974 self.textures.iter_mut().flatten()
1975 }
1976}
1977
1978#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1979#[repr(C)]
1980#[expect(missing_docs)]
1981pub struct AtlasTile {
1982 pub texture_id: AtlasTextureId,
1984 pub tile_id: TileId,
1986 pub padding: u32,
1988 pub bounds: Bounds<DevicePixels>,
1990}
1991
1992#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1993#[repr(C)]
1994#[expect(missing_docs)]
1995pub struct AtlasTextureId {
1996 pub index: u32,
1999 pub kind: AtlasTextureKind,
2001}
2002
2003#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2004#[repr(C)]
2005#[cfg_attr(
2006 all(
2007 any(target_os = "linux", target_os = "freebsd"),
2008 not(any(feature = "x11", feature = "wayland"))
2009 ),
2010 allow(dead_code)
2011)]
2012#[expect(missing_docs)]
2013pub enum AtlasTextureKind {
2014 Monochrome = 0,
2015 Polychrome = 1,
2016 Subpixel = 2,
2017}
2018
2019#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2020#[repr(C)]
2021#[expect(missing_docs)]
2022pub struct TileId(pub u32);
2023
2024impl From<etagere::AllocId> for TileId {
2025 fn from(id: etagere::AllocId) -> Self {
2026 Self(id.serialize())
2027 }
2028}
2029
2030impl From<TileId> for etagere::AllocId {
2031 fn from(id: TileId) -> Self {
2032 Self::deserialize(id.0)
2033 }
2034}
2035
2036#[expect(missing_docs)]
2037pub struct PlatformInputHandler {
2038 cx: AsyncWindowContext,
2039 handler: Box<dyn InputHandler>,
2040 focus_id: Option<crate::FocusId>,
2041}
2042
2043#[expect(missing_docs)]
2044#[cfg_attr(
2045 all(
2046 any(target_os = "linux", target_os = "freebsd"),
2047 not(any(feature = "x11", feature = "wayland"))
2048 ),
2049 allow(dead_code)
2050)]
2051impl PlatformInputHandler {
2052 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2053 Self {
2054 cx,
2055 handler,
2056 focus_id: None,
2057 }
2058 }
2059
2060 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2061 self.focus_id = Some(focus_id);
2062 self
2063 }
2064
2065 pub fn focus_id(&self) -> Option<crate::FocusId> {
2070 self.focus_id
2071 }
2072
2073 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2074 self.cx
2075 .update(|window, cx| {
2076 self.handler
2077 .selected_text_range(ignore_disabled_input, window, cx)
2078 })
2079 .ok()
2080 .flatten()
2081 }
2082
2083 #[cfg_attr(target_os = "windows", allow(dead_code))]
2084 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2085 self.cx
2086 .update(|window, cx| self.handler.marked_text_range(window, cx))
2087 .ok()
2088 .flatten()
2089 }
2090
2091 #[cfg_attr(
2092 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2093 allow(dead_code)
2094 )]
2095 pub fn text_for_range(
2096 &mut self,
2097 range_utf16: Range<usize>,
2098 adjusted: &mut Option<Range<usize>>,
2099 ) -> Option<String> {
2100 self.cx
2101 .update(|window, cx| {
2102 self.handler
2103 .text_for_range(range_utf16, adjusted, window, cx)
2104 })
2105 .ok()
2106 .flatten()
2107 }
2108
2109 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2110 self.cx
2111 .update(|window, cx| {
2112 self.handler
2113 .replace_text_in_range(replacement_range, text, window, cx);
2114 })
2115 .ok();
2116 }
2117
2118 pub fn replace_and_mark_text_in_range(
2119 &mut self,
2120 range_utf16: Option<Range<usize>>,
2121 new_text: &str,
2122 new_selected_range: Option<Range<usize>>,
2123 ) {
2124 self.cx
2125 .update(|window, cx| {
2126 self.handler.replace_and_mark_text_in_range(
2127 range_utf16,
2128 new_text,
2129 new_selected_range,
2130 window,
2131 cx,
2132 )
2133 })
2134 .ok();
2135 }
2136
2137 #[cfg_attr(target_os = "windows", allow(dead_code))]
2138 pub fn unmark_text(&mut self) {
2139 self.cx
2140 .update(|window, cx| self.handler.unmark_text(window, cx))
2141 .ok();
2142 }
2143
2144 pub fn paste(&mut self, item: ClipboardItem) {
2145 self.cx
2146 .update(|window, cx| self.handler.paste(item, window, cx))
2147 .ok();
2148 }
2149
2150 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2151 self.cx
2152 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2153 .ok()
2154 .flatten()
2155 }
2156
2157 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2158 self.cx
2159 .update(|window, cx| {
2160 self.handler
2161 .selection_bounds_for_range(range_utf16, window, cx)
2162 })
2163 .unwrap_or_default()
2164 }
2165
2166 #[allow(dead_code)]
2167 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2168 self.handler.apple_press_and_hold_enabled()
2169 }
2170
2171 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2172 self.handler.replace_text_in_range(None, input, window, cx);
2173 }
2174
2175 pub fn compute_ime_candidate_bounds(
2176 marked_range: Option<Range<usize>>,
2177 selection: &UTF16Selection,
2178 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2179 ) -> Option<Bounds<Pixels>> {
2180 if let Some(marked_range) = marked_range {
2181 let mut line_start = marked_range.start;
2183
2184 let caret = selection.range.end;
2188 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2189 for i in (marked_range.start..caret).rev() {
2190 if let Some(b) = bounds_for_range(i..i) {
2191 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2192 line_start = i + 1;
2193 break;
2194 }
2195 }
2196 }
2197 }
2198 bounds_for_range(line_start..line_start)
2199 } else {
2200 let offset = if selection.reversed {
2202 selection.range.start
2203 } else {
2204 selection.range.end
2205 };
2206 bounds_for_range(offset..offset)
2207 }
2208 }
2209
2210 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2211 let marked_range = self.handler.marked_text_range(window, cx);
2212 let selection = self.handler.selected_text_range(true, window, cx)?;
2213 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2214 self.handler.bounds_for_range(range, window, cx)
2215 })
2216 }
2217
2218 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2219 let marked_range = self.marked_text_range();
2220 let selection = self.selected_text_range(true)?;
2221 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2222 self.bounds_for_range(range)
2223 })
2224 }
2225
2226 #[allow(unused)]
2227 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2228 self.cx
2229 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2230 .ok()
2231 .flatten()
2232 }
2233
2234 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2236 self.cx
2237 .update(|window, cx| {
2238 self.handler
2239 .set_selected_text_range(range_utf16, window, cx)
2240 })
2241 .ok();
2242 }
2243
2244 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2246 self.cx
2247 .update(|window, cx| self.handler.element_bounds(window, cx))
2248 .ok()
2249 .flatten()
2250 }
2251
2252 pub fn text_length_utf16(&mut self) -> Option<usize> {
2254 self.cx
2255 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2256 .ok()
2257 .flatten()
2258 }
2259
2260 #[allow(dead_code)]
2261 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2262 self.handler.accepts_text_input(window, cx)
2263 }
2264
2265 #[allow(dead_code)]
2266 pub fn query_accepts_text_input(&mut self) -> bool {
2267 self.cx
2268 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2269 .unwrap_or(true)
2270 }
2271
2272 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2277 let focus_id = self.focus_id;
2278 self.cx
2279 .update(|window, cx| {
2280 focus_id.is_some()
2281 && window.focus == focus_id
2282 && self.handler.accepts_text_input(window, cx)
2283 })
2284 .unwrap_or(false)
2285 }
2286
2287 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2293 self.cx
2294 .update(|window, cx| {
2295 !window.has_pending_keystrokes()
2297 && self.handler.prefers_ime_for_printable_keys(window, cx)
2298 })
2299 .unwrap_or(false)
2300 }
2301
2302 pub fn text_input_configuration(
2304 &mut self,
2305 window: &mut Window,
2306 cx: &mut App,
2307 ) -> TextInputConfiguration {
2308 self.handler.text_input_configuration(window, cx)
2309 }
2310
2311 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2313 self.cx
2314 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2315 .ok()
2316 .flatten()
2317 }
2318}
2319
2320#[derive(Debug)]
2323pub struct UTF16Selection {
2324 pub range: Range<usize>,
2327 pub reversed: bool,
2330}
2331
2332pub trait InputHandler: 'static {
2337 fn selected_text_range(
2342 &mut self,
2343 ignore_disabled_input: bool,
2344 window: &mut Window,
2345 cx: &mut App,
2346 ) -> Option<UTF16Selection>;
2347
2348 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2353
2354 fn text_for_range(
2359 &mut self,
2360 range_utf16: Range<usize>,
2361 adjusted_range: &mut Option<Range<usize>>,
2362 window: &mut Window,
2363 cx: &mut App,
2364 ) -> Option<String>;
2365
2366 fn replace_text_in_range(
2371 &mut self,
2372 replacement_range: Option<Range<usize>>,
2373 text: &str,
2374 window: &mut Window,
2375 cx: &mut App,
2376 );
2377
2378 fn replace_and_mark_text_in_range(
2385 &mut self,
2386 range_utf16: Option<Range<usize>>,
2387 new_text: &str,
2388 new_selected_range: Option<Range<usize>>,
2389 window: &mut Window,
2390 cx: &mut App,
2391 );
2392
2393 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2396
2397 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2404 if let Some(text) = item.text() {
2405 self.replace_text_in_range(None, &text, window, cx);
2406 }
2407 }
2408
2409 fn bounds_for_range(
2414 &mut self,
2415 range_utf16: Range<usize>,
2416 window: &mut Window,
2417 cx: &mut App,
2418 ) -> Option<Bounds<Pixels>>;
2419
2420 fn selection_bounds_for_range(
2427 &mut self,
2428 _range_utf16: Range<usize>,
2429 _window: &mut Window,
2430 _cx: &mut App,
2431 ) -> Vec<Bounds<Pixels>> {
2432 Vec::new()
2433 }
2434
2435 fn character_index_for_point(
2439 &mut self,
2440 point: Point<Pixels>,
2441 window: &mut Window,
2442 cx: &mut App,
2443 ) -> Option<usize>;
2444
2445 fn set_selected_text_range(
2455 &mut self,
2456 _range_utf16: Range<usize>,
2457 _window: &mut Window,
2458 _cx: &mut App,
2459 ) {
2460 }
2461
2462 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2469 None
2470 }
2471
2472 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2474 None
2475 }
2476
2477 #[allow(dead_code)]
2482 fn apple_press_and_hold_enabled(&mut self) -> bool {
2483 true
2484 }
2485
2486 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2488 true
2489 }
2490
2491 fn text_input_editable_range(
2502 &mut self,
2503 _window: &mut Window,
2504 _cx: &mut App,
2505 ) -> Option<Range<usize>> {
2506 None
2507 }
2508
2509 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2518 false
2519 }
2520
2521 fn text_input_configuration(
2527 &mut self,
2528 _window: &mut Window,
2529 _cx: &mut App,
2530 ) -> TextInputConfiguration {
2531 TextInputConfiguration::default()
2532 }
2533}
2534
2535#[derive(Clone, Debug, Default, PartialEq, Eq)]
2545pub struct TextInputConfiguration {
2546 pub autocorrect: bool,
2548 pub autocapitalize: Autocapitalize,
2550 pub suggestions: bool,
2552 pub input_action: TextInputAction,
2554}
2555
2556#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2558pub enum Autocapitalize {
2559 #[default]
2561 None,
2562 Words,
2564 Sentences,
2566 Characters,
2568}
2569
2570#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2580pub enum TextInputAction {
2581 #[default]
2583 Unspecified,
2584 Enter,
2586 Done,
2588 Go,
2590 Next,
2592 Previous,
2594 Search,
2596 Send,
2598}
2599
2600#[derive(Debug)]
2602pub struct WindowOptions {
2603 pub window_bounds: Option<WindowBounds>,
2607
2608 pub titlebar: Option<TitlebarOptions>,
2610
2611 pub focus: bool,
2613
2614 pub show: bool,
2616
2617 pub kind: WindowKind,
2619
2620 pub is_movable: bool,
2624
2625 pub app_owns_titlebar_drag: bool,
2637
2638 pub inactive_frame_interval: Option<Duration>,
2643
2644 pub low_power_frame_interval: Option<Duration>,
2649
2650 pub thermal_frame_interval: Option<Duration>,
2655
2656 pub is_resizable: bool,
2658
2659 pub is_minimizable: bool,
2661
2662 pub display_id: Option<DisplayId>,
2665
2666 pub window_background: WindowBackgroundAppearance,
2668
2669 pub app_id: Option<String>,
2671
2672 pub window_min_size: Option<Size<Pixels>>,
2674
2675 pub window_decorations: Option<WindowDecorations>,
2678
2679 pub icon: Option<Arc<image::RgbaImage>>,
2681
2682 pub tabbing_identifier: Option<String>,
2684
2685 pub use_display_p3: bool,
2687}
2688
2689#[derive(Debug)]
2691#[cfg_attr(
2692 all(
2693 any(target_os = "linux", target_os = "freebsd"),
2694 not(any(feature = "x11", feature = "wayland"))
2695 ),
2696 allow(dead_code)
2697)]
2698#[allow(missing_docs)]
2699pub struct WindowParams {
2700 pub bounds: Bounds<Pixels>,
2701
2702 #[cfg_attr(feature = "wayland", allow(dead_code))]
2704 pub titlebar: Option<TitlebarOptions>,
2705
2706 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2708 pub kind: WindowKind,
2709
2710 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2712 pub is_movable: bool,
2713
2714 #[cfg_attr(
2716 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2717 allow(dead_code)
2718 )]
2719 pub app_owns_titlebar_drag: bool,
2720
2721 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2723 pub is_resizable: bool,
2724
2725 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2727 pub is_minimizable: bool,
2728
2729 #[cfg_attr(
2730 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2731 allow(dead_code)
2732 )]
2733 pub focus: bool,
2734
2735 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2736 pub show: bool,
2737
2738 #[cfg_attr(feature = "wayland", allow(dead_code))]
2740 pub icon: Option<Arc<image::RgbaImage>>,
2741
2742 #[cfg_attr(feature = "wayland", allow(dead_code))]
2743 pub display_id: Option<DisplayId>,
2744
2745 #[cfg_attr(feature = "wayland", allow(dead_code))]
2746 pub app_id: Option<String>,
2747
2748 pub window_min_size: Option<Size<Pixels>>,
2749
2750 #[cfg(target_os = "macos")]
2751 pub tabbing_identifier: Option<String>,
2752
2753 #[cfg(target_os = "macos")]
2754 pub use_display_p3: bool,
2755}
2756
2757#[derive(Debug, Copy, Clone, PartialEq)]
2759pub enum WindowBounds {
2760 Windowed(Bounds<Pixels>),
2762 Maximized(Bounds<Pixels>),
2765 Fullscreen(Bounds<Pixels>),
2768}
2769
2770impl Default for WindowBounds {
2771 fn default() -> Self {
2772 WindowBounds::Windowed(Bounds::default())
2773 }
2774}
2775
2776impl WindowBounds {
2777 pub fn get_bounds(&self) -> Bounds<Pixels> {
2779 match self {
2780 WindowBounds::Windowed(bounds) => *bounds,
2781 WindowBounds::Maximized(bounds) => *bounds,
2782 WindowBounds::Fullscreen(bounds) => *bounds,
2783 }
2784 }
2785
2786 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2788 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2789 }
2790}
2791
2792impl Default for WindowOptions {
2793 fn default() -> Self {
2794 Self {
2795 window_bounds: None,
2796 titlebar: Some(TitlebarOptions {
2797 title: Default::default(),
2798 appears_transparent: Default::default(),
2799 traffic_light_position: Default::default(),
2800 }),
2801 focus: true,
2802 show: true,
2803 kind: WindowKind::Normal,
2804 is_movable: true,
2805 app_owns_titlebar_drag: false,
2806 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2807 low_power_frame_interval: Some(Duration::from_micros(33_333)),
2808 thermal_frame_interval: Some(Duration::from_micros(16_667)),
2809 is_resizable: true,
2810 is_minimizable: true,
2811 display_id: None,
2812 window_background: WindowBackgroundAppearance::default(),
2813 icon: None,
2814 app_id: None,
2815 window_min_size: None,
2816 window_decorations: None,
2817 tabbing_identifier: None,
2818 use_display_p3: false,
2819 }
2820 }
2821}
2822
2823#[derive(Debug, Default)]
2825pub struct TitlebarOptions {
2826 pub title: Option<SharedString>,
2828
2829 pub appears_transparent: bool,
2832
2833 pub traffic_light_position: Option<Point<Pixels>>,
2835}
2836
2837#[derive(Clone, Debug, PartialEq, Eq)]
2839pub enum WindowKind {
2840 Normal,
2842
2843 PopUp,
2846
2847 AnchoredPopup(popup::PopupOptions),
2854
2855 Floating,
2857
2858 #[cfg(all(target_os = "linux", feature = "wayland"))]
2861 LayerShell(layer_shell::LayerShellOptions),
2862
2863 Dialog,
2866}
2867
2868#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2873pub enum WindowAppearance {
2874 #[default]
2878 Light,
2879
2880 VibrantLight,
2884
2885 Dark,
2889
2890 VibrantDark,
2894}
2895
2896#[derive(Copy, Clone, Debug, Default, PartialEq)]
2899pub enum WindowBackgroundAppearance {
2900 #[default]
2908 Opaque,
2909 Transparent,
2911 Blurred,
2915 MicaBackdrop,
2917 MicaAltBackdrop,
2919}
2920
2921#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2923pub enum TextRenderingMode {
2924 #[default]
2926 PlatformDefault,
2927 Subpixel,
2929 Grayscale,
2931}
2932
2933#[derive(Clone, Debug)]
2935pub struct PathPromptOptions {
2936 pub files: bool,
2938 pub directories: bool,
2940 pub multiple: bool,
2942 pub prompt: Option<SharedString>,
2944}
2945
2946#[derive(Copy, Clone, Debug, PartialEq)]
2948pub enum PromptLevel {
2949 Info,
2951
2952 Warning,
2954
2955 Critical,
2957}
2958
2959#[derive(Clone, Debug, PartialEq)]
2961pub enum PromptButton {
2962 Ok(SharedString),
2964 Cancel(SharedString),
2966 Other(SharedString),
2968}
2969
2970impl PromptButton {
2971 pub fn new(label: impl Into<SharedString>) -> Self {
2973 PromptButton::Other(label.into())
2974 }
2975
2976 pub fn ok(label: impl Into<SharedString>) -> Self {
2978 PromptButton::Ok(label.into())
2979 }
2980
2981 pub fn cancel(label: impl Into<SharedString>) -> Self {
2983 PromptButton::Cancel(label.into())
2984 }
2985
2986 #[allow(dead_code)]
2988 pub fn is_cancel(&self) -> bool {
2989 matches!(self, PromptButton::Cancel(_))
2990 }
2991
2992 pub fn label(&self) -> &SharedString {
2994 match self {
2995 PromptButton::Ok(label) => label,
2996 PromptButton::Cancel(label) => label,
2997 PromptButton::Other(label) => label,
2998 }
2999 }
3000}
3001
3002impl From<&str> for PromptButton {
3003 fn from(value: &str) -> Self {
3004 match value.to_lowercase().as_str() {
3005 "ok" => PromptButton::Ok("OK".into()),
3006 "cancel" => PromptButton::Cancel("Cancel".into()),
3007 _ => PromptButton::Other(SharedString::from(value.to_owned())),
3008 }
3009 }
3010}
3011
3012#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3014pub enum CursorStyle {
3015 #[default]
3017 Arrow,
3018
3019 IBeam,
3022
3023 Crosshair,
3026
3027 ClosedHand,
3030
3031 OpenHand,
3034
3035 PointingHand,
3038
3039 ResizeLeft,
3042
3043 ResizeRight,
3046
3047 ResizeLeftRight,
3050
3051 ResizeUp,
3054
3055 ResizeDown,
3058
3059 ResizeUpDown,
3062
3063 ResizeUpLeftDownRight,
3066
3067 ResizeUpRightDownLeft,
3070
3071 ResizeColumn,
3074
3075 ResizeRow,
3078
3079 IBeamCursorForVerticalLayout,
3082
3083 OperationNotAllowed,
3086
3087 DragLink,
3090
3091 DragCopy,
3094
3095 ContextualMenu,
3098}
3099
3100#[derive(Clone, Debug, Eq, PartialEq)]
3102pub struct ClipboardItem {
3103 pub entries: Vec<ClipboardEntry>,
3105}
3106
3107#[derive(Clone, Debug, PartialEq, Eq)]
3112pub enum ClipboardReadError {
3113 Unavailable,
3117 Denied(String),
3120 UnsupportedContent,
3123}
3124
3125impl std::fmt::Display for ClipboardReadError {
3126 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3127 match self {
3128 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3129 Self::Denied(message) => {
3130 write!(formatter, "clipboard access was denied: {message}")
3131 }
3132 Self::UnsupportedContent => {
3133 formatter.write_str("the clipboard contents are unsupported")
3134 }
3135 }
3136 }
3137}
3138
3139impl std::error::Error for ClipboardReadError {}
3140
3141#[derive(Clone, Debug, Eq, PartialEq)]
3143pub enum ClipboardEntry {
3144 String(ClipboardString),
3146 Image(Image),
3148 ExternalPaths(crate::ExternalPaths),
3150}
3151
3152impl ClipboardItem {
3153 pub fn new_string(text: String) -> Self {
3155 Self {
3156 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3157 }
3158 }
3159
3160 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3162 Self {
3163 entries: vec![ClipboardEntry::String(ClipboardString {
3164 text,
3165 metadata: Some(metadata),
3166 })],
3167 }
3168 }
3169
3170 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3172 Self {
3173 entries: vec![ClipboardEntry::String(
3174 ClipboardString::new(text).with_json_metadata(metadata),
3175 )],
3176 }
3177 }
3178
3179 pub fn new_image(image: &Image) -> Self {
3181 Self {
3182 entries: vec![ClipboardEntry::Image(image.clone())],
3183 }
3184 }
3185
3186 pub fn text(&self) -> Option<String> {
3189 let mut answer = String::new();
3190
3191 for entry in self.entries.iter() {
3192 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3193 answer.push_str(text);
3194 }
3195 }
3196
3197 if answer.is_empty() {
3198 for entry in self.entries.iter() {
3199 if let ClipboardEntry::ExternalPaths(paths) = entry {
3200 for path in &paths.0 {
3201 use std::fmt::Write as _;
3202 _ = write!(answer, "{}", path.display());
3203 }
3204 }
3205 }
3206 }
3207
3208 if !answer.is_empty() {
3209 Some(answer)
3210 } else {
3211 None
3212 }
3213 }
3214
3215 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3217 pub fn metadata(&self) -> Option<&String> {
3218 match self.entries().first() {
3219 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3220 clipboard_string.metadata.as_ref()
3221 }
3222 _ => None,
3223 }
3224 }
3225
3226 pub fn entries(&self) -> &[ClipboardEntry] {
3228 &self.entries
3229 }
3230
3231 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3233 self.entries.into_iter()
3234 }
3235}
3236
3237impl From<ClipboardString> for ClipboardEntry {
3238 fn from(value: ClipboardString) -> Self {
3239 Self::String(value)
3240 }
3241}
3242
3243impl From<String> for ClipboardEntry {
3244 fn from(value: String) -> Self {
3245 Self::from(ClipboardString::from(value))
3246 }
3247}
3248
3249impl From<Image> for ClipboardEntry {
3250 fn from(value: Image) -> Self {
3251 Self::Image(value)
3252 }
3253}
3254
3255impl From<ClipboardEntry> for ClipboardItem {
3256 fn from(value: ClipboardEntry) -> Self {
3257 Self {
3258 entries: vec![value],
3259 }
3260 }
3261}
3262
3263impl From<String> for ClipboardItem {
3264 fn from(value: String) -> Self {
3265 Self::from(ClipboardEntry::from(value))
3266 }
3267}
3268
3269impl From<Image> for ClipboardItem {
3270 fn from(value: Image) -> Self {
3271 Self::from(ClipboardEntry::from(value))
3272 }
3273}
3274
3275#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3277pub enum ImageFormat {
3278 Png,
3283 Jpeg,
3285 Webp,
3287 Gif,
3289 Svg,
3291 Bmp,
3293 Tiff,
3295 Ico,
3297 Pnm,
3299}
3300
3301impl ImageFormat {
3302 pub const fn mime_type(self) -> &'static str {
3304 match self {
3305 ImageFormat::Png => "image/png",
3306 ImageFormat::Jpeg => "image/jpeg",
3307 ImageFormat::Webp => "image/webp",
3308 ImageFormat::Gif => "image/gif",
3309 ImageFormat::Svg => "image/svg+xml",
3310 ImageFormat::Bmp => "image/bmp",
3311 ImageFormat::Tiff => "image/tiff",
3312 ImageFormat::Ico => "image/ico",
3313 ImageFormat::Pnm => "image/x-portable-anymap",
3314 }
3315 }
3316
3317 pub const fn extension(self) -> &'static str {
3319 match self {
3320 ImageFormat::Png => "png",
3321 ImageFormat::Jpeg => "jpg",
3322 ImageFormat::Webp => "webp",
3323 ImageFormat::Gif => "gif",
3324 ImageFormat::Svg => "svg",
3325 ImageFormat::Bmp => "bmp",
3326 ImageFormat::Tiff => "tiff",
3327 ImageFormat::Ico => "ico",
3328 ImageFormat::Pnm => "pnm",
3329 }
3330 }
3331
3332 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3334 use strum::IntoEnumIterator;
3335 Self::iter()
3336 .find(|format| format.mime_type() == mime_type)
3337 .or_else(|| Self::from_mime_type_alias(mime_type))
3338 }
3339
3340 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3344 match mime_type {
3345 "image/jpg" => Some(Self::Jpeg),
3346 "image/tif" => Some(Self::Tiff),
3347 _ => None,
3348 }
3349 }
3350}
3351
3352#[derive(Clone, Debug, PartialEq, Eq)]
3354pub struct Image {
3355 pub format: ImageFormat,
3357 pub bytes: Vec<u8>,
3359 pub id: u64,
3361}
3362
3363pub(crate) fn decode_static_image(
3364 bytes: &[u8],
3365 format: image::ImageFormat,
3366) -> Result<SmallVec<[Frame; 1]>> {
3367 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3368 .into_decoder()
3369 .context("creating image decoder")?;
3370 decode_static_image_from_decoder(decoder)
3371}
3372
3373pub(crate) fn decode_static_image_from_decoder(
3374 mut decoder: impl image::ImageDecoder,
3375) -> Result<SmallVec<[Frame; 1]>> {
3376 let orientation = decoder
3377 .orientation()
3378 .context("reading decoder's orientation")?;
3379 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3380 image.apply_orientation(orientation);
3381
3382 let mut data = image.into_rgba8();
3383 for pixel in data.chunks_exact_mut(4) {
3384 pixel.swap(0, 2);
3385 }
3386
3387 Ok(SmallVec::from_elem(Frame::new(data), 1))
3388}
3389
3390impl Hash for Image {
3391 fn hash<H: Hasher>(&self, state: &mut H) {
3392 state.write_u64(self.id);
3393 }
3394}
3395
3396impl Image {
3397 pub fn empty() -> Self {
3399 Self::from_bytes(ImageFormat::Png, Vec::new())
3400 }
3401
3402 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3404 Self {
3405 id: hash(&bytes),
3406 format,
3407 bytes,
3408 }
3409 }
3410
3411 pub fn id(&self) -> u64 {
3413 self.id
3414 }
3415
3416 pub fn use_render_image(
3418 self: Arc<Self>,
3419 window: &mut Window,
3420 cx: &mut App,
3421 ) -> Option<Arc<RenderImage>> {
3422 ImageSource::Image(self)
3423 .use_data(None, window, cx)
3424 .and_then(|result| result.ok())
3425 }
3426
3427 pub fn get_render_image(
3429 self: Arc<Self>,
3430 window: &mut Window,
3431 cx: &mut App,
3432 ) -> Option<Arc<RenderImage>> {
3433 ImageSource::Image(self)
3434 .get_data(None, window, cx)
3435 .and_then(|result| result.ok())
3436 }
3437
3438 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3440 ImageSource::Image(self).remove_asset(cx);
3441 }
3442
3443 #[cfg(any(test, feature = "test-support"))]
3446 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3447 ImageSource::Image(self.clone()).is_asset_cached(cx)
3448 }
3449
3450 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3452 let frames = match self.format {
3453 ImageFormat::Gif => {
3454 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3455 let mut frames = SmallVec::new();
3456
3457 for frame in decoder.into_frames() {
3458 match frame {
3459 Ok(mut frame) => {
3460 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3462 pixel.swap(0, 2);
3463 }
3464 frames.push(frame);
3465 }
3466 Err(err) => {
3467 log::debug!("Skipping GIF frame due to decode error: {err}");
3468 }
3469 }
3470 }
3471
3472 if frames.is_empty() {
3473 anyhow::bail!("GIF could not be decoded: all frames failed");
3474 }
3475
3476 frames
3477 }
3478 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3479 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3480 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3481 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3482 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3483 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3484 ImageFormat::Svg => {
3485 return svg_renderer
3486 .render_single_frame(&self.bytes, 1.0)
3487 .map_err(Into::into);
3488 }
3489 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3490 };
3491
3492 Ok(Arc::new(RenderImage::new(frames)))
3493 }
3494
3495 pub fn format(&self) -> ImageFormat {
3497 self.format
3498 }
3499
3500 pub fn bytes(&self) -> &[u8] {
3502 self.bytes.as_slice()
3503 }
3504}
3505
3506#[derive(Clone, Debug, Eq, PartialEq)]
3508pub struct ClipboardString {
3509 pub text: String,
3511 pub metadata: Option<String>,
3513}
3514
3515impl ClipboardString {
3516 pub fn new(text: String) -> Self {
3518 Self {
3519 text,
3520 metadata: None,
3521 }
3522 }
3523
3524 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3527 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3528 self
3529 }
3530
3531 pub fn text(&self) -> &String {
3533 &self.text
3534 }
3535
3536 pub fn into_text(self) -> String {
3538 self.text
3539 }
3540
3541 pub fn metadata_json<T>(&self) -> Option<T>
3543 where
3544 T: for<'a> Deserialize<'a>,
3545 {
3546 self.metadata
3547 .as_ref()
3548 .and_then(|m| serde_json::from_str(m).ok())
3549 }
3550
3551 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3552 pub fn text_hash(text: &str) -> u64 {
3554 let mut hasher = SeaHasher::new();
3555 text.hash(&mut hasher);
3556 hasher.finish()
3557 }
3558}
3559
3560impl From<String> for ClipboardString {
3561 fn from(value: String) -> Self {
3562 Self {
3563 text: value,
3564 metadata: None,
3565 }
3566 }
3567}
3568
3569#[cfg(test)]
3570mod image_tests {
3571 use super::*;
3572 use std::sync::Arc;
3573
3574 #[test]
3575 fn test_image_to_image_data_applies_exif_orientation() {
3576 let image = Image::from_bytes(
3577 ImageFormat::Jpeg,
3578 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3579 );
3580
3581 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3582
3583 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3584
3585 let bytes = render_image.as_bytes(0).unwrap();
3586 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3587 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3588 }
3589
3590 #[test]
3591 fn test_svg_image_to_image_data_converts_to_bgra() {
3592 let image = Image::from_bytes(
3593 ImageFormat::Svg,
3594 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3595<rect width="1" height="1" fill="#38BDF8"/>
3596</svg>"##
3597 .to_vec(),
3598 );
3599
3600 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3601 let bytes = render_image.as_bytes(0).unwrap();
3602
3603 for pixel in bytes.chunks_exact(4) {
3604 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3605 }
3606 }
3607}
3608
3609#[cfg(test)]
3610mod atlas_tests {
3611 use super::*;
3612
3613 const TILE_SIZE: Size<DevicePixels> = Size {
3614 width: DevicePixels(1),
3615 height: DevicePixels(1),
3616 };
3617
3618 #[derive(Default)]
3619 struct RecordingAtlasBackend {
3620 insert_calls: u32,
3621 fail_next_insert: bool,
3622 removed_tiles: Vec<AtlasTile>,
3623 }
3624
3625 impl AtlasBackend for RecordingAtlasBackend {
3626 fn insert(
3627 &mut self,
3628 kind: AtlasTextureKind,
3629 size: Size<DevicePixels>,
3630 _bytes: &[u8],
3631 ) -> Result<AtlasTile> {
3632 self.insert_calls += 1;
3633 if std::mem::take(&mut self.fail_next_insert) {
3634 anyhow::bail!("backend failed");
3635 }
3636 Ok(AtlasTile {
3637 texture_id: AtlasTextureId { index: 0, kind },
3638 tile_id: TileId(self.insert_calls),
3639 padding: 0,
3640 bounds: Bounds {
3641 origin: Point::default(),
3642 size,
3643 },
3644 })
3645 }
3646
3647 fn remove(&mut self, tile: AtlasTile) {
3648 self.removed_tiles.push(tile);
3649 }
3650 }
3651
3652 fn image_key(image_id: usize) -> AtlasKey {
3653 AtlasKey::Image(RenderImageParams {
3654 image_id: crate::ImageId(image_id),
3655 frame_index: 0,
3656 })
3657 }
3658
3659 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3660 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3661 }
3662
3663 #[test]
3664 fn only_successful_inserts_are_cached() -> Result<()> {
3665 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3666 let key = image_key(1);
3667
3668 assert_eq!(
3669 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3670 None
3671 );
3672 state
3673 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3674 .expect_err("builder error should propagate");
3675 assert!(!state.contains(&key));
3676 assert_eq!(state.backend.insert_calls, 0);
3677
3678 state.backend.fail_next_insert = true;
3679 state
3680 .get_or_insert_with(key.clone(), &mut build_tile)
3681 .expect_err("backend error should propagate");
3682 assert!(!state.contains(&key));
3683 assert_eq!(state.backend.insert_calls, 1);
3684
3685 let tile = state
3686 .get_or_insert_with(key.clone(), &mut build_tile)?
3687 .context("builder should produce a tile")?;
3688 assert_eq!(tile.texture_id.kind, key.texture_kind());
3689 assert_eq!(
3690 state.get_or_insert_with(key.clone(), &mut || {
3691 anyhow::bail!("cache hit must not call the builder")
3692 })?,
3693 Some(tile)
3694 );
3695 assert!(state.contains(&key));
3696 assert_eq!(state.backend.insert_calls, 2);
3697 Ok(())
3698 }
3699
3700 #[test]
3701 fn remove_and_clear_invalidate_keys() -> Result<()> {
3702 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3703 let key = image_key(1);
3704 let other_key = image_key(2);
3705 let tile = state
3706 .get_or_insert_with(key.clone(), &mut build_tile)?
3707 .context("builder should produce a tile")?;
3708 state
3709 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3710 .context("builder should produce another tile")?;
3711
3712 state.remove(&key);
3713 state.remove(&key);
3714 assert!(!state.contains(&key));
3715 assert!(state.contains(&other_key));
3716 assert_eq!(state.backend.removed_tiles, vec![tile]);
3717
3718 let mut reset_calls = 0;
3719 state.clear(|_| reset_calls += 1);
3720 assert_eq!(reset_calls, 1);
3721 assert!(!state.contains(&other_key));
3722 assert_eq!(state.backend.removed_tiles, vec![tile]);
3723 Ok(())
3724 }
3725}
3726
3727#[cfg(test)]
3728mod frame_signal_tests {
3729 use super::*;
3730
3731 #[cfg(feature = "profiler")]
3732 #[test]
3733 fn coalesced_signals_retain_the_earliest_until_drained() {
3734 for (first_source, later_source) in [
3735 (
3736 FrameRequestSource::NativeCallback,
3737 FrameRequestSource::LocalSchedule,
3738 ),
3739 (
3740 FrameRequestSource::LocalSchedule,
3741 FrameRequestSource::NativeCallback,
3742 ),
3743 ] {
3744 let signal = PlatformFrameSignal::new();
3745 let first =
3746 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3747 assert_eq!(signal.take(), None);
3748 signal.record(first + Duration::from_millis(16), later_source);
3749 signal.record(first, first_source);
3750 signal.record(first + Duration::from_millis(32), later_source);
3751 assert_eq!(signal.take(), Some((first, first_source)));
3752 assert_eq!(signal.take(), None);
3753 let next = first + Duration::from_millis(48);
3754 signal.record(next, later_source);
3755 assert_eq!(signal.take(), Some((next, later_source)));
3756 }
3757 }
3758
3759 #[cfg(not(feature = "profiler"))]
3760 #[test]
3761 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3762 let captured = PlatformFrameSignal::capture(|| {
3763 panic!("profiler-disabled builds must not evaluate the capture closure");
3764 });
3765 assert_eq!(captured, None);
3766 let signal = PlatformFrameSignal::new();
3767 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3768 assert_eq!(signal.take(), None);
3769 }
3770}
3771
3772#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3773mod tests {
3774 use super::*;
3775 use std::collections::HashSet;
3776
3777 #[test]
3778 fn test_refresh_interval_from_hz() {
3779 assert_eq!(
3780 refresh_interval_from_hz(50.0),
3781 Some(Duration::from_millis(20))
3782 );
3783 assert_eq!(
3784 refresh_interval_from_hz(120.0),
3785 Some(Duration::from_secs(1) / 120)
3786 );
3787 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3788 assert_eq!(refresh_interval_from_hz(unknown), None);
3789 }
3790 }
3791
3792 #[test]
3793 fn test_window_button_layout_parse_standard() {
3794 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3795 assert_eq!(
3796 layout.left,
3797 [
3798 Some(WindowButton::Close),
3799 Some(WindowButton::Minimize),
3800 None
3801 ]
3802 );
3803 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3804 }
3805
3806 #[test]
3807 fn test_window_button_layout_parse_right_only() {
3808 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3809 assert_eq!(layout.left, [None, None, None]);
3810 assert_eq!(
3811 layout.right,
3812 [
3813 Some(WindowButton::Minimize),
3814 Some(WindowButton::Maximize),
3815 Some(WindowButton::Close)
3816 ]
3817 );
3818 }
3819
3820 #[test]
3821 fn test_window_button_layout_parse_left_only() {
3822 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3823 assert_eq!(
3824 layout.left,
3825 [
3826 Some(WindowButton::Close),
3827 Some(WindowButton::Minimize),
3828 Some(WindowButton::Maximize)
3829 ]
3830 );
3831 assert_eq!(layout.right, [None, None, None]);
3832 }
3833
3834 #[test]
3835 fn test_window_button_layout_parse_with_whitespace() {
3836 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3837 assert_eq!(
3838 layout.left,
3839 [
3840 Some(WindowButton::Close),
3841 Some(WindowButton::Minimize),
3842 None
3843 ]
3844 );
3845 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3846 }
3847
3848 #[test]
3849 fn test_window_button_layout_parse_empty() {
3850 let layout = WindowButtonLayout::parse("").unwrap();
3851 assert_eq!(layout.left, [None, None, None]);
3852 assert_eq!(layout.right, [None, None, None]);
3853 }
3854
3855 #[test]
3856 fn test_window_button_layout_parse_intentionally_empty() {
3857 let layout = WindowButtonLayout::parse(":").unwrap();
3858 assert_eq!(layout.left, [None, None, None]);
3859 assert_eq!(layout.right, [None, None, None]);
3860 }
3861
3862 #[test]
3863 fn test_window_button_layout_parse_invalid_buttons() {
3864 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").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_deduplicates_same_side_buttons() {
3878 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3879 assert_eq!(
3880 layout.right,
3881 [
3882 Some(WindowButton::Close),
3883 Some(WindowButton::Minimize),
3884 None
3885 ]
3886 );
3887 assert_eq!(layout.format(), ":close,minimize");
3888 }
3889
3890 #[test]
3891 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3892 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3893 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3894 assert_eq!(
3895 layout.right,
3896 [
3897 Some(WindowButton::Maximize),
3898 Some(WindowButton::Minimize),
3899 None
3900 ]
3901 );
3902
3903 let button_ids: Vec<_> = layout
3904 .left
3905 .iter()
3906 .chain(layout.right.iter())
3907 .flatten()
3908 .map(WindowButton::id)
3909 .collect();
3910 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3911 assert_eq!(unique_button_ids.len(), button_ids.len());
3912 assert_eq!(layout.format(), "close:maximize,minimize");
3913 }
3914
3915 #[test]
3916 fn test_window_button_layout_parse_gnome_style() {
3917 let layout = WindowButtonLayout::parse("close").unwrap();
3918 assert_eq!(layout.left, [None, None, None]);
3919 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3920 }
3921
3922 #[test]
3923 fn test_window_button_layout_parse_elementary_style() {
3924 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3925 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3926 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3927 }
3928
3929 #[test]
3930 fn test_window_button_layout_round_trip() {
3931 let cases = [
3932 "close:minimize,maximize",
3933 "minimize,maximize,close:",
3934 ":close",
3935 "close:",
3936 "close:maximize",
3937 ":",
3938 ];
3939
3940 for case in cases {
3941 let layout = WindowButtonLayout::parse(case).unwrap();
3942 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3943 }
3944 }
3945
3946 #[test]
3947 fn test_window_button_layout_linux_default() {
3948 let layout = WindowButtonLayout::linux_default();
3949 assert_eq!(layout.left, [None, None, None]);
3950 assert_eq!(
3951 layout.right,
3952 [
3953 Some(WindowButton::Minimize),
3954 Some(WindowButton::Maximize),
3955 Some(WindowButton::Close)
3956 ]
3957 );
3958
3959 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3960 assert_eq!(round_tripped, layout);
3961 }
3962
3963 #[test]
3964 fn test_window_button_layout_parse_all_invalid() {
3965 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3966 }
3967}