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 set_app_identity(&self, identifier: &str, name: &str) {
524 _ = (identifier, name);
525 }
526
527 fn show_system_notification(&self, notification: SystemNotification) {
534 _ = notification;
535 }
536
537 fn dismiss_system_notification(&self, tag: &str) {
542 _ = tag;
543 }
544
545 fn on_system_notification_response(
551 &self,
552 callback: Box<dyn FnMut(SystemNotificationResponse)>,
553 ) {
554 _ = callback;
555 }
556
557 fn compositor_name(&self) -> &'static str {
558 ""
559 }
560 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
563 None
564 }
565 fn app_path(&self) -> Result<PathBuf>;
566 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
567
568 fn set_cursor_style(&self, style: CursorStyle);
569
570 fn hide_cursor_until_mouse_moves(&self);
573
574 fn is_cursor_visible(&self) -> bool;
576
577 fn should_auto_hide_scrollbars(&self) -> bool;
578
579 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
580 fn write_to_clipboard(&self, item: ClipboardItem);
581
582 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
590 Task::ready(Ok(self.read_from_clipboard()))
591 }
592
593 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
594 fn read_from_primary(&self) -> Option<ClipboardItem>;
595 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
596 fn write_to_primary(&self, item: ClipboardItem);
597
598 #[cfg(target_os = "macos")]
599 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
600 #[cfg(target_os = "macos")]
601 fn write_to_find_pasteboard(&self, item: ClipboardItem);
602
603 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
604
605 #[cfg(target_os = "macos")]
609 fn write_credentials_if_not_exists(
610 &self,
611 _url: &str,
612 _username: &str,
613 _password: &[u8],
614 ) -> Task<Result<bool>> {
615 Task::ready(Err(anyhow::anyhow!(
616 "this platform does not support atomic credential writes"
617 )))
618 }
619
620 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
621 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
622
623 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
624 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
625 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
626}
627
628pub trait PlatformDisplay: Debug {
630 fn id(&self) -> DisplayId;
632
633 fn uuid(&self) -> Result<Uuid>;
636
637 fn bounds(&self) -> Bounds<Pixels>;
639
640 fn refresh_interval(&self) -> Option<Duration>;
647
648 fn visible_bounds(&self) -> Bounds<Pixels> {
652 self.bounds()
653 }
654
655 fn default_bounds(&self) -> Bounds<Pixels> {
657 let bounds = self.bounds();
658 let center = bounds.center();
659 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
660
661 let offset = clipped_window_size / 2.0;
662 let origin = point(center.x - offset.width, center.y - offset.height);
663 Bounds::new(origin, clipped_window_size)
664 }
665}
666
667#[derive(Clone, Debug, PartialEq, Eq)]
670pub struct SystemNotification {
671 pub tag: SharedString,
675 pub title: SharedString,
677 pub body: SharedString,
679 pub actions: Vec<SystemNotificationAction>,
682}
683
684#[derive(Clone, Debug, PartialEq, Eq, Hash)]
686pub struct SystemNotificationAction {
687 pub id: SharedString,
690 pub label: SharedString,
692}
693
694#[derive(Clone, Debug, PartialEq, Eq)]
696pub struct SystemNotificationResponse {
697 pub tag: SharedString,
699 pub action_id: Option<SharedString>,
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
706pub enum ThermalState {
707 Nominal,
709 Fair,
711 Serious,
713 Critical,
715}
716
717#[derive(Clone)]
719pub struct SourceMetadata {
720 pub id: u64,
722 pub label: Option<SharedString>,
724 pub is_main: Option<bool>,
726 pub resolution: Size<DevicePixels>,
728}
729
730pub trait ScreenCaptureSource {
732 fn metadata(&self) -> Result<SourceMetadata>;
734
735 fn stream(
738 &self,
739 foreground_executor: &ForegroundExecutor,
740 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
741 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
742}
743
744pub trait ScreenCaptureStream {
746 fn metadata(&self) -> Result<SourceMetadata>;
748}
749
750pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
752
753#[derive(PartialEq, Eq, Hash, Copy, Clone)]
755pub struct DisplayId(pub(crate) u64);
756
757impl DisplayId {
758 pub fn new(id: u64) -> Self {
760 Self(id)
761 }
762}
763
764impl From<u64> for DisplayId {
765 fn from(id: u64) -> Self {
766 Self(id)
767 }
768}
769
770impl From<DisplayId> for u64 {
771 fn from(id: DisplayId) -> Self {
772 id.0
773 }
774}
775
776impl Debug for DisplayId {
777 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
778 write!(f, "DisplayId({})", self.0)
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq)]
784pub enum ResizeEdge {
785 Top,
787 TopRight,
789 Right,
791 BottomRight,
793 Bottom,
795 BottomLeft,
797 Left,
799 TopLeft,
801}
802
803#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
805pub enum WindowDecorations {
806 #[default]
807 Server,
809 Client,
811}
812
813#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
815pub enum Decorations {
816 #[default]
818 Server,
819 Client {
821 tiling: Tiling,
823 },
824}
825
826#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
828pub struct WindowControls {
829 pub fullscreen: bool,
831 pub maximize: bool,
833 pub minimize: bool,
835 pub window_menu: bool,
837}
838
839impl Default for WindowControls {
840 fn default() -> Self {
841 Self {
843 fullscreen: true,
844 maximize: true,
845 minimize: true,
846 window_menu: true,
847 }
848 }
849}
850
851#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
853pub enum WindowButton {
854 Minimize,
856 Maximize,
858 Close,
860}
861
862impl WindowButton {
863 pub fn id(&self) -> &'static str {
865 match self {
866 WindowButton::Minimize => "minimize",
867 WindowButton::Maximize => "maximize",
868 WindowButton::Close => "close",
869 }
870 }
871
872 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
873 fn index(&self) -> usize {
874 match self {
875 WindowButton::Minimize => 0,
876 WindowButton::Maximize => 1,
877 WindowButton::Close => 2,
878 }
879 }
880}
881
882pub const MAX_BUTTONS_PER_SIDE: usize = 3;
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq)]
890pub struct WindowButtonLayout {
891 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
893 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
895}
896
897#[cfg(any(target_os = "linux", target_os = "freebsd"))]
898impl WindowButtonLayout {
899 pub fn linux_default() -> Self {
901 Self {
902 left: [None; MAX_BUTTONS_PER_SIDE],
903 right: [
904 Some(WindowButton::Minimize),
905 Some(WindowButton::Maximize),
906 Some(WindowButton::Close),
907 ],
908 }
909 }
910
911 pub fn parse(layout_string: &str) -> Result<Self> {
913 fn parse_side(
914 s: &str,
915 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
916 unrecognized: &mut Vec<String>,
917 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
918 let mut result = [None; MAX_BUTTONS_PER_SIDE];
919 let mut i = 0;
920 for name in s.split(',') {
921 let trimmed = name.trim();
922 if trimmed.is_empty() {
923 continue;
924 }
925 let button = match trimmed {
926 "minimize" => Some(WindowButton::Minimize),
927 "maximize" => Some(WindowButton::Maximize),
928 "close" => Some(WindowButton::Close),
929 other => {
930 unrecognized.push(other.to_string());
931 None
932 }
933 };
934 if let Some(button) = button {
935 if seen_buttons[button.index()] {
936 continue;
937 }
938 if let Some(slot) = result.get_mut(i) {
939 *slot = Some(button);
940 seen_buttons[button.index()] = true;
941 i += 1;
942 }
943 }
944 }
945 result
946 }
947
948 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
949 let mut unrecognized = Vec::new();
950 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
951 let layout = Self {
952 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
953 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
954 };
955
956 if !unrecognized.is_empty()
957 && layout.left.iter().all(Option::is_none)
958 && layout.right.iter().all(Option::is_none)
959 {
960 bail!(
961 "button layout string {:?} contains no valid buttons (unrecognized: {})",
962 layout_string,
963 unrecognized.join(", ")
964 );
965 }
966
967 Ok(layout)
968 }
969
970 #[cfg(test)]
972 pub fn format(&self) -> String {
973 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
974 buttons
975 .iter()
976 .flatten()
977 .map(|button| match button {
978 WindowButton::Minimize => "minimize",
979 WindowButton::Maximize => "maximize",
980 WindowButton::Close => "close",
981 })
982 .collect::<Vec<_>>()
983 .join(",")
984 }
985
986 format!("{}:{}", format_side(&self.left), format_side(&self.right))
987 }
988}
989
990#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
992pub struct Tiling {
993 pub top: bool,
995 pub left: bool,
997 pub right: bool,
999 pub bottom: bool,
1001}
1002
1003impl Tiling {
1004 pub fn tiled() -> Self {
1006 Self {
1007 top: true,
1008 left: true,
1009 right: true,
1010 bottom: true,
1011 }
1012 }
1013
1014 pub fn is_tiled(&self) -> bool {
1016 self.top || self.left || self.right || self.bottom
1017 }
1018}
1019
1020pub struct A11yCallbacks {
1022 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1024 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1026 pub deactivation: Box<dyn Fn() + Send + 'static>,
1028}
1029
1030#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1032pub enum FrameRequestSource {
1033 #[default]
1035 NativeCallback,
1036 LocalSchedule,
1038}
1039
1040#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1041#[expect(missing_docs)]
1042pub struct RequestFrameOptions {
1043 pub require_presentation: bool,
1045 pub force_render: bool,
1047 pub signal_at: Option<Instant>,
1053 pub signal_source: FrameRequestSource,
1055}
1056
1057pub struct PlatformFrameSignal {
1066 #[cfg(feature = "profiler")]
1067 origin: Instant,
1068 #[cfg(feature = "profiler")]
1069 first_signal: std::sync::atomic::AtomicU64,
1070}
1071
1072impl Default for PlatformFrameSignal {
1073 fn default() -> Self {
1074 Self::new()
1075 }
1076}
1077
1078impl PlatformFrameSignal {
1079 pub fn new() -> Self {
1081 Self {
1082 #[cfg(feature = "profiler")]
1083 origin: Instant::now(),
1084 #[cfg(feature = "profiler")]
1085 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1086 }
1087 }
1088
1089 #[inline]
1093 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1094 if cfg!(feature = "profiler") {
1095 Some(capture())
1096 } else {
1097 None
1098 }
1099 }
1100
1101 #[inline]
1103 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1104 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1105 #[cfg(feature = "profiler")]
1106 {
1107 let nanoseconds = at
1108 .saturating_duration_since(self.origin)
1109 .as_nanos()
1110 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1111 let source_bit = match source {
1112 FrameRequestSource::NativeCallback => 0,
1113 FrameRequestSource::LocalSchedule => 1,
1114 };
1115 self.first_signal.fetch_min(
1116 (nanoseconds << 1) | source_bit,
1117 std::sync::atomic::Ordering::Relaxed,
1118 );
1119 }
1120 }
1121
1122 #[inline]
1124 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1125 #[cfg(feature = "profiler")]
1126 {
1127 let signal = self
1128 .first_signal
1129 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1130 (signal != u64::MAX).then(|| {
1131 let source = match signal & 1 {
1132 0 => FrameRequestSource::NativeCallback,
1133 _ => FrameRequestSource::LocalSchedule,
1134 };
1135 (self.origin + Duration::from_nanos(signal >> 1), source)
1136 })
1137 }
1138 #[cfg(not(feature = "profiler"))]
1139 {
1140 None
1141 }
1142 }
1143}
1144
1145#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1158pub enum AppLifecyclePhase {
1159 Active,
1161 Inactive,
1163 Background,
1166 Foreground,
1168 Disconnected,
1172}
1173
1174#[derive(Debug, Clone, Default, PartialEq)]
1181pub struct WindowInsets {
1182 pub safe_area: Edges<Pixels>,
1187 pub ime: Edges<Pixels>,
1191 pub corner_radii: Corners<Pixels>,
1196}
1197
1198impl WindowInsets {
1199 pub fn effective(&self) -> Edges<Pixels> {
1201 Edges {
1202 top: self.safe_area.top.max(self.ime.top),
1203 right: self.safe_area.right.max(self.ime.right),
1204 bottom: self.safe_area.bottom.max(self.ime.bottom),
1205 left: self.safe_area.left.max(self.ime.left),
1206 }
1207 }
1208}
1209
1210#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1212pub enum TextInputStateChange {
1213 FocusGained,
1215 FocusLost,
1217 SelectionChanged,
1219 ContentChanged,
1221}
1222
1223#[expect(missing_docs)]
1224pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1225 fn bounds(&self) -> Bounds<Pixels>;
1226 fn is_maximized(&self) -> bool;
1227 fn window_bounds(&self) -> WindowBounds;
1228 fn content_size(&self) -> Size<Pixels>;
1229 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1235 Bounds::new(Point::default(), self.content_size())
1236 }
1237 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1242 fn prepare_frame(&self) -> bool {
1247 false
1248 }
1249 fn resize(&mut self, size: Size<Pixels>);
1250 fn scale_factor(&self) -> f32;
1251 fn appearance(&self) -> WindowAppearance;
1252 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1253 fn mouse_position(&self) -> Point<Pixels>;
1254 fn modifiers(&self) -> Modifiers;
1255 fn capslock(&self) -> Capslock;
1256 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1257 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1258 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1263 fn prompt(
1264 &self,
1265 level: PromptLevel,
1266 msg: &str,
1267 detail: Option<&str>,
1268 answers: &[PromptButton],
1269 ) -> Option<oneshot::Receiver<usize>>;
1270 fn activate(&self);
1271 fn request_attention(&self) {}
1273 fn is_active(&self) -> bool;
1274 fn visibility(&self) -> WindowVisibility;
1277 fn is_hovered(&self) -> bool;
1278 fn background_appearance(&self) -> WindowBackgroundAppearance;
1279 fn set_title(&mut self, title: &str);
1280 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1281 fn minimize(&self);
1282 fn zoom(&self);
1283 fn toggle_fullscreen(&self);
1284 fn is_fullscreen(&self) -> bool;
1285 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1286 None
1287 }
1288 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1289 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1290 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1291 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1295 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1296 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1297 fn on_moved(&self, callback: Box<dyn FnMut()>);
1298 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1302 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1303 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1304 fn on_close(&self, callback: Box<dyn FnOnce()>);
1305 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1306 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1307 fn draw(&self, scene: &Scene);
1308 fn schedule_frame(&self) {}
1309 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1310 fn is_subpixel_rendering_supported(&self) -> bool;
1311
1312 fn get_title(&self) -> String {
1314 String::new()
1315 }
1316 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1317 None
1318 }
1319 fn tab_bar_visible(&self) -> bool {
1320 false
1321 }
1322 fn set_edited(&mut self, _edited: bool) {}
1323 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1324 fn toggle_simple_fullscreen(&self) {}
1325 fn is_simple_fullscreen(&self) -> bool {
1326 false
1327 }
1328 #[cfg(target_os = "macos")]
1329 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1330 fn show_character_palette(&self) {}
1331 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1332 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1333 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1334 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1335 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1336 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1337 fn merge_all_windows(&self) {}
1338 fn move_tab_to_new_window(&self) {}
1339 fn toggle_window_tab_overview(&self) {}
1340 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1341
1342 fn native_window_state(&self) -> Option<Vec<u8>> {
1343 None
1344 }
1345 fn restore_native_window_state(&self, _state: &[u8]) {}
1346
1347 #[cfg(target_os = "windows")]
1348 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1349
1350 fn inner_window_bounds(&self) -> WindowBounds {
1352 self.window_bounds()
1353 }
1354 fn request_decorations(&self, _decorations: WindowDecorations) {}
1355 fn show_window_menu(&self, _position: Point<Pixels>) {}
1356 fn start_window_move(&self) {}
1357 fn can_start_external_drag(&self) -> bool {
1358 false
1359 }
1360 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1361 false
1362 }
1363 fn start_window_resize(&self, _edge: ResizeEdge) {}
1364 fn set_exclusive_zone(&self, _zone: Pixels) {}
1365 #[cfg(all(target_os = "linux", feature = "wayland"))]
1366 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1367 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1368 fn window_decorations(&self) -> Decorations {
1369 Decorations::Server
1370 }
1371 fn set_app_id(&mut self, _app_id: &str) {}
1372 fn map_window(&mut self) -> anyhow::Result<()> {
1373 Ok(())
1374 }
1375 fn window_controls(&self) -> WindowControls {
1376 WindowControls::default()
1377 }
1378 fn set_client_inset(&self, _inset: Pixels) {}
1379 fn gpu_specs(&self) -> Option<GpuSpecs>;
1380
1381 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1382
1383 fn insets(&self) -> WindowInsets {
1388 WindowInsets::default()
1389 }
1390
1391 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1397
1398 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1401
1402 fn set_back_enabled(&self, _enabled: bool) {}
1405
1406 fn show_soft_keyboard(&self) {}
1408
1409 fn hide_soft_keyboard(&self) {}
1411
1412 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1414
1415 fn play_system_bell(&self) {}
1416
1417 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1419
1420 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1422
1423 fn a11y_update_window_bounds(&self) {}
1425
1426 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1427 fn as_test(&mut self) -> Option<&mut TestWindow> {
1428 None
1429 }
1430
1431 #[cfg(any(test, feature = "test-support"))]
1435 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1436 anyhow::bail!("render_to_image not implemented for this platform")
1437 }
1438}
1439
1440#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1442pub trait PlatformHeadlessRenderer {
1443 fn render_scene_to_image(
1445 &mut self,
1446 scene: &Scene,
1447 size: Size<DevicePixels>,
1448 ) -> Result<RgbaImage>;
1449
1450 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1456
1457 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1459}
1460
1461#[doc(hidden)]
1464pub type RunnableVariant = Runnable<RunnableMeta>;
1465
1466#[doc(hidden)]
1467pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1468
1469#[doc(hidden)]
1470pub enum TasksIncluded {
1471 OnlyCompleted,
1472 CompletedAndRunning,
1473}
1474
1475#[doc(hidden)]
1478pub trait PlatformDispatcher: Send + Sync {
1479 fn is_main_thread(&self) -> bool;
1480 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1481 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1482 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1483
1484 fn dispatch_on_main_thread_when_idle(
1485 &self,
1486 runnable: RunnableVariant,
1487 timeout: Option<Duration>,
1488 ) {
1489 let _ = timeout;
1490 self.dispatch_on_main_thread(runnable, Priority::Low);
1491 }
1492
1493 fn idle_time_remaining(&self) -> Option<Duration> {
1494 None
1495 }
1496
1497 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1498
1499 fn now(&self) -> Instant {
1500 Instant::now()
1501 }
1502
1503 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1504 gpui_util::defer(Box::new(|| {}))
1505 }
1506
1507 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1508 ActivityGuard::noop()
1509 }
1510
1511 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1512 fn as_test(&self) -> Option<&TestDispatcher> {
1513 None
1514 }
1515
1516 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1519 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1520 None
1521 }
1522}
1523
1524#[expect(missing_docs)]
1525pub trait PlatformTextSystem: Send + Sync {
1526 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1527 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1529 fn all_font_names(&self) -> Vec<String>;
1531 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1533 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1535 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1537 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1539 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1541 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1543 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1545 fn rasterize_glyph(
1547 &self,
1548 params: &RenderGlyphParams,
1549 raster_bounds: Bounds<DevicePixels>,
1550 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1551 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1553 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1555 -> TextRenderingMode;
1556 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1558 0
1559 }
1560}
1561
1562#[expect(missing_docs)]
1563pub struct NoopTextSystem;
1564
1565#[expect(missing_docs)]
1566impl NoopTextSystem {
1567 #[allow(dead_code)]
1568 pub fn new() -> Self {
1569 Self
1570 }
1571}
1572
1573impl PlatformTextSystem for NoopTextSystem {
1574 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1575 Ok(())
1576 }
1577
1578 fn all_font_names(&self) -> Vec<String> {
1579 Vec::new()
1580 }
1581
1582 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1583 Ok(FontId(1))
1584 }
1585
1586 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1587 FontMetrics {
1588 units_per_em: 1000,
1589 ascent: 1025.0,
1590 descent: -275.0,
1591 line_gap: 0.0,
1592 underline_position: -95.0,
1593 underline_thickness: 60.0,
1594 cap_height: 698.0,
1595 x_height: 516.0,
1596 bounding_box: Bounds {
1597 origin: Point {
1598 x: -260.0,
1599 y: -245.0,
1600 },
1601 size: Size {
1602 width: 1501.0,
1603 height: 1364.0,
1604 },
1605 },
1606 }
1607 }
1608
1609 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1610 Ok(Bounds {
1611 origin: Point { x: 54.0, y: 0.0 },
1612 size: size(392.0, 528.0),
1613 })
1614 }
1615
1616 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1617 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1618 }
1619
1620 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1621 Some(GlyphId(ch.len_utf16() as u32))
1622 }
1623
1624 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1625 Ok(Default::default())
1626 }
1627
1628 fn rasterize_glyph(
1629 &self,
1630 _params: &RenderGlyphParams,
1631 raster_bounds: Bounds<DevicePixels>,
1632 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1633 Ok((raster_bounds.size, Vec::new()))
1634 }
1635
1636 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1637 let mut position = px(0.);
1638 let metrics = self.font_metrics(FontId(0));
1639 let em_width = font_size
1640 * self
1641 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1642 .unwrap()
1643 .width
1644 / metrics.units_per_em as f32;
1645 let mut glyphs = Vec::new();
1646 for (ix, c) in text.char_indices() {
1647 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1648 glyphs.push(ShapedGlyph {
1649 id: glyph,
1650 position: point(position, px(0.)),
1651 index: ix,
1652 is_emoji: glyph.0 == 2,
1653 });
1654 if glyph.0 == 2 {
1655 position += em_width * 2.0;
1656 } else {
1657 position += em_width;
1658 }
1659 } else {
1660 position += em_width
1661 }
1662 }
1663 let mut runs = Vec::default();
1664 if !glyphs.is_empty() {
1665 runs.push(ShapedRun {
1666 font_id: FontId(0),
1667 glyphs,
1668 });
1669 } else {
1670 position = px(0.);
1671 }
1672
1673 LineLayout {
1674 font_size,
1675 width: position,
1676 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1677 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1678 runs,
1679 len: text.len(),
1680 }
1681 }
1682
1683 fn recommended_rendering_mode(
1684 &self,
1685 _font_id: FontId,
1686 _font_size: Pixels,
1687 ) -> TextRenderingMode {
1688 TextRenderingMode::Grayscale
1689 }
1690}
1691
1692#[allow(dead_code)]
1697pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1698 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1699 [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], ];
1713
1714 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1715 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1716
1717 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1718 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1719
1720 [
1721 ratios[0] * NORM13,
1722 ratios[1] * NORM24,
1723 ratios[2] * NORM13,
1724 ratios[3] * NORM24,
1725 ]
1726}
1727
1728#[derive(PartialEq, Eq, Hash, Clone)]
1729#[expect(missing_docs)]
1730pub enum AtlasKey {
1731 Glyph(RenderGlyphParams),
1732 Svg(RenderSvgParams),
1733 Image(RenderImageParams),
1734}
1735
1736impl AtlasKey {
1737 pub fn texture_kind(&self) -> AtlasTextureKind {
1739 match self {
1740 AtlasKey::Glyph(params) => {
1741 if params.is_emoji {
1742 AtlasTextureKind::Polychrome
1743 } else if params.subpixel_rendering {
1744 AtlasTextureKind::Subpixel
1745 } else {
1746 AtlasTextureKind::Monochrome
1747 }
1748 }
1749 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1750 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1751 }
1752 }
1753}
1754
1755impl From<RenderGlyphParams> for AtlasKey {
1756 fn from(params: RenderGlyphParams) -> Self {
1757 Self::Glyph(params)
1758 }
1759}
1760
1761impl From<RenderSvgParams> for AtlasKey {
1762 fn from(params: RenderSvgParams) -> Self {
1763 Self::Svg(params)
1764 }
1765}
1766
1767impl From<RenderImageParams> for AtlasKey {
1768 fn from(params: RenderImageParams) -> Self {
1769 Self::Image(params)
1770 }
1771}
1772
1773#[expect(missing_docs)]
1774pub trait PlatformAtlas {
1775 fn get_or_insert_with<'a>(
1777 &self,
1778 key: AtlasKey,
1779 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1780 ) -> Result<Option<AtlasTile>>;
1781 fn remove(&self, key: &AtlasKey);
1782
1783 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1784 fn contains(&self, _key: &AtlasKey) -> bool {
1785 false
1786 }
1787}
1788
1789#[doc(hidden)]
1790pub trait AtlasBackend {
1791 fn insert(
1792 &mut self,
1793 kind: AtlasTextureKind,
1794 size: Size<DevicePixels>,
1795 bytes: &[u8],
1796 ) -> Result<AtlasTile>;
1797
1798 fn remove(&mut self, tile: AtlasTile);
1799}
1800
1801#[doc(hidden)]
1802pub struct AtlasState<Backend> {
1803 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1804 pub backend: Backend,
1805}
1806
1807impl<Backend> AtlasState<Backend> {
1808 pub fn new(backend: Backend) -> Self {
1809 Self {
1810 tiles_by_key: FxHashMap::default(),
1811 backend,
1812 }
1813 }
1814
1815 pub fn contains(&self, key: &AtlasKey) -> bool {
1816 self.tiles_by_key.contains_key(key)
1817 }
1818
1819 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1820 self.tiles_by_key.clear();
1821 reset_backend(&mut self.backend);
1822 }
1823}
1824
1825impl<Backend: Default> Default for AtlasState<Backend> {
1826 fn default() -> Self {
1827 Self::new(Backend::default())
1828 }
1829}
1830
1831impl<Backend: AtlasBackend> AtlasState<Backend> {
1832 pub fn get_or_insert_with<'a>(
1833 &mut self,
1834 key: AtlasKey,
1835 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1836 ) -> Result<Option<AtlasTile>> {
1837 match self.tiles_by_key.entry(key) {
1838 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1839 Entry::Vacant(entry) => {
1840 profiling::scope!("new tile");
1841 let Some((size, bytes)) = build()? else {
1842 return Ok(None);
1843 };
1844 let tile = self
1845 .backend
1846 .insert(entry.key().texture_kind(), size, &bytes)?;
1847 entry.insert(tile);
1848 Ok(Some(tile))
1849 }
1850 }
1851 }
1852
1853 pub fn remove(&mut self, key: &AtlasKey) {
1854 if let Some(tile) = self.tiles_by_key.remove(key) {
1855 self.backend.remove(tile);
1856 }
1857 }
1858}
1859
1860#[derive(Default)]
1864pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1865
1866#[doc(hidden)]
1867#[derive(Default)]
1868pub struct HeadlessAtlasBackend {
1869 next_id: u32,
1870}
1871
1872impl AtlasBackend for HeadlessAtlasBackend {
1873 fn insert(
1874 &mut self,
1875 kind: AtlasTextureKind,
1876 size: Size<DevicePixels>,
1877 _bytes: &[u8],
1878 ) -> Result<AtlasTile> {
1879 self.next_id += 1;
1880 let texture_id = self.next_id;
1881 self.next_id += 1;
1882 let tile_id = self.next_id;
1883 Ok(AtlasTile {
1884 texture_id: AtlasTextureId {
1885 index: texture_id,
1886 kind,
1887 },
1888 tile_id: TileId(tile_id),
1889 padding: 0,
1890 bounds: Bounds {
1891 origin: Point::default(),
1892 size,
1893 },
1894 })
1895 }
1896
1897 fn remove(&mut self, _tile: AtlasTile) {}
1898}
1899
1900impl PlatformAtlas for HeadlessAtlas {
1901 fn get_or_insert_with<'a>(
1902 &self,
1903 key: AtlasKey,
1904 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1905 ) -> Result<Option<AtlasTile>> {
1906 self.0.lock().get_or_insert_with(key, build)
1907 }
1908
1909 fn remove(&self, key: &AtlasKey) {
1910 self.0.lock().remove(key);
1911 }
1912
1913 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1914 fn contains(&self, key: &AtlasKey) -> bool {
1915 self.0.lock().contains(key)
1916 }
1917}
1918
1919#[doc(hidden)]
1920pub struct AtlasTextureList<T> {
1921 pub textures: Vec<Option<T>>,
1922 pub free_list: Vec<usize>,
1923}
1924
1925impl<T> Default for AtlasTextureList<T> {
1926 fn default() -> Self {
1927 Self {
1928 textures: Vec::default(),
1929 free_list: Vec::default(),
1930 }
1931 }
1932}
1933
1934impl<T> ops::Index<usize> for AtlasTextureList<T> {
1935 type Output = Option<T>;
1936
1937 fn index(&self, index: usize) -> &Self::Output {
1938 &self.textures[index]
1939 }
1940}
1941
1942impl<T> AtlasTextureList<T> {
1943 #[allow(unused)]
1944 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1945 self.free_list.clear();
1946 self.textures.drain(..)
1947 }
1948
1949 #[allow(dead_code)]
1950 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1951 self.textures.iter_mut().flatten()
1952 }
1953}
1954
1955#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1956#[repr(C)]
1957#[expect(missing_docs)]
1958pub struct AtlasTile {
1959 pub texture_id: AtlasTextureId,
1961 pub tile_id: TileId,
1963 pub padding: u32,
1965 pub bounds: Bounds<DevicePixels>,
1967}
1968
1969#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1970#[repr(C)]
1971#[expect(missing_docs)]
1972pub struct AtlasTextureId {
1973 pub index: u32,
1976 pub kind: AtlasTextureKind,
1978}
1979
1980#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1981#[repr(C)]
1982#[cfg_attr(
1983 all(
1984 any(target_os = "linux", target_os = "freebsd"),
1985 not(any(feature = "x11", feature = "wayland"))
1986 ),
1987 allow(dead_code)
1988)]
1989#[expect(missing_docs)]
1990pub enum AtlasTextureKind {
1991 Monochrome = 0,
1992 Polychrome = 1,
1993 Subpixel = 2,
1994}
1995
1996#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1997#[repr(C)]
1998#[expect(missing_docs)]
1999pub struct TileId(pub u32);
2000
2001impl From<etagere::AllocId> for TileId {
2002 fn from(id: etagere::AllocId) -> Self {
2003 Self(id.serialize())
2004 }
2005}
2006
2007impl From<TileId> for etagere::AllocId {
2008 fn from(id: TileId) -> Self {
2009 Self::deserialize(id.0)
2010 }
2011}
2012
2013#[expect(missing_docs)]
2014pub struct PlatformInputHandler {
2015 cx: AsyncWindowContext,
2016 handler: Box<dyn InputHandler>,
2017 focus_id: Option<crate::FocusId>,
2018}
2019
2020#[expect(missing_docs)]
2021#[cfg_attr(
2022 all(
2023 any(target_os = "linux", target_os = "freebsd"),
2024 not(any(feature = "x11", feature = "wayland"))
2025 ),
2026 allow(dead_code)
2027)]
2028impl PlatformInputHandler {
2029 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2030 Self {
2031 cx,
2032 handler,
2033 focus_id: None,
2034 }
2035 }
2036
2037 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2038 self.focus_id = Some(focus_id);
2039 self
2040 }
2041
2042 pub fn focus_id(&self) -> Option<crate::FocusId> {
2047 self.focus_id
2048 }
2049
2050 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2051 self.cx
2052 .update(|window, cx| {
2053 self.handler
2054 .selected_text_range(ignore_disabled_input, window, cx)
2055 })
2056 .ok()
2057 .flatten()
2058 }
2059
2060 #[cfg_attr(target_os = "windows", allow(dead_code))]
2061 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2062 self.cx
2063 .update(|window, cx| self.handler.marked_text_range(window, cx))
2064 .ok()
2065 .flatten()
2066 }
2067
2068 #[cfg_attr(
2069 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2070 allow(dead_code)
2071 )]
2072 pub fn text_for_range(
2073 &mut self,
2074 range_utf16: Range<usize>,
2075 adjusted: &mut Option<Range<usize>>,
2076 ) -> Option<String> {
2077 self.cx
2078 .update(|window, cx| {
2079 self.handler
2080 .text_for_range(range_utf16, adjusted, window, cx)
2081 })
2082 .ok()
2083 .flatten()
2084 }
2085
2086 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2087 self.cx
2088 .update(|window, cx| {
2089 self.handler
2090 .replace_text_in_range(replacement_range, text, window, cx);
2091 })
2092 .ok();
2093 }
2094
2095 pub fn replace_and_mark_text_in_range(
2096 &mut self,
2097 range_utf16: Option<Range<usize>>,
2098 new_text: &str,
2099 new_selected_range: Option<Range<usize>>,
2100 ) {
2101 self.cx
2102 .update(|window, cx| {
2103 self.handler.replace_and_mark_text_in_range(
2104 range_utf16,
2105 new_text,
2106 new_selected_range,
2107 window,
2108 cx,
2109 )
2110 })
2111 .ok();
2112 }
2113
2114 #[cfg_attr(target_os = "windows", allow(dead_code))]
2115 pub fn unmark_text(&mut self) {
2116 self.cx
2117 .update(|window, cx| self.handler.unmark_text(window, cx))
2118 .ok();
2119 }
2120
2121 pub fn paste(&mut self, item: ClipboardItem) {
2122 self.cx
2123 .update(|window, cx| self.handler.paste(item, window, cx))
2124 .ok();
2125 }
2126
2127 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2128 self.cx
2129 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2130 .ok()
2131 .flatten()
2132 }
2133
2134 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2135 self.cx
2136 .update(|window, cx| {
2137 self.handler
2138 .selection_bounds_for_range(range_utf16, window, cx)
2139 })
2140 .unwrap_or_default()
2141 }
2142
2143 #[allow(dead_code)]
2144 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2145 self.handler.apple_press_and_hold_enabled()
2146 }
2147
2148 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2149 self.handler.replace_text_in_range(None, input, window, cx);
2150 }
2151
2152 pub fn compute_ime_candidate_bounds(
2153 marked_range: Option<Range<usize>>,
2154 selection: &UTF16Selection,
2155 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2156 ) -> Option<Bounds<Pixels>> {
2157 if let Some(marked_range) = marked_range {
2158 let mut line_start = marked_range.start;
2160
2161 let caret = selection.range.end;
2165 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2166 for i in (marked_range.start..caret).rev() {
2167 if let Some(b) = bounds_for_range(i..i) {
2168 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2169 line_start = i + 1;
2170 break;
2171 }
2172 }
2173 }
2174 }
2175 bounds_for_range(line_start..line_start)
2176 } else {
2177 let offset = if selection.reversed {
2179 selection.range.start
2180 } else {
2181 selection.range.end
2182 };
2183 bounds_for_range(offset..offset)
2184 }
2185 }
2186
2187 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2188 let marked_range = self.handler.marked_text_range(window, cx);
2189 let selection = self.handler.selected_text_range(true, window, cx)?;
2190 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2191 self.handler.bounds_for_range(range, window, cx)
2192 })
2193 }
2194
2195 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2196 let marked_range = self.marked_text_range();
2197 let selection = self.selected_text_range(true)?;
2198 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2199 self.bounds_for_range(range)
2200 })
2201 }
2202
2203 #[allow(unused)]
2204 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2205 self.cx
2206 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2207 .ok()
2208 .flatten()
2209 }
2210
2211 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2213 self.cx
2214 .update(|window, cx| {
2215 self.handler
2216 .set_selected_text_range(range_utf16, window, cx)
2217 })
2218 .ok();
2219 }
2220
2221 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2223 self.cx
2224 .update(|window, cx| self.handler.element_bounds(window, cx))
2225 .ok()
2226 .flatten()
2227 }
2228
2229 pub fn text_length_utf16(&mut self) -> Option<usize> {
2231 self.cx
2232 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2233 .ok()
2234 .flatten()
2235 }
2236
2237 #[allow(dead_code)]
2238 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2239 self.handler.accepts_text_input(window, cx)
2240 }
2241
2242 #[allow(dead_code)]
2243 pub fn query_accepts_text_input(&mut self) -> bool {
2244 self.cx
2245 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2246 .unwrap_or(true)
2247 }
2248
2249 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2254 let focus_id = self.focus_id;
2255 self.cx
2256 .update(|window, cx| {
2257 focus_id.is_some()
2258 && window.focus == focus_id
2259 && self.handler.accepts_text_input(window, cx)
2260 })
2261 .unwrap_or(false)
2262 }
2263
2264 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2270 self.cx
2271 .update(|window, cx| {
2272 !window.has_pending_keystrokes()
2274 && self.handler.prefers_ime_for_printable_keys(window, cx)
2275 })
2276 .unwrap_or(false)
2277 }
2278
2279 pub fn text_input_configuration(
2281 &mut self,
2282 window: &mut Window,
2283 cx: &mut App,
2284 ) -> TextInputConfiguration {
2285 self.handler.text_input_configuration(window, cx)
2286 }
2287
2288 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2290 self.cx
2291 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2292 .ok()
2293 .flatten()
2294 }
2295}
2296
2297#[derive(Debug)]
2300pub struct UTF16Selection {
2301 pub range: Range<usize>,
2304 pub reversed: bool,
2307}
2308
2309pub trait InputHandler: 'static {
2314 fn selected_text_range(
2319 &mut self,
2320 ignore_disabled_input: bool,
2321 window: &mut Window,
2322 cx: &mut App,
2323 ) -> Option<UTF16Selection>;
2324
2325 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2330
2331 fn text_for_range(
2336 &mut self,
2337 range_utf16: Range<usize>,
2338 adjusted_range: &mut Option<Range<usize>>,
2339 window: &mut Window,
2340 cx: &mut App,
2341 ) -> Option<String>;
2342
2343 fn replace_text_in_range(
2348 &mut self,
2349 replacement_range: Option<Range<usize>>,
2350 text: &str,
2351 window: &mut Window,
2352 cx: &mut App,
2353 );
2354
2355 fn replace_and_mark_text_in_range(
2362 &mut self,
2363 range_utf16: Option<Range<usize>>,
2364 new_text: &str,
2365 new_selected_range: Option<Range<usize>>,
2366 window: &mut Window,
2367 cx: &mut App,
2368 );
2369
2370 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2373
2374 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2381 if let Some(text) = item.text() {
2382 self.replace_text_in_range(None, &text, window, cx);
2383 }
2384 }
2385
2386 fn bounds_for_range(
2391 &mut self,
2392 range_utf16: Range<usize>,
2393 window: &mut Window,
2394 cx: &mut App,
2395 ) -> Option<Bounds<Pixels>>;
2396
2397 fn selection_bounds_for_range(
2404 &mut self,
2405 _range_utf16: Range<usize>,
2406 _window: &mut Window,
2407 _cx: &mut App,
2408 ) -> Vec<Bounds<Pixels>> {
2409 Vec::new()
2410 }
2411
2412 fn character_index_for_point(
2416 &mut self,
2417 point: Point<Pixels>,
2418 window: &mut Window,
2419 cx: &mut App,
2420 ) -> Option<usize>;
2421
2422 fn set_selected_text_range(
2432 &mut self,
2433 _range_utf16: Range<usize>,
2434 _window: &mut Window,
2435 _cx: &mut App,
2436 ) {
2437 }
2438
2439 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2446 None
2447 }
2448
2449 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2451 None
2452 }
2453
2454 #[allow(dead_code)]
2459 fn apple_press_and_hold_enabled(&mut self) -> bool {
2460 true
2461 }
2462
2463 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2465 true
2466 }
2467
2468 fn text_input_editable_range(
2479 &mut self,
2480 _window: &mut Window,
2481 _cx: &mut App,
2482 ) -> Option<Range<usize>> {
2483 None
2484 }
2485
2486 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2495 false
2496 }
2497
2498 fn text_input_configuration(
2504 &mut self,
2505 _window: &mut Window,
2506 _cx: &mut App,
2507 ) -> TextInputConfiguration {
2508 TextInputConfiguration::default()
2509 }
2510}
2511
2512#[derive(Clone, Debug, Default, PartialEq, Eq)]
2522pub struct TextInputConfiguration {
2523 pub autocorrect: bool,
2525 pub autocapitalize: Autocapitalize,
2527 pub suggestions: bool,
2529 pub input_action: TextInputAction,
2531}
2532
2533#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2535pub enum Autocapitalize {
2536 #[default]
2538 None,
2539 Words,
2541 Sentences,
2543 Characters,
2545}
2546
2547#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2557pub enum TextInputAction {
2558 #[default]
2560 Unspecified,
2561 Enter,
2563 Done,
2565 Go,
2567 Next,
2569 Previous,
2571 Search,
2573 Send,
2575}
2576
2577#[derive(Debug)]
2579pub struct WindowOptions {
2580 pub window_bounds: Option<WindowBounds>,
2584
2585 pub titlebar: Option<TitlebarOptions>,
2587
2588 pub focus: bool,
2590
2591 pub show: bool,
2593
2594 pub kind: WindowKind,
2596
2597 pub is_movable: bool,
2601
2602 pub app_owns_titlebar_drag: bool,
2614
2615 pub inactive_frame_interval: Option<Duration>,
2619
2620 pub is_resizable: bool,
2622
2623 pub is_minimizable: bool,
2625
2626 pub display_id: Option<DisplayId>,
2629
2630 pub window_background: WindowBackgroundAppearance,
2632
2633 pub app_id: Option<String>,
2635
2636 pub window_min_size: Option<Size<Pixels>>,
2638
2639 pub window_decorations: Option<WindowDecorations>,
2642
2643 pub icon: Option<Arc<image::RgbaImage>>,
2645
2646 pub tabbing_identifier: Option<String>,
2648
2649 pub use_display_p3: bool,
2651}
2652
2653#[derive(Debug)]
2655#[cfg_attr(
2656 all(
2657 any(target_os = "linux", target_os = "freebsd"),
2658 not(any(feature = "x11", feature = "wayland"))
2659 ),
2660 allow(dead_code)
2661)]
2662#[allow(missing_docs)]
2663pub struct WindowParams {
2664 pub bounds: Bounds<Pixels>,
2665
2666 #[cfg_attr(feature = "wayland", allow(dead_code))]
2668 pub titlebar: Option<TitlebarOptions>,
2669
2670 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2672 pub kind: WindowKind,
2673
2674 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2676 pub is_movable: bool,
2677
2678 #[cfg_attr(
2680 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2681 allow(dead_code)
2682 )]
2683 pub app_owns_titlebar_drag: bool,
2684
2685 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2687 pub is_resizable: bool,
2688
2689 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2691 pub is_minimizable: bool,
2692
2693 #[cfg_attr(
2694 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2695 allow(dead_code)
2696 )]
2697 pub focus: bool,
2698
2699 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2700 pub show: bool,
2701
2702 #[cfg_attr(feature = "wayland", allow(dead_code))]
2704 pub icon: Option<Arc<image::RgbaImage>>,
2705
2706 #[cfg_attr(feature = "wayland", allow(dead_code))]
2707 pub display_id: Option<DisplayId>,
2708
2709 #[cfg_attr(feature = "wayland", allow(dead_code))]
2710 pub app_id: Option<String>,
2711
2712 pub window_min_size: Option<Size<Pixels>>,
2713
2714 #[cfg(target_os = "macos")]
2715 pub tabbing_identifier: Option<String>,
2716
2717 #[cfg(target_os = "macos")]
2718 pub use_display_p3: bool,
2719}
2720
2721#[derive(Debug, Copy, Clone, PartialEq)]
2723pub enum WindowBounds {
2724 Windowed(Bounds<Pixels>),
2726 Maximized(Bounds<Pixels>),
2729 Fullscreen(Bounds<Pixels>),
2732}
2733
2734impl Default for WindowBounds {
2735 fn default() -> Self {
2736 WindowBounds::Windowed(Bounds::default())
2737 }
2738}
2739
2740impl WindowBounds {
2741 pub fn get_bounds(&self) -> Bounds<Pixels> {
2743 match self {
2744 WindowBounds::Windowed(bounds) => *bounds,
2745 WindowBounds::Maximized(bounds) => *bounds,
2746 WindowBounds::Fullscreen(bounds) => *bounds,
2747 }
2748 }
2749
2750 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2752 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2753 }
2754}
2755
2756impl Default for WindowOptions {
2757 fn default() -> Self {
2758 Self {
2759 window_bounds: None,
2760 titlebar: Some(TitlebarOptions {
2761 title: Default::default(),
2762 appears_transparent: Default::default(),
2763 traffic_light_position: Default::default(),
2764 }),
2765 focus: true,
2766 show: true,
2767 kind: WindowKind::Normal,
2768 is_movable: true,
2769 app_owns_titlebar_drag: false,
2770 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2771 is_resizable: true,
2772 is_minimizable: true,
2773 display_id: None,
2774 window_background: WindowBackgroundAppearance::default(),
2775 icon: None,
2776 app_id: None,
2777 window_min_size: None,
2778 window_decorations: None,
2779 tabbing_identifier: None,
2780 use_display_p3: false,
2781 }
2782 }
2783}
2784
2785#[derive(Debug, Default)]
2787pub struct TitlebarOptions {
2788 pub title: Option<SharedString>,
2790
2791 pub appears_transparent: bool,
2794
2795 pub traffic_light_position: Option<Point<Pixels>>,
2797}
2798
2799#[derive(Clone, Debug, PartialEq, Eq)]
2801pub enum WindowKind {
2802 Normal,
2804
2805 PopUp,
2808
2809 AnchoredPopup(popup::PopupOptions),
2816
2817 Floating,
2819
2820 #[cfg(all(target_os = "linux", feature = "wayland"))]
2823 LayerShell(layer_shell::LayerShellOptions),
2824
2825 Dialog,
2828}
2829
2830#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2835pub enum WindowAppearance {
2836 #[default]
2840 Light,
2841
2842 VibrantLight,
2846
2847 Dark,
2851
2852 VibrantDark,
2856}
2857
2858#[derive(Copy, Clone, Debug, Default, PartialEq)]
2861pub enum WindowBackgroundAppearance {
2862 #[default]
2870 Opaque,
2871 Transparent,
2873 Blurred,
2877 MicaBackdrop,
2879 MicaAltBackdrop,
2881}
2882
2883#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2885pub enum TextRenderingMode {
2886 #[default]
2888 PlatformDefault,
2889 Subpixel,
2891 Grayscale,
2893}
2894
2895#[derive(Clone, Debug)]
2897pub struct PathPromptOptions {
2898 pub files: bool,
2900 pub directories: bool,
2902 pub multiple: bool,
2904 pub prompt: Option<SharedString>,
2906}
2907
2908#[derive(Copy, Clone, Debug, PartialEq)]
2910pub enum PromptLevel {
2911 Info,
2913
2914 Warning,
2916
2917 Critical,
2919}
2920
2921#[derive(Clone, Debug, PartialEq)]
2923pub enum PromptButton {
2924 Ok(SharedString),
2926 Cancel(SharedString),
2928 Other(SharedString),
2930}
2931
2932impl PromptButton {
2933 pub fn new(label: impl Into<SharedString>) -> Self {
2935 PromptButton::Other(label.into())
2936 }
2937
2938 pub fn ok(label: impl Into<SharedString>) -> Self {
2940 PromptButton::Ok(label.into())
2941 }
2942
2943 pub fn cancel(label: impl Into<SharedString>) -> Self {
2945 PromptButton::Cancel(label.into())
2946 }
2947
2948 #[allow(dead_code)]
2950 pub fn is_cancel(&self) -> bool {
2951 matches!(self, PromptButton::Cancel(_))
2952 }
2953
2954 pub fn label(&self) -> &SharedString {
2956 match self {
2957 PromptButton::Ok(label) => label,
2958 PromptButton::Cancel(label) => label,
2959 PromptButton::Other(label) => label,
2960 }
2961 }
2962}
2963
2964impl From<&str> for PromptButton {
2965 fn from(value: &str) -> Self {
2966 match value.to_lowercase().as_str() {
2967 "ok" => PromptButton::Ok("OK".into()),
2968 "cancel" => PromptButton::Cancel("Cancel".into()),
2969 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2970 }
2971 }
2972}
2973
2974#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2976pub enum CursorStyle {
2977 #[default]
2979 Arrow,
2980
2981 IBeam,
2984
2985 Crosshair,
2988
2989 ClosedHand,
2992
2993 OpenHand,
2996
2997 PointingHand,
3000
3001 ResizeLeft,
3004
3005 ResizeRight,
3008
3009 ResizeLeftRight,
3012
3013 ResizeUp,
3016
3017 ResizeDown,
3020
3021 ResizeUpDown,
3024
3025 ResizeUpLeftDownRight,
3028
3029 ResizeUpRightDownLeft,
3032
3033 ResizeColumn,
3036
3037 ResizeRow,
3040
3041 IBeamCursorForVerticalLayout,
3044
3045 OperationNotAllowed,
3048
3049 DragLink,
3052
3053 DragCopy,
3056
3057 ContextualMenu,
3060}
3061
3062#[derive(Clone, Debug, Eq, PartialEq)]
3064pub struct ClipboardItem {
3065 pub entries: Vec<ClipboardEntry>,
3067}
3068
3069#[derive(Clone, Debug, PartialEq, Eq)]
3074pub enum ClipboardReadError {
3075 Unavailable,
3079 Denied(String),
3082 UnsupportedContent,
3085}
3086
3087impl std::fmt::Display for ClipboardReadError {
3088 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3089 match self {
3090 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3091 Self::Denied(message) => {
3092 write!(formatter, "clipboard access was denied: {message}")
3093 }
3094 Self::UnsupportedContent => {
3095 formatter.write_str("the clipboard contents are unsupported")
3096 }
3097 }
3098 }
3099}
3100
3101impl std::error::Error for ClipboardReadError {}
3102
3103#[derive(Clone, Debug, Eq, PartialEq)]
3105pub enum ClipboardEntry {
3106 String(ClipboardString),
3108 Image(Image),
3110 ExternalPaths(crate::ExternalPaths),
3112}
3113
3114impl ClipboardItem {
3115 pub fn new_string(text: String) -> Self {
3117 Self {
3118 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3119 }
3120 }
3121
3122 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3124 Self {
3125 entries: vec![ClipboardEntry::String(ClipboardString {
3126 text,
3127 metadata: Some(metadata),
3128 })],
3129 }
3130 }
3131
3132 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3134 Self {
3135 entries: vec![ClipboardEntry::String(
3136 ClipboardString::new(text).with_json_metadata(metadata),
3137 )],
3138 }
3139 }
3140
3141 pub fn new_image(image: &Image) -> Self {
3143 Self {
3144 entries: vec![ClipboardEntry::Image(image.clone())],
3145 }
3146 }
3147
3148 pub fn text(&self) -> Option<String> {
3151 let mut answer = String::new();
3152
3153 for entry in self.entries.iter() {
3154 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3155 answer.push_str(text);
3156 }
3157 }
3158
3159 if answer.is_empty() {
3160 for entry in self.entries.iter() {
3161 if let ClipboardEntry::ExternalPaths(paths) = entry {
3162 for path in &paths.0 {
3163 use std::fmt::Write as _;
3164 _ = write!(answer, "{}", path.display());
3165 }
3166 }
3167 }
3168 }
3169
3170 if !answer.is_empty() {
3171 Some(answer)
3172 } else {
3173 None
3174 }
3175 }
3176
3177 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3179 pub fn metadata(&self) -> Option<&String> {
3180 match self.entries().first() {
3181 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3182 clipboard_string.metadata.as_ref()
3183 }
3184 _ => None,
3185 }
3186 }
3187
3188 pub fn entries(&self) -> &[ClipboardEntry] {
3190 &self.entries
3191 }
3192
3193 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3195 self.entries.into_iter()
3196 }
3197}
3198
3199impl From<ClipboardString> for ClipboardEntry {
3200 fn from(value: ClipboardString) -> Self {
3201 Self::String(value)
3202 }
3203}
3204
3205impl From<String> for ClipboardEntry {
3206 fn from(value: String) -> Self {
3207 Self::from(ClipboardString::from(value))
3208 }
3209}
3210
3211impl From<Image> for ClipboardEntry {
3212 fn from(value: Image) -> Self {
3213 Self::Image(value)
3214 }
3215}
3216
3217impl From<ClipboardEntry> for ClipboardItem {
3218 fn from(value: ClipboardEntry) -> Self {
3219 Self {
3220 entries: vec![value],
3221 }
3222 }
3223}
3224
3225impl From<String> for ClipboardItem {
3226 fn from(value: String) -> Self {
3227 Self::from(ClipboardEntry::from(value))
3228 }
3229}
3230
3231impl From<Image> for ClipboardItem {
3232 fn from(value: Image) -> Self {
3233 Self::from(ClipboardEntry::from(value))
3234 }
3235}
3236
3237#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3239pub enum ImageFormat {
3240 Png,
3245 Jpeg,
3247 Webp,
3249 Gif,
3251 Svg,
3253 Bmp,
3255 Tiff,
3257 Ico,
3259 Pnm,
3261}
3262
3263impl ImageFormat {
3264 pub const fn mime_type(self) -> &'static str {
3266 match self {
3267 ImageFormat::Png => "image/png",
3268 ImageFormat::Jpeg => "image/jpeg",
3269 ImageFormat::Webp => "image/webp",
3270 ImageFormat::Gif => "image/gif",
3271 ImageFormat::Svg => "image/svg+xml",
3272 ImageFormat::Bmp => "image/bmp",
3273 ImageFormat::Tiff => "image/tiff",
3274 ImageFormat::Ico => "image/ico",
3275 ImageFormat::Pnm => "image/x-portable-anymap",
3276 }
3277 }
3278
3279 pub const fn extension(self) -> &'static str {
3281 match self {
3282 ImageFormat::Png => "png",
3283 ImageFormat::Jpeg => "jpg",
3284 ImageFormat::Webp => "webp",
3285 ImageFormat::Gif => "gif",
3286 ImageFormat::Svg => "svg",
3287 ImageFormat::Bmp => "bmp",
3288 ImageFormat::Tiff => "tiff",
3289 ImageFormat::Ico => "ico",
3290 ImageFormat::Pnm => "pnm",
3291 }
3292 }
3293
3294 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3296 use strum::IntoEnumIterator;
3297 Self::iter()
3298 .find(|format| format.mime_type() == mime_type)
3299 .or_else(|| Self::from_mime_type_alias(mime_type))
3300 }
3301
3302 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3306 match mime_type {
3307 "image/jpg" => Some(Self::Jpeg),
3308 "image/tif" => Some(Self::Tiff),
3309 _ => None,
3310 }
3311 }
3312}
3313
3314#[derive(Clone, Debug, PartialEq, Eq)]
3316pub struct Image {
3317 pub format: ImageFormat,
3319 pub bytes: Vec<u8>,
3321 pub id: u64,
3323}
3324
3325pub(crate) fn decode_static_image(
3326 bytes: &[u8],
3327 format: image::ImageFormat,
3328) -> Result<SmallVec<[Frame; 1]>> {
3329 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3330 .into_decoder()
3331 .context("creating image decoder")?;
3332 decode_static_image_from_decoder(decoder)
3333}
3334
3335pub(crate) fn decode_static_image_from_decoder(
3336 mut decoder: impl image::ImageDecoder,
3337) -> Result<SmallVec<[Frame; 1]>> {
3338 let orientation = decoder
3339 .orientation()
3340 .context("reading decoder's orientation")?;
3341 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3342 image.apply_orientation(orientation);
3343
3344 let mut data = image.into_rgba8();
3345 for pixel in data.chunks_exact_mut(4) {
3346 pixel.swap(0, 2);
3347 }
3348
3349 Ok(SmallVec::from_elem(Frame::new(data), 1))
3350}
3351
3352impl Hash for Image {
3353 fn hash<H: Hasher>(&self, state: &mut H) {
3354 state.write_u64(self.id);
3355 }
3356}
3357
3358impl Image {
3359 pub fn empty() -> Self {
3361 Self::from_bytes(ImageFormat::Png, Vec::new())
3362 }
3363
3364 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3366 Self {
3367 id: hash(&bytes),
3368 format,
3369 bytes,
3370 }
3371 }
3372
3373 pub fn id(&self) -> u64 {
3375 self.id
3376 }
3377
3378 pub fn use_render_image(
3380 self: Arc<Self>,
3381 window: &mut Window,
3382 cx: &mut App,
3383 ) -> Option<Arc<RenderImage>> {
3384 ImageSource::Image(self)
3385 .use_data(None, window, cx)
3386 .and_then(|result| result.ok())
3387 }
3388
3389 pub fn get_render_image(
3391 self: Arc<Self>,
3392 window: &mut Window,
3393 cx: &mut App,
3394 ) -> Option<Arc<RenderImage>> {
3395 ImageSource::Image(self)
3396 .get_data(None, window, cx)
3397 .and_then(|result| result.ok())
3398 }
3399
3400 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3402 ImageSource::Image(self).remove_asset(cx);
3403 }
3404
3405 #[cfg(any(test, feature = "test-support"))]
3408 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3409 ImageSource::Image(self.clone()).is_asset_cached(cx)
3410 }
3411
3412 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3414 let frames = match self.format {
3415 ImageFormat::Gif => {
3416 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3417 let mut frames = SmallVec::new();
3418
3419 for frame in decoder.into_frames() {
3420 match frame {
3421 Ok(mut frame) => {
3422 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3424 pixel.swap(0, 2);
3425 }
3426 frames.push(frame);
3427 }
3428 Err(err) => {
3429 log::debug!("Skipping GIF frame due to decode error: {err}");
3430 }
3431 }
3432 }
3433
3434 if frames.is_empty() {
3435 anyhow::bail!("GIF could not be decoded: all frames failed");
3436 }
3437
3438 frames
3439 }
3440 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3441 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3442 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3443 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3444 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3445 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3446 ImageFormat::Svg => {
3447 return svg_renderer
3448 .render_single_frame(&self.bytes, 1.0)
3449 .map_err(Into::into);
3450 }
3451 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3452 };
3453
3454 Ok(Arc::new(RenderImage::new(frames)))
3455 }
3456
3457 pub fn format(&self) -> ImageFormat {
3459 self.format
3460 }
3461
3462 pub fn bytes(&self) -> &[u8] {
3464 self.bytes.as_slice()
3465 }
3466}
3467
3468#[derive(Clone, Debug, Eq, PartialEq)]
3470pub struct ClipboardString {
3471 pub text: String,
3473 pub metadata: Option<String>,
3475}
3476
3477impl ClipboardString {
3478 pub fn new(text: String) -> Self {
3480 Self {
3481 text,
3482 metadata: None,
3483 }
3484 }
3485
3486 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3489 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3490 self
3491 }
3492
3493 pub fn text(&self) -> &String {
3495 &self.text
3496 }
3497
3498 pub fn into_text(self) -> String {
3500 self.text
3501 }
3502
3503 pub fn metadata_json<T>(&self) -> Option<T>
3505 where
3506 T: for<'a> Deserialize<'a>,
3507 {
3508 self.metadata
3509 .as_ref()
3510 .and_then(|m| serde_json::from_str(m).ok())
3511 }
3512
3513 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3514 pub fn text_hash(text: &str) -> u64 {
3516 let mut hasher = SeaHasher::new();
3517 text.hash(&mut hasher);
3518 hasher.finish()
3519 }
3520}
3521
3522impl From<String> for ClipboardString {
3523 fn from(value: String) -> Self {
3524 Self {
3525 text: value,
3526 metadata: None,
3527 }
3528 }
3529}
3530
3531#[cfg(test)]
3532mod image_tests {
3533 use super::*;
3534 use std::sync::Arc;
3535
3536 #[test]
3537 fn test_image_to_image_data_applies_exif_orientation() {
3538 let image = Image::from_bytes(
3539 ImageFormat::Jpeg,
3540 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3541 );
3542
3543 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3544
3545 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3546
3547 let bytes = render_image.as_bytes(0).unwrap();
3548 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3549 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3550 }
3551
3552 #[test]
3553 fn test_svg_image_to_image_data_converts_to_bgra() {
3554 let image = Image::from_bytes(
3555 ImageFormat::Svg,
3556 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3557<rect width="1" height="1" fill="#38BDF8"/>
3558</svg>"##
3559 .to_vec(),
3560 );
3561
3562 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3563 let bytes = render_image.as_bytes(0).unwrap();
3564
3565 for pixel in bytes.chunks_exact(4) {
3566 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3567 }
3568 }
3569}
3570
3571#[cfg(test)]
3572mod atlas_tests {
3573 use super::*;
3574
3575 const TILE_SIZE: Size<DevicePixels> = Size {
3576 width: DevicePixels(1),
3577 height: DevicePixels(1),
3578 };
3579
3580 #[derive(Default)]
3581 struct RecordingAtlasBackend {
3582 insert_calls: u32,
3583 fail_next_insert: bool,
3584 removed_tiles: Vec<AtlasTile>,
3585 }
3586
3587 impl AtlasBackend for RecordingAtlasBackend {
3588 fn insert(
3589 &mut self,
3590 kind: AtlasTextureKind,
3591 size: Size<DevicePixels>,
3592 _bytes: &[u8],
3593 ) -> Result<AtlasTile> {
3594 self.insert_calls += 1;
3595 if std::mem::take(&mut self.fail_next_insert) {
3596 anyhow::bail!("backend failed");
3597 }
3598 Ok(AtlasTile {
3599 texture_id: AtlasTextureId { index: 0, kind },
3600 tile_id: TileId(self.insert_calls),
3601 padding: 0,
3602 bounds: Bounds {
3603 origin: Point::default(),
3604 size,
3605 },
3606 })
3607 }
3608
3609 fn remove(&mut self, tile: AtlasTile) {
3610 self.removed_tiles.push(tile);
3611 }
3612 }
3613
3614 fn image_key(image_id: usize) -> AtlasKey {
3615 AtlasKey::Image(RenderImageParams {
3616 image_id: crate::ImageId(image_id),
3617 frame_index: 0,
3618 })
3619 }
3620
3621 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3622 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3623 }
3624
3625 #[test]
3626 fn only_successful_inserts_are_cached() -> Result<()> {
3627 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3628 let key = image_key(1);
3629
3630 assert_eq!(
3631 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3632 None
3633 );
3634 state
3635 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3636 .expect_err("builder error should propagate");
3637 assert!(!state.contains(&key));
3638 assert_eq!(state.backend.insert_calls, 0);
3639
3640 state.backend.fail_next_insert = true;
3641 state
3642 .get_or_insert_with(key.clone(), &mut build_tile)
3643 .expect_err("backend error should propagate");
3644 assert!(!state.contains(&key));
3645 assert_eq!(state.backend.insert_calls, 1);
3646
3647 let tile = state
3648 .get_or_insert_with(key.clone(), &mut build_tile)?
3649 .context("builder should produce a tile")?;
3650 assert_eq!(tile.texture_id.kind, key.texture_kind());
3651 assert_eq!(
3652 state.get_or_insert_with(key.clone(), &mut || {
3653 anyhow::bail!("cache hit must not call the builder")
3654 })?,
3655 Some(tile)
3656 );
3657 assert!(state.contains(&key));
3658 assert_eq!(state.backend.insert_calls, 2);
3659 Ok(())
3660 }
3661
3662 #[test]
3663 fn remove_and_clear_invalidate_keys() -> Result<()> {
3664 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3665 let key = image_key(1);
3666 let other_key = image_key(2);
3667 let tile = state
3668 .get_or_insert_with(key.clone(), &mut build_tile)?
3669 .context("builder should produce a tile")?;
3670 state
3671 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3672 .context("builder should produce another tile")?;
3673
3674 state.remove(&key);
3675 state.remove(&key);
3676 assert!(!state.contains(&key));
3677 assert!(state.contains(&other_key));
3678 assert_eq!(state.backend.removed_tiles, vec![tile]);
3679
3680 let mut reset_calls = 0;
3681 state.clear(|_| reset_calls += 1);
3682 assert_eq!(reset_calls, 1);
3683 assert!(!state.contains(&other_key));
3684 assert_eq!(state.backend.removed_tiles, vec![tile]);
3685 Ok(())
3686 }
3687}
3688
3689#[cfg(test)]
3690mod frame_signal_tests {
3691 use super::*;
3692
3693 #[cfg(feature = "profiler")]
3694 #[test]
3695 fn coalesced_signals_retain_the_earliest_until_drained() {
3696 for (first_source, later_source) in [
3697 (
3698 FrameRequestSource::NativeCallback,
3699 FrameRequestSource::LocalSchedule,
3700 ),
3701 (
3702 FrameRequestSource::LocalSchedule,
3703 FrameRequestSource::NativeCallback,
3704 ),
3705 ] {
3706 let signal = PlatformFrameSignal::new();
3707 let first =
3708 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3709 assert_eq!(signal.take(), None);
3710 signal.record(first + Duration::from_millis(16), later_source);
3711 signal.record(first, first_source);
3712 signal.record(first + Duration::from_millis(32), later_source);
3713 assert_eq!(signal.take(), Some((first, first_source)));
3714 assert_eq!(signal.take(), None);
3715 let next = first + Duration::from_millis(48);
3716 signal.record(next, later_source);
3717 assert_eq!(signal.take(), Some((next, later_source)));
3718 }
3719 }
3720
3721 #[cfg(not(feature = "profiler"))]
3722 #[test]
3723 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3724 let captured = PlatformFrameSignal::capture(|| {
3725 panic!("profiler-disabled builds must not evaluate the capture closure");
3726 });
3727 assert_eq!(captured, None);
3728 let signal = PlatformFrameSignal::new();
3729 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3730 assert_eq!(signal.take(), None);
3731 }
3732}
3733
3734#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3735mod tests {
3736 use super::*;
3737 use std::collections::HashSet;
3738
3739 #[test]
3740 fn test_refresh_interval_from_hz() {
3741 assert_eq!(
3742 refresh_interval_from_hz(50.0),
3743 Some(Duration::from_millis(20))
3744 );
3745 assert_eq!(
3746 refresh_interval_from_hz(120.0),
3747 Some(Duration::from_secs(1) / 120)
3748 );
3749 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3750 assert_eq!(refresh_interval_from_hz(unknown), None);
3751 }
3752 }
3753
3754 #[test]
3755 fn test_window_button_layout_parse_standard() {
3756 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3757 assert_eq!(
3758 layout.left,
3759 [
3760 Some(WindowButton::Close),
3761 Some(WindowButton::Minimize),
3762 None
3763 ]
3764 );
3765 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3766 }
3767
3768 #[test]
3769 fn test_window_button_layout_parse_right_only() {
3770 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3771 assert_eq!(layout.left, [None, None, None]);
3772 assert_eq!(
3773 layout.right,
3774 [
3775 Some(WindowButton::Minimize),
3776 Some(WindowButton::Maximize),
3777 Some(WindowButton::Close)
3778 ]
3779 );
3780 }
3781
3782 #[test]
3783 fn test_window_button_layout_parse_left_only() {
3784 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3785 assert_eq!(
3786 layout.left,
3787 [
3788 Some(WindowButton::Close),
3789 Some(WindowButton::Minimize),
3790 Some(WindowButton::Maximize)
3791 ]
3792 );
3793 assert_eq!(layout.right, [None, None, None]);
3794 }
3795
3796 #[test]
3797 fn test_window_button_layout_parse_with_whitespace() {
3798 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3799 assert_eq!(
3800 layout.left,
3801 [
3802 Some(WindowButton::Close),
3803 Some(WindowButton::Minimize),
3804 None
3805 ]
3806 );
3807 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3808 }
3809
3810 #[test]
3811 fn test_window_button_layout_parse_empty() {
3812 let layout = WindowButtonLayout::parse("").unwrap();
3813 assert_eq!(layout.left, [None, None, None]);
3814 assert_eq!(layout.right, [None, None, None]);
3815 }
3816
3817 #[test]
3818 fn test_window_button_layout_parse_intentionally_empty() {
3819 let layout = WindowButtonLayout::parse(":").unwrap();
3820 assert_eq!(layout.left, [None, None, None]);
3821 assert_eq!(layout.right, [None, None, None]);
3822 }
3823
3824 #[test]
3825 fn test_window_button_layout_parse_invalid_buttons() {
3826 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3827 assert_eq!(
3828 layout.left,
3829 [
3830 Some(WindowButton::Close),
3831 Some(WindowButton::Minimize),
3832 None
3833 ]
3834 );
3835 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3836 }
3837
3838 #[test]
3839 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3840 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3841 assert_eq!(
3842 layout.right,
3843 [
3844 Some(WindowButton::Close),
3845 Some(WindowButton::Minimize),
3846 None
3847 ]
3848 );
3849 assert_eq!(layout.format(), ":close,minimize");
3850 }
3851
3852 #[test]
3853 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3854 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3855 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3856 assert_eq!(
3857 layout.right,
3858 [
3859 Some(WindowButton::Maximize),
3860 Some(WindowButton::Minimize),
3861 None
3862 ]
3863 );
3864
3865 let button_ids: Vec<_> = layout
3866 .left
3867 .iter()
3868 .chain(layout.right.iter())
3869 .flatten()
3870 .map(WindowButton::id)
3871 .collect();
3872 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3873 assert_eq!(unique_button_ids.len(), button_ids.len());
3874 assert_eq!(layout.format(), "close:maximize,minimize");
3875 }
3876
3877 #[test]
3878 fn test_window_button_layout_parse_gnome_style() {
3879 let layout = WindowButtonLayout::parse("close").unwrap();
3880 assert_eq!(layout.left, [None, None, None]);
3881 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3882 }
3883
3884 #[test]
3885 fn test_window_button_layout_parse_elementary_style() {
3886 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3887 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3888 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3889 }
3890
3891 #[test]
3892 fn test_window_button_layout_round_trip() {
3893 let cases = [
3894 "close:minimize,maximize",
3895 "minimize,maximize,close:",
3896 ":close",
3897 "close:",
3898 "close:maximize",
3899 ":",
3900 ];
3901
3902 for case in cases {
3903 let layout = WindowButtonLayout::parse(case).unwrap();
3904 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3905 }
3906 }
3907
3908 #[test]
3909 fn test_window_button_layout_linux_default() {
3910 let layout = WindowButtonLayout::linux_default();
3911 assert_eq!(layout.left, [None, None, None]);
3912 assert_eq!(
3913 layout.right,
3914 [
3915 Some(WindowButton::Minimize),
3916 Some(WindowButton::Maximize),
3917 Some(WindowButton::Close)
3918 ]
3919 );
3920
3921 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3922 assert_eq!(round_tripped, layout);
3923 }
3924
3925 #[test]
3926 fn test_window_button_layout_parse_all_invalid() {
3927 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3928 }
3929}