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,
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}
1150
1151#[derive(Debug, Clone, Default, PartialEq)]
1158pub struct WindowInsets {
1159 pub safe_area: Edges<Pixels>,
1164 pub ime: Edges<Pixels>,
1168}
1169
1170impl WindowInsets {
1171 pub fn effective(&self) -> Edges<Pixels> {
1173 Edges {
1174 top: self.safe_area.top.max(self.ime.top),
1175 right: self.safe_area.right.max(self.ime.right),
1176 bottom: self.safe_area.bottom.max(self.ime.bottom),
1177 left: self.safe_area.left.max(self.ime.left),
1178 }
1179 }
1180}
1181
1182#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1184pub enum TextInputStateChange {
1185 FocusGained,
1187 FocusLost,
1189 SelectionChanged,
1191 ContentChanged,
1193}
1194
1195#[expect(missing_docs)]
1196pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1197 fn set_color_mode(&mut self, mode: WindowColorMode) -> anyhow::Result<WindowColorStatus> {
1199 match mode {
1200 WindowColorMode::Default => Ok(self.color_status()),
1201 _ => anyhow::bail!("wide-gamut SDR presentation is unsupported"),
1202 }
1203 }
1204 fn color_status(&self) -> WindowColorStatus {
1206 WindowColorStatus::default()
1207 }
1208
1209 fn bounds(&self) -> Bounds<Pixels>;
1210 fn is_maximized(&self) -> bool;
1211 fn window_bounds(&self) -> WindowBounds;
1212 fn content_size(&self) -> Size<Pixels>;
1213 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1219 Bounds::new(Point::default(), self.content_size())
1220 }
1221 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1226 fn prepare_frame(&self) -> bool {
1231 false
1232 }
1233 fn resize(&mut self, size: Size<Pixels>);
1234 fn scale_factor(&self) -> f32;
1235 fn appearance(&self) -> WindowAppearance;
1236 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1237 fn mouse_position(&self) -> Point<Pixels>;
1238 fn modifiers(&self) -> Modifiers;
1239 fn capslock(&self) -> Capslock;
1240 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1241 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1242 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1247 fn prompt(
1248 &self,
1249 level: PromptLevel,
1250 msg: &str,
1251 detail: Option<&str>,
1252 answers: &[PromptButton],
1253 ) -> Option<oneshot::Receiver<usize>>;
1254 fn activate(&self);
1255 fn request_attention(&self) {}
1257 fn is_active(&self) -> bool;
1258 fn visibility(&self) -> WindowVisibility;
1261 fn is_hovered(&self) -> bool;
1262 fn background_appearance(&self) -> WindowBackgroundAppearance;
1263 fn set_title(&mut self, title: &str);
1264 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1265 fn minimize(&self);
1266 fn zoom(&self);
1267 fn toggle_fullscreen(&self);
1268 fn is_fullscreen(&self) -> bool;
1269 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1270 None
1271 }
1272 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1273 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1274 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1275 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1279 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1280 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1281 fn on_moved(&self, callback: Box<dyn FnMut()>);
1282 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1286 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1287 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1288 fn on_close(&self, callback: Box<dyn FnOnce()>);
1289 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1290 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1291 fn draw(&self, scene: &Scene);
1292 #[cfg(any(test, feature = "test-support"))]
1294 fn submit_scene_for_validation(&self, _scene: &Scene) -> anyhow::Result<()> {
1295 anyhow::bail!("native window ownership validation is unsupported on this backend")
1296 }
1297 fn schedule_frame(&self) {}
1298 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1299 fn is_subpixel_rendering_supported(&self) -> bool;
1300
1301 fn get_title(&self) -> String {
1303 String::new()
1304 }
1305 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1306 None
1307 }
1308 fn tab_bar_visible(&self) -> bool {
1309 false
1310 }
1311 fn set_edited(&mut self, _edited: bool) {}
1312 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1313 fn toggle_simple_fullscreen(&self) {}
1314 fn is_simple_fullscreen(&self) -> bool {
1315 false
1316 }
1317 #[cfg(target_os = "macos")]
1318 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1319 fn show_character_palette(&self) {}
1320 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1321 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1322 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1323 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1324 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1325 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1326 fn merge_all_windows(&self) {}
1327 fn move_tab_to_new_window(&self) {}
1328 fn toggle_window_tab_overview(&self) {}
1329 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1330
1331 fn native_window_state(&self) -> Option<Vec<u8>> {
1332 None
1333 }
1334 fn restore_native_window_state(&self, _state: &[u8]) {}
1335
1336 #[cfg(target_os = "windows")]
1337 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1338
1339 fn inner_window_bounds(&self) -> WindowBounds {
1341 self.window_bounds()
1342 }
1343 fn request_decorations(&self, _decorations: WindowDecorations) {}
1344 fn show_window_menu(&self, _position: Point<Pixels>) {}
1345 fn start_window_move(&self) {}
1346 fn can_start_external_drag(&self) -> bool {
1347 false
1348 }
1349 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1350 false
1351 }
1352 fn start_window_resize(&self, _edge: ResizeEdge) {}
1353 fn set_exclusive_zone(&self, _zone: Pixels) {}
1354 #[cfg(all(target_os = "linux", feature = "wayland"))]
1355 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1356 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1357 fn window_decorations(&self) -> Decorations {
1358 Decorations::Server
1359 }
1360 fn set_app_id(&mut self, _app_id: &str) {}
1361 fn map_window(&mut self) -> anyhow::Result<()> {
1362 Ok(())
1363 }
1364 fn window_controls(&self) -> WindowControls {
1365 WindowControls::default()
1366 }
1367 fn set_client_inset(&self, _inset: Pixels) {}
1368 fn gpu_specs(&self) -> Option<GpuSpecs>;
1369
1370 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1371
1372 fn insets(&self) -> WindowInsets {
1377 WindowInsets::default()
1378 }
1379
1380 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1386
1387 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1390
1391 fn set_back_enabled(&self, _enabled: bool) {}
1394
1395 fn show_soft_keyboard(&self) {}
1397
1398 fn hide_soft_keyboard(&self) {}
1400
1401 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1403
1404 fn play_system_bell(&self) {}
1405
1406 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1408
1409 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1411
1412 fn a11y_update_window_bounds(&self) {}
1414
1415 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1416 fn as_test(&mut self) -> Option<&mut TestWindow> {
1417 None
1418 }
1419
1420 #[cfg(any(test, feature = "test-support"))]
1424 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1425 anyhow::bail!("render_to_image not implemented for this platform")
1426 }
1427}
1428
1429#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1431pub trait PlatformHeadlessRenderer {
1432 fn render_scene_to_image(
1434 &mut self,
1435 scene: &Scene,
1436 size: Size<DevicePixels>,
1437 ) -> Result<RgbaImage>;
1438
1439 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1445
1446 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1448}
1449
1450#[doc(hidden)]
1453pub type RunnableVariant = Runnable<RunnableMeta>;
1454
1455#[doc(hidden)]
1456pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1457
1458#[doc(hidden)]
1459pub enum TasksIncluded {
1460 OnlyCompleted,
1461 CompletedAndRunning,
1462}
1463
1464#[doc(hidden)]
1467pub trait PlatformDispatcher: Send + Sync {
1468 fn is_main_thread(&self) -> bool;
1469 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1470 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1471 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1472
1473 fn dispatch_on_main_thread_when_idle(
1474 &self,
1475 runnable: RunnableVariant,
1476 timeout: Option<Duration>,
1477 ) {
1478 let _ = timeout;
1479 self.dispatch_on_main_thread(runnable, Priority::Low);
1480 }
1481
1482 fn idle_time_remaining(&self) -> Option<Duration> {
1483 None
1484 }
1485
1486 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1487
1488 fn now(&self) -> Instant {
1489 Instant::now()
1490 }
1491
1492 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1493 gpui_util::defer(Box::new(|| {}))
1494 }
1495
1496 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1497 ActivityGuard::noop()
1498 }
1499
1500 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1501 fn as_test(&self) -> Option<&TestDispatcher> {
1502 None
1503 }
1504
1505 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1508 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1509 None
1510 }
1511}
1512
1513#[expect(missing_docs)]
1514pub trait PlatformTextSystem: Send + Sync {
1515 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1516 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1518 fn all_font_names(&self) -> Vec<String>;
1520 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1522 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1524 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1526 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1528 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1530 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1532 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1534 fn rasterize_glyph(
1536 &self,
1537 params: &RenderGlyphParams,
1538 raster_bounds: Bounds<DevicePixels>,
1539 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1540 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1542 fn shape_paragraph(
1544 &self,
1545 _request: crate::ParagraphRequest<'_>,
1546 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1547 anyhow::bail!("paragraph layout is unsupported by this text backend")
1548 }
1549 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1551 -> TextRenderingMode;
1552 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1554 0
1555 }
1556}
1557
1558#[expect(missing_docs)]
1559pub struct NoopTextSystem;
1560
1561#[expect(missing_docs)]
1562impl NoopTextSystem {
1563 #[allow(dead_code)]
1564 pub fn new() -> Self {
1565 Self
1566 }
1567}
1568
1569impl PlatformTextSystem for NoopTextSystem {
1570 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1571 Ok(())
1572 }
1573
1574 fn all_font_names(&self) -> Vec<String> {
1575 Vec::new()
1576 }
1577
1578 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1579 Ok(FontId(1))
1580 }
1581
1582 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1583 FontMetrics {
1584 units_per_em: 1000,
1585 ascent: 1025.0,
1586 descent: -275.0,
1587 line_gap: 0.0,
1588 underline_position: -95.0,
1589 underline_thickness: 60.0,
1590 cap_height: 698.0,
1591 x_height: 516.0,
1592 bounding_box: Bounds {
1593 origin: Point {
1594 x: -260.0,
1595 y: -245.0,
1596 },
1597 size: Size {
1598 width: 1501.0,
1599 height: 1364.0,
1600 },
1601 },
1602 }
1603 }
1604
1605 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1606 Ok(Bounds {
1607 origin: Point { x: 54.0, y: 0.0 },
1608 size: size(392.0, 528.0),
1609 })
1610 }
1611
1612 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1613 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1614 }
1615
1616 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1617 Some(GlyphId(ch.len_utf16() as u32))
1618 }
1619
1620 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1621 Ok(Default::default())
1622 }
1623
1624 fn rasterize_glyph(
1625 &self,
1626 _params: &RenderGlyphParams,
1627 raster_bounds: Bounds<DevicePixels>,
1628 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1629 Ok((raster_bounds.size, Vec::new()))
1630 }
1631
1632 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1633 let mut position = px(0.);
1634 let metrics = self.font_metrics(FontId(0));
1635 let em_width = font_size
1636 * self
1637 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1638 .unwrap()
1639 .width
1640 / metrics.units_per_em as f32;
1641 let mut glyphs = Vec::new();
1642 for (ix, c) in text.char_indices() {
1643 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1644 glyphs.push(ShapedGlyph {
1645 id: glyph,
1646 position: point(position, px(0.)),
1647 index: ix,
1648 is_emoji: glyph.0 == 2,
1649 });
1650 if glyph.0 == 2 {
1651 position += em_width * 2.0;
1652 } else {
1653 position += em_width;
1654 }
1655 } else {
1656 position += em_width
1657 }
1658 }
1659 let mut runs = Vec::default();
1660 if !glyphs.is_empty() {
1661 runs.push(ShapedRun {
1662 font_id: FontId(0),
1663 glyphs,
1664 });
1665 } else {
1666 position = px(0.);
1667 }
1668
1669 LineLayout {
1670 font_size,
1671 width: position,
1672 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1673 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1674 runs,
1675 len: text.len(),
1676 }
1677 }
1678
1679 fn recommended_rendering_mode(
1680 &self,
1681 _font_id: FontId,
1682 _font_size: Pixels,
1683 ) -> TextRenderingMode {
1684 TextRenderingMode::Grayscale
1685 }
1686}
1687
1688#[allow(dead_code)]
1693pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1694 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1695 [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], ];
1709
1710 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1711 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1712
1713 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1714 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1715
1716 [
1717 ratios[0] * NORM13,
1718 ratios[1] * NORM24,
1719 ratios[2] * NORM13,
1720 ratios[3] * NORM24,
1721 ]
1722}
1723
1724#[derive(PartialEq, Eq, Hash, Clone)]
1725#[expect(missing_docs)]
1726pub enum AtlasKey {
1727 Glyph(RenderGlyphParams),
1728 Svg(RenderSvgParams),
1729 Image(RenderImageParams),
1730}
1731
1732impl AtlasKey {
1733 pub fn texture_kind(&self) -> AtlasTextureKind {
1735 match self {
1736 AtlasKey::Glyph(params) => {
1737 if params.is_emoji {
1738 AtlasTextureKind::Polychrome
1739 } else if params.subpixel_rendering {
1740 AtlasTextureKind::Subpixel
1741 } else {
1742 AtlasTextureKind::Monochrome
1743 }
1744 }
1745 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1746 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1747 }
1748 }
1749}
1750
1751impl From<RenderGlyphParams> for AtlasKey {
1752 fn from(params: RenderGlyphParams) -> Self {
1753 Self::Glyph(params)
1754 }
1755}
1756
1757impl From<RenderSvgParams> for AtlasKey {
1758 fn from(params: RenderSvgParams) -> Self {
1759 Self::Svg(params)
1760 }
1761}
1762
1763impl From<RenderImageParams> for AtlasKey {
1764 fn from(params: RenderImageParams) -> Self {
1765 Self::Image(params)
1766 }
1767}
1768
1769#[expect(missing_docs)]
1770pub trait PlatformAtlas {
1771 fn get_or_insert_with<'a>(
1773 &self,
1774 key: AtlasKey,
1775 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1776 ) -> Result<Option<AtlasTile>>;
1777 fn remove(&self, key: &AtlasKey);
1778
1779 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1780 fn contains(&self, _key: &AtlasKey) -> bool {
1781 false
1782 }
1783}
1784
1785#[doc(hidden)]
1786pub trait AtlasBackend {
1787 fn insert(
1788 &mut self,
1789 kind: AtlasTextureKind,
1790 size: Size<DevicePixels>,
1791 bytes: &[u8],
1792 ) -> Result<AtlasTile>;
1793
1794 fn remove(&mut self, tile: AtlasTile);
1795}
1796
1797#[doc(hidden)]
1798pub struct AtlasState<Backend> {
1799 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1800 pub backend: Backend,
1801}
1802
1803impl<Backend> AtlasState<Backend> {
1804 pub fn new(backend: Backend) -> Self {
1805 Self {
1806 tiles_by_key: FxHashMap::default(),
1807 backend,
1808 }
1809 }
1810
1811 pub fn contains(&self, key: &AtlasKey) -> bool {
1812 self.tiles_by_key.contains_key(key)
1813 }
1814
1815 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1816 self.tiles_by_key.clear();
1817 reset_backend(&mut self.backend);
1818 }
1819}
1820
1821impl<Backend: Default> Default for AtlasState<Backend> {
1822 fn default() -> Self {
1823 Self::new(Backend::default())
1824 }
1825}
1826
1827impl<Backend: AtlasBackend> AtlasState<Backend> {
1828 pub fn get_or_insert_with<'a>(
1829 &mut self,
1830 key: AtlasKey,
1831 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1832 ) -> Result<Option<AtlasTile>> {
1833 match self.tiles_by_key.entry(key) {
1834 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1835 Entry::Vacant(entry) => {
1836 profiling::scope!("new tile");
1837 let Some((size, bytes)) = build()? else {
1838 return Ok(None);
1839 };
1840 let tile = self
1841 .backend
1842 .insert(entry.key().texture_kind(), size, &bytes)?;
1843 entry.insert(tile);
1844 Ok(Some(tile))
1845 }
1846 }
1847 }
1848
1849 pub fn remove(&mut self, key: &AtlasKey) {
1850 if let Some(tile) = self.tiles_by_key.remove(key) {
1851 self.backend.remove(tile);
1852 }
1853 }
1854}
1855
1856#[derive(Default)]
1860pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1861
1862#[doc(hidden)]
1863#[derive(Default)]
1864pub struct HeadlessAtlasBackend {
1865 next_id: u32,
1866}
1867
1868impl AtlasBackend for HeadlessAtlasBackend {
1869 fn insert(
1870 &mut self,
1871 kind: AtlasTextureKind,
1872 size: Size<DevicePixels>,
1873 _bytes: &[u8],
1874 ) -> Result<AtlasTile> {
1875 self.next_id += 1;
1876 let texture_id = self.next_id;
1877 self.next_id += 1;
1878 let tile_id = self.next_id;
1879 Ok(AtlasTile {
1880 texture_id: AtlasTextureId {
1881 index: texture_id,
1882 kind,
1883 },
1884 tile_id: TileId(tile_id),
1885 padding: 0,
1886 bounds: Bounds {
1887 origin: Point::default(),
1888 size,
1889 },
1890 })
1891 }
1892
1893 fn remove(&mut self, _tile: AtlasTile) {}
1894}
1895
1896impl PlatformAtlas for HeadlessAtlas {
1897 fn get_or_insert_with<'a>(
1898 &self,
1899 key: AtlasKey,
1900 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1901 ) -> Result<Option<AtlasTile>> {
1902 self.0.lock().get_or_insert_with(key, build)
1903 }
1904
1905 fn remove(&self, key: &AtlasKey) {
1906 self.0.lock().remove(key);
1907 }
1908
1909 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1910 fn contains(&self, key: &AtlasKey) -> bool {
1911 self.0.lock().contains(key)
1912 }
1913}
1914
1915#[doc(hidden)]
1916pub struct AtlasTextureList<T> {
1917 pub textures: Vec<Option<T>>,
1918 pub free_list: Vec<usize>,
1919}
1920
1921impl<T> Default for AtlasTextureList<T> {
1922 fn default() -> Self {
1923 Self {
1924 textures: Vec::default(),
1925 free_list: Vec::default(),
1926 }
1927 }
1928}
1929
1930impl<T> ops::Index<usize> for AtlasTextureList<T> {
1931 type Output = Option<T>;
1932
1933 fn index(&self, index: usize) -> &Self::Output {
1934 &self.textures[index]
1935 }
1936}
1937
1938impl<T> AtlasTextureList<T> {
1939 #[allow(unused)]
1940 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1941 self.free_list.clear();
1942 self.textures.drain(..)
1943 }
1944
1945 #[allow(dead_code)]
1946 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1947 self.textures.iter_mut().flatten()
1948 }
1949}
1950
1951#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1952#[repr(C)]
1953#[expect(missing_docs)]
1954pub struct AtlasTile {
1955 pub texture_id: AtlasTextureId,
1957 pub tile_id: TileId,
1959 pub padding: u32,
1961 pub bounds: Bounds<DevicePixels>,
1963}
1964
1965#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1966#[repr(C)]
1967#[expect(missing_docs)]
1968pub struct AtlasTextureId {
1969 pub index: u32,
1972 pub kind: AtlasTextureKind,
1974}
1975
1976#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1977#[repr(C)]
1978#[cfg_attr(
1979 all(
1980 any(target_os = "linux", target_os = "freebsd"),
1981 not(any(feature = "x11", feature = "wayland"))
1982 ),
1983 allow(dead_code)
1984)]
1985#[expect(missing_docs)]
1986pub enum AtlasTextureKind {
1987 Monochrome = 0,
1988 Polychrome = 1,
1989 Subpixel = 2,
1990}
1991
1992#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1993#[repr(C)]
1994#[expect(missing_docs)]
1995pub struct TileId(pub u32);
1996
1997impl From<etagere::AllocId> for TileId {
1998 fn from(id: etagere::AllocId) -> Self {
1999 Self(id.serialize())
2000 }
2001}
2002
2003impl From<TileId> for etagere::AllocId {
2004 fn from(id: TileId) -> Self {
2005 Self::deserialize(id.0)
2006 }
2007}
2008
2009#[expect(missing_docs)]
2010pub struct PlatformInputHandler {
2011 cx: AsyncWindowContext,
2012 handler: Box<dyn InputHandler>,
2013}
2014
2015#[expect(missing_docs)]
2016#[cfg_attr(
2017 all(
2018 any(target_os = "linux", target_os = "freebsd"),
2019 not(any(feature = "x11", feature = "wayland"))
2020 ),
2021 allow(dead_code)
2022)]
2023impl PlatformInputHandler {
2024 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2025 Self { cx, handler }
2026 }
2027
2028 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2029 self.cx
2030 .update(|window, cx| {
2031 self.handler
2032 .selected_text_range(ignore_disabled_input, window, cx)
2033 })
2034 .ok()
2035 .flatten()
2036 }
2037
2038 #[cfg_attr(target_os = "windows", allow(dead_code))]
2039 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2040 self.cx
2041 .update(|window, cx| self.handler.marked_text_range(window, cx))
2042 .ok()
2043 .flatten()
2044 }
2045
2046 #[cfg_attr(
2047 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2048 allow(dead_code)
2049 )]
2050 pub fn text_for_range(
2051 &mut self,
2052 range_utf16: Range<usize>,
2053 adjusted: &mut Option<Range<usize>>,
2054 ) -> Option<String> {
2055 self.cx
2056 .update(|window, cx| {
2057 self.handler
2058 .text_for_range(range_utf16, adjusted, window, cx)
2059 })
2060 .ok()
2061 .flatten()
2062 }
2063
2064 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2065 self.cx
2066 .update(|window, cx| {
2067 self.handler
2068 .replace_text_in_range(replacement_range, text, window, cx);
2069 })
2070 .ok();
2071 }
2072
2073 pub fn replace_and_mark_text_in_range(
2074 &mut self,
2075 range_utf16: Option<Range<usize>>,
2076 new_text: &str,
2077 new_selected_range: Option<Range<usize>>,
2078 ) {
2079 self.cx
2080 .update(|window, cx| {
2081 self.handler.replace_and_mark_text_in_range(
2082 range_utf16,
2083 new_text,
2084 new_selected_range,
2085 window,
2086 cx,
2087 )
2088 })
2089 .ok();
2090 }
2091
2092 #[cfg_attr(target_os = "windows", allow(dead_code))]
2093 pub fn unmark_text(&mut self) {
2094 self.cx
2095 .update(|window, cx| self.handler.unmark_text(window, cx))
2096 .ok();
2097 }
2098
2099 pub fn paste(&mut self, item: ClipboardItem) {
2100 self.cx
2101 .update(|window, cx| self.handler.paste(item, window, cx))
2102 .ok();
2103 }
2104
2105 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2106 self.cx
2107 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2108 .ok()
2109 .flatten()
2110 }
2111
2112 #[allow(dead_code)]
2113 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2114 self.handler.apple_press_and_hold_enabled()
2115 }
2116
2117 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2118 self.handler.replace_text_in_range(None, input, window, cx);
2119 }
2120
2121 pub fn compute_ime_candidate_bounds(
2122 marked_range: Option<Range<usize>>,
2123 selection: &UTF16Selection,
2124 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2125 ) -> Option<Bounds<Pixels>> {
2126 if let Some(marked_range) = marked_range {
2127 let mut line_start = marked_range.start;
2129
2130 let caret = selection.range.end;
2134 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2135 for i in (marked_range.start..caret).rev() {
2136 if let Some(b) = bounds_for_range(i..i) {
2137 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2138 line_start = i + 1;
2139 break;
2140 }
2141 }
2142 }
2143 }
2144 bounds_for_range(line_start..line_start)
2145 } else {
2146 let offset = if selection.reversed {
2148 selection.range.start
2149 } else {
2150 selection.range.end
2151 };
2152 bounds_for_range(offset..offset)
2153 }
2154 }
2155
2156 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2157 let marked_range = self.handler.marked_text_range(window, cx);
2158 let selection = self.handler.selected_text_range(true, window, cx)?;
2159 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2160 self.handler.bounds_for_range(range, window, cx)
2161 })
2162 }
2163
2164 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2165 let marked_range = self.marked_text_range();
2166 let selection = self.selected_text_range(true)?;
2167 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2168 self.bounds_for_range(range)
2169 })
2170 }
2171
2172 #[allow(unused)]
2173 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2174 self.cx
2175 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2176 .ok()
2177 .flatten()
2178 }
2179
2180 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2182 self.cx
2183 .update(|window, cx| {
2184 self.handler
2185 .set_selected_text_range(range_utf16, window, cx)
2186 })
2187 .ok();
2188 }
2189
2190 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2192 self.cx
2193 .update(|window, cx| self.handler.element_bounds(window, cx))
2194 .ok()
2195 .flatten()
2196 }
2197
2198 pub fn text_length_utf16(&mut self) -> Option<usize> {
2200 self.cx
2201 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2202 .ok()
2203 .flatten()
2204 }
2205
2206 #[allow(dead_code)]
2207 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2208 self.handler.accepts_text_input(window, cx)
2209 }
2210
2211 #[allow(dead_code)]
2212 pub fn query_accepts_text_input(&mut self) -> bool {
2213 self.cx
2214 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2215 .unwrap_or(true)
2216 }
2217
2218 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2224 self.cx
2225 .update(|window, cx| {
2226 !window.has_pending_keystrokes()
2228 && self.handler.prefers_ime_for_printable_keys(window, cx)
2229 })
2230 .unwrap_or(false)
2231 }
2232
2233 pub fn text_input_configuration(
2235 &mut self,
2236 window: &mut Window,
2237 cx: &mut App,
2238 ) -> TextInputConfiguration {
2239 self.handler.text_input_configuration(window, cx)
2240 }
2241
2242 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2244 self.cx
2245 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2246 .ok()
2247 .flatten()
2248 }
2249}
2250
2251#[derive(Debug)]
2254pub struct UTF16Selection {
2255 pub range: Range<usize>,
2258 pub reversed: bool,
2261}
2262
2263pub trait InputHandler: 'static {
2268 fn selected_text_range(
2273 &mut self,
2274 ignore_disabled_input: bool,
2275 window: &mut Window,
2276 cx: &mut App,
2277 ) -> Option<UTF16Selection>;
2278
2279 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2284
2285 fn text_for_range(
2290 &mut self,
2291 range_utf16: Range<usize>,
2292 adjusted_range: &mut Option<Range<usize>>,
2293 window: &mut Window,
2294 cx: &mut App,
2295 ) -> Option<String>;
2296
2297 fn replace_text_in_range(
2302 &mut self,
2303 replacement_range: Option<Range<usize>>,
2304 text: &str,
2305 window: &mut Window,
2306 cx: &mut App,
2307 );
2308
2309 fn replace_and_mark_text_in_range(
2316 &mut self,
2317 range_utf16: Option<Range<usize>>,
2318 new_text: &str,
2319 new_selected_range: Option<Range<usize>>,
2320 window: &mut Window,
2321 cx: &mut App,
2322 );
2323
2324 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2327
2328 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2335 if let Some(text) = item.text() {
2336 self.replace_text_in_range(None, &text, window, cx);
2337 }
2338 }
2339
2340 fn bounds_for_range(
2345 &mut self,
2346 range_utf16: Range<usize>,
2347 window: &mut Window,
2348 cx: &mut App,
2349 ) -> Option<Bounds<Pixels>>;
2350
2351 fn character_index_for_point(
2355 &mut self,
2356 point: Point<Pixels>,
2357 window: &mut Window,
2358 cx: &mut App,
2359 ) -> Option<usize>;
2360
2361 fn set_selected_text_range(
2371 &mut self,
2372 _range_utf16: Range<usize>,
2373 _window: &mut Window,
2374 _cx: &mut App,
2375 ) {
2376 }
2377
2378 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2385 None
2386 }
2387
2388 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2390 None
2391 }
2392
2393 #[allow(dead_code)]
2398 fn apple_press_and_hold_enabled(&mut self) -> bool {
2399 true
2400 }
2401
2402 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2404 true
2405 }
2406
2407 fn text_input_editable_range(
2418 &mut self,
2419 _window: &mut Window,
2420 _cx: &mut App,
2421 ) -> Option<Range<usize>> {
2422 None
2423 }
2424
2425 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2434 false
2435 }
2436
2437 fn text_input_configuration(
2443 &mut self,
2444 _window: &mut Window,
2445 _cx: &mut App,
2446 ) -> TextInputConfiguration {
2447 TextInputConfiguration::default()
2448 }
2449}
2450
2451#[derive(Clone, Debug, Default, PartialEq, Eq)]
2461pub struct TextInputConfiguration {
2462 pub autocorrect: bool,
2464 pub autocapitalize: Autocapitalize,
2466 pub suggestions: bool,
2468 pub input_action: TextInputAction,
2470}
2471
2472#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2474pub enum Autocapitalize {
2475 #[default]
2477 None,
2478 Words,
2480 Sentences,
2482 Characters,
2484}
2485
2486#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2496pub enum TextInputAction {
2497 #[default]
2499 Unspecified,
2500 Enter,
2502 Done,
2504 Go,
2506 Next,
2508 Previous,
2510 Search,
2512 Send,
2514}
2515
2516#[derive(Debug)]
2518pub struct WindowOptions {
2519 pub window_bounds: Option<WindowBounds>,
2523
2524 pub titlebar: Option<TitlebarOptions>,
2526
2527 pub focus: bool,
2529
2530 pub show: bool,
2532
2533 pub kind: WindowKind,
2535
2536 pub is_movable: bool,
2540
2541 pub app_owns_titlebar_drag: bool,
2553
2554 pub inactive_frame_interval: Option<Duration>,
2558
2559 pub is_resizable: bool,
2561
2562 pub is_minimizable: bool,
2564
2565 pub display_id: Option<DisplayId>,
2568
2569 pub window_background: WindowBackgroundAppearance,
2571
2572 pub app_id: Option<String>,
2574
2575 pub window_min_size: Option<Size<Pixels>>,
2577
2578 pub window_decorations: Option<WindowDecorations>,
2581
2582 pub icon: Option<Arc<image::RgbaImage>>,
2584
2585 pub tabbing_identifier: Option<String>,
2587}
2588
2589#[derive(Debug)]
2591#[cfg_attr(
2592 all(
2593 any(target_os = "linux", target_os = "freebsd"),
2594 not(any(feature = "x11", feature = "wayland"))
2595 ),
2596 allow(dead_code)
2597)]
2598#[allow(missing_docs)]
2599pub struct WindowParams {
2600 pub bounds: Bounds<Pixels>,
2601
2602 #[cfg_attr(feature = "wayland", allow(dead_code))]
2604 pub titlebar: Option<TitlebarOptions>,
2605
2606 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2608 pub kind: WindowKind,
2609
2610 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2612 pub is_movable: bool,
2613
2614 #[cfg_attr(
2616 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2617 allow(dead_code)
2618 )]
2619 pub app_owns_titlebar_drag: bool,
2620
2621 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2623 pub is_resizable: bool,
2624
2625 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2627 pub is_minimizable: bool,
2628
2629 #[cfg_attr(
2630 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2631 allow(dead_code)
2632 )]
2633 pub focus: bool,
2634
2635 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2636 pub show: bool,
2637
2638 #[cfg_attr(feature = "wayland", allow(dead_code))]
2640 pub icon: Option<Arc<image::RgbaImage>>,
2641
2642 #[cfg_attr(feature = "wayland", allow(dead_code))]
2643 pub display_id: Option<DisplayId>,
2644
2645 #[cfg_attr(feature = "wayland", allow(dead_code))]
2646 pub app_id: Option<String>,
2647
2648 pub window_min_size: Option<Size<Pixels>>,
2649
2650 #[cfg(target_os = "macos")]
2651 pub tabbing_identifier: Option<String>,
2652}
2653
2654#[derive(Debug, Copy, Clone, PartialEq)]
2656pub enum WindowBounds {
2657 Windowed(Bounds<Pixels>),
2659 Maximized(Bounds<Pixels>),
2662 Fullscreen(Bounds<Pixels>),
2665}
2666
2667impl Default for WindowBounds {
2668 fn default() -> Self {
2669 WindowBounds::Windowed(Bounds::default())
2670 }
2671}
2672
2673impl WindowBounds {
2674 pub fn get_bounds(&self) -> Bounds<Pixels> {
2676 match self {
2677 WindowBounds::Windowed(bounds) => *bounds,
2678 WindowBounds::Maximized(bounds) => *bounds,
2679 WindowBounds::Fullscreen(bounds) => *bounds,
2680 }
2681 }
2682
2683 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2685 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2686 }
2687}
2688
2689impl Default for WindowOptions {
2690 fn default() -> Self {
2691 Self {
2692 window_bounds: None,
2693 titlebar: Some(TitlebarOptions {
2694 title: Default::default(),
2695 appears_transparent: Default::default(),
2696 traffic_light_position: Default::default(),
2697 }),
2698 focus: true,
2699 show: true,
2700 kind: WindowKind::Normal,
2701 is_movable: true,
2702 app_owns_titlebar_drag: false,
2703 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2704 is_resizable: true,
2705 is_minimizable: true,
2706 display_id: None,
2707 window_background: WindowBackgroundAppearance::default(),
2708 icon: None,
2709 app_id: None,
2710 window_min_size: None,
2711 window_decorations: None,
2712 tabbing_identifier: None,
2713 }
2714 }
2715}
2716
2717#[derive(Debug, Default)]
2719pub struct TitlebarOptions {
2720 pub title: Option<SharedString>,
2722
2723 pub appears_transparent: bool,
2726
2727 pub traffic_light_position: Option<Point<Pixels>>,
2729}
2730
2731#[derive(Clone, Debug, PartialEq, Eq)]
2733pub enum WindowKind {
2734 Normal,
2736
2737 PopUp,
2740
2741 AnchoredPopup(popup::PopupOptions),
2748
2749 Floating,
2751
2752 #[cfg(all(target_os = "linux", feature = "wayland"))]
2755 LayerShell(layer_shell::LayerShellOptions),
2756
2757 Dialog,
2760}
2761
2762#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2767pub enum WindowAppearance {
2768 #[default]
2772 Light,
2773
2774 VibrantLight,
2778
2779 Dark,
2783
2784 VibrantDark,
2788}
2789
2790#[derive(Copy, Clone, Debug, Default, PartialEq)]
2793pub enum WindowBackgroundAppearance {
2794 #[default]
2802 Opaque,
2803 Transparent,
2805 Blurred,
2809 MicaBackdrop,
2811 MicaAltBackdrop,
2813}
2814
2815#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2817pub enum TextRenderingMode {
2818 #[default]
2820 PlatformDefault,
2821 Subpixel,
2823 Grayscale,
2825}
2826
2827#[derive(Clone, Debug)]
2829pub struct PathPromptOptions {
2830 pub files: bool,
2832 pub directories: bool,
2834 pub multiple: bool,
2836 pub prompt: Option<SharedString>,
2838}
2839
2840#[derive(Copy, Clone, Debug, PartialEq)]
2842pub enum PromptLevel {
2843 Info,
2845
2846 Warning,
2848
2849 Critical,
2851}
2852
2853#[derive(Clone, Debug, PartialEq)]
2855pub enum PromptButton {
2856 Ok(SharedString),
2858 Cancel(SharedString),
2860 Other(SharedString),
2862}
2863
2864impl PromptButton {
2865 pub fn new(label: impl Into<SharedString>) -> Self {
2867 PromptButton::Other(label.into())
2868 }
2869
2870 pub fn ok(label: impl Into<SharedString>) -> Self {
2872 PromptButton::Ok(label.into())
2873 }
2874
2875 pub fn cancel(label: impl Into<SharedString>) -> Self {
2877 PromptButton::Cancel(label.into())
2878 }
2879
2880 #[allow(dead_code)]
2882 pub fn is_cancel(&self) -> bool {
2883 matches!(self, PromptButton::Cancel(_))
2884 }
2885
2886 pub fn label(&self) -> &SharedString {
2888 match self {
2889 PromptButton::Ok(label) => label,
2890 PromptButton::Cancel(label) => label,
2891 PromptButton::Other(label) => label,
2892 }
2893 }
2894}
2895
2896impl From<&str> for PromptButton {
2897 fn from(value: &str) -> Self {
2898 match value.to_lowercase().as_str() {
2899 "ok" => PromptButton::Ok("OK".into()),
2900 "cancel" => PromptButton::Cancel("Cancel".into()),
2901 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2902 }
2903 }
2904}
2905
2906#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2908pub enum CursorStyle {
2909 #[default]
2911 Arrow,
2912
2913 IBeam,
2916
2917 Crosshair,
2920
2921 ClosedHand,
2924
2925 OpenHand,
2928
2929 PointingHand,
2932
2933 ResizeLeft,
2936
2937 ResizeRight,
2940
2941 ResizeLeftRight,
2944
2945 ResizeUp,
2948
2949 ResizeDown,
2952
2953 ResizeUpDown,
2956
2957 ResizeUpLeftDownRight,
2960
2961 ResizeUpRightDownLeft,
2964
2965 ResizeColumn,
2968
2969 ResizeRow,
2972
2973 IBeamCursorForVerticalLayout,
2976
2977 OperationNotAllowed,
2980
2981 DragLink,
2984
2985 DragCopy,
2988
2989 ContextualMenu,
2992}
2993
2994#[derive(Clone, Debug, Eq, PartialEq)]
2996pub struct ClipboardItem {
2997 pub entries: Vec<ClipboardEntry>,
2999}
3000
3001#[derive(Clone, Debug, PartialEq, Eq)]
3006pub enum ClipboardReadError {
3007 Unavailable,
3011 Denied(String),
3014 UnsupportedContent,
3017}
3018
3019impl std::fmt::Display for ClipboardReadError {
3020 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3021 match self {
3022 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3023 Self::Denied(message) => {
3024 write!(formatter, "clipboard access was denied: {message}")
3025 }
3026 Self::UnsupportedContent => {
3027 formatter.write_str("the clipboard contents are unsupported")
3028 }
3029 }
3030 }
3031}
3032
3033impl std::error::Error for ClipboardReadError {}
3034
3035#[derive(Clone, Debug, Eq, PartialEq)]
3037pub enum ClipboardEntry {
3038 String(ClipboardString),
3040 Image(Image),
3042 ExternalPaths(crate::ExternalPaths),
3044}
3045
3046impl ClipboardItem {
3047 pub fn new_string(text: String) -> Self {
3049 Self {
3050 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3051 }
3052 }
3053
3054 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3056 Self {
3057 entries: vec![ClipboardEntry::String(ClipboardString {
3058 text,
3059 metadata: Some(metadata),
3060 })],
3061 }
3062 }
3063
3064 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3066 Self {
3067 entries: vec![ClipboardEntry::String(
3068 ClipboardString::new(text).with_json_metadata(metadata),
3069 )],
3070 }
3071 }
3072
3073 pub fn new_image(image: &Image) -> Self {
3075 Self {
3076 entries: vec![ClipboardEntry::Image(image.clone())],
3077 }
3078 }
3079
3080 pub fn text(&self) -> Option<String> {
3083 let mut answer = String::new();
3084
3085 for entry in self.entries.iter() {
3086 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3087 answer.push_str(text);
3088 }
3089 }
3090
3091 if answer.is_empty() {
3092 for entry in self.entries.iter() {
3093 if let ClipboardEntry::ExternalPaths(paths) = entry {
3094 for path in &paths.0 {
3095 use std::fmt::Write as _;
3096 _ = write!(answer, "{}", path.display());
3097 }
3098 }
3099 }
3100 }
3101
3102 if !answer.is_empty() {
3103 Some(answer)
3104 } else {
3105 None
3106 }
3107 }
3108
3109 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3111 pub fn metadata(&self) -> Option<&String> {
3112 match self.entries().first() {
3113 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3114 clipboard_string.metadata.as_ref()
3115 }
3116 _ => None,
3117 }
3118 }
3119
3120 pub fn entries(&self) -> &[ClipboardEntry] {
3122 &self.entries
3123 }
3124
3125 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3127 self.entries.into_iter()
3128 }
3129}
3130
3131impl From<ClipboardString> for ClipboardEntry {
3132 fn from(value: ClipboardString) -> Self {
3133 Self::String(value)
3134 }
3135}
3136
3137impl From<String> for ClipboardEntry {
3138 fn from(value: String) -> Self {
3139 Self::from(ClipboardString::from(value))
3140 }
3141}
3142
3143impl From<Image> for ClipboardEntry {
3144 fn from(value: Image) -> Self {
3145 Self::Image(value)
3146 }
3147}
3148
3149impl From<ClipboardEntry> for ClipboardItem {
3150 fn from(value: ClipboardEntry) -> Self {
3151 Self {
3152 entries: vec![value],
3153 }
3154 }
3155}
3156
3157impl From<String> for ClipboardItem {
3158 fn from(value: String) -> Self {
3159 Self::from(ClipboardEntry::from(value))
3160 }
3161}
3162
3163impl From<Image> for ClipboardItem {
3164 fn from(value: Image) -> Self {
3165 Self::from(ClipboardEntry::from(value))
3166 }
3167}
3168
3169#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3171pub enum ImageFormat {
3172 Png,
3177 Jpeg,
3179 Webp,
3181 Gif,
3183 Svg,
3185 Bmp,
3187 Tiff,
3189 Ico,
3191 Pnm,
3193}
3194
3195impl ImageFormat {
3196 pub const fn mime_type(self) -> &'static str {
3198 match self {
3199 ImageFormat::Png => "image/png",
3200 ImageFormat::Jpeg => "image/jpeg",
3201 ImageFormat::Webp => "image/webp",
3202 ImageFormat::Gif => "image/gif",
3203 ImageFormat::Svg => "image/svg+xml",
3204 ImageFormat::Bmp => "image/bmp",
3205 ImageFormat::Tiff => "image/tiff",
3206 ImageFormat::Ico => "image/ico",
3207 ImageFormat::Pnm => "image/x-portable-anymap",
3208 }
3209 }
3210
3211 pub const fn extension(self) -> &'static str {
3213 match self {
3214 ImageFormat::Png => "png",
3215 ImageFormat::Jpeg => "jpg",
3216 ImageFormat::Webp => "webp",
3217 ImageFormat::Gif => "gif",
3218 ImageFormat::Svg => "svg",
3219 ImageFormat::Bmp => "bmp",
3220 ImageFormat::Tiff => "tiff",
3221 ImageFormat::Ico => "ico",
3222 ImageFormat::Pnm => "pnm",
3223 }
3224 }
3225
3226 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3228 use strum::IntoEnumIterator;
3229 Self::iter()
3230 .find(|format| format.mime_type() == mime_type)
3231 .or_else(|| Self::from_mime_type_alias(mime_type))
3232 }
3233
3234 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3238 match mime_type {
3239 "image/jpg" => Some(Self::Jpeg),
3240 "image/tif" => Some(Self::Tiff),
3241 _ => None,
3242 }
3243 }
3244}
3245
3246#[derive(Clone, Debug, PartialEq, Eq)]
3248pub struct Image {
3249 pub format: ImageFormat,
3251 pub bytes: Vec<u8>,
3253 pub id: u64,
3255}
3256
3257pub(crate) fn decode_static_image(
3258 bytes: &[u8],
3259 format: image::ImageFormat,
3260) -> Result<SmallVec<[Frame; 1]>> {
3261 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3262 .into_decoder()
3263 .context("creating image decoder")?;
3264 decode_static_image_from_decoder(decoder)
3265}
3266
3267pub(crate) fn decode_static_image_from_decoder(
3268 mut decoder: impl image::ImageDecoder,
3269) -> Result<SmallVec<[Frame; 1]>> {
3270 let orientation = decoder
3271 .orientation()
3272 .context("reading decoder's orientation")?;
3273 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3274 image.apply_orientation(orientation);
3275
3276 let mut data = image.into_rgba8();
3277 for pixel in data.chunks_exact_mut(4) {
3278 pixel.swap(0, 2);
3279 }
3280
3281 Ok(SmallVec::from_elem(Frame::new(data), 1))
3282}
3283
3284impl Hash for Image {
3285 fn hash<H: Hasher>(&self, state: &mut H) {
3286 state.write_u64(self.id);
3287 }
3288}
3289
3290impl Image {
3291 pub fn empty() -> Self {
3293 Self::from_bytes(ImageFormat::Png, Vec::new())
3294 }
3295
3296 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3298 Self {
3299 id: hash(&bytes),
3300 format,
3301 bytes,
3302 }
3303 }
3304
3305 pub fn id(&self) -> u64 {
3307 self.id
3308 }
3309
3310 pub fn use_render_image(
3312 self: Arc<Self>,
3313 window: &mut Window,
3314 cx: &mut App,
3315 ) -> Option<Arc<RenderImage>> {
3316 ImageSource::Image(self)
3317 .use_data(None, window, cx)
3318 .and_then(|result| result.ok())
3319 }
3320
3321 pub fn get_render_image(
3323 self: Arc<Self>,
3324 window: &mut Window,
3325 cx: &mut App,
3326 ) -> Option<Arc<RenderImage>> {
3327 ImageSource::Image(self)
3328 .get_data(None, window, cx)
3329 .and_then(|result| result.ok())
3330 }
3331
3332 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3334 ImageSource::Image(self).remove_asset(cx);
3335 }
3336
3337 #[cfg(any(test, feature = "test-support"))]
3340 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3341 ImageSource::Image(self.clone()).is_asset_cached(cx)
3342 }
3343
3344 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3346 let frames = match self.format {
3347 ImageFormat::Gif => {
3348 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3349 let mut frames = SmallVec::new();
3350
3351 for frame in decoder.into_frames() {
3352 match frame {
3353 Ok(mut frame) => {
3354 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3356 pixel.swap(0, 2);
3357 }
3358 frames.push(frame);
3359 }
3360 Err(err) => {
3361 log::debug!("Skipping GIF frame due to decode error: {err}");
3362 }
3363 }
3364 }
3365
3366 if frames.is_empty() {
3367 anyhow::bail!("GIF could not be decoded: all frames failed");
3368 }
3369
3370 frames
3371 }
3372 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3373 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3374 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3375 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3376 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3377 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3378 ImageFormat::Svg => {
3379 return svg_renderer
3380 .render_single_frame(&self.bytes, 1.0)
3381 .map_err(Into::into);
3382 }
3383 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3384 };
3385
3386 Ok(Arc::new(RenderImage::new(frames)))
3387 }
3388
3389 pub fn format(&self) -> ImageFormat {
3391 self.format
3392 }
3393
3394 pub fn bytes(&self) -> &[u8] {
3396 self.bytes.as_slice()
3397 }
3398}
3399
3400#[derive(Clone, Debug, Eq, PartialEq)]
3402pub struct ClipboardString {
3403 pub text: String,
3405 pub metadata: Option<String>,
3407}
3408
3409impl ClipboardString {
3410 pub fn new(text: String) -> Self {
3412 Self {
3413 text,
3414 metadata: None,
3415 }
3416 }
3417
3418 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3421 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3422 self
3423 }
3424
3425 pub fn text(&self) -> &String {
3427 &self.text
3428 }
3429
3430 pub fn into_text(self) -> String {
3432 self.text
3433 }
3434
3435 pub fn metadata_json<T>(&self) -> Option<T>
3437 where
3438 T: for<'a> Deserialize<'a>,
3439 {
3440 self.metadata
3441 .as_ref()
3442 .and_then(|m| serde_json::from_str(m).ok())
3443 }
3444
3445 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3446 pub fn text_hash(text: &str) -> u64 {
3448 let mut hasher = SeaHasher::new();
3449 text.hash(&mut hasher);
3450 hasher.finish()
3451 }
3452}
3453
3454impl From<String> for ClipboardString {
3455 fn from(value: String) -> Self {
3456 Self {
3457 text: value,
3458 metadata: None,
3459 }
3460 }
3461}
3462
3463#[cfg(test)]
3464mod image_tests {
3465 use super::*;
3466 use std::sync::Arc;
3467
3468 #[test]
3469 fn test_image_to_image_data_applies_exif_orientation() {
3470 let image = Image::from_bytes(
3471 ImageFormat::Jpeg,
3472 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3473 );
3474
3475 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3476
3477 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3478
3479 let bytes = render_image.as_bytes(0).unwrap();
3480 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3481 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3482 }
3483
3484 #[test]
3485 fn test_svg_image_to_image_data_converts_to_bgra() {
3486 let image = Image::from_bytes(
3487 ImageFormat::Svg,
3488 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3489<rect width="1" height="1" fill="#38BDF8"/>
3490</svg>"##
3491 .to_vec(),
3492 );
3493
3494 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3495 let bytes = render_image.as_bytes(0).unwrap();
3496
3497 for pixel in bytes.chunks_exact(4) {
3498 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3499 }
3500 }
3501}
3502
3503#[cfg(test)]
3504mod atlas_tests {
3505 use super::*;
3506
3507 const TILE_SIZE: Size<DevicePixels> = Size {
3508 width: DevicePixels(1),
3509 height: DevicePixels(1),
3510 };
3511
3512 #[derive(Default)]
3513 struct RecordingAtlasBackend {
3514 insert_calls: u32,
3515 fail_next_insert: bool,
3516 removed_tiles: Vec<AtlasTile>,
3517 }
3518
3519 impl AtlasBackend for RecordingAtlasBackend {
3520 fn insert(
3521 &mut self,
3522 kind: AtlasTextureKind,
3523 size: Size<DevicePixels>,
3524 _bytes: &[u8],
3525 ) -> Result<AtlasTile> {
3526 self.insert_calls += 1;
3527 if std::mem::take(&mut self.fail_next_insert) {
3528 anyhow::bail!("backend failed");
3529 }
3530 Ok(AtlasTile {
3531 texture_id: AtlasTextureId { index: 0, kind },
3532 tile_id: TileId(self.insert_calls),
3533 padding: 0,
3534 bounds: Bounds {
3535 origin: Point::default(),
3536 size,
3537 },
3538 })
3539 }
3540
3541 fn remove(&mut self, tile: AtlasTile) {
3542 self.removed_tiles.push(tile);
3543 }
3544 }
3545
3546 fn image_key(image_id: usize) -> AtlasKey {
3547 AtlasKey::Image(RenderImageParams {
3548 image_id: crate::ImageId(image_id),
3549 frame_index: 0,
3550 })
3551 }
3552
3553 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3554 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3555 }
3556
3557 #[test]
3558 fn only_successful_inserts_are_cached() -> Result<()> {
3559 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3560 let key = image_key(1);
3561
3562 assert_eq!(
3563 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3564 None
3565 );
3566 state
3567 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3568 .expect_err("builder error should propagate");
3569 assert!(!state.contains(&key));
3570 assert_eq!(state.backend.insert_calls, 0);
3571
3572 state.backend.fail_next_insert = true;
3573 state
3574 .get_or_insert_with(key.clone(), &mut build_tile)
3575 .expect_err("backend error should propagate");
3576 assert!(!state.contains(&key));
3577 assert_eq!(state.backend.insert_calls, 1);
3578
3579 let tile = state
3580 .get_or_insert_with(key.clone(), &mut build_tile)?
3581 .context("builder should produce a tile")?;
3582 assert_eq!(tile.texture_id.kind, key.texture_kind());
3583 assert_eq!(
3584 state.get_or_insert_with(key.clone(), &mut || {
3585 anyhow::bail!("cache hit must not call the builder")
3586 })?,
3587 Some(tile)
3588 );
3589 assert!(state.contains(&key));
3590 assert_eq!(state.backend.insert_calls, 2);
3591 Ok(())
3592 }
3593
3594 #[test]
3595 fn remove_and_clear_invalidate_keys() -> Result<()> {
3596 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3597 let key = image_key(1);
3598 let other_key = image_key(2);
3599 let tile = state
3600 .get_or_insert_with(key.clone(), &mut build_tile)?
3601 .context("builder should produce a tile")?;
3602 state
3603 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3604 .context("builder should produce another tile")?;
3605
3606 state.remove(&key);
3607 state.remove(&key);
3608 assert!(!state.contains(&key));
3609 assert!(state.contains(&other_key));
3610 assert_eq!(state.backend.removed_tiles, vec![tile]);
3611
3612 let mut reset_calls = 0;
3613 state.clear(|_| reset_calls += 1);
3614 assert_eq!(reset_calls, 1);
3615 assert!(!state.contains(&other_key));
3616 assert_eq!(state.backend.removed_tiles, vec![tile]);
3617 Ok(())
3618 }
3619}
3620
3621#[cfg(test)]
3622mod frame_signal_tests {
3623 use super::*;
3624
3625 #[cfg(feature = "profiler")]
3626 #[test]
3627 fn coalesced_signals_retain_the_earliest_until_drained() {
3628 for (first_source, later_source) in [
3629 (
3630 FrameRequestSource::NativeCallback,
3631 FrameRequestSource::LocalSchedule,
3632 ),
3633 (
3634 FrameRequestSource::LocalSchedule,
3635 FrameRequestSource::NativeCallback,
3636 ),
3637 ] {
3638 let signal = PlatformFrameSignal::new();
3639 let first =
3640 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3641 assert_eq!(signal.take(), None);
3642 signal.record(first + Duration::from_millis(16), later_source);
3643 signal.record(first, first_source);
3644 signal.record(first + Duration::from_millis(32), later_source);
3645 assert_eq!(signal.take(), Some((first, first_source)));
3646 assert_eq!(signal.take(), None);
3647 let next = first + Duration::from_millis(48);
3648 signal.record(next, later_source);
3649 assert_eq!(signal.take(), Some((next, later_source)));
3650 }
3651 }
3652
3653 #[cfg(not(feature = "profiler"))]
3654 #[test]
3655 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3656 let captured = PlatformFrameSignal::capture(|| {
3657 panic!("profiler-disabled builds must not evaluate the capture closure");
3658 });
3659 assert_eq!(captured, None);
3660 let signal = PlatformFrameSignal::new();
3661 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3662 assert_eq!(signal.take(), None);
3663 }
3664}
3665
3666#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3667mod tests {
3668 use super::*;
3669 use std::collections::HashSet;
3670
3671 #[test]
3672 fn test_refresh_interval_from_hz() {
3673 assert_eq!(
3674 refresh_interval_from_hz(50.0),
3675 Some(Duration::from_millis(20))
3676 );
3677 assert_eq!(
3678 refresh_interval_from_hz(120.0),
3679 Some(Duration::from_secs(1) / 120)
3680 );
3681 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3682 assert_eq!(refresh_interval_from_hz(unknown), None);
3683 }
3684 }
3685
3686 #[test]
3687 fn test_window_button_layout_parse_standard() {
3688 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3689 assert_eq!(
3690 layout.left,
3691 [
3692 Some(WindowButton::Close),
3693 Some(WindowButton::Minimize),
3694 None
3695 ]
3696 );
3697 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3698 }
3699
3700 #[test]
3701 fn test_window_button_layout_parse_right_only() {
3702 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3703 assert_eq!(layout.left, [None, None, None]);
3704 assert_eq!(
3705 layout.right,
3706 [
3707 Some(WindowButton::Minimize),
3708 Some(WindowButton::Maximize),
3709 Some(WindowButton::Close)
3710 ]
3711 );
3712 }
3713
3714 #[test]
3715 fn test_window_button_layout_parse_left_only() {
3716 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3717 assert_eq!(
3718 layout.left,
3719 [
3720 Some(WindowButton::Close),
3721 Some(WindowButton::Minimize),
3722 Some(WindowButton::Maximize)
3723 ]
3724 );
3725 assert_eq!(layout.right, [None, None, None]);
3726 }
3727
3728 #[test]
3729 fn test_window_button_layout_parse_with_whitespace() {
3730 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3731 assert_eq!(
3732 layout.left,
3733 [
3734 Some(WindowButton::Close),
3735 Some(WindowButton::Minimize),
3736 None
3737 ]
3738 );
3739 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3740 }
3741
3742 #[test]
3743 fn test_window_button_layout_parse_empty() {
3744 let layout = WindowButtonLayout::parse("").unwrap();
3745 assert_eq!(layout.left, [None, None, None]);
3746 assert_eq!(layout.right, [None, None, None]);
3747 }
3748
3749 #[test]
3750 fn test_window_button_layout_parse_intentionally_empty() {
3751 let layout = WindowButtonLayout::parse(":").unwrap();
3752 assert_eq!(layout.left, [None, None, None]);
3753 assert_eq!(layout.right, [None, None, None]);
3754 }
3755
3756 #[test]
3757 fn test_window_button_layout_parse_invalid_buttons() {
3758 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3759 assert_eq!(
3760 layout.left,
3761 [
3762 Some(WindowButton::Close),
3763 Some(WindowButton::Minimize),
3764 None
3765 ]
3766 );
3767 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3768 }
3769
3770 #[test]
3771 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3772 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3773 assert_eq!(
3774 layout.right,
3775 [
3776 Some(WindowButton::Close),
3777 Some(WindowButton::Minimize),
3778 None
3779 ]
3780 );
3781 assert_eq!(layout.format(), ":close,minimize");
3782 }
3783
3784 #[test]
3785 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3786 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3787 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3788 assert_eq!(
3789 layout.right,
3790 [
3791 Some(WindowButton::Maximize),
3792 Some(WindowButton::Minimize),
3793 None
3794 ]
3795 );
3796
3797 let button_ids: Vec<_> = layout
3798 .left
3799 .iter()
3800 .chain(layout.right.iter())
3801 .flatten()
3802 .map(WindowButton::id)
3803 .collect();
3804 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3805 assert_eq!(unique_button_ids.len(), button_ids.len());
3806 assert_eq!(layout.format(), "close:maximize,minimize");
3807 }
3808
3809 #[test]
3810 fn test_window_button_layout_parse_gnome_style() {
3811 let layout = WindowButtonLayout::parse("close").unwrap();
3812 assert_eq!(layout.left, [None, None, None]);
3813 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3814 }
3815
3816 #[test]
3817 fn test_window_button_layout_parse_elementary_style() {
3818 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3819 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3820 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3821 }
3822
3823 #[test]
3824 fn test_window_button_layout_round_trip() {
3825 let cases = [
3826 "close:minimize,maximize",
3827 "minimize,maximize,close:",
3828 ":close",
3829 "close:",
3830 "close:maximize",
3831 ":",
3832 ];
3833
3834 for case in cases {
3835 let layout = WindowButtonLayout::parse(case).unwrap();
3836 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3837 }
3838 }
3839
3840 #[test]
3841 fn test_window_button_layout_linux_default() {
3842 let layout = WindowButtonLayout::linux_default();
3843 assert_eq!(layout.left, [None, None, None]);
3844 assert_eq!(
3845 layout.right,
3846 [
3847 Some(WindowButton::Minimize),
3848 Some(WindowButton::Maximize),
3849 Some(WindowButton::Close)
3850 ]
3851 );
3852
3853 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3854 assert_eq!(round_tripped, layout);
3855 }
3856
3857 #[test]
3858 fn test_window_button_layout_parse_all_invalid() {
3859 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3860 }
3861}