1mod color_presentation;
2pub use color_presentation::{WindowColorFallback, WindowColorMode, WindowColorStatus};
3
4mod app_menu;
5mod keyboard;
6mod keystroke;
7
8#[cfg(all(target_os = "linux", feature = "wayland"))]
9#[expect(missing_docs)]
10pub mod layer_shell;
11
12pub mod popup;
14
15#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
16mod threaded_dispatcher;
17
18#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
19mod test;
20
21#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
22mod visual_test;
23
24#[cfg(all(
25 feature = "screen-capture",
26 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
27))]
28pub mod scap_screen_capture;
29
30#[cfg(all(
31 any(target_os = "windows", target_os = "linux"),
32 feature = "screen-capture"
33))]
34pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
35#[cfg(not(feature = "screen-capture"))]
36pub(crate) type PlatformScreenCaptureFrame = ();
37#[cfg(all(target_os = "macos", feature = "screen-capture"))]
38pub(crate) type PlatformScreenCaptureFrame =
39 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
40
41use crate::{
42 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, Corners,
43 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
44 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
45 LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority,
46 RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
47 ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea,
48 hash, point, px, size,
49};
50#[cfg(any(target_os = "linux", target_os = "freebsd"))]
51use anyhow::bail;
52use anyhow::{Context as _, Result};
53use async_task::Runnable;
54use collections::FxHashMap;
55use futures::channel::oneshot;
56#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
57use image::RgbaImage;
58use image::codecs::gif::GifDecoder;
59use image::{AnimationDecoder as _, DynamicImage, Frame};
60use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
61use scheduler::Instant;
62pub use scheduler::RunnableMeta;
63use schemars::JsonSchema;
64use seahash::SeaHasher;
65use serde::{Deserialize, Serialize};
66use smallvec::SmallVec;
67use std::borrow::Cow;
68use std::collections::hash_map::Entry;
69use std::hash::{Hash, Hasher};
70use std::io::Cursor;
71use std::ops;
72use std::time::Duration;
73use std::{
74 ffi::OsString,
75 fmt::{self, Debug},
76 ops::Range,
77 path::{Path, PathBuf},
78 rc::Rc,
79 sync::Arc,
80};
81use strum::EnumIter;
82use uuid::Uuid;
83
84pub use app_menu::*;
85pub use keyboard::*;
86pub use keystroke::*;
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum WindowVisibility {
110 Visible,
113 Hidden,
116}
117
118impl WindowVisibility {
119 pub fn is_visible(self) -> bool {
121 self == Self::Visible
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128#[non_exhaustive]
129pub enum DisplayEvent {
130 Added(DisplayId),
132 Removed(DisplayId),
135 Changed {
137 id: DisplayId,
139 changes: DisplayChanges,
141 },
142}
143
144bitflags::bitflags! {
145 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147 pub struct DisplayChanges: u8 {
148 const BOUNDS = 1 << 0;
150 const REFRESH_INTERVAL = 1 << 1;
152 }
153}
154
155pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
158 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
159}
160
161#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
168pub enum ActivationPolicy {
169 #[default]
171 Regular,
172 Accessory,
174}
175
176#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
177pub(crate) use test::*;
178
179#[cfg(any(test, feature = "test-support"))]
180pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
181
182#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
183pub use threaded_dispatcher::ThreadedDispatcher;
184
185#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
186pub use visual_test::VisualTestPlatform;
187
188pub struct ActivityGuard {
190 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
191}
192
193impl ActivityGuard {
194 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
196 Self {
197 _release: gpui_util::defer(Box::new(release)),
198 }
199 }
200
201 pub fn noop() -> Self {
203 Self::new(|| {})
204 }
205}
206
207#[cfg(any(target_os = "linux", target_os = "freebsd"))]
211#[inline]
212pub fn guess_compositor() -> &'static str {
213 if std::env::var_os("ZED_HEADLESS").is_some() {
214 return "Headless";
215 }
216 GraphicalEnvironment::detect().guess_compositor()
217}
218
219#[cfg(any(target_os = "linux", target_os = "freebsd"))]
228#[derive(Clone, Debug, Default)]
229pub struct GraphicalEnvironment {
230 pub wayland_display: Option<OsString>,
232 pub x11_display: Option<OsString>,
234 pub xdg_runtime_dir: Option<OsString>,
236 pub activation_token: Option<String>,
242}
243
244#[cfg(target_os = "windows")]
250#[derive(Clone, Debug, Default)]
251pub struct GraphicalEnvironment {
252 pub session_id: Option<u32>,
255}
256
257#[cfg(target_os = "windows")]
258impl GraphicalEnvironment {
259 pub fn detect() -> Self {
261 let mut session_id = 0;
262 let result = unsafe {
264 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
265 windows::Win32::System::Threading::GetCurrentProcessId(),
266 &mut session_id,
267 )
268 };
269 Self {
270 session_id: result.is_ok().then_some(session_id),
271 }
272 }
273
274 pub fn apply_to(&self, _command: &mut std::process::Command) {}
277}
278
279#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
281#[derive(Clone, Debug, Default)]
282pub struct GraphicalEnvironment;
283
284#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
285impl GraphicalEnvironment {
286 pub fn detect() -> Self {
288 Self
289 }
290
291 pub fn apply_to(&self, _command: &mut std::process::Command) {}
293}
294
295#[derive(Clone, Debug)]
297pub enum WindowingRequest {
298 Headless,
300 Windowed(GraphicalEnvironment),
302}
303
304#[cfg(any(target_os = "linux", target_os = "freebsd"))]
305impl GraphicalEnvironment {
306 pub fn detect() -> Self {
311 Self {
312 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
313 x11_display: std::env::var_os("DISPLAY"),
314 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
315 activation_token: None,
316 }
317 }
318
319 pub fn apply_to(&self, command: &mut std::process::Command) {
323 for (name, value) in [
324 ("WAYLAND_DISPLAY", &self.wayland_display),
325 ("DISPLAY", &self.x11_display),
326 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
327 ] {
328 match value {
329 Some(value) => command.env(name, value),
330 None => command.env_remove(name),
331 };
332 }
333 }
334
335 pub fn guess_compositor(&self) -> &'static str {
339 let is_set =
340 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
341 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
342 "Wayland"
343 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
344 "X11"
345 } else {
346 "Headless"
347 }
348 }
349}
350
351#[cfg(any(target_os = "linux", target_os = "freebsd"))]
352bitflags::bitflags! {
353 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
355 pub struct WindowingModes: u8 {
356 const WAYLAND = 1 << 0;
358 const X11 = 1 << 1;
360 const HEADLESS = 1 << 2;
362 }
363}
364
365#[expect(missing_docs)]
366pub trait Platform: 'static {
367 fn background_executor(&self) -> BackgroundExecutor;
368 fn foreground_executor(&self) -> ForegroundExecutor;
369 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
370
371 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
372 fn quit(&self);
373 fn set_initial_windowing(&self, _request: WindowingRequest) {}
378 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
379 Task::ready(Err(anyhow::anyhow!(
380 "this platform cannot switch between headless and windowed modes"
381 )))
382 }
383 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
384 fn activate(&self, ignoring_other_apps: bool);
385 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
387 fn hide(&self);
388 fn hide_other_apps(&self);
389 fn unhide_other_apps(&self);
390
391 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
392 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
393 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
398 fn active_window(&self) -> Option<AnyWindowHandle>;
399 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
400 None
401 }
402
403 fn is_screen_capture_supported(&self) -> bool {
404 false
405 }
406
407 fn screen_capture_sources(
408 &self,
409 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
410 let (sources_tx, sources_rx) = oneshot::channel();
411 sources_tx
412 .send(Err(anyhow::anyhow!(
413 "gpui was compiled without the screen-capture feature"
414 )))
415 .ok();
416 sources_rx
417 }
418
419 fn open_window(
420 &self,
421 handle: AnyWindowHandle,
422 options: WindowParams,
423 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
424
425 fn window_appearance(&self) -> WindowAppearance;
427
428 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
437
438 fn button_layout(&self) -> Option<WindowButtonLayout> {
440 None
441 }
442
443 fn open_url(&self, url: &str);
444 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
445 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
446
447 fn prompt_for_paths(
448 &self,
449 options: PathPromptOptions,
450 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
451 fn prompt_for_new_path(
452 &self,
453 directory: &Path,
454 suggested_name: Option<&str>,
455 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
456 fn can_select_mixed_files_and_dirs(&self) -> bool;
457 fn reveal_path(&self, path: &Path);
458 fn open_with_system(&self, path: &Path);
459
460 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
461 fn on_reopen(&self, callback: Box<dyn FnMut()>);
462 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
464 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
466
467 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
477
478 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
483
484 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
488 None
489 }
490
491 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
492 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
493 None
494 }
495
496 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
497 fn perform_dock_menu_action(&self, _action: usize) {}
498 fn add_recent_document(&self, _path: &Path) {}
499 fn update_jump_list(
500 &self,
501 _menus: Vec<MenuItem>,
502 _entries: Vec<SmallVec<[PathBuf; 2]>>,
503 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
504 Task::ready(Vec::new())
505 }
506 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
507 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
508 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
509
510 fn thermal_state(&self) -> ThermalState;
511 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
512 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
513
514 fn set_app_identity(&self, identifier: &str, name: &str) {
521 _ = (identifier, name);
522 }
523
524 fn show_system_notification(&self, notification: SystemNotification) {
531 _ = notification;
532 }
533
534 fn dismiss_system_notification(&self, tag: &str) {
539 _ = tag;
540 }
541
542 fn on_system_notification_response(
548 &self,
549 callback: Box<dyn FnMut(SystemNotificationResponse)>,
550 ) {
551 _ = callback;
552 }
553
554 fn compositor_name(&self) -> &'static str {
555 ""
556 }
557 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
560 None
561 }
562 fn app_path(&self) -> Result<PathBuf>;
563 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
564
565 fn set_cursor_style(&self, style: CursorStyle);
566
567 fn hide_cursor_until_mouse_moves(&self);
570
571 fn is_cursor_visible(&self) -> bool;
573
574 fn should_auto_hide_scrollbars(&self) -> bool;
575
576 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
577 fn write_to_clipboard(&self, item: ClipboardItem);
578
579 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
587 Task::ready(Ok(self.read_from_clipboard()))
588 }
589
590 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
591 fn read_from_primary(&self) -> Option<ClipboardItem>;
592 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
593 fn write_to_primary(&self, item: ClipboardItem);
594
595 #[cfg(target_os = "macos")]
596 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
597 #[cfg(target_os = "macos")]
598 fn write_to_find_pasteboard(&self, item: ClipboardItem);
599
600 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
601 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
602 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
603
604 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
605 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
606 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
607}
608
609pub trait PlatformDisplay: Debug {
611 fn id(&self) -> DisplayId;
613
614 fn uuid(&self) -> Result<Uuid>;
617
618 fn bounds(&self) -> Bounds<Pixels>;
620
621 fn refresh_interval(&self) -> Option<Duration>;
628
629 fn visible_bounds(&self) -> Bounds<Pixels> {
633 self.bounds()
634 }
635
636 fn default_bounds(&self) -> Bounds<Pixels> {
638 let bounds = self.bounds();
639 let center = bounds.center();
640 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
641
642 let offset = clipped_window_size / 2.0;
643 let origin = point(center.x - offset.width, center.y - offset.height);
644 Bounds::new(origin, clipped_window_size)
645 }
646}
647
648#[derive(Clone, Debug, PartialEq, Eq)]
651pub struct SystemNotification {
652 pub tag: SharedString,
656 pub title: SharedString,
658 pub body: SharedString,
660 pub actions: Vec<SystemNotificationAction>,
663}
664
665#[derive(Clone, Debug, PartialEq, Eq, Hash)]
667pub struct SystemNotificationAction {
668 pub id: SharedString,
671 pub label: SharedString,
673}
674
675#[derive(Clone, Debug, PartialEq, Eq)]
677pub struct SystemNotificationResponse {
678 pub tag: SharedString,
680 pub action_id: Option<SharedString>,
683}
684
685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
687pub enum ThermalState {
688 Nominal,
690 Fair,
692 Serious,
694 Critical,
696}
697
698#[derive(Clone)]
700pub struct SourceMetadata {
701 pub id: u64,
703 pub label: Option<SharedString>,
705 pub is_main: Option<bool>,
707 pub resolution: Size<DevicePixels>,
709}
710
711pub trait ScreenCaptureSource {
713 fn metadata(&self) -> Result<SourceMetadata>;
715
716 fn stream(
719 &self,
720 foreground_executor: &ForegroundExecutor,
721 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
722 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
723}
724
725pub trait ScreenCaptureStream {
727 fn metadata(&self) -> Result<SourceMetadata>;
729}
730
731pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
733
734#[derive(PartialEq, Eq, Hash, Copy, Clone)]
736pub struct DisplayId(pub(crate) u64);
737
738impl DisplayId {
739 pub fn new(id: u64) -> Self {
741 Self(id)
742 }
743}
744
745impl From<u64> for DisplayId {
746 fn from(id: u64) -> Self {
747 Self(id)
748 }
749}
750
751impl From<DisplayId> for u64 {
752 fn from(id: DisplayId) -> Self {
753 id.0
754 }
755}
756
757impl Debug for DisplayId {
758 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759 write!(f, "DisplayId({})", self.0)
760 }
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
765pub enum ResizeEdge {
766 Top,
768 TopRight,
770 Right,
772 BottomRight,
774 Bottom,
776 BottomLeft,
778 Left,
780 TopLeft,
782}
783
784#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
786pub enum WindowDecorations {
787 #[default]
788 Server,
790 Client,
792}
793
794#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
796pub enum Decorations {
797 #[default]
799 Server,
800 Client {
802 tiling: Tiling,
804 },
805}
806
807#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
809pub struct WindowControls {
810 pub fullscreen: bool,
812 pub maximize: bool,
814 pub minimize: bool,
816 pub window_menu: bool,
818}
819
820impl Default for WindowControls {
821 fn default() -> Self {
822 Self {
824 fullscreen: true,
825 maximize: true,
826 minimize: true,
827 window_menu: true,
828 }
829 }
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
834pub enum WindowButton {
835 Minimize,
837 Maximize,
839 Close,
841}
842
843impl WindowButton {
844 pub fn id(&self) -> &'static str {
846 match self {
847 WindowButton::Minimize => "minimize",
848 WindowButton::Maximize => "maximize",
849 WindowButton::Close => "close",
850 }
851 }
852
853 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
854 fn index(&self) -> usize {
855 match self {
856 WindowButton::Minimize => 0,
857 WindowButton::Maximize => 1,
858 WindowButton::Close => 2,
859 }
860 }
861}
862
863pub const MAX_BUTTONS_PER_SIDE: usize = 3;
865
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
871pub struct WindowButtonLayout {
872 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
874 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
876}
877
878#[cfg(any(target_os = "linux", target_os = "freebsd"))]
879impl WindowButtonLayout {
880 pub fn linux_default() -> Self {
882 Self {
883 left: [None; MAX_BUTTONS_PER_SIDE],
884 right: [
885 Some(WindowButton::Minimize),
886 Some(WindowButton::Maximize),
887 Some(WindowButton::Close),
888 ],
889 }
890 }
891
892 pub fn parse(layout_string: &str) -> Result<Self> {
894 fn parse_side(
895 s: &str,
896 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
897 unrecognized: &mut Vec<String>,
898 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
899 let mut result = [None; MAX_BUTTONS_PER_SIDE];
900 let mut i = 0;
901 for name in s.split(',') {
902 let trimmed = name.trim();
903 if trimmed.is_empty() {
904 continue;
905 }
906 let button = match trimmed {
907 "minimize" => Some(WindowButton::Minimize),
908 "maximize" => Some(WindowButton::Maximize),
909 "close" => Some(WindowButton::Close),
910 other => {
911 unrecognized.push(other.to_string());
912 None
913 }
914 };
915 if let Some(button) = button {
916 if seen_buttons[button.index()] {
917 continue;
918 }
919 if let Some(slot) = result.get_mut(i) {
920 *slot = Some(button);
921 seen_buttons[button.index()] = true;
922 i += 1;
923 }
924 }
925 }
926 result
927 }
928
929 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
930 let mut unrecognized = Vec::new();
931 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
932 let layout = Self {
933 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
934 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
935 };
936
937 if !unrecognized.is_empty()
938 && layout.left.iter().all(Option::is_none)
939 && layout.right.iter().all(Option::is_none)
940 {
941 bail!(
942 "button layout string {:?} contains no valid buttons (unrecognized: {})",
943 layout_string,
944 unrecognized.join(", ")
945 );
946 }
947
948 Ok(layout)
949 }
950
951 #[cfg(test)]
953 pub fn format(&self) -> String {
954 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
955 buttons
956 .iter()
957 .flatten()
958 .map(|button| match button {
959 WindowButton::Minimize => "minimize",
960 WindowButton::Maximize => "maximize",
961 WindowButton::Close => "close",
962 })
963 .collect::<Vec<_>>()
964 .join(",")
965 }
966
967 format!("{}:{}", format_side(&self.left), format_side(&self.right))
968 }
969}
970
971#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
973pub struct Tiling {
974 pub top: bool,
976 pub left: bool,
978 pub right: bool,
980 pub bottom: bool,
982}
983
984impl Tiling {
985 pub fn tiled() -> Self {
987 Self {
988 top: true,
989 left: true,
990 right: true,
991 bottom: true,
992 }
993 }
994
995 pub fn is_tiled(&self) -> bool {
997 self.top || self.left || self.right || self.bottom
998 }
999}
1000
1001pub struct A11yCallbacks {
1003 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1005 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1007 pub deactivation: Box<dyn Fn() + Send + 'static>,
1009}
1010
1011#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1013pub enum FrameRequestSource {
1014 #[default]
1016 NativeCallback,
1017 LocalSchedule,
1019}
1020
1021#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1022#[expect(missing_docs)]
1023pub struct RequestFrameOptions {
1024 pub require_presentation: bool,
1026 pub force_render: bool,
1028 pub signal_at: Option<Instant>,
1034 pub signal_source: FrameRequestSource,
1036}
1037
1038pub struct PlatformFrameSignal {
1047 #[cfg(feature = "profiler")]
1048 origin: Instant,
1049 #[cfg(feature = "profiler")]
1050 first_signal: std::sync::atomic::AtomicU64,
1051}
1052
1053impl Default for PlatformFrameSignal {
1054 fn default() -> Self {
1055 Self::new()
1056 }
1057}
1058
1059impl PlatformFrameSignal {
1060 pub fn new() -> Self {
1062 Self {
1063 #[cfg(feature = "profiler")]
1064 origin: Instant::now(),
1065 #[cfg(feature = "profiler")]
1066 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1067 }
1068 }
1069
1070 #[inline]
1074 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1075 if cfg!(feature = "profiler") {
1076 Some(capture())
1077 } else {
1078 None
1079 }
1080 }
1081
1082 #[inline]
1084 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1085 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1086 #[cfg(feature = "profiler")]
1087 {
1088 let nanoseconds = at
1089 .saturating_duration_since(self.origin)
1090 .as_nanos()
1091 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1092 let source_bit = match source {
1093 FrameRequestSource::NativeCallback => 0,
1094 FrameRequestSource::LocalSchedule => 1,
1095 };
1096 self.first_signal.fetch_min(
1097 (nanoseconds << 1) | source_bit,
1098 std::sync::atomic::Ordering::Relaxed,
1099 );
1100 }
1101 }
1102
1103 #[inline]
1105 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1106 #[cfg(feature = "profiler")]
1107 {
1108 let signal = self
1109 .first_signal
1110 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1111 (signal != u64::MAX).then(|| {
1112 let source = match signal & 1 {
1113 0 => FrameRequestSource::NativeCallback,
1114 _ => FrameRequestSource::LocalSchedule,
1115 };
1116 (self.origin + Duration::from_nanos(signal >> 1), source)
1117 })
1118 }
1119 #[cfg(not(feature = "profiler"))]
1120 {
1121 None
1122 }
1123 }
1124}
1125
1126#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1139pub enum AppLifecyclePhase {
1140 Active,
1142 Inactive,
1144 Background,
1147 Foreground,
1149 Disconnected,
1153}
1154
1155#[derive(Debug, Clone, Default, PartialEq)]
1162pub struct WindowInsets {
1163 pub safe_area: Edges<Pixels>,
1168 pub ime: Edges<Pixels>,
1172 pub corner_radii: Corners<Pixels>,
1177}
1178
1179impl WindowInsets {
1180 pub fn effective(&self) -> Edges<Pixels> {
1182 Edges {
1183 top: self.safe_area.top.max(self.ime.top),
1184 right: self.safe_area.right.max(self.ime.right),
1185 bottom: self.safe_area.bottom.max(self.ime.bottom),
1186 left: self.safe_area.left.max(self.ime.left),
1187 }
1188 }
1189}
1190
1191#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1193pub enum TextInputStateChange {
1194 FocusGained,
1196 FocusLost,
1198 SelectionChanged,
1200 ContentChanged,
1202}
1203
1204#[expect(missing_docs)]
1205pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1206 fn set_color_mode(&mut self, mode: WindowColorMode) -> anyhow::Result<WindowColorStatus> {
1208 match mode {
1209 WindowColorMode::Default => Ok(self.color_status()),
1210 _ => anyhow::bail!("wide-gamut SDR presentation is unsupported"),
1211 }
1212 }
1213 fn color_status(&self) -> WindowColorStatus {
1215 WindowColorStatus::default()
1216 }
1217
1218 fn bounds(&self) -> Bounds<Pixels>;
1219 fn is_maximized(&self) -> bool;
1220 fn window_bounds(&self) -> WindowBounds;
1221 fn content_size(&self) -> Size<Pixels>;
1222 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1228 Bounds::new(Point::default(), self.content_size())
1229 }
1230 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1235 fn prepare_frame(&self) -> bool {
1240 false
1241 }
1242 fn resize(&mut self, size: Size<Pixels>);
1243 fn scale_factor(&self) -> f32;
1244 fn appearance(&self) -> WindowAppearance;
1245 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1246 fn mouse_position(&self) -> Point<Pixels>;
1247 fn modifiers(&self) -> Modifiers;
1248 fn capslock(&self) -> Capslock;
1249 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1250 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1251 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1256 fn prompt(
1257 &self,
1258 level: PromptLevel,
1259 msg: &str,
1260 detail: Option<&str>,
1261 answers: &[PromptButton],
1262 ) -> Option<oneshot::Receiver<usize>>;
1263 fn activate(&self);
1264 fn request_attention(&self) {}
1266 fn is_active(&self) -> bool;
1267 fn visibility(&self) -> WindowVisibility;
1270 fn is_hovered(&self) -> bool;
1271 fn background_appearance(&self) -> WindowBackgroundAppearance;
1272 fn set_title(&mut self, title: &str);
1273 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1274 fn minimize(&self);
1275 fn zoom(&self);
1276 fn toggle_fullscreen(&self);
1277 fn is_fullscreen(&self) -> bool;
1278 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1279 None
1280 }
1281 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1282 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1283 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1284 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1288 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1289 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1290 fn on_moved(&self, callback: Box<dyn FnMut()>);
1291 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1295 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1296 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1297 fn on_close(&self, callback: Box<dyn FnOnce()>);
1298 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1299 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1300 fn draw(&self, scene: &Scene);
1301 #[cfg(any(test, feature = "test-support"))]
1303 fn submit_scene_for_validation(&self, _scene: &Scene) -> anyhow::Result<()> {
1304 anyhow::bail!("native window ownership validation is unsupported on this backend")
1305 }
1306 fn schedule_frame(&self) {}
1307 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1308 fn is_subpixel_rendering_supported(&self) -> bool;
1309
1310 fn get_title(&self) -> String {
1312 String::new()
1313 }
1314 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1315 None
1316 }
1317 fn tab_bar_visible(&self) -> bool {
1318 false
1319 }
1320 fn set_edited(&mut self, _edited: bool) {}
1321 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1322 fn toggle_simple_fullscreen(&self) {}
1323 fn is_simple_fullscreen(&self) -> bool {
1324 false
1325 }
1326 #[cfg(target_os = "macos")]
1327 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1328 fn show_character_palette(&self) {}
1329 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1330 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1331 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1332 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1333 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1334 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1335 fn merge_all_windows(&self) {}
1336 fn move_tab_to_new_window(&self) {}
1337 fn toggle_window_tab_overview(&self) {}
1338 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1339
1340 fn native_window_state(&self) -> Option<Vec<u8>> {
1341 None
1342 }
1343 fn restore_native_window_state(&self, _state: &[u8]) {}
1344
1345 #[cfg(target_os = "windows")]
1346 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1347
1348 fn inner_window_bounds(&self) -> WindowBounds {
1350 self.window_bounds()
1351 }
1352 fn request_decorations(&self, _decorations: WindowDecorations) {}
1353 fn show_window_menu(&self, _position: Point<Pixels>) {}
1354 fn start_window_move(&self) {}
1355 fn can_start_external_drag(&self) -> bool {
1356 false
1357 }
1358 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1359 false
1360 }
1361 fn start_window_resize(&self, _edge: ResizeEdge) {}
1362 fn set_exclusive_zone(&self, _zone: Pixels) {}
1363 #[cfg(all(target_os = "linux", feature = "wayland"))]
1364 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1365 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1366 fn window_decorations(&self) -> Decorations {
1367 Decorations::Server
1368 }
1369 fn set_app_id(&mut self, _app_id: &str) {}
1370 fn map_window(&mut self) -> anyhow::Result<()> {
1371 Ok(())
1372 }
1373 fn window_controls(&self) -> WindowControls {
1374 WindowControls::default()
1375 }
1376 fn set_client_inset(&self, _inset: Pixels) {}
1377 fn gpu_specs(&self) -> Option<GpuSpecs>;
1378
1379 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1380
1381 fn insets(&self) -> WindowInsets {
1386 WindowInsets::default()
1387 }
1388
1389 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1395
1396 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1399
1400 fn set_back_enabled(&self, _enabled: bool) {}
1403
1404 fn show_soft_keyboard(&self) {}
1406
1407 fn hide_soft_keyboard(&self) {}
1409
1410 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1412
1413 fn play_system_bell(&self) {}
1414
1415 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1417
1418 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1420
1421 fn a11y_update_window_bounds(&self) {}
1423
1424 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1425 fn as_test(&mut self) -> Option<&mut TestWindow> {
1426 None
1427 }
1428
1429 #[cfg(any(test, feature = "test-support"))]
1432 fn render_to_linear_p3(&self, _scene: &Scene) -> Result<LinearP3Image> {
1433 anyhow::bail!("linear P3 readback is unsupported on this backend")
1434 }
1435
1436 #[cfg(any(test, feature = "test-support"))]
1440 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1441 anyhow::bail!("render_to_image not implemented for this platform")
1442 }
1443}
1444
1445#[cfg(any(test, feature = "test-support"))]
1449#[derive(Debug)]
1450pub struct LinearP3Image {
1451 pub size: Size<DevicePixels>,
1453 pub pixels: Vec<[u16; 4]>,
1455}
1456
1457#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1459pub trait PlatformHeadlessRenderer {
1460 fn render_scene_to_image(
1462 &mut self,
1463 scene: &Scene,
1464 size: Size<DevicePixels>,
1465 ) -> Result<RgbaImage>;
1466
1467 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1473
1474 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1476}
1477
1478#[doc(hidden)]
1481pub type RunnableVariant = Runnable<RunnableMeta>;
1482
1483#[doc(hidden)]
1484pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1485
1486#[doc(hidden)]
1487pub enum TasksIncluded {
1488 OnlyCompleted,
1489 CompletedAndRunning,
1490}
1491
1492#[doc(hidden)]
1495pub trait PlatformDispatcher: Send + Sync {
1496 fn is_main_thread(&self) -> bool;
1497 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1498 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1499 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1500
1501 fn dispatch_on_main_thread_when_idle(
1502 &self,
1503 runnable: RunnableVariant,
1504 timeout: Option<Duration>,
1505 ) {
1506 let _ = timeout;
1507 self.dispatch_on_main_thread(runnable, Priority::Low);
1508 }
1509
1510 fn idle_time_remaining(&self) -> Option<Duration> {
1511 None
1512 }
1513
1514 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1515
1516 fn now(&self) -> Instant {
1517 Instant::now()
1518 }
1519
1520 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1521 gpui_util::defer(Box::new(|| {}))
1522 }
1523
1524 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1525 ActivityGuard::noop()
1526 }
1527
1528 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1529 fn as_test(&self) -> Option<&TestDispatcher> {
1530 None
1531 }
1532
1533 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1536 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1537 None
1538 }
1539}
1540
1541#[expect(missing_docs)]
1542pub trait PlatformTextSystem: Send + Sync {
1543 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1544 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1546 fn all_font_names(&self) -> Vec<String>;
1548 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1550 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1552 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1554 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1556 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1558 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1560 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1562 fn rasterize_glyph(
1564 &self,
1565 params: &RenderGlyphParams,
1566 raster_bounds: Bounds<DevicePixels>,
1567 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1568 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1570 fn shape_paragraph(
1572 &self,
1573 _request: crate::ParagraphRequest<'_>,
1574 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1575 anyhow::bail!("paragraph layout is unsupported by this text backend")
1576 }
1577 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1579 -> TextRenderingMode;
1580 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1582 0
1583 }
1584}
1585
1586#[expect(missing_docs)]
1587pub struct NoopTextSystem;
1588
1589#[expect(missing_docs)]
1590impl NoopTextSystem {
1591 #[allow(dead_code)]
1592 pub fn new() -> Self {
1593 Self
1594 }
1595}
1596
1597impl PlatformTextSystem for NoopTextSystem {
1598 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1599 Ok(())
1600 }
1601
1602 fn all_font_names(&self) -> Vec<String> {
1603 Vec::new()
1604 }
1605
1606 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1607 Ok(FontId(1))
1608 }
1609
1610 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1611 FontMetrics {
1612 units_per_em: 1000,
1613 ascent: 1025.0,
1614 descent: -275.0,
1615 line_gap: 0.0,
1616 underline_position: -95.0,
1617 underline_thickness: 60.0,
1618 cap_height: 698.0,
1619 x_height: 516.0,
1620 bounding_box: Bounds {
1621 origin: Point {
1622 x: -260.0,
1623 y: -245.0,
1624 },
1625 size: Size {
1626 width: 1501.0,
1627 height: 1364.0,
1628 },
1629 },
1630 }
1631 }
1632
1633 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1634 Ok(Bounds {
1635 origin: Point { x: 54.0, y: 0.0 },
1636 size: size(392.0, 528.0),
1637 })
1638 }
1639
1640 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1641 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1642 }
1643
1644 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1645 Some(GlyphId(ch.len_utf16() as u32))
1646 }
1647
1648 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1649 Ok(Default::default())
1650 }
1651
1652 fn rasterize_glyph(
1653 &self,
1654 _params: &RenderGlyphParams,
1655 raster_bounds: Bounds<DevicePixels>,
1656 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1657 Ok((raster_bounds.size, Vec::new()))
1658 }
1659
1660 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1661 let mut position = px(0.);
1662 let metrics = self.font_metrics(FontId(0));
1663 let em_width = font_size
1664 * self
1665 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1666 .unwrap()
1667 .width
1668 / metrics.units_per_em as f32;
1669 let mut glyphs = Vec::new();
1670 for (ix, c) in text.char_indices() {
1671 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1672 glyphs.push(ShapedGlyph {
1673 id: glyph,
1674 position: point(position, px(0.)),
1675 index: ix,
1676 is_emoji: glyph.0 == 2,
1677 });
1678 if glyph.0 == 2 {
1679 position += em_width * 2.0;
1680 } else {
1681 position += em_width;
1682 }
1683 } else {
1684 position += em_width
1685 }
1686 }
1687 let mut runs = Vec::default();
1688 if !glyphs.is_empty() {
1689 runs.push(ShapedRun {
1690 font_id: FontId(0),
1691 glyphs,
1692 });
1693 } else {
1694 position = px(0.);
1695 }
1696
1697 LineLayout {
1698 font_size,
1699 width: position,
1700 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1701 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1702 runs,
1703 len: text.len(),
1704 }
1705 }
1706
1707 fn recommended_rendering_mode(
1708 &self,
1709 _font_id: FontId,
1710 _font_size: Pixels,
1711 ) -> TextRenderingMode {
1712 TextRenderingMode::Grayscale
1713 }
1714}
1715
1716#[allow(dead_code)]
1721pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1722 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1723 [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], ];
1737
1738 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1739 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1740
1741 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1742 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1743
1744 [
1745 ratios[0] * NORM13,
1746 ratios[1] * NORM24,
1747 ratios[2] * NORM13,
1748 ratios[3] * NORM24,
1749 ]
1750}
1751
1752#[derive(PartialEq, Eq, Hash, Clone)]
1753#[expect(missing_docs)]
1754pub enum AtlasKey {
1755 Glyph(RenderGlyphParams),
1756 Svg(RenderSvgParams),
1757 Image(RenderImageParams),
1758}
1759
1760impl AtlasKey {
1761 pub fn texture_kind(&self) -> AtlasTextureKind {
1763 match self {
1764 AtlasKey::Glyph(params) => {
1765 if params.is_emoji {
1766 AtlasTextureKind::Polychrome
1767 } else if params.subpixel_rendering {
1768 AtlasTextureKind::Subpixel
1769 } else {
1770 AtlasTextureKind::Monochrome
1771 }
1772 }
1773 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1774 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1775 }
1776 }
1777}
1778
1779impl From<RenderGlyphParams> for AtlasKey {
1780 fn from(params: RenderGlyphParams) -> Self {
1781 Self::Glyph(params)
1782 }
1783}
1784
1785impl From<RenderSvgParams> for AtlasKey {
1786 fn from(params: RenderSvgParams) -> Self {
1787 Self::Svg(params)
1788 }
1789}
1790
1791impl From<RenderImageParams> for AtlasKey {
1792 fn from(params: RenderImageParams) -> Self {
1793 Self::Image(params)
1794 }
1795}
1796
1797#[expect(missing_docs)]
1798pub trait PlatformAtlas {
1799 fn get_or_insert_with<'a>(
1801 &self,
1802 key: AtlasKey,
1803 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1804 ) -> Result<Option<AtlasTile>>;
1805 fn remove(&self, key: &AtlasKey);
1806
1807 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1808 fn contains(&self, _key: &AtlasKey) -> bool {
1809 false
1810 }
1811}
1812
1813#[doc(hidden)]
1814pub trait AtlasBackend {
1815 fn insert(
1816 &mut self,
1817 kind: AtlasTextureKind,
1818 size: Size<DevicePixels>,
1819 bytes: &[u8],
1820 ) -> Result<AtlasTile>;
1821
1822 fn remove(&mut self, tile: AtlasTile);
1823}
1824
1825#[doc(hidden)]
1826pub struct AtlasState<Backend> {
1827 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1828 pub backend: Backend,
1829}
1830
1831impl<Backend> AtlasState<Backend> {
1832 pub fn new(backend: Backend) -> Self {
1833 Self {
1834 tiles_by_key: FxHashMap::default(),
1835 backend,
1836 }
1837 }
1838
1839 pub fn contains(&self, key: &AtlasKey) -> bool {
1840 self.tiles_by_key.contains_key(key)
1841 }
1842
1843 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1844 self.tiles_by_key.clear();
1845 reset_backend(&mut self.backend);
1846 }
1847}
1848
1849impl<Backend: Default> Default for AtlasState<Backend> {
1850 fn default() -> Self {
1851 Self::new(Backend::default())
1852 }
1853}
1854
1855impl<Backend: AtlasBackend> AtlasState<Backend> {
1856 pub fn get_or_insert_with<'a>(
1857 &mut self,
1858 key: AtlasKey,
1859 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1860 ) -> Result<Option<AtlasTile>> {
1861 match self.tiles_by_key.entry(key) {
1862 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1863 Entry::Vacant(entry) => {
1864 profiling::scope!("new tile");
1865 let Some((size, bytes)) = build()? else {
1866 return Ok(None);
1867 };
1868 let tile = self
1869 .backend
1870 .insert(entry.key().texture_kind(), size, &bytes)?;
1871 entry.insert(tile);
1872 Ok(Some(tile))
1873 }
1874 }
1875 }
1876
1877 pub fn remove(&mut self, key: &AtlasKey) {
1878 if let Some(tile) = self.tiles_by_key.remove(key) {
1879 self.backend.remove(tile);
1880 }
1881 }
1882}
1883
1884#[derive(Default)]
1888pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1889
1890#[doc(hidden)]
1891#[derive(Default)]
1892pub struct HeadlessAtlasBackend {
1893 next_id: u32,
1894}
1895
1896impl AtlasBackend for HeadlessAtlasBackend {
1897 fn insert(
1898 &mut self,
1899 kind: AtlasTextureKind,
1900 size: Size<DevicePixels>,
1901 _bytes: &[u8],
1902 ) -> Result<AtlasTile> {
1903 self.next_id += 1;
1904 let texture_id = self.next_id;
1905 self.next_id += 1;
1906 let tile_id = self.next_id;
1907 Ok(AtlasTile {
1908 texture_id: AtlasTextureId {
1909 index: texture_id,
1910 kind,
1911 },
1912 tile_id: TileId(tile_id),
1913 padding: 0,
1914 bounds: Bounds {
1915 origin: Point::default(),
1916 size,
1917 },
1918 })
1919 }
1920
1921 fn remove(&mut self, _tile: AtlasTile) {}
1922}
1923
1924impl PlatformAtlas for HeadlessAtlas {
1925 fn get_or_insert_with<'a>(
1926 &self,
1927 key: AtlasKey,
1928 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1929 ) -> Result<Option<AtlasTile>> {
1930 self.0.lock().get_or_insert_with(key, build)
1931 }
1932
1933 fn remove(&self, key: &AtlasKey) {
1934 self.0.lock().remove(key);
1935 }
1936
1937 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1938 fn contains(&self, key: &AtlasKey) -> bool {
1939 self.0.lock().contains(key)
1940 }
1941}
1942
1943#[doc(hidden)]
1944pub struct AtlasTextureList<T> {
1945 pub textures: Vec<Option<T>>,
1946 pub free_list: Vec<usize>,
1947}
1948
1949impl<T> Default for AtlasTextureList<T> {
1950 fn default() -> Self {
1951 Self {
1952 textures: Vec::default(),
1953 free_list: Vec::default(),
1954 }
1955 }
1956}
1957
1958impl<T> ops::Index<usize> for AtlasTextureList<T> {
1959 type Output = Option<T>;
1960
1961 fn index(&self, index: usize) -> &Self::Output {
1962 &self.textures[index]
1963 }
1964}
1965
1966impl<T> AtlasTextureList<T> {
1967 #[allow(unused)]
1968 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1969 self.free_list.clear();
1970 self.textures.drain(..)
1971 }
1972
1973 #[allow(dead_code)]
1974 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1975 self.textures.iter_mut().flatten()
1976 }
1977}
1978
1979#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1980#[repr(C)]
1981#[expect(missing_docs)]
1982pub struct AtlasTile {
1983 pub texture_id: AtlasTextureId,
1985 pub tile_id: TileId,
1987 pub padding: u32,
1989 pub bounds: Bounds<DevicePixels>,
1991}
1992
1993#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1994#[repr(C)]
1995#[expect(missing_docs)]
1996pub struct AtlasTextureId {
1997 pub index: u32,
2000 pub kind: AtlasTextureKind,
2002}
2003
2004#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2005#[repr(C)]
2006#[cfg_attr(
2007 all(
2008 any(target_os = "linux", target_os = "freebsd"),
2009 not(any(feature = "x11", feature = "wayland"))
2010 ),
2011 allow(dead_code)
2012)]
2013#[expect(missing_docs)]
2014pub enum AtlasTextureKind {
2015 Monochrome = 0,
2016 Polychrome = 1,
2017 Subpixel = 2,
2018}
2019
2020#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2021#[repr(C)]
2022#[expect(missing_docs)]
2023pub struct TileId(pub u32);
2024
2025impl From<etagere::AllocId> for TileId {
2026 fn from(id: etagere::AllocId) -> Self {
2027 Self(id.serialize())
2028 }
2029}
2030
2031impl From<TileId> for etagere::AllocId {
2032 fn from(id: TileId) -> Self {
2033 Self::deserialize(id.0)
2034 }
2035}
2036
2037#[expect(missing_docs)]
2038pub struct PlatformInputHandler {
2039 cx: AsyncWindowContext,
2040 handler: Box<dyn InputHandler>,
2041 focus_id: Option<crate::FocusId>,
2042}
2043
2044#[expect(missing_docs)]
2045#[cfg_attr(
2046 all(
2047 any(target_os = "linux", target_os = "freebsd"),
2048 not(any(feature = "x11", feature = "wayland"))
2049 ),
2050 allow(dead_code)
2051)]
2052impl PlatformInputHandler {
2053 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2054 Self {
2055 cx,
2056 handler,
2057 focus_id: None,
2058 }
2059 }
2060
2061 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2062 self.focus_id = Some(focus_id);
2063 self
2064 }
2065
2066 pub fn focus_id(&self) -> Option<crate::FocusId> {
2071 self.focus_id
2072 }
2073
2074 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2075 self.cx
2076 .update(|window, cx| {
2077 self.handler
2078 .selected_text_range(ignore_disabled_input, window, cx)
2079 })
2080 .ok()
2081 .flatten()
2082 }
2083
2084 #[cfg_attr(target_os = "windows", allow(dead_code))]
2085 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2086 self.cx
2087 .update(|window, cx| self.handler.marked_text_range(window, cx))
2088 .ok()
2089 .flatten()
2090 }
2091
2092 #[cfg_attr(
2093 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2094 allow(dead_code)
2095 )]
2096 pub fn text_for_range(
2097 &mut self,
2098 range_utf16: Range<usize>,
2099 adjusted: &mut Option<Range<usize>>,
2100 ) -> Option<String> {
2101 self.cx
2102 .update(|window, cx| {
2103 self.handler
2104 .text_for_range(range_utf16, adjusted, window, cx)
2105 })
2106 .ok()
2107 .flatten()
2108 }
2109
2110 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2111 self.cx
2112 .update(|window, cx| {
2113 self.handler
2114 .replace_text_in_range(replacement_range, text, window, cx);
2115 })
2116 .ok();
2117 }
2118
2119 pub fn replace_and_mark_text_in_range(
2120 &mut self,
2121 range_utf16: Option<Range<usize>>,
2122 new_text: &str,
2123 new_selected_range: Option<Range<usize>>,
2124 ) {
2125 self.cx
2126 .update(|window, cx| {
2127 self.handler.replace_and_mark_text_in_range(
2128 range_utf16,
2129 new_text,
2130 new_selected_range,
2131 window,
2132 cx,
2133 )
2134 })
2135 .ok();
2136 }
2137
2138 #[cfg_attr(target_os = "windows", allow(dead_code))]
2139 pub fn unmark_text(&mut self) {
2140 self.cx
2141 .update(|window, cx| self.handler.unmark_text(window, cx))
2142 .ok();
2143 }
2144
2145 pub fn paste(&mut self, item: ClipboardItem) {
2146 self.cx
2147 .update(|window, cx| self.handler.paste(item, window, cx))
2148 .ok();
2149 }
2150
2151 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2152 self.cx
2153 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2154 .ok()
2155 .flatten()
2156 }
2157
2158 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2159 self.cx
2160 .update(|window, cx| {
2161 self.handler
2162 .selection_bounds_for_range(range_utf16, window, cx)
2163 })
2164 .unwrap_or_default()
2165 }
2166
2167 #[allow(dead_code)]
2168 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2169 self.handler.apple_press_and_hold_enabled()
2170 }
2171
2172 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2173 self.handler.replace_text_in_range(None, input, window, cx);
2174 }
2175
2176 pub fn compute_ime_candidate_bounds(
2177 marked_range: Option<Range<usize>>,
2178 selection: &UTF16Selection,
2179 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2180 ) -> Option<Bounds<Pixels>> {
2181 if let Some(marked_range) = marked_range {
2182 let mut line_start = marked_range.start;
2184
2185 let caret = selection.range.end;
2189 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2190 for i in (marked_range.start..caret).rev() {
2191 if let Some(b) = bounds_for_range(i..i) {
2192 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2193 line_start = i + 1;
2194 break;
2195 }
2196 }
2197 }
2198 }
2199 bounds_for_range(line_start..line_start)
2200 } else {
2201 let offset = if selection.reversed {
2203 selection.range.start
2204 } else {
2205 selection.range.end
2206 };
2207 bounds_for_range(offset..offset)
2208 }
2209 }
2210
2211 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2212 let marked_range = self.handler.marked_text_range(window, cx);
2213 let selection = self.handler.selected_text_range(true, window, cx)?;
2214 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2215 self.handler.bounds_for_range(range, window, cx)
2216 })
2217 }
2218
2219 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2220 let marked_range = self.marked_text_range();
2221 let selection = self.selected_text_range(true)?;
2222 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2223 self.bounds_for_range(range)
2224 })
2225 }
2226
2227 #[allow(unused)]
2228 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2229 self.cx
2230 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2231 .ok()
2232 .flatten()
2233 }
2234
2235 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2237 self.cx
2238 .update(|window, cx| {
2239 self.handler
2240 .set_selected_text_range(range_utf16, window, cx)
2241 })
2242 .ok();
2243 }
2244
2245 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2247 self.cx
2248 .update(|window, cx| self.handler.element_bounds(window, cx))
2249 .ok()
2250 .flatten()
2251 }
2252
2253 pub fn text_length_utf16(&mut self) -> Option<usize> {
2255 self.cx
2256 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2257 .ok()
2258 .flatten()
2259 }
2260
2261 #[allow(dead_code)]
2262 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2263 self.handler.accepts_text_input(window, cx)
2264 }
2265
2266 #[allow(dead_code)]
2267 pub fn query_accepts_text_input(&mut self) -> bool {
2268 self.cx
2269 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2270 .unwrap_or(true)
2271 }
2272
2273 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2278 let focus_id = self.focus_id;
2279 self.cx
2280 .update(|window, cx| {
2281 focus_id.is_some()
2282 && window.focus == focus_id
2283 && self.handler.accepts_text_input(window, cx)
2284 })
2285 .unwrap_or(false)
2286 }
2287
2288 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2294 self.cx
2295 .update(|window, cx| {
2296 !window.has_pending_keystrokes()
2298 && self.handler.prefers_ime_for_printable_keys(window, cx)
2299 })
2300 .unwrap_or(false)
2301 }
2302
2303 pub fn text_input_configuration(
2305 &mut self,
2306 window: &mut Window,
2307 cx: &mut App,
2308 ) -> TextInputConfiguration {
2309 self.handler.text_input_configuration(window, cx)
2310 }
2311
2312 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2314 self.cx
2315 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2316 .ok()
2317 .flatten()
2318 }
2319}
2320
2321#[derive(Debug)]
2324pub struct UTF16Selection {
2325 pub range: Range<usize>,
2328 pub reversed: bool,
2331}
2332
2333pub trait InputHandler: 'static {
2338 fn selected_text_range(
2343 &mut self,
2344 ignore_disabled_input: bool,
2345 window: &mut Window,
2346 cx: &mut App,
2347 ) -> Option<UTF16Selection>;
2348
2349 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2354
2355 fn text_for_range(
2360 &mut self,
2361 range_utf16: Range<usize>,
2362 adjusted_range: &mut Option<Range<usize>>,
2363 window: &mut Window,
2364 cx: &mut App,
2365 ) -> Option<String>;
2366
2367 fn replace_text_in_range(
2372 &mut self,
2373 replacement_range: Option<Range<usize>>,
2374 text: &str,
2375 window: &mut Window,
2376 cx: &mut App,
2377 );
2378
2379 fn replace_and_mark_text_in_range(
2386 &mut self,
2387 range_utf16: Option<Range<usize>>,
2388 new_text: &str,
2389 new_selected_range: Option<Range<usize>>,
2390 window: &mut Window,
2391 cx: &mut App,
2392 );
2393
2394 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2397
2398 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2405 if let Some(text) = item.text() {
2406 self.replace_text_in_range(None, &text, window, cx);
2407 }
2408 }
2409
2410 fn bounds_for_range(
2415 &mut self,
2416 range_utf16: Range<usize>,
2417 window: &mut Window,
2418 cx: &mut App,
2419 ) -> Option<Bounds<Pixels>>;
2420
2421 fn selection_bounds_for_range(
2428 &mut self,
2429 _range_utf16: Range<usize>,
2430 _window: &mut Window,
2431 _cx: &mut App,
2432 ) -> Vec<Bounds<Pixels>> {
2433 Vec::new()
2434 }
2435
2436 fn character_index_for_point(
2440 &mut self,
2441 point: Point<Pixels>,
2442 window: &mut Window,
2443 cx: &mut App,
2444 ) -> Option<usize>;
2445
2446 fn set_selected_text_range(
2456 &mut self,
2457 _range_utf16: Range<usize>,
2458 _window: &mut Window,
2459 _cx: &mut App,
2460 ) {
2461 }
2462
2463 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2470 None
2471 }
2472
2473 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2475 None
2476 }
2477
2478 #[allow(dead_code)]
2483 fn apple_press_and_hold_enabled(&mut self) -> bool {
2484 true
2485 }
2486
2487 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2489 true
2490 }
2491
2492 fn text_input_editable_range(
2503 &mut self,
2504 _window: &mut Window,
2505 _cx: &mut App,
2506 ) -> Option<Range<usize>> {
2507 None
2508 }
2509
2510 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2519 false
2520 }
2521
2522 fn text_input_configuration(
2528 &mut self,
2529 _window: &mut Window,
2530 _cx: &mut App,
2531 ) -> TextInputConfiguration {
2532 TextInputConfiguration::default()
2533 }
2534}
2535
2536#[derive(Clone, Debug, Default, PartialEq, Eq)]
2546pub struct TextInputConfiguration {
2547 pub autocorrect: bool,
2549 pub autocapitalize: Autocapitalize,
2551 pub suggestions: bool,
2553 pub input_action: TextInputAction,
2555}
2556
2557#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2559pub enum Autocapitalize {
2560 #[default]
2562 None,
2563 Words,
2565 Sentences,
2567 Characters,
2569}
2570
2571#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2581pub enum TextInputAction {
2582 #[default]
2584 Unspecified,
2585 Enter,
2587 Done,
2589 Go,
2591 Next,
2593 Previous,
2595 Search,
2597 Send,
2599}
2600
2601#[derive(Debug)]
2603pub struct WindowOptions {
2604 pub window_bounds: Option<WindowBounds>,
2608
2609 pub titlebar: Option<TitlebarOptions>,
2611
2612 pub focus: bool,
2614
2615 pub show: bool,
2617
2618 pub kind: WindowKind,
2620
2621 pub is_movable: bool,
2625
2626 pub app_owns_titlebar_drag: bool,
2638
2639 pub inactive_frame_interval: Option<Duration>,
2643
2644 pub is_resizable: bool,
2646
2647 pub is_minimizable: bool,
2649
2650 pub display_id: Option<DisplayId>,
2653
2654 pub window_background: WindowBackgroundAppearance,
2656
2657 pub app_id: Option<String>,
2659
2660 pub window_min_size: Option<Size<Pixels>>,
2662
2663 pub window_decorations: Option<WindowDecorations>,
2666
2667 pub icon: Option<Arc<image::RgbaImage>>,
2669
2670 pub tabbing_identifier: Option<String>,
2672
2673 pub use_display_p3: bool,
2678}
2679
2680#[derive(Debug)]
2682#[cfg_attr(
2683 all(
2684 any(target_os = "linux", target_os = "freebsd"),
2685 not(any(feature = "x11", feature = "wayland"))
2686 ),
2687 allow(dead_code)
2688)]
2689#[allow(missing_docs)]
2690pub struct WindowParams {
2691 pub bounds: Bounds<Pixels>,
2692
2693 #[cfg_attr(feature = "wayland", allow(dead_code))]
2695 pub titlebar: Option<TitlebarOptions>,
2696
2697 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2699 pub kind: WindowKind,
2700
2701 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2703 pub is_movable: bool,
2704
2705 #[cfg_attr(
2707 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2708 allow(dead_code)
2709 )]
2710 pub app_owns_titlebar_drag: bool,
2711
2712 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2714 pub is_resizable: bool,
2715
2716 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2718 pub is_minimizable: bool,
2719
2720 #[cfg_attr(
2721 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2722 allow(dead_code)
2723 )]
2724 pub focus: bool,
2725
2726 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2727 pub show: bool,
2728
2729 #[cfg_attr(feature = "wayland", allow(dead_code))]
2731 pub icon: Option<Arc<image::RgbaImage>>,
2732
2733 #[cfg_attr(feature = "wayland", allow(dead_code))]
2734 pub display_id: Option<DisplayId>,
2735
2736 #[cfg_attr(feature = "wayland", allow(dead_code))]
2737 pub app_id: Option<String>,
2738
2739 pub window_min_size: Option<Size<Pixels>>,
2740
2741 #[cfg(target_os = "macos")]
2742 pub tabbing_identifier: Option<String>,
2743
2744 #[cfg(target_os = "macos")]
2745 pub use_display_p3: bool,
2746}
2747
2748#[derive(Debug, Copy, Clone, PartialEq)]
2750pub enum WindowBounds {
2751 Windowed(Bounds<Pixels>),
2753 Maximized(Bounds<Pixels>),
2756 Fullscreen(Bounds<Pixels>),
2759}
2760
2761impl Default for WindowBounds {
2762 fn default() -> Self {
2763 WindowBounds::Windowed(Bounds::default())
2764 }
2765}
2766
2767impl WindowBounds {
2768 pub fn get_bounds(&self) -> Bounds<Pixels> {
2770 match self {
2771 WindowBounds::Windowed(bounds) => *bounds,
2772 WindowBounds::Maximized(bounds) => *bounds,
2773 WindowBounds::Fullscreen(bounds) => *bounds,
2774 }
2775 }
2776
2777 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2779 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2780 }
2781}
2782
2783impl Default for WindowOptions {
2784 fn default() -> Self {
2785 Self {
2786 window_bounds: None,
2787 titlebar: Some(TitlebarOptions {
2788 title: Default::default(),
2789 appears_transparent: Default::default(),
2790 traffic_light_position: Default::default(),
2791 }),
2792 focus: true,
2793 show: true,
2794 kind: WindowKind::Normal,
2795 is_movable: true,
2796 app_owns_titlebar_drag: false,
2797 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2798 is_resizable: true,
2799 is_minimizable: true,
2800 display_id: None,
2801 window_background: WindowBackgroundAppearance::default(),
2802 icon: None,
2803 app_id: None,
2804 window_min_size: None,
2805 window_decorations: None,
2806 tabbing_identifier: None,
2807 use_display_p3: false,
2808 }
2809 }
2810}
2811
2812#[derive(Debug, Default)]
2814pub struct TitlebarOptions {
2815 pub title: Option<SharedString>,
2817
2818 pub appears_transparent: bool,
2821
2822 pub traffic_light_position: Option<Point<Pixels>>,
2824}
2825
2826#[derive(Clone, Debug, PartialEq, Eq)]
2828pub enum WindowKind {
2829 Normal,
2831
2832 PopUp,
2835
2836 AnchoredPopup(popup::PopupOptions),
2843
2844 Floating,
2846
2847 #[cfg(all(target_os = "linux", feature = "wayland"))]
2850 LayerShell(layer_shell::LayerShellOptions),
2851
2852 Dialog,
2855}
2856
2857#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2862pub enum WindowAppearance {
2863 #[default]
2867 Light,
2868
2869 VibrantLight,
2873
2874 Dark,
2878
2879 VibrantDark,
2883}
2884
2885#[derive(Copy, Clone, Debug, Default, PartialEq)]
2888pub enum WindowBackgroundAppearance {
2889 #[default]
2897 Opaque,
2898 Transparent,
2900 Blurred,
2904 MicaBackdrop,
2906 MicaAltBackdrop,
2908}
2909
2910#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2912pub enum TextRenderingMode {
2913 #[default]
2915 PlatformDefault,
2916 Subpixel,
2918 Grayscale,
2920}
2921
2922#[derive(Clone, Debug)]
2924pub struct PathPromptOptions {
2925 pub files: bool,
2927 pub directories: bool,
2929 pub multiple: bool,
2931 pub prompt: Option<SharedString>,
2933}
2934
2935#[derive(Copy, Clone, Debug, PartialEq)]
2937pub enum PromptLevel {
2938 Info,
2940
2941 Warning,
2943
2944 Critical,
2946}
2947
2948#[derive(Clone, Debug, PartialEq)]
2950pub enum PromptButton {
2951 Ok(SharedString),
2953 Cancel(SharedString),
2955 Other(SharedString),
2957}
2958
2959impl PromptButton {
2960 pub fn new(label: impl Into<SharedString>) -> Self {
2962 PromptButton::Other(label.into())
2963 }
2964
2965 pub fn ok(label: impl Into<SharedString>) -> Self {
2967 PromptButton::Ok(label.into())
2968 }
2969
2970 pub fn cancel(label: impl Into<SharedString>) -> Self {
2972 PromptButton::Cancel(label.into())
2973 }
2974
2975 #[allow(dead_code)]
2977 pub fn is_cancel(&self) -> bool {
2978 matches!(self, PromptButton::Cancel(_))
2979 }
2980
2981 pub fn label(&self) -> &SharedString {
2983 match self {
2984 PromptButton::Ok(label) => label,
2985 PromptButton::Cancel(label) => label,
2986 PromptButton::Other(label) => label,
2987 }
2988 }
2989}
2990
2991impl From<&str> for PromptButton {
2992 fn from(value: &str) -> Self {
2993 match value.to_lowercase().as_str() {
2994 "ok" => PromptButton::Ok("OK".into()),
2995 "cancel" => PromptButton::Cancel("Cancel".into()),
2996 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2997 }
2998 }
2999}
3000
3001#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3003pub enum CursorStyle {
3004 #[default]
3006 Arrow,
3007
3008 IBeam,
3011
3012 Crosshair,
3015
3016 ClosedHand,
3019
3020 OpenHand,
3023
3024 PointingHand,
3027
3028 ResizeLeft,
3031
3032 ResizeRight,
3035
3036 ResizeLeftRight,
3039
3040 ResizeUp,
3043
3044 ResizeDown,
3047
3048 ResizeUpDown,
3051
3052 ResizeUpLeftDownRight,
3055
3056 ResizeUpRightDownLeft,
3059
3060 ResizeColumn,
3063
3064 ResizeRow,
3067
3068 IBeamCursorForVerticalLayout,
3071
3072 OperationNotAllowed,
3075
3076 DragLink,
3079
3080 DragCopy,
3083
3084 ContextualMenu,
3087}
3088
3089#[derive(Clone, Debug, Eq, PartialEq)]
3091pub struct ClipboardItem {
3092 pub entries: Vec<ClipboardEntry>,
3094}
3095
3096#[derive(Clone, Debug, PartialEq, Eq)]
3101pub enum ClipboardReadError {
3102 Unavailable,
3106 Denied(String),
3109 UnsupportedContent,
3112}
3113
3114impl std::fmt::Display for ClipboardReadError {
3115 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3116 match self {
3117 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3118 Self::Denied(message) => {
3119 write!(formatter, "clipboard access was denied: {message}")
3120 }
3121 Self::UnsupportedContent => {
3122 formatter.write_str("the clipboard contents are unsupported")
3123 }
3124 }
3125 }
3126}
3127
3128impl std::error::Error for ClipboardReadError {}
3129
3130#[derive(Clone, Debug, Eq, PartialEq)]
3132pub enum ClipboardEntry {
3133 String(ClipboardString),
3135 Image(Image),
3137 ExternalPaths(crate::ExternalPaths),
3139}
3140
3141impl ClipboardItem {
3142 pub fn new_string(text: String) -> Self {
3144 Self {
3145 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3146 }
3147 }
3148
3149 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3151 Self {
3152 entries: vec![ClipboardEntry::String(ClipboardString {
3153 text,
3154 metadata: Some(metadata),
3155 })],
3156 }
3157 }
3158
3159 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3161 Self {
3162 entries: vec![ClipboardEntry::String(
3163 ClipboardString::new(text).with_json_metadata(metadata),
3164 )],
3165 }
3166 }
3167
3168 pub fn new_image(image: &Image) -> Self {
3170 Self {
3171 entries: vec![ClipboardEntry::Image(image.clone())],
3172 }
3173 }
3174
3175 pub fn text(&self) -> Option<String> {
3178 let mut answer = String::new();
3179
3180 for entry in self.entries.iter() {
3181 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3182 answer.push_str(text);
3183 }
3184 }
3185
3186 if answer.is_empty() {
3187 for entry in self.entries.iter() {
3188 if let ClipboardEntry::ExternalPaths(paths) = entry {
3189 for path in &paths.0 {
3190 use std::fmt::Write as _;
3191 _ = write!(answer, "{}", path.display());
3192 }
3193 }
3194 }
3195 }
3196
3197 if !answer.is_empty() {
3198 Some(answer)
3199 } else {
3200 None
3201 }
3202 }
3203
3204 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3206 pub fn metadata(&self) -> Option<&String> {
3207 match self.entries().first() {
3208 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3209 clipboard_string.metadata.as_ref()
3210 }
3211 _ => None,
3212 }
3213 }
3214
3215 pub fn entries(&self) -> &[ClipboardEntry] {
3217 &self.entries
3218 }
3219
3220 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3222 self.entries.into_iter()
3223 }
3224}
3225
3226impl From<ClipboardString> for ClipboardEntry {
3227 fn from(value: ClipboardString) -> Self {
3228 Self::String(value)
3229 }
3230}
3231
3232impl From<String> for ClipboardEntry {
3233 fn from(value: String) -> Self {
3234 Self::from(ClipboardString::from(value))
3235 }
3236}
3237
3238impl From<Image> for ClipboardEntry {
3239 fn from(value: Image) -> Self {
3240 Self::Image(value)
3241 }
3242}
3243
3244impl From<ClipboardEntry> for ClipboardItem {
3245 fn from(value: ClipboardEntry) -> Self {
3246 Self {
3247 entries: vec![value],
3248 }
3249 }
3250}
3251
3252impl From<String> for ClipboardItem {
3253 fn from(value: String) -> Self {
3254 Self::from(ClipboardEntry::from(value))
3255 }
3256}
3257
3258impl From<Image> for ClipboardItem {
3259 fn from(value: Image) -> Self {
3260 Self::from(ClipboardEntry::from(value))
3261 }
3262}
3263
3264#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3266pub enum ImageFormat {
3267 Png,
3272 Jpeg,
3274 Webp,
3276 Gif,
3278 Svg,
3280 Bmp,
3282 Tiff,
3284 Ico,
3286 Pnm,
3288}
3289
3290impl ImageFormat {
3291 pub const fn mime_type(self) -> &'static str {
3293 match self {
3294 ImageFormat::Png => "image/png",
3295 ImageFormat::Jpeg => "image/jpeg",
3296 ImageFormat::Webp => "image/webp",
3297 ImageFormat::Gif => "image/gif",
3298 ImageFormat::Svg => "image/svg+xml",
3299 ImageFormat::Bmp => "image/bmp",
3300 ImageFormat::Tiff => "image/tiff",
3301 ImageFormat::Ico => "image/ico",
3302 ImageFormat::Pnm => "image/x-portable-anymap",
3303 }
3304 }
3305
3306 pub const fn extension(self) -> &'static str {
3308 match self {
3309 ImageFormat::Png => "png",
3310 ImageFormat::Jpeg => "jpg",
3311 ImageFormat::Webp => "webp",
3312 ImageFormat::Gif => "gif",
3313 ImageFormat::Svg => "svg",
3314 ImageFormat::Bmp => "bmp",
3315 ImageFormat::Tiff => "tiff",
3316 ImageFormat::Ico => "ico",
3317 ImageFormat::Pnm => "pnm",
3318 }
3319 }
3320
3321 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3323 use strum::IntoEnumIterator;
3324 Self::iter()
3325 .find(|format| format.mime_type() == mime_type)
3326 .or_else(|| Self::from_mime_type_alias(mime_type))
3327 }
3328
3329 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3333 match mime_type {
3334 "image/jpg" => Some(Self::Jpeg),
3335 "image/tif" => Some(Self::Tiff),
3336 _ => None,
3337 }
3338 }
3339}
3340
3341#[derive(Clone, Debug, PartialEq, Eq)]
3343pub struct Image {
3344 pub format: ImageFormat,
3346 pub bytes: Vec<u8>,
3348 pub id: u64,
3350}
3351
3352pub(crate) fn decode_static_image(
3353 bytes: &[u8],
3354 format: image::ImageFormat,
3355) -> Result<SmallVec<[Frame; 1]>> {
3356 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3357 .into_decoder()
3358 .context("creating image decoder")?;
3359 decode_static_image_from_decoder(decoder)
3360}
3361
3362pub(crate) fn decode_static_image_from_decoder(
3363 mut decoder: impl image::ImageDecoder,
3364) -> Result<SmallVec<[Frame; 1]>> {
3365 let orientation = decoder
3366 .orientation()
3367 .context("reading decoder's orientation")?;
3368 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3369 image.apply_orientation(orientation);
3370
3371 let mut data = image.into_rgba8();
3372 for pixel in data.chunks_exact_mut(4) {
3373 pixel.swap(0, 2);
3374 }
3375
3376 Ok(SmallVec::from_elem(Frame::new(data), 1))
3377}
3378
3379impl Hash for Image {
3380 fn hash<H: Hasher>(&self, state: &mut H) {
3381 state.write_u64(self.id);
3382 }
3383}
3384
3385impl Image {
3386 pub fn empty() -> Self {
3388 Self::from_bytes(ImageFormat::Png, Vec::new())
3389 }
3390
3391 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3393 Self {
3394 id: hash(&bytes),
3395 format,
3396 bytes,
3397 }
3398 }
3399
3400 pub fn id(&self) -> u64 {
3402 self.id
3403 }
3404
3405 pub fn use_render_image(
3407 self: Arc<Self>,
3408 window: &mut Window,
3409 cx: &mut App,
3410 ) -> Option<Arc<RenderImage>> {
3411 ImageSource::Image(self)
3412 .use_data(None, window, cx)
3413 .and_then(|result| result.ok())
3414 }
3415
3416 pub fn get_render_image(
3418 self: Arc<Self>,
3419 window: &mut Window,
3420 cx: &mut App,
3421 ) -> Option<Arc<RenderImage>> {
3422 ImageSource::Image(self)
3423 .get_data(None, window, cx)
3424 .and_then(|result| result.ok())
3425 }
3426
3427 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3429 ImageSource::Image(self).remove_asset(cx);
3430 }
3431
3432 #[cfg(any(test, feature = "test-support"))]
3435 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3436 ImageSource::Image(self.clone()).is_asset_cached(cx)
3437 }
3438
3439 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3441 let frames = match self.format {
3442 ImageFormat::Gif => {
3443 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3444 let mut frames = SmallVec::new();
3445
3446 for frame in decoder.into_frames() {
3447 match frame {
3448 Ok(mut frame) => {
3449 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3451 pixel.swap(0, 2);
3452 }
3453 frames.push(frame);
3454 }
3455 Err(err) => {
3456 log::debug!("Skipping GIF frame due to decode error: {err}");
3457 }
3458 }
3459 }
3460
3461 if frames.is_empty() {
3462 anyhow::bail!("GIF could not be decoded: all frames failed");
3463 }
3464
3465 frames
3466 }
3467 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3468 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3469 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3470 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3471 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3472 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3473 ImageFormat::Svg => {
3474 return svg_renderer
3475 .render_single_frame(&self.bytes, 1.0)
3476 .map_err(Into::into);
3477 }
3478 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3479 };
3480
3481 Ok(Arc::new(RenderImage::new(frames)))
3482 }
3483
3484 pub fn format(&self) -> ImageFormat {
3486 self.format
3487 }
3488
3489 pub fn bytes(&self) -> &[u8] {
3491 self.bytes.as_slice()
3492 }
3493}
3494
3495#[derive(Clone, Debug, Eq, PartialEq)]
3497pub struct ClipboardString {
3498 pub text: String,
3500 pub metadata: Option<String>,
3502}
3503
3504impl ClipboardString {
3505 pub fn new(text: String) -> Self {
3507 Self {
3508 text,
3509 metadata: None,
3510 }
3511 }
3512
3513 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3516 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3517 self
3518 }
3519
3520 pub fn text(&self) -> &String {
3522 &self.text
3523 }
3524
3525 pub fn into_text(self) -> String {
3527 self.text
3528 }
3529
3530 pub fn metadata_json<T>(&self) -> Option<T>
3532 where
3533 T: for<'a> Deserialize<'a>,
3534 {
3535 self.metadata
3536 .as_ref()
3537 .and_then(|m| serde_json::from_str(m).ok())
3538 }
3539
3540 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3541 pub fn text_hash(text: &str) -> u64 {
3543 let mut hasher = SeaHasher::new();
3544 text.hash(&mut hasher);
3545 hasher.finish()
3546 }
3547}
3548
3549impl From<String> for ClipboardString {
3550 fn from(value: String) -> Self {
3551 Self {
3552 text: value,
3553 metadata: None,
3554 }
3555 }
3556}
3557
3558#[cfg(test)]
3559mod image_tests {
3560 use super::*;
3561 use std::sync::Arc;
3562
3563 #[test]
3564 fn test_image_to_image_data_applies_exif_orientation() {
3565 let image = Image::from_bytes(
3566 ImageFormat::Jpeg,
3567 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3568 );
3569
3570 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3571
3572 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3573
3574 let bytes = render_image.as_bytes(0).unwrap();
3575 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3576 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3577 }
3578
3579 #[test]
3580 fn test_svg_image_to_image_data_converts_to_bgra() {
3581 let image = Image::from_bytes(
3582 ImageFormat::Svg,
3583 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3584<rect width="1" height="1" fill="#38BDF8"/>
3585</svg>"##
3586 .to_vec(),
3587 );
3588
3589 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3590 let bytes = render_image.as_bytes(0).unwrap();
3591
3592 for pixel in bytes.chunks_exact(4) {
3593 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3594 }
3595 }
3596}
3597
3598#[cfg(test)]
3599mod atlas_tests {
3600 use super::*;
3601
3602 const TILE_SIZE: Size<DevicePixels> = Size {
3603 width: DevicePixels(1),
3604 height: DevicePixels(1),
3605 };
3606
3607 #[derive(Default)]
3608 struct RecordingAtlasBackend {
3609 insert_calls: u32,
3610 fail_next_insert: bool,
3611 removed_tiles: Vec<AtlasTile>,
3612 }
3613
3614 impl AtlasBackend for RecordingAtlasBackend {
3615 fn insert(
3616 &mut self,
3617 kind: AtlasTextureKind,
3618 size: Size<DevicePixels>,
3619 _bytes: &[u8],
3620 ) -> Result<AtlasTile> {
3621 self.insert_calls += 1;
3622 if std::mem::take(&mut self.fail_next_insert) {
3623 anyhow::bail!("backend failed");
3624 }
3625 Ok(AtlasTile {
3626 texture_id: AtlasTextureId { index: 0, kind },
3627 tile_id: TileId(self.insert_calls),
3628 padding: 0,
3629 bounds: Bounds {
3630 origin: Point::default(),
3631 size,
3632 },
3633 })
3634 }
3635
3636 fn remove(&mut self, tile: AtlasTile) {
3637 self.removed_tiles.push(tile);
3638 }
3639 }
3640
3641 fn image_key(image_id: usize) -> AtlasKey {
3642 AtlasKey::Image(RenderImageParams {
3643 image_id: crate::ImageId(image_id),
3644 frame_index: 0,
3645 })
3646 }
3647
3648 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3649 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3650 }
3651
3652 #[test]
3653 fn only_successful_inserts_are_cached() -> Result<()> {
3654 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3655 let key = image_key(1);
3656
3657 assert_eq!(
3658 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3659 None
3660 );
3661 state
3662 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3663 .expect_err("builder error should propagate");
3664 assert!(!state.contains(&key));
3665 assert_eq!(state.backend.insert_calls, 0);
3666
3667 state.backend.fail_next_insert = true;
3668 state
3669 .get_or_insert_with(key.clone(), &mut build_tile)
3670 .expect_err("backend error should propagate");
3671 assert!(!state.contains(&key));
3672 assert_eq!(state.backend.insert_calls, 1);
3673
3674 let tile = state
3675 .get_or_insert_with(key.clone(), &mut build_tile)?
3676 .context("builder should produce a tile")?;
3677 assert_eq!(tile.texture_id.kind, key.texture_kind());
3678 assert_eq!(
3679 state.get_or_insert_with(key.clone(), &mut || {
3680 anyhow::bail!("cache hit must not call the builder")
3681 })?,
3682 Some(tile)
3683 );
3684 assert!(state.contains(&key));
3685 assert_eq!(state.backend.insert_calls, 2);
3686 Ok(())
3687 }
3688
3689 #[test]
3690 fn remove_and_clear_invalidate_keys() -> Result<()> {
3691 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3692 let key = image_key(1);
3693 let other_key = image_key(2);
3694 let tile = state
3695 .get_or_insert_with(key.clone(), &mut build_tile)?
3696 .context("builder should produce a tile")?;
3697 state
3698 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3699 .context("builder should produce another tile")?;
3700
3701 state.remove(&key);
3702 state.remove(&key);
3703 assert!(!state.contains(&key));
3704 assert!(state.contains(&other_key));
3705 assert_eq!(state.backend.removed_tiles, vec![tile]);
3706
3707 let mut reset_calls = 0;
3708 state.clear(|_| reset_calls += 1);
3709 assert_eq!(reset_calls, 1);
3710 assert!(!state.contains(&other_key));
3711 assert_eq!(state.backend.removed_tiles, vec![tile]);
3712 Ok(())
3713 }
3714}
3715
3716#[cfg(test)]
3717mod frame_signal_tests {
3718 use super::*;
3719
3720 #[cfg(feature = "profiler")]
3721 #[test]
3722 fn coalesced_signals_retain_the_earliest_until_drained() {
3723 for (first_source, later_source) in [
3724 (
3725 FrameRequestSource::NativeCallback,
3726 FrameRequestSource::LocalSchedule,
3727 ),
3728 (
3729 FrameRequestSource::LocalSchedule,
3730 FrameRequestSource::NativeCallback,
3731 ),
3732 ] {
3733 let signal = PlatformFrameSignal::new();
3734 let first =
3735 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3736 assert_eq!(signal.take(), None);
3737 signal.record(first + Duration::from_millis(16), later_source);
3738 signal.record(first, first_source);
3739 signal.record(first + Duration::from_millis(32), later_source);
3740 assert_eq!(signal.take(), Some((first, first_source)));
3741 assert_eq!(signal.take(), None);
3742 let next = first + Duration::from_millis(48);
3743 signal.record(next, later_source);
3744 assert_eq!(signal.take(), Some((next, later_source)));
3745 }
3746 }
3747
3748 #[cfg(not(feature = "profiler"))]
3749 #[test]
3750 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3751 let captured = PlatformFrameSignal::capture(|| {
3752 panic!("profiler-disabled builds must not evaluate the capture closure");
3753 });
3754 assert_eq!(captured, None);
3755 let signal = PlatformFrameSignal::new();
3756 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3757 assert_eq!(signal.take(), None);
3758 }
3759}
3760
3761#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3762mod tests {
3763 use super::*;
3764 use std::collections::HashSet;
3765
3766 #[test]
3767 fn test_refresh_interval_from_hz() {
3768 assert_eq!(
3769 refresh_interval_from_hz(50.0),
3770 Some(Duration::from_millis(20))
3771 );
3772 assert_eq!(
3773 refresh_interval_from_hz(120.0),
3774 Some(Duration::from_secs(1) / 120)
3775 );
3776 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3777 assert_eq!(refresh_interval_from_hz(unknown), None);
3778 }
3779 }
3780
3781 #[test]
3782 fn test_window_button_layout_parse_standard() {
3783 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3784 assert_eq!(
3785 layout.left,
3786 [
3787 Some(WindowButton::Close),
3788 Some(WindowButton::Minimize),
3789 None
3790 ]
3791 );
3792 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3793 }
3794
3795 #[test]
3796 fn test_window_button_layout_parse_right_only() {
3797 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3798 assert_eq!(layout.left, [None, None, None]);
3799 assert_eq!(
3800 layout.right,
3801 [
3802 Some(WindowButton::Minimize),
3803 Some(WindowButton::Maximize),
3804 Some(WindowButton::Close)
3805 ]
3806 );
3807 }
3808
3809 #[test]
3810 fn test_window_button_layout_parse_left_only() {
3811 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3812 assert_eq!(
3813 layout.left,
3814 [
3815 Some(WindowButton::Close),
3816 Some(WindowButton::Minimize),
3817 Some(WindowButton::Maximize)
3818 ]
3819 );
3820 assert_eq!(layout.right, [None, None, None]);
3821 }
3822
3823 #[test]
3824 fn test_window_button_layout_parse_with_whitespace() {
3825 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3826 assert_eq!(
3827 layout.left,
3828 [
3829 Some(WindowButton::Close),
3830 Some(WindowButton::Minimize),
3831 None
3832 ]
3833 );
3834 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3835 }
3836
3837 #[test]
3838 fn test_window_button_layout_parse_empty() {
3839 let layout = WindowButtonLayout::parse("").unwrap();
3840 assert_eq!(layout.left, [None, None, None]);
3841 assert_eq!(layout.right, [None, None, None]);
3842 }
3843
3844 #[test]
3845 fn test_window_button_layout_parse_intentionally_empty() {
3846 let layout = WindowButtonLayout::parse(":").unwrap();
3847 assert_eq!(layout.left, [None, None, None]);
3848 assert_eq!(layout.right, [None, None, None]);
3849 }
3850
3851 #[test]
3852 fn test_window_button_layout_parse_invalid_buttons() {
3853 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3854 assert_eq!(
3855 layout.left,
3856 [
3857 Some(WindowButton::Close),
3858 Some(WindowButton::Minimize),
3859 None
3860 ]
3861 );
3862 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3863 }
3864
3865 #[test]
3866 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3867 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3868 assert_eq!(
3869 layout.right,
3870 [
3871 Some(WindowButton::Close),
3872 Some(WindowButton::Minimize),
3873 None
3874 ]
3875 );
3876 assert_eq!(layout.format(), ":close,minimize");
3877 }
3878
3879 #[test]
3880 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3881 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3882 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3883 assert_eq!(
3884 layout.right,
3885 [
3886 Some(WindowButton::Maximize),
3887 Some(WindowButton::Minimize),
3888 None
3889 ]
3890 );
3891
3892 let button_ids: Vec<_> = layout
3893 .left
3894 .iter()
3895 .chain(layout.right.iter())
3896 .flatten()
3897 .map(WindowButton::id)
3898 .collect();
3899 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3900 assert_eq!(unique_button_ids.len(), button_ids.len());
3901 assert_eq!(layout.format(), "close:maximize,minimize");
3902 }
3903
3904 #[test]
3905 fn test_window_button_layout_parse_gnome_style() {
3906 let layout = WindowButtonLayout::parse("close").unwrap();
3907 assert_eq!(layout.left, [None, None, None]);
3908 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3909 }
3910
3911 #[test]
3912 fn test_window_button_layout_parse_elementary_style() {
3913 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3914 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3915 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3916 }
3917
3918 #[test]
3919 fn test_window_button_layout_round_trip() {
3920 let cases = [
3921 "close:minimize,maximize",
3922 "minimize,maximize,close:",
3923 ":close",
3924 "close:",
3925 "close:maximize",
3926 ":",
3927 ];
3928
3929 for case in cases {
3930 let layout = WindowButtonLayout::parse(case).unwrap();
3931 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3932 }
3933 }
3934
3935 #[test]
3936 fn test_window_button_layout_linux_default() {
3937 let layout = WindowButtonLayout::linux_default();
3938 assert_eq!(layout.left, [None, None, None]);
3939 assert_eq!(
3940 layout.right,
3941 [
3942 Some(WindowButton::Minimize),
3943 Some(WindowButton::Maximize),
3944 Some(WindowButton::Close)
3945 ]
3946 );
3947
3948 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3949 assert_eq!(round_tripped, layout);
3950 }
3951
3952 #[test]
3953 fn test_window_button_layout_parse_all_invalid() {
3954 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3955 }
3956}