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