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 fn schedule_frame(&self) {}
1293 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1294 fn is_subpixel_rendering_supported(&self) -> bool;
1295
1296 fn get_title(&self) -> String {
1298 String::new()
1299 }
1300 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1301 None
1302 }
1303 fn tab_bar_visible(&self) -> bool {
1304 false
1305 }
1306 fn set_edited(&mut self, _edited: bool) {}
1307 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1308 fn toggle_simple_fullscreen(&self) {}
1309 fn is_simple_fullscreen(&self) -> bool {
1310 false
1311 }
1312 #[cfg(target_os = "macos")]
1313 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1314 fn show_character_palette(&self) {}
1315 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1316 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1317 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1318 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1319 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1320 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1321 fn merge_all_windows(&self) {}
1322 fn move_tab_to_new_window(&self) {}
1323 fn toggle_window_tab_overview(&self) {}
1324 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1325
1326 fn native_window_state(&self) -> Option<Vec<u8>> {
1327 None
1328 }
1329 fn restore_native_window_state(&self, _state: &[u8]) {}
1330
1331 #[cfg(target_os = "windows")]
1332 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1333
1334 fn inner_window_bounds(&self) -> WindowBounds {
1336 self.window_bounds()
1337 }
1338 fn request_decorations(&self, _decorations: WindowDecorations) {}
1339 fn show_window_menu(&self, _position: Point<Pixels>) {}
1340 fn start_window_move(&self) {}
1341 fn can_start_external_drag(&self) -> bool {
1342 false
1343 }
1344 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1345 false
1346 }
1347 fn start_window_resize(&self, _edge: ResizeEdge) {}
1348 fn set_exclusive_zone(&self, _zone: Pixels) {}
1349 #[cfg(all(target_os = "linux", feature = "wayland"))]
1350 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1351 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1352 fn window_decorations(&self) -> Decorations {
1353 Decorations::Server
1354 }
1355 fn set_app_id(&mut self, _app_id: &str) {}
1356 fn map_window(&mut self) -> anyhow::Result<()> {
1357 Ok(())
1358 }
1359 fn window_controls(&self) -> WindowControls {
1360 WindowControls::default()
1361 }
1362 fn set_client_inset(&self, _inset: Pixels) {}
1363 fn gpu_specs(&self) -> Option<GpuSpecs>;
1364
1365 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1366
1367 fn insets(&self) -> WindowInsets {
1372 WindowInsets::default()
1373 }
1374
1375 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1381
1382 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1385
1386 fn set_back_enabled(&self, _enabled: bool) {}
1389
1390 fn show_soft_keyboard(&self) {}
1392
1393 fn hide_soft_keyboard(&self) {}
1395
1396 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1398
1399 fn play_system_bell(&self) {}
1400
1401 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1403
1404 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1406
1407 fn a11y_update_window_bounds(&self) {}
1409
1410 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1411 fn as_test(&mut self) -> Option<&mut TestWindow> {
1412 None
1413 }
1414
1415 #[cfg(any(test, feature = "test-support"))]
1419 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1420 anyhow::bail!("render_to_image not implemented for this platform")
1421 }
1422}
1423
1424#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1426pub trait PlatformHeadlessRenderer {
1427 fn render_scene_to_image(
1429 &mut self,
1430 scene: &Scene,
1431 size: Size<DevicePixels>,
1432 ) -> Result<RgbaImage>;
1433
1434 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1440
1441 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1443}
1444
1445#[doc(hidden)]
1448pub type RunnableVariant = Runnable<RunnableMeta>;
1449
1450#[doc(hidden)]
1451pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1452
1453#[doc(hidden)]
1454pub enum TasksIncluded {
1455 OnlyCompleted,
1456 CompletedAndRunning,
1457}
1458
1459#[doc(hidden)]
1462pub trait PlatformDispatcher: Send + Sync {
1463 fn is_main_thread(&self) -> bool;
1464 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1465 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1466 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1467
1468 fn dispatch_on_main_thread_when_idle(
1469 &self,
1470 runnable: RunnableVariant,
1471 timeout: Option<Duration>,
1472 ) {
1473 let _ = timeout;
1474 self.dispatch_on_main_thread(runnable, Priority::Low);
1475 }
1476
1477 fn idle_time_remaining(&self) -> Option<Duration> {
1478 None
1479 }
1480
1481 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1482
1483 fn now(&self) -> Instant {
1484 Instant::now()
1485 }
1486
1487 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1488 gpui_util::defer(Box::new(|| {}))
1489 }
1490
1491 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1492 ActivityGuard::noop()
1493 }
1494
1495 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1496 fn as_test(&self) -> Option<&TestDispatcher> {
1497 None
1498 }
1499
1500 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1503 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1504 None
1505 }
1506}
1507
1508#[expect(missing_docs)]
1509pub trait PlatformTextSystem: Send + Sync {
1510 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1511 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1513 fn all_font_names(&self) -> Vec<String>;
1515 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1517 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1519 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1521 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1523 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1525 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1527 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1529 fn rasterize_glyph(
1531 &self,
1532 params: &RenderGlyphParams,
1533 raster_bounds: Bounds<DevicePixels>,
1534 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1535 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1537 fn shape_paragraph(
1539 &self,
1540 _request: crate::ParagraphRequest<'_>,
1541 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1542 anyhow::bail!("paragraph layout is unsupported by this text backend")
1543 }
1544 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1546 -> TextRenderingMode;
1547 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1549 0
1550 }
1551}
1552
1553#[expect(missing_docs)]
1554pub struct NoopTextSystem;
1555
1556#[expect(missing_docs)]
1557impl NoopTextSystem {
1558 #[allow(dead_code)]
1559 pub fn new() -> Self {
1560 Self
1561 }
1562}
1563
1564impl PlatformTextSystem for NoopTextSystem {
1565 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1566 Ok(())
1567 }
1568
1569 fn all_font_names(&self) -> Vec<String> {
1570 Vec::new()
1571 }
1572
1573 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1574 Ok(FontId(1))
1575 }
1576
1577 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1578 FontMetrics {
1579 units_per_em: 1000,
1580 ascent: 1025.0,
1581 descent: -275.0,
1582 line_gap: 0.0,
1583 underline_position: -95.0,
1584 underline_thickness: 60.0,
1585 cap_height: 698.0,
1586 x_height: 516.0,
1587 bounding_box: Bounds {
1588 origin: Point {
1589 x: -260.0,
1590 y: -245.0,
1591 },
1592 size: Size {
1593 width: 1501.0,
1594 height: 1364.0,
1595 },
1596 },
1597 }
1598 }
1599
1600 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1601 Ok(Bounds {
1602 origin: Point { x: 54.0, y: 0.0 },
1603 size: size(392.0, 528.0),
1604 })
1605 }
1606
1607 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1608 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1609 }
1610
1611 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1612 Some(GlyphId(ch.len_utf16() as u32))
1613 }
1614
1615 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1616 Ok(Default::default())
1617 }
1618
1619 fn rasterize_glyph(
1620 &self,
1621 _params: &RenderGlyphParams,
1622 raster_bounds: Bounds<DevicePixels>,
1623 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1624 Ok((raster_bounds.size, Vec::new()))
1625 }
1626
1627 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1628 let mut position = px(0.);
1629 let metrics = self.font_metrics(FontId(0));
1630 let em_width = font_size
1631 * self
1632 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1633 .unwrap()
1634 .width
1635 / metrics.units_per_em as f32;
1636 let mut glyphs = Vec::new();
1637 for (ix, c) in text.char_indices() {
1638 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1639 glyphs.push(ShapedGlyph {
1640 id: glyph,
1641 position: point(position, px(0.)),
1642 index: ix,
1643 is_emoji: glyph.0 == 2,
1644 });
1645 if glyph.0 == 2 {
1646 position += em_width * 2.0;
1647 } else {
1648 position += em_width;
1649 }
1650 } else {
1651 position += em_width
1652 }
1653 }
1654 let mut runs = Vec::default();
1655 if !glyphs.is_empty() {
1656 runs.push(ShapedRun {
1657 font_id: FontId(0),
1658 glyphs,
1659 });
1660 } else {
1661 position = px(0.);
1662 }
1663
1664 LineLayout {
1665 font_size,
1666 width: position,
1667 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1668 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1669 runs,
1670 len: text.len(),
1671 }
1672 }
1673
1674 fn recommended_rendering_mode(
1675 &self,
1676 _font_id: FontId,
1677 _font_size: Pixels,
1678 ) -> TextRenderingMode {
1679 TextRenderingMode::Grayscale
1680 }
1681}
1682
1683#[allow(dead_code)]
1688pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1689 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1690 [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], ];
1704
1705 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1706 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1707
1708 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1709 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1710
1711 [
1712 ratios[0] * NORM13,
1713 ratios[1] * NORM24,
1714 ratios[2] * NORM13,
1715 ratios[3] * NORM24,
1716 ]
1717}
1718
1719#[derive(PartialEq, Eq, Hash, Clone)]
1720#[expect(missing_docs)]
1721pub enum AtlasKey {
1722 Glyph(RenderGlyphParams),
1723 Svg(RenderSvgParams),
1724 Image(RenderImageParams),
1725}
1726
1727impl AtlasKey {
1728 pub fn texture_kind(&self) -> AtlasTextureKind {
1730 match self {
1731 AtlasKey::Glyph(params) => {
1732 if params.is_emoji {
1733 AtlasTextureKind::Polychrome
1734 } else if params.subpixel_rendering {
1735 AtlasTextureKind::Subpixel
1736 } else {
1737 AtlasTextureKind::Monochrome
1738 }
1739 }
1740 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1741 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1742 }
1743 }
1744}
1745
1746impl From<RenderGlyphParams> for AtlasKey {
1747 fn from(params: RenderGlyphParams) -> Self {
1748 Self::Glyph(params)
1749 }
1750}
1751
1752impl From<RenderSvgParams> for AtlasKey {
1753 fn from(params: RenderSvgParams) -> Self {
1754 Self::Svg(params)
1755 }
1756}
1757
1758impl From<RenderImageParams> for AtlasKey {
1759 fn from(params: RenderImageParams) -> Self {
1760 Self::Image(params)
1761 }
1762}
1763
1764#[expect(missing_docs)]
1765pub trait PlatformAtlas {
1766 fn get_or_insert_with<'a>(
1768 &self,
1769 key: AtlasKey,
1770 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1771 ) -> Result<Option<AtlasTile>>;
1772 fn remove(&self, key: &AtlasKey);
1773
1774 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1775 fn contains(&self, _key: &AtlasKey) -> bool {
1776 false
1777 }
1778}
1779
1780#[doc(hidden)]
1781pub trait AtlasBackend {
1782 fn insert(
1783 &mut self,
1784 kind: AtlasTextureKind,
1785 size: Size<DevicePixels>,
1786 bytes: &[u8],
1787 ) -> Result<AtlasTile>;
1788
1789 fn remove(&mut self, tile: AtlasTile);
1790}
1791
1792#[doc(hidden)]
1793pub struct AtlasState<Backend> {
1794 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1795 pub backend: Backend,
1796}
1797
1798impl<Backend> AtlasState<Backend> {
1799 pub fn new(backend: Backend) -> Self {
1800 Self {
1801 tiles_by_key: FxHashMap::default(),
1802 backend,
1803 }
1804 }
1805
1806 pub fn contains(&self, key: &AtlasKey) -> bool {
1807 self.tiles_by_key.contains_key(key)
1808 }
1809
1810 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1811 self.tiles_by_key.clear();
1812 reset_backend(&mut self.backend);
1813 }
1814}
1815
1816impl<Backend: Default> Default for AtlasState<Backend> {
1817 fn default() -> Self {
1818 Self::new(Backend::default())
1819 }
1820}
1821
1822impl<Backend: AtlasBackend> AtlasState<Backend> {
1823 pub fn get_or_insert_with<'a>(
1824 &mut self,
1825 key: AtlasKey,
1826 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1827 ) -> Result<Option<AtlasTile>> {
1828 match self.tiles_by_key.entry(key) {
1829 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1830 Entry::Vacant(entry) => {
1831 profiling::scope!("new tile");
1832 let Some((size, bytes)) = build()? else {
1833 return Ok(None);
1834 };
1835 let tile = self
1836 .backend
1837 .insert(entry.key().texture_kind(), size, &bytes)?;
1838 entry.insert(tile);
1839 Ok(Some(tile))
1840 }
1841 }
1842 }
1843
1844 pub fn remove(&mut self, key: &AtlasKey) {
1845 if let Some(tile) = self.tiles_by_key.remove(key) {
1846 self.backend.remove(tile);
1847 }
1848 }
1849}
1850
1851#[derive(Default)]
1855pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1856
1857#[doc(hidden)]
1858#[derive(Default)]
1859pub struct HeadlessAtlasBackend {
1860 next_id: u32,
1861}
1862
1863impl AtlasBackend for HeadlessAtlasBackend {
1864 fn insert(
1865 &mut self,
1866 kind: AtlasTextureKind,
1867 size: Size<DevicePixels>,
1868 _bytes: &[u8],
1869 ) -> Result<AtlasTile> {
1870 self.next_id += 1;
1871 let texture_id = self.next_id;
1872 self.next_id += 1;
1873 let tile_id = self.next_id;
1874 Ok(AtlasTile {
1875 texture_id: AtlasTextureId {
1876 index: texture_id,
1877 kind,
1878 },
1879 tile_id: TileId(tile_id),
1880 padding: 0,
1881 bounds: Bounds {
1882 origin: Point::default(),
1883 size,
1884 },
1885 })
1886 }
1887
1888 fn remove(&mut self, _tile: AtlasTile) {}
1889}
1890
1891impl PlatformAtlas for HeadlessAtlas {
1892 fn get_or_insert_with<'a>(
1893 &self,
1894 key: AtlasKey,
1895 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1896 ) -> Result<Option<AtlasTile>> {
1897 self.0.lock().get_or_insert_with(key, build)
1898 }
1899
1900 fn remove(&self, key: &AtlasKey) {
1901 self.0.lock().remove(key);
1902 }
1903
1904 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1905 fn contains(&self, key: &AtlasKey) -> bool {
1906 self.0.lock().contains(key)
1907 }
1908}
1909
1910#[doc(hidden)]
1911pub struct AtlasTextureList<T> {
1912 pub textures: Vec<Option<T>>,
1913 pub free_list: Vec<usize>,
1914}
1915
1916impl<T> Default for AtlasTextureList<T> {
1917 fn default() -> Self {
1918 Self {
1919 textures: Vec::default(),
1920 free_list: Vec::default(),
1921 }
1922 }
1923}
1924
1925impl<T> ops::Index<usize> for AtlasTextureList<T> {
1926 type Output = Option<T>;
1927
1928 fn index(&self, index: usize) -> &Self::Output {
1929 &self.textures[index]
1930 }
1931}
1932
1933impl<T> AtlasTextureList<T> {
1934 #[allow(unused)]
1935 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1936 self.free_list.clear();
1937 self.textures.drain(..)
1938 }
1939
1940 #[allow(dead_code)]
1941 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1942 self.textures.iter_mut().flatten()
1943 }
1944}
1945
1946#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1947#[repr(C)]
1948#[expect(missing_docs)]
1949pub struct AtlasTile {
1950 pub texture_id: AtlasTextureId,
1952 pub tile_id: TileId,
1954 pub padding: u32,
1956 pub bounds: Bounds<DevicePixels>,
1958}
1959
1960#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1961#[repr(C)]
1962#[expect(missing_docs)]
1963pub struct AtlasTextureId {
1964 pub index: u32,
1967 pub kind: AtlasTextureKind,
1969}
1970
1971#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1972#[repr(C)]
1973#[cfg_attr(
1974 all(
1975 any(target_os = "linux", target_os = "freebsd"),
1976 not(any(feature = "x11", feature = "wayland"))
1977 ),
1978 allow(dead_code)
1979)]
1980#[expect(missing_docs)]
1981pub enum AtlasTextureKind {
1982 Monochrome = 0,
1983 Polychrome = 1,
1984 Subpixel = 2,
1985}
1986
1987#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1988#[repr(C)]
1989#[expect(missing_docs)]
1990pub struct TileId(pub u32);
1991
1992impl From<etagere::AllocId> for TileId {
1993 fn from(id: etagere::AllocId) -> Self {
1994 Self(id.serialize())
1995 }
1996}
1997
1998impl From<TileId> for etagere::AllocId {
1999 fn from(id: TileId) -> Self {
2000 Self::deserialize(id.0)
2001 }
2002}
2003
2004#[expect(missing_docs)]
2005pub struct PlatformInputHandler {
2006 cx: AsyncWindowContext,
2007 handler: Box<dyn InputHandler>,
2008}
2009
2010#[expect(missing_docs)]
2011#[cfg_attr(
2012 all(
2013 any(target_os = "linux", target_os = "freebsd"),
2014 not(any(feature = "x11", feature = "wayland"))
2015 ),
2016 allow(dead_code)
2017)]
2018impl PlatformInputHandler {
2019 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2020 Self { cx, handler }
2021 }
2022
2023 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2024 self.cx
2025 .update(|window, cx| {
2026 self.handler
2027 .selected_text_range(ignore_disabled_input, window, cx)
2028 })
2029 .ok()
2030 .flatten()
2031 }
2032
2033 #[cfg_attr(target_os = "windows", allow(dead_code))]
2034 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2035 self.cx
2036 .update(|window, cx| self.handler.marked_text_range(window, cx))
2037 .ok()
2038 .flatten()
2039 }
2040
2041 #[cfg_attr(
2042 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2043 allow(dead_code)
2044 )]
2045 pub fn text_for_range(
2046 &mut self,
2047 range_utf16: Range<usize>,
2048 adjusted: &mut Option<Range<usize>>,
2049 ) -> Option<String> {
2050 self.cx
2051 .update(|window, cx| {
2052 self.handler
2053 .text_for_range(range_utf16, adjusted, window, cx)
2054 })
2055 .ok()
2056 .flatten()
2057 }
2058
2059 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2060 self.cx
2061 .update(|window, cx| {
2062 self.handler
2063 .replace_text_in_range(replacement_range, text, window, cx);
2064 })
2065 .ok();
2066 }
2067
2068 pub fn replace_and_mark_text_in_range(
2069 &mut self,
2070 range_utf16: Option<Range<usize>>,
2071 new_text: &str,
2072 new_selected_range: Option<Range<usize>>,
2073 ) {
2074 self.cx
2075 .update(|window, cx| {
2076 self.handler.replace_and_mark_text_in_range(
2077 range_utf16,
2078 new_text,
2079 new_selected_range,
2080 window,
2081 cx,
2082 )
2083 })
2084 .ok();
2085 }
2086
2087 #[cfg_attr(target_os = "windows", allow(dead_code))]
2088 pub fn unmark_text(&mut self) {
2089 self.cx
2090 .update(|window, cx| self.handler.unmark_text(window, cx))
2091 .ok();
2092 }
2093
2094 pub fn paste(&mut self, item: ClipboardItem) {
2095 self.cx
2096 .update(|window, cx| self.handler.paste(item, window, cx))
2097 .ok();
2098 }
2099
2100 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2101 self.cx
2102 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2103 .ok()
2104 .flatten()
2105 }
2106
2107 #[allow(dead_code)]
2108 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2109 self.handler.apple_press_and_hold_enabled()
2110 }
2111
2112 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2113 self.handler.replace_text_in_range(None, input, window, cx);
2114 }
2115
2116 pub fn compute_ime_candidate_bounds(
2117 marked_range: Option<Range<usize>>,
2118 selection: &UTF16Selection,
2119 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2120 ) -> Option<Bounds<Pixels>> {
2121 if let Some(marked_range) = marked_range {
2122 let mut line_start = marked_range.start;
2124
2125 let caret = selection.range.end;
2129 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2130 for i in (marked_range.start..caret).rev() {
2131 if let Some(b) = bounds_for_range(i..i) {
2132 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2133 line_start = i + 1;
2134 break;
2135 }
2136 }
2137 }
2138 }
2139 bounds_for_range(line_start..line_start)
2140 } else {
2141 let offset = if selection.reversed {
2143 selection.range.start
2144 } else {
2145 selection.range.end
2146 };
2147 bounds_for_range(offset..offset)
2148 }
2149 }
2150
2151 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2152 let marked_range = self.handler.marked_text_range(window, cx);
2153 let selection = self.handler.selected_text_range(true, window, cx)?;
2154 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2155 self.handler.bounds_for_range(range, window, cx)
2156 })
2157 }
2158
2159 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2160 let marked_range = self.marked_text_range();
2161 let selection = self.selected_text_range(true)?;
2162 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2163 self.bounds_for_range(range)
2164 })
2165 }
2166
2167 #[allow(unused)]
2168 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2169 self.cx
2170 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2171 .ok()
2172 .flatten()
2173 }
2174
2175 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2177 self.cx
2178 .update(|window, cx| {
2179 self.handler
2180 .set_selected_text_range(range_utf16, window, cx)
2181 })
2182 .ok();
2183 }
2184
2185 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2187 self.cx
2188 .update(|window, cx| self.handler.element_bounds(window, cx))
2189 .ok()
2190 .flatten()
2191 }
2192
2193 pub fn text_length_utf16(&mut self) -> Option<usize> {
2195 self.cx
2196 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2197 .ok()
2198 .flatten()
2199 }
2200
2201 #[allow(dead_code)]
2202 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2203 self.handler.accepts_text_input(window, cx)
2204 }
2205
2206 #[allow(dead_code)]
2207 pub fn query_accepts_text_input(&mut self) -> bool {
2208 self.cx
2209 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2210 .unwrap_or(true)
2211 }
2212
2213 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2219 self.cx
2220 .update(|window, cx| {
2221 !window.has_pending_keystrokes()
2223 && self.handler.prefers_ime_for_printable_keys(window, cx)
2224 })
2225 .unwrap_or(false)
2226 }
2227
2228 pub fn text_input_configuration(
2230 &mut self,
2231 window: &mut Window,
2232 cx: &mut App,
2233 ) -> TextInputConfiguration {
2234 self.handler.text_input_configuration(window, cx)
2235 }
2236
2237 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2239 self.cx
2240 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2241 .ok()
2242 .flatten()
2243 }
2244}
2245
2246#[derive(Debug)]
2249pub struct UTF16Selection {
2250 pub range: Range<usize>,
2253 pub reversed: bool,
2256}
2257
2258pub trait InputHandler: 'static {
2263 fn selected_text_range(
2268 &mut self,
2269 ignore_disabled_input: bool,
2270 window: &mut Window,
2271 cx: &mut App,
2272 ) -> Option<UTF16Selection>;
2273
2274 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2279
2280 fn text_for_range(
2285 &mut self,
2286 range_utf16: Range<usize>,
2287 adjusted_range: &mut Option<Range<usize>>,
2288 window: &mut Window,
2289 cx: &mut App,
2290 ) -> Option<String>;
2291
2292 fn replace_text_in_range(
2297 &mut self,
2298 replacement_range: Option<Range<usize>>,
2299 text: &str,
2300 window: &mut Window,
2301 cx: &mut App,
2302 );
2303
2304 fn replace_and_mark_text_in_range(
2311 &mut self,
2312 range_utf16: Option<Range<usize>>,
2313 new_text: &str,
2314 new_selected_range: Option<Range<usize>>,
2315 window: &mut Window,
2316 cx: &mut App,
2317 );
2318
2319 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2322
2323 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2330 if let Some(text) = item.text() {
2331 self.replace_text_in_range(None, &text, window, cx);
2332 }
2333 }
2334
2335 fn bounds_for_range(
2340 &mut self,
2341 range_utf16: Range<usize>,
2342 window: &mut Window,
2343 cx: &mut App,
2344 ) -> Option<Bounds<Pixels>>;
2345
2346 fn character_index_for_point(
2350 &mut self,
2351 point: Point<Pixels>,
2352 window: &mut Window,
2353 cx: &mut App,
2354 ) -> Option<usize>;
2355
2356 fn set_selected_text_range(
2366 &mut self,
2367 _range_utf16: Range<usize>,
2368 _window: &mut Window,
2369 _cx: &mut App,
2370 ) {
2371 }
2372
2373 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2380 None
2381 }
2382
2383 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2385 None
2386 }
2387
2388 #[allow(dead_code)]
2393 fn apple_press_and_hold_enabled(&mut self) -> bool {
2394 true
2395 }
2396
2397 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2399 true
2400 }
2401
2402 fn text_input_editable_range(
2413 &mut self,
2414 _window: &mut Window,
2415 _cx: &mut App,
2416 ) -> Option<Range<usize>> {
2417 None
2418 }
2419
2420 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2429 false
2430 }
2431
2432 fn text_input_configuration(
2438 &mut self,
2439 _window: &mut Window,
2440 _cx: &mut App,
2441 ) -> TextInputConfiguration {
2442 TextInputConfiguration::default()
2443 }
2444}
2445
2446#[derive(Clone, Debug, Default, PartialEq, Eq)]
2456pub struct TextInputConfiguration {
2457 pub autocorrect: bool,
2459 pub autocapitalize: Autocapitalize,
2461 pub suggestions: bool,
2463 pub input_action: TextInputAction,
2465}
2466
2467#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2469pub enum Autocapitalize {
2470 #[default]
2472 None,
2473 Words,
2475 Sentences,
2477 Characters,
2479}
2480
2481#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2491pub enum TextInputAction {
2492 #[default]
2494 Unspecified,
2495 Enter,
2497 Done,
2499 Go,
2501 Next,
2503 Previous,
2505 Search,
2507 Send,
2509}
2510
2511#[derive(Debug)]
2513pub struct WindowOptions {
2514 pub window_bounds: Option<WindowBounds>,
2518
2519 pub titlebar: Option<TitlebarOptions>,
2521
2522 pub focus: bool,
2524
2525 pub show: bool,
2527
2528 pub kind: WindowKind,
2530
2531 pub is_movable: bool,
2535
2536 pub app_owns_titlebar_drag: bool,
2548
2549 pub inactive_frame_interval: Option<Duration>,
2553
2554 pub is_resizable: bool,
2556
2557 pub is_minimizable: bool,
2559
2560 pub display_id: Option<DisplayId>,
2563
2564 pub window_background: WindowBackgroundAppearance,
2566
2567 pub app_id: Option<String>,
2569
2570 pub window_min_size: Option<Size<Pixels>>,
2572
2573 pub window_decorations: Option<WindowDecorations>,
2576
2577 pub icon: Option<Arc<image::RgbaImage>>,
2579
2580 pub tabbing_identifier: Option<String>,
2582}
2583
2584#[derive(Debug)]
2586#[cfg_attr(
2587 all(
2588 any(target_os = "linux", target_os = "freebsd"),
2589 not(any(feature = "x11", feature = "wayland"))
2590 ),
2591 allow(dead_code)
2592)]
2593#[allow(missing_docs)]
2594pub struct WindowParams {
2595 pub bounds: Bounds<Pixels>,
2596
2597 #[cfg_attr(feature = "wayland", allow(dead_code))]
2599 pub titlebar: Option<TitlebarOptions>,
2600
2601 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2603 pub kind: WindowKind,
2604
2605 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2607 pub is_movable: bool,
2608
2609 #[cfg_attr(
2611 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2612 allow(dead_code)
2613 )]
2614 pub app_owns_titlebar_drag: bool,
2615
2616 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2618 pub is_resizable: bool,
2619
2620 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2622 pub is_minimizable: bool,
2623
2624 #[cfg_attr(
2625 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2626 allow(dead_code)
2627 )]
2628 pub focus: bool,
2629
2630 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2631 pub show: bool,
2632
2633 #[cfg_attr(feature = "wayland", allow(dead_code))]
2635 pub icon: Option<Arc<image::RgbaImage>>,
2636
2637 #[cfg_attr(feature = "wayland", allow(dead_code))]
2638 pub display_id: Option<DisplayId>,
2639
2640 #[cfg_attr(feature = "wayland", allow(dead_code))]
2641 pub app_id: Option<String>,
2642
2643 pub window_min_size: Option<Size<Pixels>>,
2644
2645 #[cfg(target_os = "macos")]
2646 pub tabbing_identifier: Option<String>,
2647}
2648
2649#[derive(Debug, Copy, Clone, PartialEq)]
2651pub enum WindowBounds {
2652 Windowed(Bounds<Pixels>),
2654 Maximized(Bounds<Pixels>),
2657 Fullscreen(Bounds<Pixels>),
2660}
2661
2662impl Default for WindowBounds {
2663 fn default() -> Self {
2664 WindowBounds::Windowed(Bounds::default())
2665 }
2666}
2667
2668impl WindowBounds {
2669 pub fn get_bounds(&self) -> Bounds<Pixels> {
2671 match self {
2672 WindowBounds::Windowed(bounds) => *bounds,
2673 WindowBounds::Maximized(bounds) => *bounds,
2674 WindowBounds::Fullscreen(bounds) => *bounds,
2675 }
2676 }
2677
2678 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2680 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2681 }
2682}
2683
2684impl Default for WindowOptions {
2685 fn default() -> Self {
2686 Self {
2687 window_bounds: None,
2688 titlebar: Some(TitlebarOptions {
2689 title: Default::default(),
2690 appears_transparent: Default::default(),
2691 traffic_light_position: Default::default(),
2692 }),
2693 focus: true,
2694 show: true,
2695 kind: WindowKind::Normal,
2696 is_movable: true,
2697 app_owns_titlebar_drag: false,
2698 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2699 is_resizable: true,
2700 is_minimizable: true,
2701 display_id: None,
2702 window_background: WindowBackgroundAppearance::default(),
2703 icon: None,
2704 app_id: None,
2705 window_min_size: None,
2706 window_decorations: None,
2707 tabbing_identifier: None,
2708 }
2709 }
2710}
2711
2712#[derive(Debug, Default)]
2714pub struct TitlebarOptions {
2715 pub title: Option<SharedString>,
2717
2718 pub appears_transparent: bool,
2721
2722 pub traffic_light_position: Option<Point<Pixels>>,
2724}
2725
2726#[derive(Clone, Debug, PartialEq, Eq)]
2728pub enum WindowKind {
2729 Normal,
2731
2732 PopUp,
2735
2736 AnchoredPopup(popup::PopupOptions),
2743
2744 Floating,
2746
2747 #[cfg(all(target_os = "linux", feature = "wayland"))]
2750 LayerShell(layer_shell::LayerShellOptions),
2751
2752 Dialog,
2755}
2756
2757#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2762pub enum WindowAppearance {
2763 #[default]
2767 Light,
2768
2769 VibrantLight,
2773
2774 Dark,
2778
2779 VibrantDark,
2783}
2784
2785#[derive(Copy, Clone, Debug, Default, PartialEq)]
2788pub enum WindowBackgroundAppearance {
2789 #[default]
2797 Opaque,
2798 Transparent,
2800 Blurred,
2804 MicaBackdrop,
2806 MicaAltBackdrop,
2808}
2809
2810#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2812pub enum TextRenderingMode {
2813 #[default]
2815 PlatformDefault,
2816 Subpixel,
2818 Grayscale,
2820}
2821
2822#[derive(Clone, Debug)]
2824pub struct PathPromptOptions {
2825 pub files: bool,
2827 pub directories: bool,
2829 pub multiple: bool,
2831 pub prompt: Option<SharedString>,
2833}
2834
2835#[derive(Copy, Clone, Debug, PartialEq)]
2837pub enum PromptLevel {
2838 Info,
2840
2841 Warning,
2843
2844 Critical,
2846}
2847
2848#[derive(Clone, Debug, PartialEq)]
2850pub enum PromptButton {
2851 Ok(SharedString),
2853 Cancel(SharedString),
2855 Other(SharedString),
2857}
2858
2859impl PromptButton {
2860 pub fn new(label: impl Into<SharedString>) -> Self {
2862 PromptButton::Other(label.into())
2863 }
2864
2865 pub fn ok(label: impl Into<SharedString>) -> Self {
2867 PromptButton::Ok(label.into())
2868 }
2869
2870 pub fn cancel(label: impl Into<SharedString>) -> Self {
2872 PromptButton::Cancel(label.into())
2873 }
2874
2875 #[allow(dead_code)]
2877 pub fn is_cancel(&self) -> bool {
2878 matches!(self, PromptButton::Cancel(_))
2879 }
2880
2881 pub fn label(&self) -> &SharedString {
2883 match self {
2884 PromptButton::Ok(label) => label,
2885 PromptButton::Cancel(label) => label,
2886 PromptButton::Other(label) => label,
2887 }
2888 }
2889}
2890
2891impl From<&str> for PromptButton {
2892 fn from(value: &str) -> Self {
2893 match value.to_lowercase().as_str() {
2894 "ok" => PromptButton::Ok("OK".into()),
2895 "cancel" => PromptButton::Cancel("Cancel".into()),
2896 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2897 }
2898 }
2899}
2900
2901#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2903pub enum CursorStyle {
2904 #[default]
2906 Arrow,
2907
2908 IBeam,
2911
2912 Crosshair,
2915
2916 ClosedHand,
2919
2920 OpenHand,
2923
2924 PointingHand,
2927
2928 ResizeLeft,
2931
2932 ResizeRight,
2935
2936 ResizeLeftRight,
2939
2940 ResizeUp,
2943
2944 ResizeDown,
2947
2948 ResizeUpDown,
2951
2952 ResizeUpLeftDownRight,
2955
2956 ResizeUpRightDownLeft,
2959
2960 ResizeColumn,
2963
2964 ResizeRow,
2967
2968 IBeamCursorForVerticalLayout,
2971
2972 OperationNotAllowed,
2975
2976 DragLink,
2979
2980 DragCopy,
2983
2984 ContextualMenu,
2987}
2988
2989#[derive(Clone, Debug, Eq, PartialEq)]
2991pub struct ClipboardItem {
2992 pub entries: Vec<ClipboardEntry>,
2994}
2995
2996#[derive(Clone, Debug, PartialEq, Eq)]
3001pub enum ClipboardReadError {
3002 Unavailable,
3006 Denied(String),
3009 UnsupportedContent,
3012}
3013
3014impl std::fmt::Display for ClipboardReadError {
3015 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3016 match self {
3017 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3018 Self::Denied(message) => {
3019 write!(formatter, "clipboard access was denied: {message}")
3020 }
3021 Self::UnsupportedContent => {
3022 formatter.write_str("the clipboard contents are unsupported")
3023 }
3024 }
3025 }
3026}
3027
3028impl std::error::Error for ClipboardReadError {}
3029
3030#[derive(Clone, Debug, Eq, PartialEq)]
3032pub enum ClipboardEntry {
3033 String(ClipboardString),
3035 Image(Image),
3037 ExternalPaths(crate::ExternalPaths),
3039}
3040
3041impl ClipboardItem {
3042 pub fn new_string(text: String) -> Self {
3044 Self {
3045 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3046 }
3047 }
3048
3049 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3051 Self {
3052 entries: vec![ClipboardEntry::String(ClipboardString {
3053 text,
3054 metadata: Some(metadata),
3055 })],
3056 }
3057 }
3058
3059 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3061 Self {
3062 entries: vec![ClipboardEntry::String(
3063 ClipboardString::new(text).with_json_metadata(metadata),
3064 )],
3065 }
3066 }
3067
3068 pub fn new_image(image: &Image) -> Self {
3070 Self {
3071 entries: vec![ClipboardEntry::Image(image.clone())],
3072 }
3073 }
3074
3075 pub fn text(&self) -> Option<String> {
3078 let mut answer = String::new();
3079
3080 for entry in self.entries.iter() {
3081 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3082 answer.push_str(text);
3083 }
3084 }
3085
3086 if answer.is_empty() {
3087 for entry in self.entries.iter() {
3088 if let ClipboardEntry::ExternalPaths(paths) = entry {
3089 for path in &paths.0 {
3090 use std::fmt::Write as _;
3091 _ = write!(answer, "{}", path.display());
3092 }
3093 }
3094 }
3095 }
3096
3097 if !answer.is_empty() {
3098 Some(answer)
3099 } else {
3100 None
3101 }
3102 }
3103
3104 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3106 pub fn metadata(&self) -> Option<&String> {
3107 match self.entries().first() {
3108 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3109 clipboard_string.metadata.as_ref()
3110 }
3111 _ => None,
3112 }
3113 }
3114
3115 pub fn entries(&self) -> &[ClipboardEntry] {
3117 &self.entries
3118 }
3119
3120 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3122 self.entries.into_iter()
3123 }
3124}
3125
3126impl From<ClipboardString> for ClipboardEntry {
3127 fn from(value: ClipboardString) -> Self {
3128 Self::String(value)
3129 }
3130}
3131
3132impl From<String> for ClipboardEntry {
3133 fn from(value: String) -> Self {
3134 Self::from(ClipboardString::from(value))
3135 }
3136}
3137
3138impl From<Image> for ClipboardEntry {
3139 fn from(value: Image) -> Self {
3140 Self::Image(value)
3141 }
3142}
3143
3144impl From<ClipboardEntry> for ClipboardItem {
3145 fn from(value: ClipboardEntry) -> Self {
3146 Self {
3147 entries: vec![value],
3148 }
3149 }
3150}
3151
3152impl From<String> for ClipboardItem {
3153 fn from(value: String) -> Self {
3154 Self::from(ClipboardEntry::from(value))
3155 }
3156}
3157
3158impl From<Image> for ClipboardItem {
3159 fn from(value: Image) -> Self {
3160 Self::from(ClipboardEntry::from(value))
3161 }
3162}
3163
3164#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3166pub enum ImageFormat {
3167 Png,
3172 Jpeg,
3174 Webp,
3176 Gif,
3178 Svg,
3180 Bmp,
3182 Tiff,
3184 Ico,
3186 Pnm,
3188}
3189
3190impl ImageFormat {
3191 pub const fn mime_type(self) -> &'static str {
3193 match self {
3194 ImageFormat::Png => "image/png",
3195 ImageFormat::Jpeg => "image/jpeg",
3196 ImageFormat::Webp => "image/webp",
3197 ImageFormat::Gif => "image/gif",
3198 ImageFormat::Svg => "image/svg+xml",
3199 ImageFormat::Bmp => "image/bmp",
3200 ImageFormat::Tiff => "image/tiff",
3201 ImageFormat::Ico => "image/ico",
3202 ImageFormat::Pnm => "image/x-portable-anymap",
3203 }
3204 }
3205
3206 pub const fn extension(self) -> &'static str {
3208 match self {
3209 ImageFormat::Png => "png",
3210 ImageFormat::Jpeg => "jpg",
3211 ImageFormat::Webp => "webp",
3212 ImageFormat::Gif => "gif",
3213 ImageFormat::Svg => "svg",
3214 ImageFormat::Bmp => "bmp",
3215 ImageFormat::Tiff => "tiff",
3216 ImageFormat::Ico => "ico",
3217 ImageFormat::Pnm => "pnm",
3218 }
3219 }
3220
3221 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3223 use strum::IntoEnumIterator;
3224 Self::iter()
3225 .find(|format| format.mime_type() == mime_type)
3226 .or_else(|| Self::from_mime_type_alias(mime_type))
3227 }
3228
3229 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3233 match mime_type {
3234 "image/jpg" => Some(Self::Jpeg),
3235 "image/tif" => Some(Self::Tiff),
3236 _ => None,
3237 }
3238 }
3239}
3240
3241#[derive(Clone, Debug, PartialEq, Eq)]
3243pub struct Image {
3244 pub format: ImageFormat,
3246 pub bytes: Vec<u8>,
3248 pub id: u64,
3250}
3251
3252pub(crate) fn decode_static_image(
3253 bytes: &[u8],
3254 format: image::ImageFormat,
3255) -> Result<SmallVec<[Frame; 1]>> {
3256 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3257 .into_decoder()
3258 .context("creating image decoder")?;
3259 decode_static_image_from_decoder(decoder)
3260}
3261
3262pub(crate) fn decode_static_image_from_decoder(
3263 mut decoder: impl image::ImageDecoder,
3264) -> Result<SmallVec<[Frame; 1]>> {
3265 let orientation = decoder
3266 .orientation()
3267 .context("reading decoder's orientation")?;
3268 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3269 image.apply_orientation(orientation);
3270
3271 let mut data = image.into_rgba8();
3272 for pixel in data.chunks_exact_mut(4) {
3273 pixel.swap(0, 2);
3274 }
3275
3276 Ok(SmallVec::from_elem(Frame::new(data), 1))
3277}
3278
3279impl Hash for Image {
3280 fn hash<H: Hasher>(&self, state: &mut H) {
3281 state.write_u64(self.id);
3282 }
3283}
3284
3285impl Image {
3286 pub fn empty() -> Self {
3288 Self::from_bytes(ImageFormat::Png, Vec::new())
3289 }
3290
3291 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3293 Self {
3294 id: hash(&bytes),
3295 format,
3296 bytes,
3297 }
3298 }
3299
3300 pub fn id(&self) -> u64 {
3302 self.id
3303 }
3304
3305 pub fn use_render_image(
3307 self: Arc<Self>,
3308 window: &mut Window,
3309 cx: &mut App,
3310 ) -> Option<Arc<RenderImage>> {
3311 ImageSource::Image(self)
3312 .use_data(None, window, cx)
3313 .and_then(|result| result.ok())
3314 }
3315
3316 pub fn get_render_image(
3318 self: Arc<Self>,
3319 window: &mut Window,
3320 cx: &mut App,
3321 ) -> Option<Arc<RenderImage>> {
3322 ImageSource::Image(self)
3323 .get_data(None, window, cx)
3324 .and_then(|result| result.ok())
3325 }
3326
3327 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3329 ImageSource::Image(self).remove_asset(cx);
3330 }
3331
3332 #[cfg(any(test, feature = "test-support"))]
3335 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3336 ImageSource::Image(self.clone()).is_asset_cached(cx)
3337 }
3338
3339 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3341 let frames = match self.format {
3342 ImageFormat::Gif => {
3343 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3344 let mut frames = SmallVec::new();
3345
3346 for frame in decoder.into_frames() {
3347 match frame {
3348 Ok(mut frame) => {
3349 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3351 pixel.swap(0, 2);
3352 }
3353 frames.push(frame);
3354 }
3355 Err(err) => {
3356 log::debug!("Skipping GIF frame due to decode error: {err}");
3357 }
3358 }
3359 }
3360
3361 if frames.is_empty() {
3362 anyhow::bail!("GIF could not be decoded: all frames failed");
3363 }
3364
3365 frames
3366 }
3367 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3368 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3369 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3370 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3371 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3372 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3373 ImageFormat::Svg => {
3374 return svg_renderer
3375 .render_single_frame(&self.bytes, 1.0)
3376 .map_err(Into::into);
3377 }
3378 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3379 };
3380
3381 Ok(Arc::new(RenderImage::new(frames)))
3382 }
3383
3384 pub fn format(&self) -> ImageFormat {
3386 self.format
3387 }
3388
3389 pub fn bytes(&self) -> &[u8] {
3391 self.bytes.as_slice()
3392 }
3393}
3394
3395#[derive(Clone, Debug, Eq, PartialEq)]
3397pub struct ClipboardString {
3398 pub text: String,
3400 pub metadata: Option<String>,
3402}
3403
3404impl ClipboardString {
3405 pub fn new(text: String) -> Self {
3407 Self {
3408 text,
3409 metadata: None,
3410 }
3411 }
3412
3413 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3416 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3417 self
3418 }
3419
3420 pub fn text(&self) -> &String {
3422 &self.text
3423 }
3424
3425 pub fn into_text(self) -> String {
3427 self.text
3428 }
3429
3430 pub fn metadata_json<T>(&self) -> Option<T>
3432 where
3433 T: for<'a> Deserialize<'a>,
3434 {
3435 self.metadata
3436 .as_ref()
3437 .and_then(|m| serde_json::from_str(m).ok())
3438 }
3439
3440 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3441 pub fn text_hash(text: &str) -> u64 {
3443 let mut hasher = SeaHasher::new();
3444 text.hash(&mut hasher);
3445 hasher.finish()
3446 }
3447}
3448
3449impl From<String> for ClipboardString {
3450 fn from(value: String) -> Self {
3451 Self {
3452 text: value,
3453 metadata: None,
3454 }
3455 }
3456}
3457
3458#[cfg(test)]
3459mod image_tests {
3460 use super::*;
3461 use std::sync::Arc;
3462
3463 #[test]
3464 fn test_image_to_image_data_applies_exif_orientation() {
3465 let image = Image::from_bytes(
3466 ImageFormat::Jpeg,
3467 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3468 );
3469
3470 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3471
3472 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3473
3474 let bytes = render_image.as_bytes(0).unwrap();
3475 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3476 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3477 }
3478
3479 #[test]
3480 fn test_svg_image_to_image_data_converts_to_bgra() {
3481 let image = Image::from_bytes(
3482 ImageFormat::Svg,
3483 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3484<rect width="1" height="1" fill="#38BDF8"/>
3485</svg>"##
3486 .to_vec(),
3487 );
3488
3489 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3490 let bytes = render_image.as_bytes(0).unwrap();
3491
3492 for pixel in bytes.chunks_exact(4) {
3493 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3494 }
3495 }
3496}
3497
3498#[cfg(test)]
3499mod atlas_tests {
3500 use super::*;
3501
3502 const TILE_SIZE: Size<DevicePixels> = Size {
3503 width: DevicePixels(1),
3504 height: DevicePixels(1),
3505 };
3506
3507 #[derive(Default)]
3508 struct RecordingAtlasBackend {
3509 insert_calls: u32,
3510 fail_next_insert: bool,
3511 removed_tiles: Vec<AtlasTile>,
3512 }
3513
3514 impl AtlasBackend for RecordingAtlasBackend {
3515 fn insert(
3516 &mut self,
3517 kind: AtlasTextureKind,
3518 size: Size<DevicePixels>,
3519 _bytes: &[u8],
3520 ) -> Result<AtlasTile> {
3521 self.insert_calls += 1;
3522 if std::mem::take(&mut self.fail_next_insert) {
3523 anyhow::bail!("backend failed");
3524 }
3525 Ok(AtlasTile {
3526 texture_id: AtlasTextureId { index: 0, kind },
3527 tile_id: TileId(self.insert_calls),
3528 padding: 0,
3529 bounds: Bounds {
3530 origin: Point::default(),
3531 size,
3532 },
3533 })
3534 }
3535
3536 fn remove(&mut self, tile: AtlasTile) {
3537 self.removed_tiles.push(tile);
3538 }
3539 }
3540
3541 fn image_key(image_id: usize) -> AtlasKey {
3542 AtlasKey::Image(RenderImageParams {
3543 image_id: crate::ImageId(image_id),
3544 frame_index: 0,
3545 })
3546 }
3547
3548 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3549 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3550 }
3551
3552 #[test]
3553 fn only_successful_inserts_are_cached() -> Result<()> {
3554 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3555 let key = image_key(1);
3556
3557 assert_eq!(
3558 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3559 None
3560 );
3561 state
3562 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3563 .expect_err("builder error should propagate");
3564 assert!(!state.contains(&key));
3565 assert_eq!(state.backend.insert_calls, 0);
3566
3567 state.backend.fail_next_insert = true;
3568 state
3569 .get_or_insert_with(key.clone(), &mut build_tile)
3570 .expect_err("backend error should propagate");
3571 assert!(!state.contains(&key));
3572 assert_eq!(state.backend.insert_calls, 1);
3573
3574 let tile = state
3575 .get_or_insert_with(key.clone(), &mut build_tile)?
3576 .context("builder should produce a tile")?;
3577 assert_eq!(tile.texture_id.kind, key.texture_kind());
3578 assert_eq!(
3579 state.get_or_insert_with(key.clone(), &mut || {
3580 anyhow::bail!("cache hit must not call the builder")
3581 })?,
3582 Some(tile)
3583 );
3584 assert!(state.contains(&key));
3585 assert_eq!(state.backend.insert_calls, 2);
3586 Ok(())
3587 }
3588
3589 #[test]
3590 fn remove_and_clear_invalidate_keys() -> Result<()> {
3591 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3592 let key = image_key(1);
3593 let other_key = image_key(2);
3594 let tile = state
3595 .get_or_insert_with(key.clone(), &mut build_tile)?
3596 .context("builder should produce a tile")?;
3597 state
3598 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3599 .context("builder should produce another tile")?;
3600
3601 state.remove(&key);
3602 state.remove(&key);
3603 assert!(!state.contains(&key));
3604 assert!(state.contains(&other_key));
3605 assert_eq!(state.backend.removed_tiles, vec![tile]);
3606
3607 let mut reset_calls = 0;
3608 state.clear(|_| reset_calls += 1);
3609 assert_eq!(reset_calls, 1);
3610 assert!(!state.contains(&other_key));
3611 assert_eq!(state.backend.removed_tiles, vec![tile]);
3612 Ok(())
3613 }
3614}
3615
3616#[cfg(test)]
3617mod frame_signal_tests {
3618 use super::*;
3619
3620 #[cfg(feature = "profiler")]
3621 #[test]
3622 fn coalesced_signals_retain_the_earliest_until_drained() {
3623 for (first_source, later_source) in [
3624 (
3625 FrameRequestSource::NativeCallback,
3626 FrameRequestSource::LocalSchedule,
3627 ),
3628 (
3629 FrameRequestSource::LocalSchedule,
3630 FrameRequestSource::NativeCallback,
3631 ),
3632 ] {
3633 let signal = PlatformFrameSignal::new();
3634 let first =
3635 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3636 assert_eq!(signal.take(), None);
3637 signal.record(first + Duration::from_millis(16), later_source);
3638 signal.record(first, first_source);
3639 signal.record(first + Duration::from_millis(32), later_source);
3640 assert_eq!(signal.take(), Some((first, first_source)));
3641 assert_eq!(signal.take(), None);
3642 let next = first + Duration::from_millis(48);
3643 signal.record(next, later_source);
3644 assert_eq!(signal.take(), Some((next, later_source)));
3645 }
3646 }
3647
3648 #[cfg(not(feature = "profiler"))]
3649 #[test]
3650 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3651 let captured = PlatformFrameSignal::capture(|| {
3652 panic!("profiler-disabled builds must not evaluate the capture closure");
3653 });
3654 assert_eq!(captured, None);
3655 let signal = PlatformFrameSignal::new();
3656 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3657 assert_eq!(signal.take(), None);
3658 }
3659}
3660
3661#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3662mod tests {
3663 use super::*;
3664 use std::collections::HashSet;
3665
3666 #[test]
3667 fn test_refresh_interval_from_hz() {
3668 assert_eq!(
3669 refresh_interval_from_hz(50.0),
3670 Some(Duration::from_millis(20))
3671 );
3672 assert_eq!(
3673 refresh_interval_from_hz(120.0),
3674 Some(Duration::from_secs(1) / 120)
3675 );
3676 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3677 assert_eq!(refresh_interval_from_hz(unknown), None);
3678 }
3679 }
3680
3681 #[test]
3682 fn test_window_button_layout_parse_standard() {
3683 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3684 assert_eq!(
3685 layout.left,
3686 [
3687 Some(WindowButton::Close),
3688 Some(WindowButton::Minimize),
3689 None
3690 ]
3691 );
3692 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3693 }
3694
3695 #[test]
3696 fn test_window_button_layout_parse_right_only() {
3697 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3698 assert_eq!(layout.left, [None, None, None]);
3699 assert_eq!(
3700 layout.right,
3701 [
3702 Some(WindowButton::Minimize),
3703 Some(WindowButton::Maximize),
3704 Some(WindowButton::Close)
3705 ]
3706 );
3707 }
3708
3709 #[test]
3710 fn test_window_button_layout_parse_left_only() {
3711 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3712 assert_eq!(
3713 layout.left,
3714 [
3715 Some(WindowButton::Close),
3716 Some(WindowButton::Minimize),
3717 Some(WindowButton::Maximize)
3718 ]
3719 );
3720 assert_eq!(layout.right, [None, None, None]);
3721 }
3722
3723 #[test]
3724 fn test_window_button_layout_parse_with_whitespace() {
3725 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3726 assert_eq!(
3727 layout.left,
3728 [
3729 Some(WindowButton::Close),
3730 Some(WindowButton::Minimize),
3731 None
3732 ]
3733 );
3734 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3735 }
3736
3737 #[test]
3738 fn test_window_button_layout_parse_empty() {
3739 let layout = WindowButtonLayout::parse("").unwrap();
3740 assert_eq!(layout.left, [None, None, None]);
3741 assert_eq!(layout.right, [None, None, None]);
3742 }
3743
3744 #[test]
3745 fn test_window_button_layout_parse_intentionally_empty() {
3746 let layout = WindowButtonLayout::parse(":").unwrap();
3747 assert_eq!(layout.left, [None, None, None]);
3748 assert_eq!(layout.right, [None, None, None]);
3749 }
3750
3751 #[test]
3752 fn test_window_button_layout_parse_invalid_buttons() {
3753 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3754 assert_eq!(
3755 layout.left,
3756 [
3757 Some(WindowButton::Close),
3758 Some(WindowButton::Minimize),
3759 None
3760 ]
3761 );
3762 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3763 }
3764
3765 #[test]
3766 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3767 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3768 assert_eq!(
3769 layout.right,
3770 [
3771 Some(WindowButton::Close),
3772 Some(WindowButton::Minimize),
3773 None
3774 ]
3775 );
3776 assert_eq!(layout.format(), ":close,minimize");
3777 }
3778
3779 #[test]
3780 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3781 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3782 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3783 assert_eq!(
3784 layout.right,
3785 [
3786 Some(WindowButton::Maximize),
3787 Some(WindowButton::Minimize),
3788 None
3789 ]
3790 );
3791
3792 let button_ids: Vec<_> = layout
3793 .left
3794 .iter()
3795 .chain(layout.right.iter())
3796 .flatten()
3797 .map(WindowButton::id)
3798 .collect();
3799 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3800 assert_eq!(unique_button_ids.len(), button_ids.len());
3801 assert_eq!(layout.format(), "close:maximize,minimize");
3802 }
3803
3804 #[test]
3805 fn test_window_button_layout_parse_gnome_style() {
3806 let layout = WindowButtonLayout::parse("close").unwrap();
3807 assert_eq!(layout.left, [None, None, None]);
3808 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3809 }
3810
3811 #[test]
3812 fn test_window_button_layout_parse_elementary_style() {
3813 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3814 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3815 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3816 }
3817
3818 #[test]
3819 fn test_window_button_layout_round_trip() {
3820 let cases = [
3821 "close:minimize,maximize",
3822 "minimize,maximize,close:",
3823 ":close",
3824 "close:",
3825 "close:maximize",
3826 ":",
3827 ];
3828
3829 for case in cases {
3830 let layout = WindowButtonLayout::parse(case).unwrap();
3831 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3832 }
3833 }
3834
3835 #[test]
3836 fn test_window_button_layout_linux_default() {
3837 let layout = WindowButtonLayout::linux_default();
3838 assert_eq!(layout.left, [None, None, None]);
3839 assert_eq!(
3840 layout.right,
3841 [
3842 Some(WindowButton::Minimize),
3843 Some(WindowButton::Maximize),
3844 Some(WindowButton::Close)
3845 ]
3846 );
3847
3848 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3849 assert_eq!(round_tripped, layout);
3850 }
3851
3852 #[test]
3853 fn test_window_button_layout_parse_all_invalid() {
3854 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3855 }
3856}