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