1mod app_menu;
2mod keyboard;
3mod keystroke;
4
5#[cfg(all(target_os = "linux", feature = "wayland"))]
6#[expect(missing_docs)]
7pub mod layer_shell;
8
9pub mod popup;
11
12#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
13mod threaded_dispatcher;
14
15#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
16mod test;
17
18#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
19mod visual_test;
20
21#[cfg(all(
22 feature = "screen-capture",
23 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
24))]
25pub mod scap_screen_capture;
26
27#[cfg(all(
28 any(target_os = "windows", target_os = "linux"),
29 feature = "screen-capture"
30))]
31pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
32#[cfg(not(feature = "screen-capture"))]
33pub(crate) type PlatformScreenCaptureFrame = ();
34#[cfg(all(target_os = "macos", feature = "screen-capture"))]
35pub(crate) type PlatformScreenCaptureFrame =
36 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
37
38use crate::{
39 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, Corners,
40 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
41 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
42 LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority,
43 RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
44 ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea,
45 hash, point, px, size,
46};
47#[cfg(any(target_os = "linux", target_os = "freebsd"))]
48use anyhow::bail;
49use anyhow::{Context as _, Result};
50use async_task::Runnable;
51use collections::FxHashMap;
52use futures::channel::oneshot;
53#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
54use image::RgbaImage;
55use image::codecs::gif::GifDecoder;
56use image::{AnimationDecoder as _, DynamicImage, Frame};
57use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
58use scheduler::Instant;
59pub use scheduler::RunnableMeta;
60use schemars::JsonSchema;
61use seahash::SeaHasher;
62use serde::{Deserialize, Serialize};
63use smallvec::SmallVec;
64use std::borrow::Cow;
65use std::collections::hash_map::Entry;
66use std::hash::{Hash, Hasher};
67use std::io::Cursor;
68use std::ops;
69use std::time::Duration;
70use std::{
71 ffi::OsString,
72 fmt::{self, Debug},
73 ops::Range,
74 path::{Path, PathBuf},
75 rc::Rc,
76 sync::Arc,
77};
78use strum::EnumIter;
79use uuid::Uuid;
80
81pub use app_menu::*;
82pub use keyboard::*;
83pub use keystroke::*;
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum WindowVisibility {
107 Visible,
110 Hidden,
113}
114
115impl WindowVisibility {
116 pub fn is_visible(self) -> bool {
118 self == Self::Visible
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125#[non_exhaustive]
126pub enum DisplayEvent {
127 Added(DisplayId),
129 Removed(DisplayId),
132 Changed {
134 id: DisplayId,
136 changes: DisplayChanges,
138 },
139}
140
141bitflags::bitflags! {
142 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144 pub struct DisplayChanges: u8 {
145 const BOUNDS = 1 << 0;
147 const REFRESH_INTERVAL = 1 << 1;
149 }
150}
151
152pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
155 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
156}
157
158#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
165pub enum ActivationPolicy {
166 #[default]
168 Regular,
169 Accessory,
171}
172
173#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
174pub(crate) use test::*;
175
176#[cfg(any(test, feature = "test-support"))]
177pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
178
179#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
180pub use threaded_dispatcher::ThreadedDispatcher;
181
182#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
183pub use visual_test::VisualTestPlatform;
184
185pub struct ActivityGuard {
187 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
188}
189
190impl ActivityGuard {
191 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
193 Self {
194 _release: gpui_util::defer(Box::new(release)),
195 }
196 }
197
198 pub fn noop() -> Self {
200 Self::new(|| {})
201 }
202}
203
204#[cfg(any(target_os = "linux", target_os = "freebsd"))]
208#[inline]
209pub fn guess_compositor() -> &'static str {
210 if std::env::var_os("ZED_HEADLESS").is_some() {
211 return "Headless";
212 }
213 GraphicalEnvironment::detect().guess_compositor()
214}
215
216#[cfg(any(target_os = "linux", target_os = "freebsd"))]
225#[derive(Clone, Debug, Default)]
226pub struct GraphicalEnvironment {
227 pub wayland_display: Option<OsString>,
229 pub x11_display: Option<OsString>,
231 pub xdg_runtime_dir: Option<OsString>,
233 pub activation_token: Option<String>,
239}
240
241#[cfg(target_os = "windows")]
247#[derive(Clone, Debug, Default)]
248pub struct GraphicalEnvironment {
249 pub session_id: Option<u32>,
252}
253
254#[cfg(target_os = "windows")]
255impl GraphicalEnvironment {
256 pub fn detect() -> Self {
258 let mut session_id = 0;
259 let result = unsafe {
261 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
262 windows::Win32::System::Threading::GetCurrentProcessId(),
263 &mut session_id,
264 )
265 };
266 Self {
267 session_id: result.is_ok().then_some(session_id),
268 }
269 }
270
271 pub fn apply_to(&self, _command: &mut std::process::Command) {}
274}
275
276#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
278#[derive(Clone, Debug, Default)]
279pub struct GraphicalEnvironment;
280
281#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
282impl GraphicalEnvironment {
283 pub fn detect() -> Self {
285 Self
286 }
287
288 pub fn apply_to(&self, _command: &mut std::process::Command) {}
290}
291
292#[derive(Clone, Debug)]
294pub enum WindowingRequest {
295 Headless,
297 Windowed(GraphicalEnvironment),
299}
300
301#[cfg(any(target_os = "linux", target_os = "freebsd"))]
302impl GraphicalEnvironment {
303 pub fn detect() -> Self {
308 Self {
309 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
310 x11_display: std::env::var_os("DISPLAY"),
311 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
312 activation_token: None,
313 }
314 }
315
316 pub fn apply_to(&self, command: &mut std::process::Command) {
320 for (name, value) in [
321 ("WAYLAND_DISPLAY", &self.wayland_display),
322 ("DISPLAY", &self.x11_display),
323 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
324 ] {
325 match value {
326 Some(value) => command.env(name, value),
327 None => command.env_remove(name),
328 };
329 }
330 }
331
332 pub fn guess_compositor(&self) -> &'static str {
336 let is_set =
337 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
338 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
339 "Wayland"
340 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
341 "X11"
342 } else {
343 "Headless"
344 }
345 }
346}
347
348#[cfg(any(target_os = "linux", target_os = "freebsd"))]
349bitflags::bitflags! {
350 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
352 pub struct WindowingModes: u8 {
353 const WAYLAND = 1 << 0;
355 const X11 = 1 << 1;
357 const HEADLESS = 1 << 2;
359 }
360}
361
362#[expect(missing_docs)]
363pub trait Platform: 'static {
364 fn background_executor(&self) -> BackgroundExecutor;
365 fn foreground_executor(&self) -> ForegroundExecutor;
366 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
367
368 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
369 fn quit(&self);
370 fn set_initial_windowing(&self, _request: WindowingRequest) {}
375 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
376 Task::ready(Err(anyhow::anyhow!(
377 "this platform cannot switch between headless and windowed modes"
378 )))
379 }
380 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
381 fn activate(&self, ignoring_other_apps: bool);
382 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
384 fn hide(&self);
385 fn hide_other_apps(&self);
386 fn unhide_other_apps(&self);
387
388 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
389 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
390 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
395 fn active_window(&self) -> Option<AnyWindowHandle>;
396 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
397 None
398 }
399
400 fn is_screen_capture_supported(&self) -> bool {
401 false
402 }
403
404 fn screen_capture_sources(
405 &self,
406 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
407 let (sources_tx, sources_rx) = oneshot::channel();
408 sources_tx
409 .send(Err(anyhow::anyhow!(
410 "gpui was compiled without the screen-capture feature"
411 )))
412 .ok();
413 sources_rx
414 }
415
416 fn open_window(
417 &self,
418 handle: AnyWindowHandle,
419 options: WindowParams,
420 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
421
422 fn window_appearance(&self) -> WindowAppearance;
424
425 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
434
435 fn button_layout(&self) -> Option<WindowButtonLayout> {
437 None
438 }
439
440 fn open_url(&self, url: &str);
441 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
442 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
443
444 fn prompt_for_paths(
445 &self,
446 options: PathPromptOptions,
447 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
448 fn prompt_for_new_path(
449 &self,
450 directory: &Path,
451 suggested_name: Option<&str>,
452 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
453 fn can_select_mixed_files_and_dirs(&self) -> bool;
454 fn reveal_path(&self, path: &Path);
455 fn open_with_system(&self, path: &Path);
456
457 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
458 fn on_reopen(&self, callback: Box<dyn FnMut()>);
459 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
461 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
463
464 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
474
475 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
480
481 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
485 None
486 }
487
488 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
489 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
490 None
491 }
492
493 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
494 fn perform_dock_menu_action(&self, _action: usize) {}
495 fn add_recent_document(&self, _path: &Path) {}
496 fn update_jump_list(
497 &self,
498 _menus: Vec<MenuItem>,
499 _entries: Vec<SmallVec<[PathBuf; 2]>>,
500 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
501 Task::ready(Vec::new())
502 }
503 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
504 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
505 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
506
507 fn thermal_state(&self) -> ThermalState;
508 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
509 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
510
511 fn set_app_identity(&self, identifier: &str, name: &str) {
518 _ = (identifier, name);
519 }
520
521 fn show_system_notification(&self, notification: SystemNotification) {
528 _ = notification;
529 }
530
531 fn dismiss_system_notification(&self, tag: &str) {
536 _ = tag;
537 }
538
539 fn on_system_notification_response(
545 &self,
546 callback: Box<dyn FnMut(SystemNotificationResponse)>,
547 ) {
548 _ = callback;
549 }
550
551 fn compositor_name(&self) -> &'static str {
552 ""
553 }
554 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
557 None
558 }
559 fn app_path(&self) -> Result<PathBuf>;
560 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
561
562 fn set_cursor_style(&self, style: CursorStyle);
563
564 fn hide_cursor_until_mouse_moves(&self);
567
568 fn is_cursor_visible(&self) -> bool;
570
571 fn should_auto_hide_scrollbars(&self) -> bool;
572
573 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
574 fn write_to_clipboard(&self, item: ClipboardItem);
575
576 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
584 Task::ready(Ok(self.read_from_clipboard()))
585 }
586
587 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
588 fn read_from_primary(&self) -> Option<ClipboardItem>;
589 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
590 fn write_to_primary(&self, item: ClipboardItem);
591
592 #[cfg(target_os = "macos")]
593 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
594 #[cfg(target_os = "macos")]
595 fn write_to_find_pasteboard(&self, item: ClipboardItem);
596
597 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
598 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
599 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
600
601 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
602 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
603 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
604}
605
606pub trait PlatformDisplay: Debug {
608 fn id(&self) -> DisplayId;
610
611 fn uuid(&self) -> Result<Uuid>;
614
615 fn bounds(&self) -> Bounds<Pixels>;
617
618 fn refresh_interval(&self) -> Option<Duration>;
625
626 fn visible_bounds(&self) -> Bounds<Pixels> {
630 self.bounds()
631 }
632
633 fn default_bounds(&self) -> Bounds<Pixels> {
635 let bounds = self.bounds();
636 let center = bounds.center();
637 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
638
639 let offset = clipped_window_size / 2.0;
640 let origin = point(center.x - offset.width, center.y - offset.height);
641 Bounds::new(origin, clipped_window_size)
642 }
643}
644
645#[derive(Clone, Debug, PartialEq, Eq)]
648pub struct SystemNotification {
649 pub tag: SharedString,
653 pub title: SharedString,
655 pub body: SharedString,
657 pub actions: Vec<SystemNotificationAction>,
660}
661
662#[derive(Clone, Debug, PartialEq, Eq, Hash)]
664pub struct SystemNotificationAction {
665 pub id: SharedString,
668 pub label: SharedString,
670}
671
672#[derive(Clone, Debug, PartialEq, Eq)]
674pub struct SystemNotificationResponse {
675 pub tag: SharedString,
677 pub action_id: Option<SharedString>,
680}
681
682#[derive(Debug, Clone, Copy, PartialEq, Eq)]
684pub enum ThermalState {
685 Nominal,
687 Fair,
689 Serious,
691 Critical,
693}
694
695#[derive(Clone)]
697pub struct SourceMetadata {
698 pub id: u64,
700 pub label: Option<SharedString>,
702 pub is_main: Option<bool>,
704 pub resolution: Size<DevicePixels>,
706}
707
708pub trait ScreenCaptureSource {
710 fn metadata(&self) -> Result<SourceMetadata>;
712
713 fn stream(
716 &self,
717 foreground_executor: &ForegroundExecutor,
718 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
719 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
720}
721
722pub trait ScreenCaptureStream {
724 fn metadata(&self) -> Result<SourceMetadata>;
726}
727
728pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
730
731#[derive(PartialEq, Eq, Hash, Copy, Clone)]
733pub struct DisplayId(pub(crate) u64);
734
735impl DisplayId {
736 pub fn new(id: u64) -> Self {
738 Self(id)
739 }
740}
741
742impl From<u64> for DisplayId {
743 fn from(id: u64) -> Self {
744 Self(id)
745 }
746}
747
748impl From<DisplayId> for u64 {
749 fn from(id: DisplayId) -> Self {
750 id.0
751 }
752}
753
754impl Debug for DisplayId {
755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756 write!(f, "DisplayId({})", self.0)
757 }
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
762pub enum ResizeEdge {
763 Top,
765 TopRight,
767 Right,
769 BottomRight,
771 Bottom,
773 BottomLeft,
775 Left,
777 TopLeft,
779}
780
781#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
783pub enum WindowDecorations {
784 #[default]
785 Server,
787 Client,
789}
790
791#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
793pub enum Decorations {
794 #[default]
796 Server,
797 Client {
799 tiling: Tiling,
801 },
802}
803
804#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
806pub struct WindowControls {
807 pub fullscreen: bool,
809 pub maximize: bool,
811 pub minimize: bool,
813 pub window_menu: bool,
815}
816
817impl Default for WindowControls {
818 fn default() -> Self {
819 Self {
821 fullscreen: true,
822 maximize: true,
823 minimize: true,
824 window_menu: true,
825 }
826 }
827}
828
829#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
831pub enum WindowButton {
832 Minimize,
834 Maximize,
836 Close,
838}
839
840impl WindowButton {
841 pub fn id(&self) -> &'static str {
843 match self {
844 WindowButton::Minimize => "minimize",
845 WindowButton::Maximize => "maximize",
846 WindowButton::Close => "close",
847 }
848 }
849
850 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
851 fn index(&self) -> usize {
852 match self {
853 WindowButton::Minimize => 0,
854 WindowButton::Maximize => 1,
855 WindowButton::Close => 2,
856 }
857 }
858}
859
860pub const MAX_BUTTONS_PER_SIDE: usize = 3;
862
863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
868pub struct WindowButtonLayout {
869 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
871 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
873}
874
875#[cfg(any(target_os = "linux", target_os = "freebsd"))]
876impl WindowButtonLayout {
877 pub fn linux_default() -> Self {
879 Self {
880 left: [None; MAX_BUTTONS_PER_SIDE],
881 right: [
882 Some(WindowButton::Minimize),
883 Some(WindowButton::Maximize),
884 Some(WindowButton::Close),
885 ],
886 }
887 }
888
889 pub fn parse(layout_string: &str) -> Result<Self> {
891 fn parse_side(
892 s: &str,
893 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
894 unrecognized: &mut Vec<String>,
895 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
896 let mut result = [None; MAX_BUTTONS_PER_SIDE];
897 let mut i = 0;
898 for name in s.split(',') {
899 let trimmed = name.trim();
900 if trimmed.is_empty() {
901 continue;
902 }
903 let button = match trimmed {
904 "minimize" => Some(WindowButton::Minimize),
905 "maximize" => Some(WindowButton::Maximize),
906 "close" => Some(WindowButton::Close),
907 other => {
908 unrecognized.push(other.to_string());
909 None
910 }
911 };
912 if let Some(button) = button {
913 if seen_buttons[button.index()] {
914 continue;
915 }
916 if let Some(slot) = result.get_mut(i) {
917 *slot = Some(button);
918 seen_buttons[button.index()] = true;
919 i += 1;
920 }
921 }
922 }
923 result
924 }
925
926 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
927 let mut unrecognized = Vec::new();
928 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
929 let layout = Self {
930 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
931 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
932 };
933
934 if !unrecognized.is_empty()
935 && layout.left.iter().all(Option::is_none)
936 && layout.right.iter().all(Option::is_none)
937 {
938 bail!(
939 "button layout string {:?} contains no valid buttons (unrecognized: {})",
940 layout_string,
941 unrecognized.join(", ")
942 );
943 }
944
945 Ok(layout)
946 }
947
948 #[cfg(test)]
950 pub fn format(&self) -> String {
951 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
952 buttons
953 .iter()
954 .flatten()
955 .map(|button| match button {
956 WindowButton::Minimize => "minimize",
957 WindowButton::Maximize => "maximize",
958 WindowButton::Close => "close",
959 })
960 .collect::<Vec<_>>()
961 .join(",")
962 }
963
964 format!("{}:{}", format_side(&self.left), format_side(&self.right))
965 }
966}
967
968#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
970pub struct Tiling {
971 pub top: bool,
973 pub left: bool,
975 pub right: bool,
977 pub bottom: bool,
979}
980
981impl Tiling {
982 pub fn tiled() -> Self {
984 Self {
985 top: true,
986 left: true,
987 right: true,
988 bottom: true,
989 }
990 }
991
992 pub fn is_tiled(&self) -> bool {
994 self.top || self.left || self.right || self.bottom
995 }
996}
997
998pub struct A11yCallbacks {
1000 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1002 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1004 pub deactivation: Box<dyn Fn() + Send + 'static>,
1006}
1007
1008#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1010pub enum FrameRequestSource {
1011 #[default]
1013 NativeCallback,
1014 LocalSchedule,
1016}
1017
1018#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1019#[expect(missing_docs)]
1020pub struct RequestFrameOptions {
1021 pub require_presentation: bool,
1023 pub force_render: bool,
1025 pub signal_at: Option<Instant>,
1031 pub signal_source: FrameRequestSource,
1033}
1034
1035pub struct PlatformFrameSignal {
1044 #[cfg(feature = "profiler")]
1045 origin: Instant,
1046 #[cfg(feature = "profiler")]
1047 first_signal: std::sync::atomic::AtomicU64,
1048}
1049
1050impl Default for PlatformFrameSignal {
1051 fn default() -> Self {
1052 Self::new()
1053 }
1054}
1055
1056impl PlatformFrameSignal {
1057 pub fn new() -> Self {
1059 Self {
1060 #[cfg(feature = "profiler")]
1061 origin: Instant::now(),
1062 #[cfg(feature = "profiler")]
1063 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1064 }
1065 }
1066
1067 #[inline]
1071 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1072 if cfg!(feature = "profiler") {
1073 Some(capture())
1074 } else {
1075 None
1076 }
1077 }
1078
1079 #[inline]
1081 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1082 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1083 #[cfg(feature = "profiler")]
1084 {
1085 let nanoseconds = at
1086 .saturating_duration_since(self.origin)
1087 .as_nanos()
1088 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1089 let source_bit = match source {
1090 FrameRequestSource::NativeCallback => 0,
1091 FrameRequestSource::LocalSchedule => 1,
1092 };
1093 self.first_signal.fetch_min(
1094 (nanoseconds << 1) | source_bit,
1095 std::sync::atomic::Ordering::Relaxed,
1096 );
1097 }
1098 }
1099
1100 #[inline]
1102 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1103 #[cfg(feature = "profiler")]
1104 {
1105 let signal = self
1106 .first_signal
1107 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1108 (signal != u64::MAX).then(|| {
1109 let source = match signal & 1 {
1110 0 => FrameRequestSource::NativeCallback,
1111 _ => FrameRequestSource::LocalSchedule,
1112 };
1113 (self.origin + Duration::from_nanos(signal >> 1), source)
1114 })
1115 }
1116 #[cfg(not(feature = "profiler"))]
1117 {
1118 None
1119 }
1120 }
1121}
1122
1123#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1136pub enum AppLifecyclePhase {
1137 Active,
1139 Inactive,
1141 Background,
1144 Foreground,
1146 Disconnected,
1150}
1151
1152#[derive(Debug, Clone, Default, PartialEq)]
1159pub struct WindowInsets {
1160 pub safe_area: Edges<Pixels>,
1165 pub ime: Edges<Pixels>,
1169 pub corner_radii: Corners<Pixels>,
1174}
1175
1176impl WindowInsets {
1177 pub fn effective(&self) -> Edges<Pixels> {
1179 Edges {
1180 top: self.safe_area.top.max(self.ime.top),
1181 right: self.safe_area.right.max(self.ime.right),
1182 bottom: self.safe_area.bottom.max(self.ime.bottom),
1183 left: self.safe_area.left.max(self.ime.left),
1184 }
1185 }
1186}
1187
1188#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1190pub enum TextInputStateChange {
1191 FocusGained,
1193 FocusLost,
1195 SelectionChanged,
1197 ContentChanged,
1199}
1200
1201#[expect(missing_docs)]
1202pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1203 fn bounds(&self) -> Bounds<Pixels>;
1204 fn is_maximized(&self) -> bool;
1205 fn window_bounds(&self) -> WindowBounds;
1206 fn content_size(&self) -> Size<Pixels>;
1207 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1213 Bounds::new(Point::default(), self.content_size())
1214 }
1215 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1220 fn prepare_frame(&self) -> bool {
1225 false
1226 }
1227 fn resize(&mut self, size: Size<Pixels>);
1228 fn scale_factor(&self) -> f32;
1229 fn appearance(&self) -> WindowAppearance;
1230 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1231 fn mouse_position(&self) -> Point<Pixels>;
1232 fn modifiers(&self) -> Modifiers;
1233 fn capslock(&self) -> Capslock;
1234 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1235 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1236 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1241 fn prompt(
1242 &self,
1243 level: PromptLevel,
1244 msg: &str,
1245 detail: Option<&str>,
1246 answers: &[PromptButton],
1247 ) -> Option<oneshot::Receiver<usize>>;
1248 fn activate(&self);
1249 fn request_attention(&self) {}
1251 fn is_active(&self) -> bool;
1252 fn visibility(&self) -> WindowVisibility;
1255 fn is_hovered(&self) -> bool;
1256 fn background_appearance(&self) -> WindowBackgroundAppearance;
1257 fn set_title(&mut self, title: &str);
1258 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1259 fn minimize(&self);
1260 fn zoom(&self);
1261 fn toggle_fullscreen(&self);
1262 fn is_fullscreen(&self) -> bool;
1263 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1264 None
1265 }
1266 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1267 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1268 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1269 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1273 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1274 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1275 fn on_moved(&self, callback: Box<dyn FnMut()>);
1276 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1280 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1281 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1282 fn on_close(&self, callback: Box<dyn FnOnce()>);
1283 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1284 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1285 fn draw(&self, scene: &Scene);
1286 fn schedule_frame(&self) {}
1287 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1288 fn is_subpixel_rendering_supported(&self) -> bool;
1289
1290 fn get_title(&self) -> String {
1292 String::new()
1293 }
1294 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1295 None
1296 }
1297 fn tab_bar_visible(&self) -> bool {
1298 false
1299 }
1300 fn set_edited(&mut self, _edited: bool) {}
1301 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1302 fn toggle_simple_fullscreen(&self) {}
1303 fn is_simple_fullscreen(&self) -> bool {
1304 false
1305 }
1306 #[cfg(target_os = "macos")]
1307 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1308 fn show_character_palette(&self) {}
1309 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1310 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1311 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1312 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1313 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1314 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1315 fn merge_all_windows(&self) {}
1316 fn move_tab_to_new_window(&self) {}
1317 fn toggle_window_tab_overview(&self) {}
1318 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1319
1320 fn native_window_state(&self) -> Option<Vec<u8>> {
1321 None
1322 }
1323 fn restore_native_window_state(&self, _state: &[u8]) {}
1324
1325 #[cfg(target_os = "windows")]
1326 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1327
1328 fn inner_window_bounds(&self) -> WindowBounds {
1330 self.window_bounds()
1331 }
1332 fn request_decorations(&self, _decorations: WindowDecorations) {}
1333 fn show_window_menu(&self, _position: Point<Pixels>) {}
1334 fn start_window_move(&self) {}
1335 fn can_start_external_drag(&self) -> bool {
1336 false
1337 }
1338 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1339 false
1340 }
1341 fn start_window_resize(&self, _edge: ResizeEdge) {}
1342 fn set_exclusive_zone(&self, _zone: Pixels) {}
1343 #[cfg(all(target_os = "linux", feature = "wayland"))]
1344 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1345 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1346 fn window_decorations(&self) -> Decorations {
1347 Decorations::Server
1348 }
1349 fn set_app_id(&mut self, _app_id: &str) {}
1350 fn map_window(&mut self) -> anyhow::Result<()> {
1351 Ok(())
1352 }
1353 fn window_controls(&self) -> WindowControls {
1354 WindowControls::default()
1355 }
1356 fn set_client_inset(&self, _inset: Pixels) {}
1357 fn gpu_specs(&self) -> Option<GpuSpecs>;
1358
1359 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1360
1361 fn insets(&self) -> WindowInsets {
1366 WindowInsets::default()
1367 }
1368
1369 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1375
1376 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1379
1380 fn set_back_enabled(&self, _enabled: bool) {}
1383
1384 fn show_soft_keyboard(&self) {}
1386
1387 fn hide_soft_keyboard(&self) {}
1389
1390 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1392
1393 fn play_system_bell(&self) {}
1394
1395 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1397
1398 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1400
1401 fn a11y_update_window_bounds(&self) {}
1403
1404 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1405 fn as_test(&mut self) -> Option<&mut TestWindow> {
1406 None
1407 }
1408
1409 #[cfg(any(test, feature = "test-support"))]
1413 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1414 anyhow::bail!("render_to_image not implemented for this platform")
1415 }
1416}
1417
1418#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1420pub trait PlatformHeadlessRenderer {
1421 fn render_scene_to_image(
1423 &mut self,
1424 scene: &Scene,
1425 size: Size<DevicePixels>,
1426 ) -> Result<RgbaImage>;
1427
1428 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1434
1435 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1437}
1438
1439#[doc(hidden)]
1442pub type RunnableVariant = Runnable<RunnableMeta>;
1443
1444#[doc(hidden)]
1445pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1446
1447#[doc(hidden)]
1448pub enum TasksIncluded {
1449 OnlyCompleted,
1450 CompletedAndRunning,
1451}
1452
1453#[doc(hidden)]
1456pub trait PlatformDispatcher: Send + Sync {
1457 fn is_main_thread(&self) -> bool;
1458 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1459 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1460 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1461
1462 fn dispatch_on_main_thread_when_idle(
1463 &self,
1464 runnable: RunnableVariant,
1465 timeout: Option<Duration>,
1466 ) {
1467 let _ = timeout;
1468 self.dispatch_on_main_thread(runnable, Priority::Low);
1469 }
1470
1471 fn idle_time_remaining(&self) -> Option<Duration> {
1472 None
1473 }
1474
1475 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1476
1477 fn now(&self) -> Instant {
1478 Instant::now()
1479 }
1480
1481 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1482 gpui_util::defer(Box::new(|| {}))
1483 }
1484
1485 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1486 ActivityGuard::noop()
1487 }
1488
1489 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1490 fn as_test(&self) -> Option<&TestDispatcher> {
1491 None
1492 }
1493
1494 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1497 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1498 None
1499 }
1500}
1501
1502#[expect(missing_docs)]
1503pub trait PlatformTextSystem: Send + Sync {
1504 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1505 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1507 fn all_font_names(&self) -> Vec<String>;
1509 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1511 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1513 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1515 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1517 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1519 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1521 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1523 fn rasterize_glyph(
1525 &self,
1526 params: &RenderGlyphParams,
1527 raster_bounds: Bounds<DevicePixels>,
1528 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1529 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1531 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1533 -> TextRenderingMode;
1534 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1536 0
1537 }
1538}
1539
1540#[expect(missing_docs)]
1541pub struct NoopTextSystem;
1542
1543#[expect(missing_docs)]
1544impl NoopTextSystem {
1545 #[allow(dead_code)]
1546 pub fn new() -> Self {
1547 Self
1548 }
1549}
1550
1551impl PlatformTextSystem for NoopTextSystem {
1552 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1553 Ok(())
1554 }
1555
1556 fn all_font_names(&self) -> Vec<String> {
1557 Vec::new()
1558 }
1559
1560 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1561 Ok(FontId(1))
1562 }
1563
1564 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1565 FontMetrics {
1566 units_per_em: 1000,
1567 ascent: 1025.0,
1568 descent: -275.0,
1569 line_gap: 0.0,
1570 underline_position: -95.0,
1571 underline_thickness: 60.0,
1572 cap_height: 698.0,
1573 x_height: 516.0,
1574 bounding_box: Bounds {
1575 origin: Point {
1576 x: -260.0,
1577 y: -245.0,
1578 },
1579 size: Size {
1580 width: 1501.0,
1581 height: 1364.0,
1582 },
1583 },
1584 }
1585 }
1586
1587 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1588 Ok(Bounds {
1589 origin: Point { x: 54.0, y: 0.0 },
1590 size: size(392.0, 528.0),
1591 })
1592 }
1593
1594 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1595 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1596 }
1597
1598 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1599 Some(GlyphId(ch.len_utf16() as u32))
1600 }
1601
1602 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1603 Ok(Default::default())
1604 }
1605
1606 fn rasterize_glyph(
1607 &self,
1608 _params: &RenderGlyphParams,
1609 raster_bounds: Bounds<DevicePixels>,
1610 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1611 Ok((raster_bounds.size, Vec::new()))
1612 }
1613
1614 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1615 let mut position = px(0.);
1616 let metrics = self.font_metrics(FontId(0));
1617 let em_width = font_size
1618 * self
1619 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1620 .unwrap()
1621 .width
1622 / metrics.units_per_em as f32;
1623 let mut glyphs = Vec::new();
1624 for (ix, c) in text.char_indices() {
1625 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1626 glyphs.push(ShapedGlyph {
1627 id: glyph,
1628 position: point(position, px(0.)),
1629 index: ix,
1630 is_emoji: glyph.0 == 2,
1631 });
1632 if glyph.0 == 2 {
1633 position += em_width * 2.0;
1634 } else {
1635 position += em_width;
1636 }
1637 } else {
1638 position += em_width
1639 }
1640 }
1641 let mut runs = Vec::default();
1642 if !glyphs.is_empty() {
1643 runs.push(ShapedRun {
1644 font_id: FontId(0),
1645 glyphs,
1646 });
1647 } else {
1648 position = px(0.);
1649 }
1650
1651 LineLayout {
1652 font_size,
1653 width: position,
1654 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1655 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1656 runs,
1657 len: text.len(),
1658 }
1659 }
1660
1661 fn recommended_rendering_mode(
1662 &self,
1663 _font_id: FontId,
1664 _font_size: Pixels,
1665 ) -> TextRenderingMode {
1666 TextRenderingMode::Grayscale
1667 }
1668}
1669
1670#[allow(dead_code)]
1675pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1676 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1677 [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], ];
1691
1692 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1693 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1694
1695 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1696 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1697
1698 [
1699 ratios[0] * NORM13,
1700 ratios[1] * NORM24,
1701 ratios[2] * NORM13,
1702 ratios[3] * NORM24,
1703 ]
1704}
1705
1706#[derive(PartialEq, Eq, Hash, Clone)]
1707#[expect(missing_docs)]
1708pub enum AtlasKey {
1709 Glyph(RenderGlyphParams),
1710 Svg(RenderSvgParams),
1711 Image(RenderImageParams),
1712}
1713
1714impl AtlasKey {
1715 pub fn texture_kind(&self) -> AtlasTextureKind {
1717 match self {
1718 AtlasKey::Glyph(params) => {
1719 if params.is_emoji {
1720 AtlasTextureKind::Polychrome
1721 } else if params.subpixel_rendering {
1722 AtlasTextureKind::Subpixel
1723 } else {
1724 AtlasTextureKind::Monochrome
1725 }
1726 }
1727 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1728 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1729 }
1730 }
1731}
1732
1733impl From<RenderGlyphParams> for AtlasKey {
1734 fn from(params: RenderGlyphParams) -> Self {
1735 Self::Glyph(params)
1736 }
1737}
1738
1739impl From<RenderSvgParams> for AtlasKey {
1740 fn from(params: RenderSvgParams) -> Self {
1741 Self::Svg(params)
1742 }
1743}
1744
1745impl From<RenderImageParams> for AtlasKey {
1746 fn from(params: RenderImageParams) -> Self {
1747 Self::Image(params)
1748 }
1749}
1750
1751#[expect(missing_docs)]
1752pub trait PlatformAtlas {
1753 fn get_or_insert_with<'a>(
1755 &self,
1756 key: AtlasKey,
1757 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1758 ) -> Result<Option<AtlasTile>>;
1759 fn remove(&self, key: &AtlasKey);
1760
1761 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1762 fn contains(&self, _key: &AtlasKey) -> bool {
1763 false
1764 }
1765}
1766
1767#[doc(hidden)]
1768pub trait AtlasBackend {
1769 fn insert(
1770 &mut self,
1771 kind: AtlasTextureKind,
1772 size: Size<DevicePixels>,
1773 bytes: &[u8],
1774 ) -> Result<AtlasTile>;
1775
1776 fn remove(&mut self, tile: AtlasTile);
1777}
1778
1779#[doc(hidden)]
1780pub struct AtlasState<Backend> {
1781 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1782 pub backend: Backend,
1783}
1784
1785impl<Backend> AtlasState<Backend> {
1786 pub fn new(backend: Backend) -> Self {
1787 Self {
1788 tiles_by_key: FxHashMap::default(),
1789 backend,
1790 }
1791 }
1792
1793 pub fn contains(&self, key: &AtlasKey) -> bool {
1794 self.tiles_by_key.contains_key(key)
1795 }
1796
1797 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1798 self.tiles_by_key.clear();
1799 reset_backend(&mut self.backend);
1800 }
1801}
1802
1803impl<Backend: Default> Default for AtlasState<Backend> {
1804 fn default() -> Self {
1805 Self::new(Backend::default())
1806 }
1807}
1808
1809impl<Backend: AtlasBackend> AtlasState<Backend> {
1810 pub fn get_or_insert_with<'a>(
1811 &mut self,
1812 key: AtlasKey,
1813 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1814 ) -> Result<Option<AtlasTile>> {
1815 match self.tiles_by_key.entry(key) {
1816 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1817 Entry::Vacant(entry) => {
1818 profiling::scope!("new tile");
1819 let Some((size, bytes)) = build()? else {
1820 return Ok(None);
1821 };
1822 let tile = self
1823 .backend
1824 .insert(entry.key().texture_kind(), size, &bytes)?;
1825 entry.insert(tile);
1826 Ok(Some(tile))
1827 }
1828 }
1829 }
1830
1831 pub fn remove(&mut self, key: &AtlasKey) {
1832 if let Some(tile) = self.tiles_by_key.remove(key) {
1833 self.backend.remove(tile);
1834 }
1835 }
1836}
1837
1838#[derive(Default)]
1842pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1843
1844#[doc(hidden)]
1845#[derive(Default)]
1846pub struct HeadlessAtlasBackend {
1847 next_id: u32,
1848}
1849
1850impl AtlasBackend for HeadlessAtlasBackend {
1851 fn insert(
1852 &mut self,
1853 kind: AtlasTextureKind,
1854 size: Size<DevicePixels>,
1855 _bytes: &[u8],
1856 ) -> Result<AtlasTile> {
1857 self.next_id += 1;
1858 let texture_id = self.next_id;
1859 self.next_id += 1;
1860 let tile_id = self.next_id;
1861 Ok(AtlasTile {
1862 texture_id: AtlasTextureId {
1863 index: texture_id,
1864 kind,
1865 },
1866 tile_id: TileId(tile_id),
1867 padding: 0,
1868 bounds: Bounds {
1869 origin: Point::default(),
1870 size,
1871 },
1872 })
1873 }
1874
1875 fn remove(&mut self, _tile: AtlasTile) {}
1876}
1877
1878impl PlatformAtlas for HeadlessAtlas {
1879 fn get_or_insert_with<'a>(
1880 &self,
1881 key: AtlasKey,
1882 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1883 ) -> Result<Option<AtlasTile>> {
1884 self.0.lock().get_or_insert_with(key, build)
1885 }
1886
1887 fn remove(&self, key: &AtlasKey) {
1888 self.0.lock().remove(key);
1889 }
1890
1891 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1892 fn contains(&self, key: &AtlasKey) -> bool {
1893 self.0.lock().contains(key)
1894 }
1895}
1896
1897#[doc(hidden)]
1898pub struct AtlasTextureList<T> {
1899 pub textures: Vec<Option<T>>,
1900 pub free_list: Vec<usize>,
1901}
1902
1903impl<T> Default for AtlasTextureList<T> {
1904 fn default() -> Self {
1905 Self {
1906 textures: Vec::default(),
1907 free_list: Vec::default(),
1908 }
1909 }
1910}
1911
1912impl<T> ops::Index<usize> for AtlasTextureList<T> {
1913 type Output = Option<T>;
1914
1915 fn index(&self, index: usize) -> &Self::Output {
1916 &self.textures[index]
1917 }
1918}
1919
1920impl<T> AtlasTextureList<T> {
1921 #[allow(unused)]
1922 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1923 self.free_list.clear();
1924 self.textures.drain(..)
1925 }
1926
1927 #[allow(dead_code)]
1928 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1929 self.textures.iter_mut().flatten()
1930 }
1931}
1932
1933#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1934#[repr(C)]
1935#[expect(missing_docs)]
1936pub struct AtlasTile {
1937 pub texture_id: AtlasTextureId,
1939 pub tile_id: TileId,
1941 pub padding: u32,
1943 pub bounds: Bounds<DevicePixels>,
1945}
1946
1947#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1948#[repr(C)]
1949#[expect(missing_docs)]
1950pub struct AtlasTextureId {
1951 pub index: u32,
1954 pub kind: AtlasTextureKind,
1956}
1957
1958#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1959#[repr(C)]
1960#[cfg_attr(
1961 all(
1962 any(target_os = "linux", target_os = "freebsd"),
1963 not(any(feature = "x11", feature = "wayland"))
1964 ),
1965 allow(dead_code)
1966)]
1967#[expect(missing_docs)]
1968pub enum AtlasTextureKind {
1969 Monochrome = 0,
1970 Polychrome = 1,
1971 Subpixel = 2,
1972}
1973
1974#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1975#[repr(C)]
1976#[expect(missing_docs)]
1977pub struct TileId(pub u32);
1978
1979impl From<etagere::AllocId> for TileId {
1980 fn from(id: etagere::AllocId) -> Self {
1981 Self(id.serialize())
1982 }
1983}
1984
1985impl From<TileId> for etagere::AllocId {
1986 fn from(id: TileId) -> Self {
1987 Self::deserialize(id.0)
1988 }
1989}
1990
1991#[expect(missing_docs)]
1992pub struct PlatformInputHandler {
1993 cx: AsyncWindowContext,
1994 handler: Box<dyn InputHandler>,
1995 focus_id: Option<crate::FocusId>,
1996}
1997
1998#[expect(missing_docs)]
1999#[cfg_attr(
2000 all(
2001 any(target_os = "linux", target_os = "freebsd"),
2002 not(any(feature = "x11", feature = "wayland"))
2003 ),
2004 allow(dead_code)
2005)]
2006impl PlatformInputHandler {
2007 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2008 Self {
2009 cx,
2010 handler,
2011 focus_id: None,
2012 }
2013 }
2014
2015 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2016 self.focus_id = Some(focus_id);
2017 self
2018 }
2019
2020 pub fn focus_id(&self) -> Option<crate::FocusId> {
2025 self.focus_id
2026 }
2027
2028 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2029 self.cx
2030 .update(|window, cx| {
2031 self.handler
2032 .selected_text_range(ignore_disabled_input, window, cx)
2033 })
2034 .ok()
2035 .flatten()
2036 }
2037
2038 #[cfg_attr(target_os = "windows", allow(dead_code))]
2039 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2040 self.cx
2041 .update(|window, cx| self.handler.marked_text_range(window, cx))
2042 .ok()
2043 .flatten()
2044 }
2045
2046 #[cfg_attr(
2047 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2048 allow(dead_code)
2049 )]
2050 pub fn text_for_range(
2051 &mut self,
2052 range_utf16: Range<usize>,
2053 adjusted: &mut Option<Range<usize>>,
2054 ) -> Option<String> {
2055 self.cx
2056 .update(|window, cx| {
2057 self.handler
2058 .text_for_range(range_utf16, adjusted, window, cx)
2059 })
2060 .ok()
2061 .flatten()
2062 }
2063
2064 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2065 self.cx
2066 .update(|window, cx| {
2067 self.handler
2068 .replace_text_in_range(replacement_range, text, window, cx);
2069 })
2070 .ok();
2071 }
2072
2073 pub fn replace_and_mark_text_in_range(
2074 &mut self,
2075 range_utf16: Option<Range<usize>>,
2076 new_text: &str,
2077 new_selected_range: Option<Range<usize>>,
2078 ) {
2079 self.cx
2080 .update(|window, cx| {
2081 self.handler.replace_and_mark_text_in_range(
2082 range_utf16,
2083 new_text,
2084 new_selected_range,
2085 window,
2086 cx,
2087 )
2088 })
2089 .ok();
2090 }
2091
2092 #[cfg_attr(target_os = "windows", allow(dead_code))]
2093 pub fn unmark_text(&mut self) {
2094 self.cx
2095 .update(|window, cx| self.handler.unmark_text(window, cx))
2096 .ok();
2097 }
2098
2099 pub fn paste(&mut self, item: ClipboardItem) {
2100 self.cx
2101 .update(|window, cx| self.handler.paste(item, window, cx))
2102 .ok();
2103 }
2104
2105 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2106 self.cx
2107 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2108 .ok()
2109 .flatten()
2110 }
2111
2112 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2113 self.cx
2114 .update(|window, cx| {
2115 self.handler
2116 .selection_bounds_for_range(range_utf16, window, cx)
2117 })
2118 .unwrap_or_default()
2119 }
2120
2121 #[allow(dead_code)]
2122 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2123 self.handler.apple_press_and_hold_enabled()
2124 }
2125
2126 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2127 self.handler.replace_text_in_range(None, input, window, cx);
2128 }
2129
2130 pub fn compute_ime_candidate_bounds(
2131 marked_range: Option<Range<usize>>,
2132 selection: &UTF16Selection,
2133 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2134 ) -> Option<Bounds<Pixels>> {
2135 if let Some(marked_range) = marked_range {
2136 let mut line_start = marked_range.start;
2138
2139 let caret = selection.range.end;
2143 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2144 for i in (marked_range.start..caret).rev() {
2145 if let Some(b) = bounds_for_range(i..i) {
2146 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2147 line_start = i + 1;
2148 break;
2149 }
2150 }
2151 }
2152 }
2153 bounds_for_range(line_start..line_start)
2154 } else {
2155 let offset = if selection.reversed {
2157 selection.range.start
2158 } else {
2159 selection.range.end
2160 };
2161 bounds_for_range(offset..offset)
2162 }
2163 }
2164
2165 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2166 let marked_range = self.handler.marked_text_range(window, cx);
2167 let selection = self.handler.selected_text_range(true, window, cx)?;
2168 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2169 self.handler.bounds_for_range(range, window, cx)
2170 })
2171 }
2172
2173 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2174 let marked_range = self.marked_text_range();
2175 let selection = self.selected_text_range(true)?;
2176 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2177 self.bounds_for_range(range)
2178 })
2179 }
2180
2181 #[allow(unused)]
2182 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2183 self.cx
2184 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2185 .ok()
2186 .flatten()
2187 }
2188
2189 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2191 self.cx
2192 .update(|window, cx| {
2193 self.handler
2194 .set_selected_text_range(range_utf16, window, cx)
2195 })
2196 .ok();
2197 }
2198
2199 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2201 self.cx
2202 .update(|window, cx| self.handler.element_bounds(window, cx))
2203 .ok()
2204 .flatten()
2205 }
2206
2207 pub fn text_length_utf16(&mut self) -> Option<usize> {
2209 self.cx
2210 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2211 .ok()
2212 .flatten()
2213 }
2214
2215 #[allow(dead_code)]
2216 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2217 self.handler.accepts_text_input(window, cx)
2218 }
2219
2220 #[allow(dead_code)]
2221 pub fn query_accepts_text_input(&mut self) -> bool {
2222 self.cx
2223 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2224 .unwrap_or(true)
2225 }
2226
2227 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2232 let focus_id = self.focus_id;
2233 self.cx
2234 .update(|window, cx| {
2235 focus_id.is_some()
2236 && window.focus == focus_id
2237 && self.handler.accepts_text_input(window, cx)
2238 })
2239 .unwrap_or(false)
2240 }
2241
2242 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2248 self.cx
2249 .update(|window, cx| {
2250 !window.has_pending_keystrokes()
2252 && self.handler.prefers_ime_for_printable_keys(window, cx)
2253 })
2254 .unwrap_or(false)
2255 }
2256
2257 pub fn text_input_configuration(
2259 &mut self,
2260 window: &mut Window,
2261 cx: &mut App,
2262 ) -> TextInputConfiguration {
2263 self.handler.text_input_configuration(window, cx)
2264 }
2265
2266 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2268 self.cx
2269 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2270 .ok()
2271 .flatten()
2272 }
2273}
2274
2275#[derive(Debug)]
2278pub struct UTF16Selection {
2279 pub range: Range<usize>,
2282 pub reversed: bool,
2285}
2286
2287pub trait InputHandler: 'static {
2292 fn selected_text_range(
2297 &mut self,
2298 ignore_disabled_input: bool,
2299 window: &mut Window,
2300 cx: &mut App,
2301 ) -> Option<UTF16Selection>;
2302
2303 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2308
2309 fn text_for_range(
2314 &mut self,
2315 range_utf16: Range<usize>,
2316 adjusted_range: &mut Option<Range<usize>>,
2317 window: &mut Window,
2318 cx: &mut App,
2319 ) -> Option<String>;
2320
2321 fn replace_text_in_range(
2326 &mut self,
2327 replacement_range: Option<Range<usize>>,
2328 text: &str,
2329 window: &mut Window,
2330 cx: &mut App,
2331 );
2332
2333 fn replace_and_mark_text_in_range(
2340 &mut self,
2341 range_utf16: Option<Range<usize>>,
2342 new_text: &str,
2343 new_selected_range: Option<Range<usize>>,
2344 window: &mut Window,
2345 cx: &mut App,
2346 );
2347
2348 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2351
2352 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2359 if let Some(text) = item.text() {
2360 self.replace_text_in_range(None, &text, window, cx);
2361 }
2362 }
2363
2364 fn bounds_for_range(
2369 &mut self,
2370 range_utf16: Range<usize>,
2371 window: &mut Window,
2372 cx: &mut App,
2373 ) -> Option<Bounds<Pixels>>;
2374
2375 fn selection_bounds_for_range(
2382 &mut self,
2383 _range_utf16: Range<usize>,
2384 _window: &mut Window,
2385 _cx: &mut App,
2386 ) -> Vec<Bounds<Pixels>> {
2387 Vec::new()
2388 }
2389
2390 fn character_index_for_point(
2394 &mut self,
2395 point: Point<Pixels>,
2396 window: &mut Window,
2397 cx: &mut App,
2398 ) -> Option<usize>;
2399
2400 fn set_selected_text_range(
2410 &mut self,
2411 _range_utf16: Range<usize>,
2412 _window: &mut Window,
2413 _cx: &mut App,
2414 ) {
2415 }
2416
2417 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2424 None
2425 }
2426
2427 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2429 None
2430 }
2431
2432 #[allow(dead_code)]
2437 fn apple_press_and_hold_enabled(&mut self) -> bool {
2438 true
2439 }
2440
2441 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2443 true
2444 }
2445
2446 fn text_input_editable_range(
2457 &mut self,
2458 _window: &mut Window,
2459 _cx: &mut App,
2460 ) -> Option<Range<usize>> {
2461 None
2462 }
2463
2464 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2473 false
2474 }
2475
2476 fn text_input_configuration(
2482 &mut self,
2483 _window: &mut Window,
2484 _cx: &mut App,
2485 ) -> TextInputConfiguration {
2486 TextInputConfiguration::default()
2487 }
2488}
2489
2490#[derive(Clone, Debug, Default, PartialEq, Eq)]
2500pub struct TextInputConfiguration {
2501 pub autocorrect: bool,
2503 pub autocapitalize: Autocapitalize,
2505 pub suggestions: bool,
2507 pub input_action: TextInputAction,
2509}
2510
2511#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2513pub enum Autocapitalize {
2514 #[default]
2516 None,
2517 Words,
2519 Sentences,
2521 Characters,
2523}
2524
2525#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2535pub enum TextInputAction {
2536 #[default]
2538 Unspecified,
2539 Enter,
2541 Done,
2543 Go,
2545 Next,
2547 Previous,
2549 Search,
2551 Send,
2553}
2554
2555#[derive(Debug)]
2557pub struct WindowOptions {
2558 pub window_bounds: Option<WindowBounds>,
2562
2563 pub titlebar: Option<TitlebarOptions>,
2565
2566 pub focus: bool,
2568
2569 pub show: bool,
2571
2572 pub kind: WindowKind,
2574
2575 pub is_movable: bool,
2579
2580 pub app_owns_titlebar_drag: bool,
2592
2593 pub inactive_frame_interval: Option<Duration>,
2597
2598 pub is_resizable: bool,
2600
2601 pub is_minimizable: bool,
2603
2604 pub display_id: Option<DisplayId>,
2607
2608 pub window_background: WindowBackgroundAppearance,
2610
2611 pub app_id: Option<String>,
2613
2614 pub window_min_size: Option<Size<Pixels>>,
2616
2617 pub window_decorations: Option<WindowDecorations>,
2620
2621 pub icon: Option<Arc<image::RgbaImage>>,
2623
2624 pub tabbing_identifier: Option<String>,
2626
2627 pub use_display_p3: bool,
2629}
2630
2631#[derive(Debug)]
2633#[cfg_attr(
2634 all(
2635 any(target_os = "linux", target_os = "freebsd"),
2636 not(any(feature = "x11", feature = "wayland"))
2637 ),
2638 allow(dead_code)
2639)]
2640#[allow(missing_docs)]
2641pub struct WindowParams {
2642 pub bounds: Bounds<Pixels>,
2643
2644 #[cfg_attr(feature = "wayland", allow(dead_code))]
2646 pub titlebar: Option<TitlebarOptions>,
2647
2648 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2650 pub kind: WindowKind,
2651
2652 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2654 pub is_movable: bool,
2655
2656 #[cfg_attr(
2658 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2659 allow(dead_code)
2660 )]
2661 pub app_owns_titlebar_drag: bool,
2662
2663 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2665 pub is_resizable: bool,
2666
2667 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2669 pub is_minimizable: bool,
2670
2671 #[cfg_attr(
2672 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2673 allow(dead_code)
2674 )]
2675 pub focus: bool,
2676
2677 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2678 pub show: bool,
2679
2680 #[cfg_attr(feature = "wayland", allow(dead_code))]
2682 pub icon: Option<Arc<image::RgbaImage>>,
2683
2684 #[cfg_attr(feature = "wayland", allow(dead_code))]
2685 pub display_id: Option<DisplayId>,
2686
2687 #[cfg_attr(feature = "wayland", allow(dead_code))]
2688 pub app_id: Option<String>,
2689
2690 pub window_min_size: Option<Size<Pixels>>,
2691
2692 #[cfg(target_os = "macos")]
2693 pub tabbing_identifier: Option<String>,
2694
2695 #[cfg(target_os = "macos")]
2696 pub use_display_p3: bool,
2697}
2698
2699#[derive(Debug, Copy, Clone, PartialEq)]
2701pub enum WindowBounds {
2702 Windowed(Bounds<Pixels>),
2704 Maximized(Bounds<Pixels>),
2707 Fullscreen(Bounds<Pixels>),
2710}
2711
2712impl Default for WindowBounds {
2713 fn default() -> Self {
2714 WindowBounds::Windowed(Bounds::default())
2715 }
2716}
2717
2718impl WindowBounds {
2719 pub fn get_bounds(&self) -> Bounds<Pixels> {
2721 match self {
2722 WindowBounds::Windowed(bounds) => *bounds,
2723 WindowBounds::Maximized(bounds) => *bounds,
2724 WindowBounds::Fullscreen(bounds) => *bounds,
2725 }
2726 }
2727
2728 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2730 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2731 }
2732}
2733
2734impl Default for WindowOptions {
2735 fn default() -> Self {
2736 Self {
2737 window_bounds: None,
2738 titlebar: Some(TitlebarOptions {
2739 title: Default::default(),
2740 appears_transparent: Default::default(),
2741 traffic_light_position: Default::default(),
2742 }),
2743 focus: true,
2744 show: true,
2745 kind: WindowKind::Normal,
2746 is_movable: true,
2747 app_owns_titlebar_drag: false,
2748 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2749 is_resizable: true,
2750 is_minimizable: true,
2751 display_id: None,
2752 window_background: WindowBackgroundAppearance::default(),
2753 icon: None,
2754 app_id: None,
2755 window_min_size: None,
2756 window_decorations: None,
2757 tabbing_identifier: None,
2758 use_display_p3: false,
2759 }
2760 }
2761}
2762
2763#[derive(Debug, Default)]
2765pub struct TitlebarOptions {
2766 pub title: Option<SharedString>,
2768
2769 pub appears_transparent: bool,
2772
2773 pub traffic_light_position: Option<Point<Pixels>>,
2775}
2776
2777#[derive(Clone, Debug, PartialEq, Eq)]
2779pub enum WindowKind {
2780 Normal,
2782
2783 PopUp,
2786
2787 AnchoredPopup(popup::PopupOptions),
2794
2795 Floating,
2797
2798 #[cfg(all(target_os = "linux", feature = "wayland"))]
2801 LayerShell(layer_shell::LayerShellOptions),
2802
2803 Dialog,
2806}
2807
2808#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2813pub enum WindowAppearance {
2814 #[default]
2818 Light,
2819
2820 VibrantLight,
2824
2825 Dark,
2829
2830 VibrantDark,
2834}
2835
2836#[derive(Copy, Clone, Debug, Default, PartialEq)]
2839pub enum WindowBackgroundAppearance {
2840 #[default]
2848 Opaque,
2849 Transparent,
2851 Blurred,
2855 MicaBackdrop,
2857 MicaAltBackdrop,
2859}
2860
2861#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2863pub enum TextRenderingMode {
2864 #[default]
2866 PlatformDefault,
2867 Subpixel,
2869 Grayscale,
2871}
2872
2873#[derive(Clone, Debug)]
2875pub struct PathPromptOptions {
2876 pub files: bool,
2878 pub directories: bool,
2880 pub multiple: bool,
2882 pub prompt: Option<SharedString>,
2884}
2885
2886#[derive(Copy, Clone, Debug, PartialEq)]
2888pub enum PromptLevel {
2889 Info,
2891
2892 Warning,
2894
2895 Critical,
2897}
2898
2899#[derive(Clone, Debug, PartialEq)]
2901pub enum PromptButton {
2902 Ok(SharedString),
2904 Cancel(SharedString),
2906 Other(SharedString),
2908}
2909
2910impl PromptButton {
2911 pub fn new(label: impl Into<SharedString>) -> Self {
2913 PromptButton::Other(label.into())
2914 }
2915
2916 pub fn ok(label: impl Into<SharedString>) -> Self {
2918 PromptButton::Ok(label.into())
2919 }
2920
2921 pub fn cancel(label: impl Into<SharedString>) -> Self {
2923 PromptButton::Cancel(label.into())
2924 }
2925
2926 #[allow(dead_code)]
2928 pub fn is_cancel(&self) -> bool {
2929 matches!(self, PromptButton::Cancel(_))
2930 }
2931
2932 pub fn label(&self) -> &SharedString {
2934 match self {
2935 PromptButton::Ok(label) => label,
2936 PromptButton::Cancel(label) => label,
2937 PromptButton::Other(label) => label,
2938 }
2939 }
2940}
2941
2942impl From<&str> for PromptButton {
2943 fn from(value: &str) -> Self {
2944 match value.to_lowercase().as_str() {
2945 "ok" => PromptButton::Ok("OK".into()),
2946 "cancel" => PromptButton::Cancel("Cancel".into()),
2947 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2948 }
2949 }
2950}
2951
2952#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2954pub enum CursorStyle {
2955 #[default]
2957 Arrow,
2958
2959 IBeam,
2962
2963 Crosshair,
2966
2967 ClosedHand,
2970
2971 OpenHand,
2974
2975 PointingHand,
2978
2979 ResizeLeft,
2982
2983 ResizeRight,
2986
2987 ResizeLeftRight,
2990
2991 ResizeUp,
2994
2995 ResizeDown,
2998
2999 ResizeUpDown,
3002
3003 ResizeUpLeftDownRight,
3006
3007 ResizeUpRightDownLeft,
3010
3011 ResizeColumn,
3014
3015 ResizeRow,
3018
3019 IBeamCursorForVerticalLayout,
3022
3023 OperationNotAllowed,
3026
3027 DragLink,
3030
3031 DragCopy,
3034
3035 ContextualMenu,
3038}
3039
3040#[derive(Clone, Debug, Eq, PartialEq)]
3042pub struct ClipboardItem {
3043 pub entries: Vec<ClipboardEntry>,
3045}
3046
3047#[derive(Clone, Debug, PartialEq, Eq)]
3052pub enum ClipboardReadError {
3053 Unavailable,
3057 Denied(String),
3060 UnsupportedContent,
3063}
3064
3065impl std::fmt::Display for ClipboardReadError {
3066 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3067 match self {
3068 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3069 Self::Denied(message) => {
3070 write!(formatter, "clipboard access was denied: {message}")
3071 }
3072 Self::UnsupportedContent => {
3073 formatter.write_str("the clipboard contents are unsupported")
3074 }
3075 }
3076 }
3077}
3078
3079impl std::error::Error for ClipboardReadError {}
3080
3081#[derive(Clone, Debug, Eq, PartialEq)]
3083pub enum ClipboardEntry {
3084 String(ClipboardString),
3086 Image(Image),
3088 ExternalPaths(crate::ExternalPaths),
3090}
3091
3092impl ClipboardItem {
3093 pub fn new_string(text: String) -> Self {
3095 Self {
3096 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3097 }
3098 }
3099
3100 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3102 Self {
3103 entries: vec![ClipboardEntry::String(ClipboardString {
3104 text,
3105 metadata: Some(metadata),
3106 })],
3107 }
3108 }
3109
3110 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3112 Self {
3113 entries: vec![ClipboardEntry::String(
3114 ClipboardString::new(text).with_json_metadata(metadata),
3115 )],
3116 }
3117 }
3118
3119 pub fn new_image(image: &Image) -> Self {
3121 Self {
3122 entries: vec![ClipboardEntry::Image(image.clone())],
3123 }
3124 }
3125
3126 pub fn text(&self) -> Option<String> {
3129 let mut answer = String::new();
3130
3131 for entry in self.entries.iter() {
3132 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3133 answer.push_str(text);
3134 }
3135 }
3136
3137 if answer.is_empty() {
3138 for entry in self.entries.iter() {
3139 if let ClipboardEntry::ExternalPaths(paths) = entry {
3140 for path in &paths.0 {
3141 use std::fmt::Write as _;
3142 _ = write!(answer, "{}", path.display());
3143 }
3144 }
3145 }
3146 }
3147
3148 if !answer.is_empty() {
3149 Some(answer)
3150 } else {
3151 None
3152 }
3153 }
3154
3155 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3157 pub fn metadata(&self) -> Option<&String> {
3158 match self.entries().first() {
3159 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3160 clipboard_string.metadata.as_ref()
3161 }
3162 _ => None,
3163 }
3164 }
3165
3166 pub fn entries(&self) -> &[ClipboardEntry] {
3168 &self.entries
3169 }
3170
3171 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3173 self.entries.into_iter()
3174 }
3175}
3176
3177impl From<ClipboardString> for ClipboardEntry {
3178 fn from(value: ClipboardString) -> Self {
3179 Self::String(value)
3180 }
3181}
3182
3183impl From<String> for ClipboardEntry {
3184 fn from(value: String) -> Self {
3185 Self::from(ClipboardString::from(value))
3186 }
3187}
3188
3189impl From<Image> for ClipboardEntry {
3190 fn from(value: Image) -> Self {
3191 Self::Image(value)
3192 }
3193}
3194
3195impl From<ClipboardEntry> for ClipboardItem {
3196 fn from(value: ClipboardEntry) -> Self {
3197 Self {
3198 entries: vec![value],
3199 }
3200 }
3201}
3202
3203impl From<String> for ClipboardItem {
3204 fn from(value: String) -> Self {
3205 Self::from(ClipboardEntry::from(value))
3206 }
3207}
3208
3209impl From<Image> for ClipboardItem {
3210 fn from(value: Image) -> Self {
3211 Self::from(ClipboardEntry::from(value))
3212 }
3213}
3214
3215#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3217pub enum ImageFormat {
3218 Png,
3223 Jpeg,
3225 Webp,
3227 Gif,
3229 Svg,
3231 Bmp,
3233 Tiff,
3235 Ico,
3237 Pnm,
3239}
3240
3241impl ImageFormat {
3242 pub const fn mime_type(self) -> &'static str {
3244 match self {
3245 ImageFormat::Png => "image/png",
3246 ImageFormat::Jpeg => "image/jpeg",
3247 ImageFormat::Webp => "image/webp",
3248 ImageFormat::Gif => "image/gif",
3249 ImageFormat::Svg => "image/svg+xml",
3250 ImageFormat::Bmp => "image/bmp",
3251 ImageFormat::Tiff => "image/tiff",
3252 ImageFormat::Ico => "image/ico",
3253 ImageFormat::Pnm => "image/x-portable-anymap",
3254 }
3255 }
3256
3257 pub const fn extension(self) -> &'static str {
3259 match self {
3260 ImageFormat::Png => "png",
3261 ImageFormat::Jpeg => "jpg",
3262 ImageFormat::Webp => "webp",
3263 ImageFormat::Gif => "gif",
3264 ImageFormat::Svg => "svg",
3265 ImageFormat::Bmp => "bmp",
3266 ImageFormat::Tiff => "tiff",
3267 ImageFormat::Ico => "ico",
3268 ImageFormat::Pnm => "pnm",
3269 }
3270 }
3271
3272 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3274 use strum::IntoEnumIterator;
3275 Self::iter()
3276 .find(|format| format.mime_type() == mime_type)
3277 .or_else(|| Self::from_mime_type_alias(mime_type))
3278 }
3279
3280 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3284 match mime_type {
3285 "image/jpg" => Some(Self::Jpeg),
3286 "image/tif" => Some(Self::Tiff),
3287 _ => None,
3288 }
3289 }
3290}
3291
3292#[derive(Clone, Debug, PartialEq, Eq)]
3294pub struct Image {
3295 pub format: ImageFormat,
3297 pub bytes: Vec<u8>,
3299 pub id: u64,
3301}
3302
3303pub(crate) fn decode_static_image(
3304 bytes: &[u8],
3305 format: image::ImageFormat,
3306) -> Result<SmallVec<[Frame; 1]>> {
3307 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3308 .into_decoder()
3309 .context("creating image decoder")?;
3310 decode_static_image_from_decoder(decoder)
3311}
3312
3313pub(crate) fn decode_static_image_from_decoder(
3314 mut decoder: impl image::ImageDecoder,
3315) -> Result<SmallVec<[Frame; 1]>> {
3316 let orientation = decoder
3317 .orientation()
3318 .context("reading decoder's orientation")?;
3319 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3320 image.apply_orientation(orientation);
3321
3322 let mut data = image.into_rgba8();
3323 for pixel in data.chunks_exact_mut(4) {
3324 pixel.swap(0, 2);
3325 }
3326
3327 Ok(SmallVec::from_elem(Frame::new(data), 1))
3328}
3329
3330impl Hash for Image {
3331 fn hash<H: Hasher>(&self, state: &mut H) {
3332 state.write_u64(self.id);
3333 }
3334}
3335
3336impl Image {
3337 pub fn empty() -> Self {
3339 Self::from_bytes(ImageFormat::Png, Vec::new())
3340 }
3341
3342 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3344 Self {
3345 id: hash(&bytes),
3346 format,
3347 bytes,
3348 }
3349 }
3350
3351 pub fn id(&self) -> u64 {
3353 self.id
3354 }
3355
3356 pub fn use_render_image(
3358 self: Arc<Self>,
3359 window: &mut Window,
3360 cx: &mut App,
3361 ) -> Option<Arc<RenderImage>> {
3362 ImageSource::Image(self)
3363 .use_data(None, window, cx)
3364 .and_then(|result| result.ok())
3365 }
3366
3367 pub fn get_render_image(
3369 self: Arc<Self>,
3370 window: &mut Window,
3371 cx: &mut App,
3372 ) -> Option<Arc<RenderImage>> {
3373 ImageSource::Image(self)
3374 .get_data(None, window, cx)
3375 .and_then(|result| result.ok())
3376 }
3377
3378 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3380 ImageSource::Image(self).remove_asset(cx);
3381 }
3382
3383 #[cfg(any(test, feature = "test-support"))]
3386 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3387 ImageSource::Image(self.clone()).is_asset_cached(cx)
3388 }
3389
3390 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3392 let frames = match self.format {
3393 ImageFormat::Gif => {
3394 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3395 let mut frames = SmallVec::new();
3396
3397 for frame in decoder.into_frames() {
3398 match frame {
3399 Ok(mut frame) => {
3400 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3402 pixel.swap(0, 2);
3403 }
3404 frames.push(frame);
3405 }
3406 Err(err) => {
3407 log::debug!("Skipping GIF frame due to decode error: {err}");
3408 }
3409 }
3410 }
3411
3412 if frames.is_empty() {
3413 anyhow::bail!("GIF could not be decoded: all frames failed");
3414 }
3415
3416 frames
3417 }
3418 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3419 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3420 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3421 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3422 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3423 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3424 ImageFormat::Svg => {
3425 return svg_renderer
3426 .render_single_frame(&self.bytes, 1.0)
3427 .map_err(Into::into);
3428 }
3429 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3430 };
3431
3432 Ok(Arc::new(RenderImage::new(frames)))
3433 }
3434
3435 pub fn format(&self) -> ImageFormat {
3437 self.format
3438 }
3439
3440 pub fn bytes(&self) -> &[u8] {
3442 self.bytes.as_slice()
3443 }
3444}
3445
3446#[derive(Clone, Debug, Eq, PartialEq)]
3448pub struct ClipboardString {
3449 pub text: String,
3451 pub metadata: Option<String>,
3453}
3454
3455impl ClipboardString {
3456 pub fn new(text: String) -> Self {
3458 Self {
3459 text,
3460 metadata: None,
3461 }
3462 }
3463
3464 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3467 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3468 self
3469 }
3470
3471 pub fn text(&self) -> &String {
3473 &self.text
3474 }
3475
3476 pub fn into_text(self) -> String {
3478 self.text
3479 }
3480
3481 pub fn metadata_json<T>(&self) -> Option<T>
3483 where
3484 T: for<'a> Deserialize<'a>,
3485 {
3486 self.metadata
3487 .as_ref()
3488 .and_then(|m| serde_json::from_str(m).ok())
3489 }
3490
3491 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3492 pub fn text_hash(text: &str) -> u64 {
3494 let mut hasher = SeaHasher::new();
3495 text.hash(&mut hasher);
3496 hasher.finish()
3497 }
3498}
3499
3500impl From<String> for ClipboardString {
3501 fn from(value: String) -> Self {
3502 Self {
3503 text: value,
3504 metadata: None,
3505 }
3506 }
3507}
3508
3509#[cfg(test)]
3510mod image_tests {
3511 use super::*;
3512 use std::sync::Arc;
3513
3514 #[test]
3515 fn test_image_to_image_data_applies_exif_orientation() {
3516 let image = Image::from_bytes(
3517 ImageFormat::Jpeg,
3518 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3519 );
3520
3521 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3522
3523 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3524
3525 let bytes = render_image.as_bytes(0).unwrap();
3526 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3527 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3528 }
3529
3530 #[test]
3531 fn test_svg_image_to_image_data_converts_to_bgra() {
3532 let image = Image::from_bytes(
3533 ImageFormat::Svg,
3534 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3535<rect width="1" height="1" fill="#38BDF8"/>
3536</svg>"##
3537 .to_vec(),
3538 );
3539
3540 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3541 let bytes = render_image.as_bytes(0).unwrap();
3542
3543 for pixel in bytes.chunks_exact(4) {
3544 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3545 }
3546 }
3547}
3548
3549#[cfg(test)]
3550mod atlas_tests {
3551 use super::*;
3552
3553 const TILE_SIZE: Size<DevicePixels> = Size {
3554 width: DevicePixels(1),
3555 height: DevicePixels(1),
3556 };
3557
3558 #[derive(Default)]
3559 struct RecordingAtlasBackend {
3560 insert_calls: u32,
3561 fail_next_insert: bool,
3562 removed_tiles: Vec<AtlasTile>,
3563 }
3564
3565 impl AtlasBackend for RecordingAtlasBackend {
3566 fn insert(
3567 &mut self,
3568 kind: AtlasTextureKind,
3569 size: Size<DevicePixels>,
3570 _bytes: &[u8],
3571 ) -> Result<AtlasTile> {
3572 self.insert_calls += 1;
3573 if std::mem::take(&mut self.fail_next_insert) {
3574 anyhow::bail!("backend failed");
3575 }
3576 Ok(AtlasTile {
3577 texture_id: AtlasTextureId { index: 0, kind },
3578 tile_id: TileId(self.insert_calls),
3579 padding: 0,
3580 bounds: Bounds {
3581 origin: Point::default(),
3582 size,
3583 },
3584 })
3585 }
3586
3587 fn remove(&mut self, tile: AtlasTile) {
3588 self.removed_tiles.push(tile);
3589 }
3590 }
3591
3592 fn image_key(image_id: usize) -> AtlasKey {
3593 AtlasKey::Image(RenderImageParams {
3594 image_id: crate::ImageId(image_id),
3595 frame_index: 0,
3596 })
3597 }
3598
3599 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3600 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3601 }
3602
3603 #[test]
3604 fn only_successful_inserts_are_cached() -> Result<()> {
3605 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3606 let key = image_key(1);
3607
3608 assert_eq!(
3609 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3610 None
3611 );
3612 state
3613 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3614 .expect_err("builder error should propagate");
3615 assert!(!state.contains(&key));
3616 assert_eq!(state.backend.insert_calls, 0);
3617
3618 state.backend.fail_next_insert = true;
3619 state
3620 .get_or_insert_with(key.clone(), &mut build_tile)
3621 .expect_err("backend error should propagate");
3622 assert!(!state.contains(&key));
3623 assert_eq!(state.backend.insert_calls, 1);
3624
3625 let tile = state
3626 .get_or_insert_with(key.clone(), &mut build_tile)?
3627 .context("builder should produce a tile")?;
3628 assert_eq!(tile.texture_id.kind, key.texture_kind());
3629 assert_eq!(
3630 state.get_or_insert_with(key.clone(), &mut || {
3631 anyhow::bail!("cache hit must not call the builder")
3632 })?,
3633 Some(tile)
3634 );
3635 assert!(state.contains(&key));
3636 assert_eq!(state.backend.insert_calls, 2);
3637 Ok(())
3638 }
3639
3640 #[test]
3641 fn remove_and_clear_invalidate_keys() -> Result<()> {
3642 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3643 let key = image_key(1);
3644 let other_key = image_key(2);
3645 let tile = state
3646 .get_or_insert_with(key.clone(), &mut build_tile)?
3647 .context("builder should produce a tile")?;
3648 state
3649 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3650 .context("builder should produce another tile")?;
3651
3652 state.remove(&key);
3653 state.remove(&key);
3654 assert!(!state.contains(&key));
3655 assert!(state.contains(&other_key));
3656 assert_eq!(state.backend.removed_tiles, vec![tile]);
3657
3658 let mut reset_calls = 0;
3659 state.clear(|_| reset_calls += 1);
3660 assert_eq!(reset_calls, 1);
3661 assert!(!state.contains(&other_key));
3662 assert_eq!(state.backend.removed_tiles, vec![tile]);
3663 Ok(())
3664 }
3665}
3666
3667#[cfg(test)]
3668mod frame_signal_tests {
3669 use super::*;
3670
3671 #[cfg(feature = "profiler")]
3672 #[test]
3673 fn coalesced_signals_retain_the_earliest_until_drained() {
3674 for (first_source, later_source) in [
3675 (
3676 FrameRequestSource::NativeCallback,
3677 FrameRequestSource::LocalSchedule,
3678 ),
3679 (
3680 FrameRequestSource::LocalSchedule,
3681 FrameRequestSource::NativeCallback,
3682 ),
3683 ] {
3684 let signal = PlatformFrameSignal::new();
3685 let first =
3686 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3687 assert_eq!(signal.take(), None);
3688 signal.record(first + Duration::from_millis(16), later_source);
3689 signal.record(first, first_source);
3690 signal.record(first + Duration::from_millis(32), later_source);
3691 assert_eq!(signal.take(), Some((first, first_source)));
3692 assert_eq!(signal.take(), None);
3693 let next = first + Duration::from_millis(48);
3694 signal.record(next, later_source);
3695 assert_eq!(signal.take(), Some((next, later_source)));
3696 }
3697 }
3698
3699 #[cfg(not(feature = "profiler"))]
3700 #[test]
3701 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3702 let captured = PlatformFrameSignal::capture(|| {
3703 panic!("profiler-disabled builds must not evaluate the capture closure");
3704 });
3705 assert_eq!(captured, None);
3706 let signal = PlatformFrameSignal::new();
3707 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3708 assert_eq!(signal.take(), None);
3709 }
3710}
3711
3712#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3713mod tests {
3714 use super::*;
3715 use std::collections::HashSet;
3716
3717 #[test]
3718 fn test_refresh_interval_from_hz() {
3719 assert_eq!(
3720 refresh_interval_from_hz(50.0),
3721 Some(Duration::from_millis(20))
3722 );
3723 assert_eq!(
3724 refresh_interval_from_hz(120.0),
3725 Some(Duration::from_secs(1) / 120)
3726 );
3727 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3728 assert_eq!(refresh_interval_from_hz(unknown), None);
3729 }
3730 }
3731
3732 #[test]
3733 fn test_window_button_layout_parse_standard() {
3734 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3735 assert_eq!(
3736 layout.left,
3737 [
3738 Some(WindowButton::Close),
3739 Some(WindowButton::Minimize),
3740 None
3741 ]
3742 );
3743 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3744 }
3745
3746 #[test]
3747 fn test_window_button_layout_parse_right_only() {
3748 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3749 assert_eq!(layout.left, [None, None, None]);
3750 assert_eq!(
3751 layout.right,
3752 [
3753 Some(WindowButton::Minimize),
3754 Some(WindowButton::Maximize),
3755 Some(WindowButton::Close)
3756 ]
3757 );
3758 }
3759
3760 #[test]
3761 fn test_window_button_layout_parse_left_only() {
3762 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3763 assert_eq!(
3764 layout.left,
3765 [
3766 Some(WindowButton::Close),
3767 Some(WindowButton::Minimize),
3768 Some(WindowButton::Maximize)
3769 ]
3770 );
3771 assert_eq!(layout.right, [None, None, None]);
3772 }
3773
3774 #[test]
3775 fn test_window_button_layout_parse_with_whitespace() {
3776 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3777 assert_eq!(
3778 layout.left,
3779 [
3780 Some(WindowButton::Close),
3781 Some(WindowButton::Minimize),
3782 None
3783 ]
3784 );
3785 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3786 }
3787
3788 #[test]
3789 fn test_window_button_layout_parse_empty() {
3790 let layout = WindowButtonLayout::parse("").unwrap();
3791 assert_eq!(layout.left, [None, None, None]);
3792 assert_eq!(layout.right, [None, None, None]);
3793 }
3794
3795 #[test]
3796 fn test_window_button_layout_parse_intentionally_empty() {
3797 let layout = WindowButtonLayout::parse(":").unwrap();
3798 assert_eq!(layout.left, [None, None, None]);
3799 assert_eq!(layout.right, [None, None, None]);
3800 }
3801
3802 #[test]
3803 fn test_window_button_layout_parse_invalid_buttons() {
3804 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3805 assert_eq!(
3806 layout.left,
3807 [
3808 Some(WindowButton::Close),
3809 Some(WindowButton::Minimize),
3810 None
3811 ]
3812 );
3813 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3814 }
3815
3816 #[test]
3817 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3818 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3819 assert_eq!(
3820 layout.right,
3821 [
3822 Some(WindowButton::Close),
3823 Some(WindowButton::Minimize),
3824 None
3825 ]
3826 );
3827 assert_eq!(layout.format(), ":close,minimize");
3828 }
3829
3830 #[test]
3831 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3832 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3833 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3834 assert_eq!(
3835 layout.right,
3836 [
3837 Some(WindowButton::Maximize),
3838 Some(WindowButton::Minimize),
3839 None
3840 ]
3841 );
3842
3843 let button_ids: Vec<_> = layout
3844 .left
3845 .iter()
3846 .chain(layout.right.iter())
3847 .flatten()
3848 .map(WindowButton::id)
3849 .collect();
3850 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3851 assert_eq!(unique_button_ids.len(), button_ids.len());
3852 assert_eq!(layout.format(), "close:maximize,minimize");
3853 }
3854
3855 #[test]
3856 fn test_window_button_layout_parse_gnome_style() {
3857 let layout = WindowButtonLayout::parse("close").unwrap();
3858 assert_eq!(layout.left, [None, None, None]);
3859 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3860 }
3861
3862 #[test]
3863 fn test_window_button_layout_parse_elementary_style() {
3864 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3865 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3866 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3867 }
3868
3869 #[test]
3870 fn test_window_button_layout_round_trip() {
3871 let cases = [
3872 "close:minimize,maximize",
3873 "minimize,maximize,close:",
3874 ":close",
3875 "close:",
3876 "close:maximize",
3877 ":",
3878 ];
3879
3880 for case in cases {
3881 let layout = WindowButtonLayout::parse(case).unwrap();
3882 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3883 }
3884 }
3885
3886 #[test]
3887 fn test_window_button_layout_linux_default() {
3888 let layout = WindowButtonLayout::linux_default();
3889 assert_eq!(layout.left, [None, None, None]);
3890 assert_eq!(
3891 layout.right,
3892 [
3893 Some(WindowButton::Minimize),
3894 Some(WindowButton::Maximize),
3895 Some(WindowButton::Close)
3896 ]
3897 );
3898
3899 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3900 assert_eq!(round_tripped, layout);
3901 }
3902
3903 #[test]
3904 fn test_window_button_layout_parse_all_invalid() {
3905 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3906 }
3907}