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,
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}
1147
1148#[derive(Debug, Clone, Default, PartialEq)]
1155pub struct WindowInsets {
1156 pub safe_area: Edges<Pixels>,
1161 pub ime: Edges<Pixels>,
1165}
1166
1167impl WindowInsets {
1168 pub fn effective(&self) -> Edges<Pixels> {
1170 Edges {
1171 top: self.safe_area.top.max(self.ime.top),
1172 right: self.safe_area.right.max(self.ime.right),
1173 bottom: self.safe_area.bottom.max(self.ime.bottom),
1174 left: self.safe_area.left.max(self.ime.left),
1175 }
1176 }
1177}
1178
1179#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1181pub enum TextInputStateChange {
1182 FocusGained,
1184 FocusLost,
1186 SelectionChanged,
1188 ContentChanged,
1190}
1191
1192#[expect(missing_docs)]
1193pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1194 fn bounds(&self) -> Bounds<Pixels>;
1195 fn is_maximized(&self) -> bool;
1196 fn window_bounds(&self) -> WindowBounds;
1197 fn content_size(&self) -> Size<Pixels>;
1198 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1204 Bounds::new(Point::default(), self.content_size())
1205 }
1206 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1211 fn prepare_frame(&self) -> bool {
1216 false
1217 }
1218 fn resize(&mut self, size: Size<Pixels>);
1219 fn scale_factor(&self) -> f32;
1220 fn appearance(&self) -> WindowAppearance;
1221 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1222 fn mouse_position(&self) -> Point<Pixels>;
1223 fn modifiers(&self) -> Modifiers;
1224 fn capslock(&self) -> Capslock;
1225 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1226 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1227 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1232 fn prompt(
1233 &self,
1234 level: PromptLevel,
1235 msg: &str,
1236 detail: Option<&str>,
1237 answers: &[PromptButton],
1238 ) -> Option<oneshot::Receiver<usize>>;
1239 fn activate(&self);
1240 fn request_attention(&self) {}
1242 fn is_active(&self) -> bool;
1243 fn visibility(&self) -> WindowVisibility;
1246 fn is_hovered(&self) -> bool;
1247 fn background_appearance(&self) -> WindowBackgroundAppearance;
1248 fn set_title(&mut self, title: &str);
1249 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1250 fn minimize(&self);
1251 fn zoom(&self);
1252 fn toggle_fullscreen(&self);
1253 fn is_fullscreen(&self) -> bool;
1254 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1255 None
1256 }
1257 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1258 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1259 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1260 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1264 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1265 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1266 fn on_moved(&self, callback: Box<dyn FnMut()>);
1267 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1271 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1272 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1273 fn on_close(&self, callback: Box<dyn FnOnce()>);
1274 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1275 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1276 fn draw(&self, scene: &Scene);
1277 fn schedule_frame(&self) {}
1278 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1279 fn is_subpixel_rendering_supported(&self) -> bool;
1280
1281 fn get_title(&self) -> String {
1283 String::new()
1284 }
1285 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1286 None
1287 }
1288 fn tab_bar_visible(&self) -> bool {
1289 false
1290 }
1291 fn set_edited(&mut self, _edited: bool) {}
1292 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1293 fn toggle_simple_fullscreen(&self) {}
1294 fn is_simple_fullscreen(&self) -> bool {
1295 false
1296 }
1297 #[cfg(target_os = "macos")]
1298 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1299 fn show_character_palette(&self) {}
1300 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1301 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1302 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1303 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1304 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1305 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1306 fn merge_all_windows(&self) {}
1307 fn move_tab_to_new_window(&self) {}
1308 fn toggle_window_tab_overview(&self) {}
1309 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1310
1311 fn native_window_state(&self) -> Option<Vec<u8>> {
1312 None
1313 }
1314 fn restore_native_window_state(&self, _state: &[u8]) {}
1315
1316 #[cfg(target_os = "windows")]
1317 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1318
1319 fn inner_window_bounds(&self) -> WindowBounds {
1321 self.window_bounds()
1322 }
1323 fn request_decorations(&self, _decorations: WindowDecorations) {}
1324 fn show_window_menu(&self, _position: Point<Pixels>) {}
1325 fn start_window_move(&self) {}
1326 fn can_start_external_drag(&self) -> bool {
1327 false
1328 }
1329 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1330 false
1331 }
1332 fn start_window_resize(&self, _edge: ResizeEdge) {}
1333 fn set_exclusive_zone(&self, _zone: Pixels) {}
1334 #[cfg(all(target_os = "linux", feature = "wayland"))]
1335 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1336 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1337 fn window_decorations(&self) -> Decorations {
1338 Decorations::Server
1339 }
1340 fn set_app_id(&mut self, _app_id: &str) {}
1341 fn map_window(&mut self) -> anyhow::Result<()> {
1342 Ok(())
1343 }
1344 fn window_controls(&self) -> WindowControls {
1345 WindowControls::default()
1346 }
1347 fn set_client_inset(&self, _inset: Pixels) {}
1348 fn gpu_specs(&self) -> Option<GpuSpecs>;
1349
1350 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1351
1352 fn insets(&self) -> WindowInsets {
1357 WindowInsets::default()
1358 }
1359
1360 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1366
1367 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1370
1371 fn set_back_enabled(&self, _enabled: bool) {}
1374
1375 fn show_soft_keyboard(&self) {}
1377
1378 fn hide_soft_keyboard(&self) {}
1380
1381 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1383
1384 fn play_system_bell(&self) {}
1385
1386 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1388
1389 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1391
1392 fn a11y_update_window_bounds(&self) {}
1394
1395 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1396 fn as_test(&mut self) -> Option<&mut TestWindow> {
1397 None
1398 }
1399
1400 #[cfg(any(test, feature = "test-support"))]
1404 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1405 anyhow::bail!("render_to_image not implemented for this platform")
1406 }
1407}
1408
1409#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1411pub trait PlatformHeadlessRenderer {
1412 fn render_scene_to_image(
1414 &mut self,
1415 scene: &Scene,
1416 size: Size<DevicePixels>,
1417 ) -> Result<RgbaImage>;
1418
1419 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1425
1426 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1428}
1429
1430#[doc(hidden)]
1433pub type RunnableVariant = Runnable<RunnableMeta>;
1434
1435#[doc(hidden)]
1436pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1437
1438#[doc(hidden)]
1439pub enum TasksIncluded {
1440 OnlyCompleted,
1441 CompletedAndRunning,
1442}
1443
1444#[doc(hidden)]
1447pub trait PlatformDispatcher: Send + Sync {
1448 fn is_main_thread(&self) -> bool;
1449 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1450 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1451 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1452
1453 fn dispatch_on_main_thread_when_idle(
1454 &self,
1455 runnable: RunnableVariant,
1456 timeout: Option<Duration>,
1457 ) {
1458 let _ = timeout;
1459 self.dispatch_on_main_thread(runnable, Priority::Low);
1460 }
1461
1462 fn idle_time_remaining(&self) -> Option<Duration> {
1463 None
1464 }
1465
1466 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1467
1468 fn now(&self) -> Instant {
1469 Instant::now()
1470 }
1471
1472 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1473 gpui_util::defer(Box::new(|| {}))
1474 }
1475
1476 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1477 ActivityGuard::noop()
1478 }
1479
1480 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1481 fn as_test(&self) -> Option<&TestDispatcher> {
1482 None
1483 }
1484
1485 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1488 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1489 None
1490 }
1491}
1492
1493#[expect(missing_docs)]
1494pub trait PlatformTextSystem: Send + Sync {
1495 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1496 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1498 fn all_font_names(&self) -> Vec<String>;
1500 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1502 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1504 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1506 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1508 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1510 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1512 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1514 fn rasterize_glyph(
1516 &self,
1517 params: &RenderGlyphParams,
1518 raster_bounds: Bounds<DevicePixels>,
1519 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1520 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1522 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1524 -> TextRenderingMode;
1525 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1527 0
1528 }
1529}
1530
1531#[expect(missing_docs)]
1532pub struct NoopTextSystem;
1533
1534#[expect(missing_docs)]
1535impl NoopTextSystem {
1536 #[allow(dead_code)]
1537 pub fn new() -> Self {
1538 Self
1539 }
1540}
1541
1542impl PlatformTextSystem for NoopTextSystem {
1543 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1544 Ok(())
1545 }
1546
1547 fn all_font_names(&self) -> Vec<String> {
1548 Vec::new()
1549 }
1550
1551 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1552 Ok(FontId(1))
1553 }
1554
1555 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1556 FontMetrics {
1557 units_per_em: 1000,
1558 ascent: 1025.0,
1559 descent: -275.0,
1560 line_gap: 0.0,
1561 underline_position: -95.0,
1562 underline_thickness: 60.0,
1563 cap_height: 698.0,
1564 x_height: 516.0,
1565 bounding_box: Bounds {
1566 origin: Point {
1567 x: -260.0,
1568 y: -245.0,
1569 },
1570 size: Size {
1571 width: 1501.0,
1572 height: 1364.0,
1573 },
1574 },
1575 }
1576 }
1577
1578 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1579 Ok(Bounds {
1580 origin: Point { x: 54.0, y: 0.0 },
1581 size: size(392.0, 528.0),
1582 })
1583 }
1584
1585 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1586 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1587 }
1588
1589 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1590 Some(GlyphId(ch.len_utf16() as u32))
1591 }
1592
1593 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1594 Ok(Default::default())
1595 }
1596
1597 fn rasterize_glyph(
1598 &self,
1599 _params: &RenderGlyphParams,
1600 raster_bounds: Bounds<DevicePixels>,
1601 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1602 Ok((raster_bounds.size, Vec::new()))
1603 }
1604
1605 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1606 let mut position = px(0.);
1607 let metrics = self.font_metrics(FontId(0));
1608 let em_width = font_size
1609 * self
1610 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1611 .unwrap()
1612 .width
1613 / metrics.units_per_em as f32;
1614 let mut glyphs = Vec::new();
1615 for (ix, c) in text.char_indices() {
1616 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1617 glyphs.push(ShapedGlyph {
1618 id: glyph,
1619 position: point(position, px(0.)),
1620 index: ix,
1621 is_emoji: glyph.0 == 2,
1622 });
1623 if glyph.0 == 2 {
1624 position += em_width * 2.0;
1625 } else {
1626 position += em_width;
1627 }
1628 } else {
1629 position += em_width
1630 }
1631 }
1632 let mut runs = Vec::default();
1633 if !glyphs.is_empty() {
1634 runs.push(ShapedRun {
1635 font_id: FontId(0),
1636 glyphs,
1637 });
1638 } else {
1639 position = px(0.);
1640 }
1641
1642 LineLayout {
1643 font_size,
1644 width: position,
1645 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1646 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1647 runs,
1648 len: text.len(),
1649 }
1650 }
1651
1652 fn recommended_rendering_mode(
1653 &self,
1654 _font_id: FontId,
1655 _font_size: Pixels,
1656 ) -> TextRenderingMode {
1657 TextRenderingMode::Grayscale
1658 }
1659}
1660
1661#[allow(dead_code)]
1666pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1667 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1668 [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], ];
1682
1683 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1684 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1685
1686 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1687 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1688
1689 [
1690 ratios[0] * NORM13,
1691 ratios[1] * NORM24,
1692 ratios[2] * NORM13,
1693 ratios[3] * NORM24,
1694 ]
1695}
1696
1697#[derive(PartialEq, Eq, Hash, Clone)]
1698#[expect(missing_docs)]
1699pub enum AtlasKey {
1700 Glyph(RenderGlyphParams),
1701 Svg(RenderSvgParams),
1702 Image(RenderImageParams),
1703}
1704
1705impl AtlasKey {
1706 pub fn texture_kind(&self) -> AtlasTextureKind {
1708 match self {
1709 AtlasKey::Glyph(params) => {
1710 if params.is_emoji {
1711 AtlasTextureKind::Polychrome
1712 } else if params.subpixel_rendering {
1713 AtlasTextureKind::Subpixel
1714 } else {
1715 AtlasTextureKind::Monochrome
1716 }
1717 }
1718 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1719 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1720 }
1721 }
1722}
1723
1724impl From<RenderGlyphParams> for AtlasKey {
1725 fn from(params: RenderGlyphParams) -> Self {
1726 Self::Glyph(params)
1727 }
1728}
1729
1730impl From<RenderSvgParams> for AtlasKey {
1731 fn from(params: RenderSvgParams) -> Self {
1732 Self::Svg(params)
1733 }
1734}
1735
1736impl From<RenderImageParams> for AtlasKey {
1737 fn from(params: RenderImageParams) -> Self {
1738 Self::Image(params)
1739 }
1740}
1741
1742#[expect(missing_docs)]
1743pub trait PlatformAtlas {
1744 fn get_or_insert_with<'a>(
1746 &self,
1747 key: AtlasKey,
1748 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1749 ) -> Result<Option<AtlasTile>>;
1750 fn remove(&self, key: &AtlasKey);
1751
1752 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1753 fn contains(&self, _key: &AtlasKey) -> bool {
1754 false
1755 }
1756}
1757
1758#[doc(hidden)]
1759pub trait AtlasBackend {
1760 fn insert(
1761 &mut self,
1762 kind: AtlasTextureKind,
1763 size: Size<DevicePixels>,
1764 bytes: &[u8],
1765 ) -> Result<AtlasTile>;
1766
1767 fn remove(&mut self, tile: AtlasTile);
1768}
1769
1770#[doc(hidden)]
1771pub struct AtlasState<Backend> {
1772 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1773 pub backend: Backend,
1774}
1775
1776impl<Backend> AtlasState<Backend> {
1777 pub fn new(backend: Backend) -> Self {
1778 Self {
1779 tiles_by_key: FxHashMap::default(),
1780 backend,
1781 }
1782 }
1783
1784 pub fn contains(&self, key: &AtlasKey) -> bool {
1785 self.tiles_by_key.contains_key(key)
1786 }
1787
1788 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1789 self.tiles_by_key.clear();
1790 reset_backend(&mut self.backend);
1791 }
1792}
1793
1794impl<Backend: Default> Default for AtlasState<Backend> {
1795 fn default() -> Self {
1796 Self::new(Backend::default())
1797 }
1798}
1799
1800impl<Backend: AtlasBackend> AtlasState<Backend> {
1801 pub fn get_or_insert_with<'a>(
1802 &mut self,
1803 key: AtlasKey,
1804 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1805 ) -> Result<Option<AtlasTile>> {
1806 match self.tiles_by_key.entry(key) {
1807 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1808 Entry::Vacant(entry) => {
1809 profiling::scope!("new tile");
1810 let Some((size, bytes)) = build()? else {
1811 return Ok(None);
1812 };
1813 let tile = self
1814 .backend
1815 .insert(entry.key().texture_kind(), size, &bytes)?;
1816 entry.insert(tile);
1817 Ok(Some(tile))
1818 }
1819 }
1820 }
1821
1822 pub fn remove(&mut self, key: &AtlasKey) {
1823 if let Some(tile) = self.tiles_by_key.remove(key) {
1824 self.backend.remove(tile);
1825 }
1826 }
1827}
1828
1829#[derive(Default)]
1833pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1834
1835#[doc(hidden)]
1836#[derive(Default)]
1837pub struct HeadlessAtlasBackend {
1838 next_id: u32,
1839}
1840
1841impl AtlasBackend for HeadlessAtlasBackend {
1842 fn insert(
1843 &mut self,
1844 kind: AtlasTextureKind,
1845 size: Size<DevicePixels>,
1846 _bytes: &[u8],
1847 ) -> Result<AtlasTile> {
1848 self.next_id += 1;
1849 let texture_id = self.next_id;
1850 self.next_id += 1;
1851 let tile_id = self.next_id;
1852 Ok(AtlasTile {
1853 texture_id: AtlasTextureId {
1854 index: texture_id,
1855 kind,
1856 },
1857 tile_id: TileId(tile_id),
1858 padding: 0,
1859 bounds: Bounds {
1860 origin: Point::default(),
1861 size,
1862 },
1863 })
1864 }
1865
1866 fn remove(&mut self, _tile: AtlasTile) {}
1867}
1868
1869impl PlatformAtlas for HeadlessAtlas {
1870 fn get_or_insert_with<'a>(
1871 &self,
1872 key: AtlasKey,
1873 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1874 ) -> Result<Option<AtlasTile>> {
1875 self.0.lock().get_or_insert_with(key, build)
1876 }
1877
1878 fn remove(&self, key: &AtlasKey) {
1879 self.0.lock().remove(key);
1880 }
1881
1882 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1883 fn contains(&self, key: &AtlasKey) -> bool {
1884 self.0.lock().contains(key)
1885 }
1886}
1887
1888#[doc(hidden)]
1889pub struct AtlasTextureList<T> {
1890 pub textures: Vec<Option<T>>,
1891 pub free_list: Vec<usize>,
1892}
1893
1894impl<T> Default for AtlasTextureList<T> {
1895 fn default() -> Self {
1896 Self {
1897 textures: Vec::default(),
1898 free_list: Vec::default(),
1899 }
1900 }
1901}
1902
1903impl<T> ops::Index<usize> for AtlasTextureList<T> {
1904 type Output = Option<T>;
1905
1906 fn index(&self, index: usize) -> &Self::Output {
1907 &self.textures[index]
1908 }
1909}
1910
1911impl<T> AtlasTextureList<T> {
1912 #[allow(unused)]
1913 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1914 self.free_list.clear();
1915 self.textures.drain(..)
1916 }
1917
1918 #[allow(dead_code)]
1919 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1920 self.textures.iter_mut().flatten()
1921 }
1922}
1923
1924#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1925#[repr(C)]
1926#[expect(missing_docs)]
1927pub struct AtlasTile {
1928 pub texture_id: AtlasTextureId,
1930 pub tile_id: TileId,
1932 pub padding: u32,
1934 pub bounds: Bounds<DevicePixels>,
1936}
1937
1938#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1939#[repr(C)]
1940#[expect(missing_docs)]
1941pub struct AtlasTextureId {
1942 pub index: u32,
1945 pub kind: AtlasTextureKind,
1947}
1948
1949#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1950#[repr(C)]
1951#[cfg_attr(
1952 all(
1953 any(target_os = "linux", target_os = "freebsd"),
1954 not(any(feature = "x11", feature = "wayland"))
1955 ),
1956 allow(dead_code)
1957)]
1958#[expect(missing_docs)]
1959pub enum AtlasTextureKind {
1960 Monochrome = 0,
1961 Polychrome = 1,
1962 Subpixel = 2,
1963}
1964
1965#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1966#[repr(C)]
1967#[expect(missing_docs)]
1968pub struct TileId(pub u32);
1969
1970impl From<etagere::AllocId> for TileId {
1971 fn from(id: etagere::AllocId) -> Self {
1972 Self(id.serialize())
1973 }
1974}
1975
1976impl From<TileId> for etagere::AllocId {
1977 fn from(id: TileId) -> Self {
1978 Self::deserialize(id.0)
1979 }
1980}
1981
1982#[expect(missing_docs)]
1983pub struct PlatformInputHandler {
1984 cx: AsyncWindowContext,
1985 handler: Box<dyn InputHandler>,
1986}
1987
1988#[expect(missing_docs)]
1989#[cfg_attr(
1990 all(
1991 any(target_os = "linux", target_os = "freebsd"),
1992 not(any(feature = "x11", feature = "wayland"))
1993 ),
1994 allow(dead_code)
1995)]
1996impl PlatformInputHandler {
1997 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1998 Self { cx, handler }
1999 }
2000
2001 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2002 self.cx
2003 .update(|window, cx| {
2004 self.handler
2005 .selected_text_range(ignore_disabled_input, window, cx)
2006 })
2007 .ok()
2008 .flatten()
2009 }
2010
2011 #[cfg_attr(target_os = "windows", allow(dead_code))]
2012 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2013 self.cx
2014 .update(|window, cx| self.handler.marked_text_range(window, cx))
2015 .ok()
2016 .flatten()
2017 }
2018
2019 #[cfg_attr(
2020 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2021 allow(dead_code)
2022 )]
2023 pub fn text_for_range(
2024 &mut self,
2025 range_utf16: Range<usize>,
2026 adjusted: &mut Option<Range<usize>>,
2027 ) -> Option<String> {
2028 self.cx
2029 .update(|window, cx| {
2030 self.handler
2031 .text_for_range(range_utf16, adjusted, window, cx)
2032 })
2033 .ok()
2034 .flatten()
2035 }
2036
2037 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2038 self.cx
2039 .update(|window, cx| {
2040 self.handler
2041 .replace_text_in_range(replacement_range, text, window, cx);
2042 })
2043 .ok();
2044 }
2045
2046 pub fn replace_and_mark_text_in_range(
2047 &mut self,
2048 range_utf16: Option<Range<usize>>,
2049 new_text: &str,
2050 new_selected_range: Option<Range<usize>>,
2051 ) {
2052 self.cx
2053 .update(|window, cx| {
2054 self.handler.replace_and_mark_text_in_range(
2055 range_utf16,
2056 new_text,
2057 new_selected_range,
2058 window,
2059 cx,
2060 )
2061 })
2062 .ok();
2063 }
2064
2065 #[cfg_attr(target_os = "windows", allow(dead_code))]
2066 pub fn unmark_text(&mut self) {
2067 self.cx
2068 .update(|window, cx| self.handler.unmark_text(window, cx))
2069 .ok();
2070 }
2071
2072 pub fn paste(&mut self, item: ClipboardItem) {
2073 self.cx
2074 .update(|window, cx| self.handler.paste(item, window, cx))
2075 .ok();
2076 }
2077
2078 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2079 self.cx
2080 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2081 .ok()
2082 .flatten()
2083 }
2084
2085 #[allow(dead_code)]
2086 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2087 self.handler.apple_press_and_hold_enabled()
2088 }
2089
2090 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2091 self.handler.replace_text_in_range(None, input, window, cx);
2092 }
2093
2094 pub fn compute_ime_candidate_bounds(
2095 marked_range: Option<Range<usize>>,
2096 selection: &UTF16Selection,
2097 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2098 ) -> Option<Bounds<Pixels>> {
2099 if let Some(marked_range) = marked_range {
2100 let mut line_start = marked_range.start;
2102
2103 let caret = selection.range.end;
2107 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2108 for i in (marked_range.start..caret).rev() {
2109 if let Some(b) = bounds_for_range(i..i) {
2110 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2111 line_start = i + 1;
2112 break;
2113 }
2114 }
2115 }
2116 }
2117 bounds_for_range(line_start..line_start)
2118 } else {
2119 let offset = if selection.reversed {
2121 selection.range.start
2122 } else {
2123 selection.range.end
2124 };
2125 bounds_for_range(offset..offset)
2126 }
2127 }
2128
2129 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2130 let marked_range = self.handler.marked_text_range(window, cx);
2131 let selection = self.handler.selected_text_range(true, window, cx)?;
2132 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2133 self.handler.bounds_for_range(range, window, cx)
2134 })
2135 }
2136
2137 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2138 let marked_range = self.marked_text_range();
2139 let selection = self.selected_text_range(true)?;
2140 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2141 self.bounds_for_range(range)
2142 })
2143 }
2144
2145 #[allow(unused)]
2146 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2147 self.cx
2148 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2149 .ok()
2150 .flatten()
2151 }
2152
2153 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2155 self.cx
2156 .update(|window, cx| {
2157 self.handler
2158 .set_selected_text_range(range_utf16, window, cx)
2159 })
2160 .ok();
2161 }
2162
2163 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2165 self.cx
2166 .update(|window, cx| self.handler.element_bounds(window, cx))
2167 .ok()
2168 .flatten()
2169 }
2170
2171 pub fn text_length_utf16(&mut self) -> Option<usize> {
2173 self.cx
2174 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2175 .ok()
2176 .flatten()
2177 }
2178
2179 #[allow(dead_code)]
2180 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2181 self.handler.accepts_text_input(window, cx)
2182 }
2183
2184 #[allow(dead_code)]
2185 pub fn query_accepts_text_input(&mut self) -> bool {
2186 self.cx
2187 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2188 .unwrap_or(true)
2189 }
2190
2191 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2197 self.cx
2198 .update(|window, cx| {
2199 !window.has_pending_keystrokes()
2201 && self.handler.prefers_ime_for_printable_keys(window, cx)
2202 })
2203 .unwrap_or(false)
2204 }
2205
2206 pub fn text_input_configuration(
2208 &mut self,
2209 window: &mut Window,
2210 cx: &mut App,
2211 ) -> TextInputConfiguration {
2212 self.handler.text_input_configuration(window, cx)
2213 }
2214
2215 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2217 self.cx
2218 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2219 .ok()
2220 .flatten()
2221 }
2222}
2223
2224#[derive(Debug)]
2227pub struct UTF16Selection {
2228 pub range: Range<usize>,
2231 pub reversed: bool,
2234}
2235
2236pub trait InputHandler: 'static {
2241 fn selected_text_range(
2246 &mut self,
2247 ignore_disabled_input: bool,
2248 window: &mut Window,
2249 cx: &mut App,
2250 ) -> Option<UTF16Selection>;
2251
2252 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2257
2258 fn text_for_range(
2263 &mut self,
2264 range_utf16: Range<usize>,
2265 adjusted_range: &mut Option<Range<usize>>,
2266 window: &mut Window,
2267 cx: &mut App,
2268 ) -> Option<String>;
2269
2270 fn replace_text_in_range(
2275 &mut self,
2276 replacement_range: Option<Range<usize>>,
2277 text: &str,
2278 window: &mut Window,
2279 cx: &mut App,
2280 );
2281
2282 fn replace_and_mark_text_in_range(
2289 &mut self,
2290 range_utf16: Option<Range<usize>>,
2291 new_text: &str,
2292 new_selected_range: Option<Range<usize>>,
2293 window: &mut Window,
2294 cx: &mut App,
2295 );
2296
2297 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2300
2301 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2308 if let Some(text) = item.text() {
2309 self.replace_text_in_range(None, &text, window, cx);
2310 }
2311 }
2312
2313 fn bounds_for_range(
2318 &mut self,
2319 range_utf16: Range<usize>,
2320 window: &mut Window,
2321 cx: &mut App,
2322 ) -> Option<Bounds<Pixels>>;
2323
2324 fn character_index_for_point(
2328 &mut self,
2329 point: Point<Pixels>,
2330 window: &mut Window,
2331 cx: &mut App,
2332 ) -> Option<usize>;
2333
2334 fn set_selected_text_range(
2344 &mut self,
2345 _range_utf16: Range<usize>,
2346 _window: &mut Window,
2347 _cx: &mut App,
2348 ) {
2349 }
2350
2351 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2358 None
2359 }
2360
2361 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2363 None
2364 }
2365
2366 #[allow(dead_code)]
2371 fn apple_press_and_hold_enabled(&mut self) -> bool {
2372 true
2373 }
2374
2375 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2377 true
2378 }
2379
2380 fn text_input_editable_range(
2391 &mut self,
2392 _window: &mut Window,
2393 _cx: &mut App,
2394 ) -> Option<Range<usize>> {
2395 None
2396 }
2397
2398 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2407 false
2408 }
2409
2410 fn text_input_configuration(
2416 &mut self,
2417 _window: &mut Window,
2418 _cx: &mut App,
2419 ) -> TextInputConfiguration {
2420 TextInputConfiguration::default()
2421 }
2422}
2423
2424#[derive(Clone, Debug, Default, PartialEq, Eq)]
2434pub struct TextInputConfiguration {
2435 pub autocorrect: bool,
2437 pub autocapitalize: Autocapitalize,
2439 pub suggestions: bool,
2441 pub input_action: TextInputAction,
2443}
2444
2445#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2447pub enum Autocapitalize {
2448 #[default]
2450 None,
2451 Words,
2453 Sentences,
2455 Characters,
2457}
2458
2459#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2469pub enum TextInputAction {
2470 #[default]
2472 Unspecified,
2473 Enter,
2475 Done,
2477 Go,
2479 Next,
2481 Previous,
2483 Search,
2485 Send,
2487}
2488
2489#[derive(Debug)]
2491pub struct WindowOptions {
2492 pub window_bounds: Option<WindowBounds>,
2496
2497 pub titlebar: Option<TitlebarOptions>,
2499
2500 pub focus: bool,
2502
2503 pub show: bool,
2505
2506 pub kind: WindowKind,
2508
2509 pub is_movable: bool,
2513
2514 pub app_owns_titlebar_drag: bool,
2526
2527 pub inactive_frame_interval: Option<Duration>,
2531
2532 pub is_resizable: bool,
2534
2535 pub is_minimizable: bool,
2537
2538 pub display_id: Option<DisplayId>,
2541
2542 pub window_background: WindowBackgroundAppearance,
2544
2545 pub app_id: Option<String>,
2547
2548 pub window_min_size: Option<Size<Pixels>>,
2550
2551 pub window_decorations: Option<WindowDecorations>,
2554
2555 pub icon: Option<Arc<image::RgbaImage>>,
2557
2558 pub tabbing_identifier: Option<String>,
2560}
2561
2562#[derive(Debug)]
2564#[cfg_attr(
2565 all(
2566 any(target_os = "linux", target_os = "freebsd"),
2567 not(any(feature = "x11", feature = "wayland"))
2568 ),
2569 allow(dead_code)
2570)]
2571#[allow(missing_docs)]
2572pub struct WindowParams {
2573 pub bounds: Bounds<Pixels>,
2574
2575 #[cfg_attr(feature = "wayland", allow(dead_code))]
2577 pub titlebar: Option<TitlebarOptions>,
2578
2579 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2581 pub kind: WindowKind,
2582
2583 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2585 pub is_movable: bool,
2586
2587 #[cfg_attr(
2589 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2590 allow(dead_code)
2591 )]
2592 pub app_owns_titlebar_drag: bool,
2593
2594 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2596 pub is_resizable: bool,
2597
2598 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2600 pub is_minimizable: bool,
2601
2602 #[cfg_attr(
2603 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2604 allow(dead_code)
2605 )]
2606 pub focus: bool,
2607
2608 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2609 pub show: bool,
2610
2611 #[cfg_attr(feature = "wayland", allow(dead_code))]
2613 pub icon: Option<Arc<image::RgbaImage>>,
2614
2615 #[cfg_attr(feature = "wayland", allow(dead_code))]
2616 pub display_id: Option<DisplayId>,
2617
2618 #[cfg_attr(feature = "wayland", allow(dead_code))]
2619 pub app_id: Option<String>,
2620
2621 pub window_min_size: Option<Size<Pixels>>,
2622
2623 #[cfg(target_os = "macos")]
2624 pub tabbing_identifier: Option<String>,
2625}
2626
2627#[derive(Debug, Copy, Clone, PartialEq)]
2629pub enum WindowBounds {
2630 Windowed(Bounds<Pixels>),
2632 Maximized(Bounds<Pixels>),
2635 Fullscreen(Bounds<Pixels>),
2638}
2639
2640impl Default for WindowBounds {
2641 fn default() -> Self {
2642 WindowBounds::Windowed(Bounds::default())
2643 }
2644}
2645
2646impl WindowBounds {
2647 pub fn get_bounds(&self) -> Bounds<Pixels> {
2649 match self {
2650 WindowBounds::Windowed(bounds) => *bounds,
2651 WindowBounds::Maximized(bounds) => *bounds,
2652 WindowBounds::Fullscreen(bounds) => *bounds,
2653 }
2654 }
2655
2656 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2658 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2659 }
2660}
2661
2662impl Default for WindowOptions {
2663 fn default() -> Self {
2664 Self {
2665 window_bounds: None,
2666 titlebar: Some(TitlebarOptions {
2667 title: Default::default(),
2668 appears_transparent: Default::default(),
2669 traffic_light_position: Default::default(),
2670 }),
2671 focus: true,
2672 show: true,
2673 kind: WindowKind::Normal,
2674 is_movable: true,
2675 app_owns_titlebar_drag: false,
2676 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2677 is_resizable: true,
2678 is_minimizable: true,
2679 display_id: None,
2680 window_background: WindowBackgroundAppearance::default(),
2681 icon: None,
2682 app_id: None,
2683 window_min_size: None,
2684 window_decorations: None,
2685 tabbing_identifier: None,
2686 }
2687 }
2688}
2689
2690#[derive(Debug, Default)]
2692pub struct TitlebarOptions {
2693 pub title: Option<SharedString>,
2695
2696 pub appears_transparent: bool,
2699
2700 pub traffic_light_position: Option<Point<Pixels>>,
2702}
2703
2704#[derive(Clone, Debug, PartialEq, Eq)]
2706pub enum WindowKind {
2707 Normal,
2709
2710 PopUp,
2713
2714 AnchoredPopup(popup::PopupOptions),
2721
2722 Floating,
2724
2725 #[cfg(all(target_os = "linux", feature = "wayland"))]
2728 LayerShell(layer_shell::LayerShellOptions),
2729
2730 Dialog,
2733}
2734
2735#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2740pub enum WindowAppearance {
2741 #[default]
2745 Light,
2746
2747 VibrantLight,
2751
2752 Dark,
2756
2757 VibrantDark,
2761}
2762
2763#[derive(Copy, Clone, Debug, Default, PartialEq)]
2766pub enum WindowBackgroundAppearance {
2767 #[default]
2775 Opaque,
2776 Transparent,
2778 Blurred,
2782 MicaBackdrop,
2784 MicaAltBackdrop,
2786}
2787
2788#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2790pub enum TextRenderingMode {
2791 #[default]
2793 PlatformDefault,
2794 Subpixel,
2796 Grayscale,
2798}
2799
2800#[derive(Clone, Debug)]
2802pub struct PathPromptOptions {
2803 pub files: bool,
2805 pub directories: bool,
2807 pub multiple: bool,
2809 pub prompt: Option<SharedString>,
2811}
2812
2813#[derive(Copy, Clone, Debug, PartialEq)]
2815pub enum PromptLevel {
2816 Info,
2818
2819 Warning,
2821
2822 Critical,
2824}
2825
2826#[derive(Clone, Debug, PartialEq)]
2828pub enum PromptButton {
2829 Ok(SharedString),
2831 Cancel(SharedString),
2833 Other(SharedString),
2835}
2836
2837impl PromptButton {
2838 pub fn new(label: impl Into<SharedString>) -> Self {
2840 PromptButton::Other(label.into())
2841 }
2842
2843 pub fn ok(label: impl Into<SharedString>) -> Self {
2845 PromptButton::Ok(label.into())
2846 }
2847
2848 pub fn cancel(label: impl Into<SharedString>) -> Self {
2850 PromptButton::Cancel(label.into())
2851 }
2852
2853 #[allow(dead_code)]
2855 pub fn is_cancel(&self) -> bool {
2856 matches!(self, PromptButton::Cancel(_))
2857 }
2858
2859 pub fn label(&self) -> &SharedString {
2861 match self {
2862 PromptButton::Ok(label) => label,
2863 PromptButton::Cancel(label) => label,
2864 PromptButton::Other(label) => label,
2865 }
2866 }
2867}
2868
2869impl From<&str> for PromptButton {
2870 fn from(value: &str) -> Self {
2871 match value.to_lowercase().as_str() {
2872 "ok" => PromptButton::Ok("OK".into()),
2873 "cancel" => PromptButton::Cancel("Cancel".into()),
2874 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2875 }
2876 }
2877}
2878
2879#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2881pub enum CursorStyle {
2882 #[default]
2884 Arrow,
2885
2886 IBeam,
2889
2890 Crosshair,
2893
2894 ClosedHand,
2897
2898 OpenHand,
2901
2902 PointingHand,
2905
2906 ResizeLeft,
2909
2910 ResizeRight,
2913
2914 ResizeLeftRight,
2917
2918 ResizeUp,
2921
2922 ResizeDown,
2925
2926 ResizeUpDown,
2929
2930 ResizeUpLeftDownRight,
2933
2934 ResizeUpRightDownLeft,
2937
2938 ResizeColumn,
2941
2942 ResizeRow,
2945
2946 IBeamCursorForVerticalLayout,
2949
2950 OperationNotAllowed,
2953
2954 DragLink,
2957
2958 DragCopy,
2961
2962 ContextualMenu,
2965}
2966
2967#[derive(Clone, Debug, Eq, PartialEq)]
2969pub struct ClipboardItem {
2970 pub entries: Vec<ClipboardEntry>,
2972}
2973
2974#[derive(Clone, Debug, PartialEq, Eq)]
2979pub enum ClipboardReadError {
2980 Unavailable,
2984 Denied(String),
2987 UnsupportedContent,
2990}
2991
2992impl std::fmt::Display for ClipboardReadError {
2993 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2994 match self {
2995 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
2996 Self::Denied(message) => {
2997 write!(formatter, "clipboard access was denied: {message}")
2998 }
2999 Self::UnsupportedContent => {
3000 formatter.write_str("the clipboard contents are unsupported")
3001 }
3002 }
3003 }
3004}
3005
3006impl std::error::Error for ClipboardReadError {}
3007
3008#[derive(Clone, Debug, Eq, PartialEq)]
3010pub enum ClipboardEntry {
3011 String(ClipboardString),
3013 Image(Image),
3015 ExternalPaths(crate::ExternalPaths),
3017}
3018
3019impl ClipboardItem {
3020 pub fn new_string(text: String) -> Self {
3022 Self {
3023 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3024 }
3025 }
3026
3027 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3029 Self {
3030 entries: vec![ClipboardEntry::String(ClipboardString {
3031 text,
3032 metadata: Some(metadata),
3033 })],
3034 }
3035 }
3036
3037 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3039 Self {
3040 entries: vec![ClipboardEntry::String(
3041 ClipboardString::new(text).with_json_metadata(metadata),
3042 )],
3043 }
3044 }
3045
3046 pub fn new_image(image: &Image) -> Self {
3048 Self {
3049 entries: vec![ClipboardEntry::Image(image.clone())],
3050 }
3051 }
3052
3053 pub fn text(&self) -> Option<String> {
3056 let mut answer = String::new();
3057
3058 for entry in self.entries.iter() {
3059 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3060 answer.push_str(text);
3061 }
3062 }
3063
3064 if answer.is_empty() {
3065 for entry in self.entries.iter() {
3066 if let ClipboardEntry::ExternalPaths(paths) = entry {
3067 for path in &paths.0 {
3068 use std::fmt::Write as _;
3069 _ = write!(answer, "{}", path.display());
3070 }
3071 }
3072 }
3073 }
3074
3075 if !answer.is_empty() {
3076 Some(answer)
3077 } else {
3078 None
3079 }
3080 }
3081
3082 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3084 pub fn metadata(&self) -> Option<&String> {
3085 match self.entries().first() {
3086 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3087 clipboard_string.metadata.as_ref()
3088 }
3089 _ => None,
3090 }
3091 }
3092
3093 pub fn entries(&self) -> &[ClipboardEntry] {
3095 &self.entries
3096 }
3097
3098 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3100 self.entries.into_iter()
3101 }
3102}
3103
3104impl From<ClipboardString> for ClipboardEntry {
3105 fn from(value: ClipboardString) -> Self {
3106 Self::String(value)
3107 }
3108}
3109
3110impl From<String> for ClipboardEntry {
3111 fn from(value: String) -> Self {
3112 Self::from(ClipboardString::from(value))
3113 }
3114}
3115
3116impl From<Image> for ClipboardEntry {
3117 fn from(value: Image) -> Self {
3118 Self::Image(value)
3119 }
3120}
3121
3122impl From<ClipboardEntry> for ClipboardItem {
3123 fn from(value: ClipboardEntry) -> Self {
3124 Self {
3125 entries: vec![value],
3126 }
3127 }
3128}
3129
3130impl From<String> for ClipboardItem {
3131 fn from(value: String) -> Self {
3132 Self::from(ClipboardEntry::from(value))
3133 }
3134}
3135
3136impl From<Image> for ClipboardItem {
3137 fn from(value: Image) -> Self {
3138 Self::from(ClipboardEntry::from(value))
3139 }
3140}
3141
3142#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3144pub enum ImageFormat {
3145 Png,
3150 Jpeg,
3152 Webp,
3154 Gif,
3156 Svg,
3158 Bmp,
3160 Tiff,
3162 Ico,
3164 Pnm,
3166}
3167
3168impl ImageFormat {
3169 pub const fn mime_type(self) -> &'static str {
3171 match self {
3172 ImageFormat::Png => "image/png",
3173 ImageFormat::Jpeg => "image/jpeg",
3174 ImageFormat::Webp => "image/webp",
3175 ImageFormat::Gif => "image/gif",
3176 ImageFormat::Svg => "image/svg+xml",
3177 ImageFormat::Bmp => "image/bmp",
3178 ImageFormat::Tiff => "image/tiff",
3179 ImageFormat::Ico => "image/ico",
3180 ImageFormat::Pnm => "image/x-portable-anymap",
3181 }
3182 }
3183
3184 pub const fn extension(self) -> &'static str {
3186 match self {
3187 ImageFormat::Png => "png",
3188 ImageFormat::Jpeg => "jpg",
3189 ImageFormat::Webp => "webp",
3190 ImageFormat::Gif => "gif",
3191 ImageFormat::Svg => "svg",
3192 ImageFormat::Bmp => "bmp",
3193 ImageFormat::Tiff => "tiff",
3194 ImageFormat::Ico => "ico",
3195 ImageFormat::Pnm => "pnm",
3196 }
3197 }
3198
3199 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3201 use strum::IntoEnumIterator;
3202 Self::iter()
3203 .find(|format| format.mime_type() == mime_type)
3204 .or_else(|| Self::from_mime_type_alias(mime_type))
3205 }
3206
3207 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3211 match mime_type {
3212 "image/jpg" => Some(Self::Jpeg),
3213 "image/tif" => Some(Self::Tiff),
3214 _ => None,
3215 }
3216 }
3217}
3218
3219#[derive(Clone, Debug, PartialEq, Eq)]
3221pub struct Image {
3222 pub format: ImageFormat,
3224 pub bytes: Vec<u8>,
3226 pub id: u64,
3228}
3229
3230pub(crate) fn decode_static_image(
3231 bytes: &[u8],
3232 format: image::ImageFormat,
3233) -> Result<SmallVec<[Frame; 1]>> {
3234 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3235 .into_decoder()
3236 .context("creating image decoder")?;
3237 decode_static_image_from_decoder(decoder)
3238}
3239
3240pub(crate) fn decode_static_image_from_decoder(
3241 mut decoder: impl image::ImageDecoder,
3242) -> Result<SmallVec<[Frame; 1]>> {
3243 let orientation = decoder
3244 .orientation()
3245 .context("reading decoder's orientation")?;
3246 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3247 image.apply_orientation(orientation);
3248
3249 let mut data = image.into_rgba8();
3250 for pixel in data.chunks_exact_mut(4) {
3251 pixel.swap(0, 2);
3252 }
3253
3254 Ok(SmallVec::from_elem(Frame::new(data), 1))
3255}
3256
3257impl Hash for Image {
3258 fn hash<H: Hasher>(&self, state: &mut H) {
3259 state.write_u64(self.id);
3260 }
3261}
3262
3263impl Image {
3264 pub fn empty() -> Self {
3266 Self::from_bytes(ImageFormat::Png, Vec::new())
3267 }
3268
3269 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3271 Self {
3272 id: hash(&bytes),
3273 format,
3274 bytes,
3275 }
3276 }
3277
3278 pub fn id(&self) -> u64 {
3280 self.id
3281 }
3282
3283 pub fn use_render_image(
3285 self: Arc<Self>,
3286 window: &mut Window,
3287 cx: &mut App,
3288 ) -> Option<Arc<RenderImage>> {
3289 ImageSource::Image(self)
3290 .use_data(None, window, cx)
3291 .and_then(|result| result.ok())
3292 }
3293
3294 pub fn get_render_image(
3296 self: Arc<Self>,
3297 window: &mut Window,
3298 cx: &mut App,
3299 ) -> Option<Arc<RenderImage>> {
3300 ImageSource::Image(self)
3301 .get_data(None, window, cx)
3302 .and_then(|result| result.ok())
3303 }
3304
3305 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3307 ImageSource::Image(self).remove_asset(cx);
3308 }
3309
3310 #[cfg(any(test, feature = "test-support"))]
3313 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3314 ImageSource::Image(self.clone()).is_asset_cached(cx)
3315 }
3316
3317 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3319 let frames = match self.format {
3320 ImageFormat::Gif => {
3321 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3322 let mut frames = SmallVec::new();
3323
3324 for frame in decoder.into_frames() {
3325 match frame {
3326 Ok(mut frame) => {
3327 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3329 pixel.swap(0, 2);
3330 }
3331 frames.push(frame);
3332 }
3333 Err(err) => {
3334 log::debug!("Skipping GIF frame due to decode error: {err}");
3335 }
3336 }
3337 }
3338
3339 if frames.is_empty() {
3340 anyhow::bail!("GIF could not be decoded: all frames failed");
3341 }
3342
3343 frames
3344 }
3345 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3346 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3347 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3348 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3349 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3350 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3351 ImageFormat::Svg => {
3352 return svg_renderer
3353 .render_single_frame(&self.bytes, 1.0)
3354 .map_err(Into::into);
3355 }
3356 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3357 };
3358
3359 Ok(Arc::new(RenderImage::new(frames)))
3360 }
3361
3362 pub fn format(&self) -> ImageFormat {
3364 self.format
3365 }
3366
3367 pub fn bytes(&self) -> &[u8] {
3369 self.bytes.as_slice()
3370 }
3371}
3372
3373#[derive(Clone, Debug, Eq, PartialEq)]
3375pub struct ClipboardString {
3376 pub text: String,
3378 pub metadata: Option<String>,
3380}
3381
3382impl ClipboardString {
3383 pub fn new(text: String) -> Self {
3385 Self {
3386 text,
3387 metadata: None,
3388 }
3389 }
3390
3391 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3394 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3395 self
3396 }
3397
3398 pub fn text(&self) -> &String {
3400 &self.text
3401 }
3402
3403 pub fn into_text(self) -> String {
3405 self.text
3406 }
3407
3408 pub fn metadata_json<T>(&self) -> Option<T>
3410 where
3411 T: for<'a> Deserialize<'a>,
3412 {
3413 self.metadata
3414 .as_ref()
3415 .and_then(|m| serde_json::from_str(m).ok())
3416 }
3417
3418 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3419 pub fn text_hash(text: &str) -> u64 {
3421 let mut hasher = SeaHasher::new();
3422 text.hash(&mut hasher);
3423 hasher.finish()
3424 }
3425}
3426
3427impl From<String> for ClipboardString {
3428 fn from(value: String) -> Self {
3429 Self {
3430 text: value,
3431 metadata: None,
3432 }
3433 }
3434}
3435
3436#[cfg(test)]
3437mod image_tests {
3438 use super::*;
3439 use std::sync::Arc;
3440
3441 #[test]
3442 fn test_image_to_image_data_applies_exif_orientation() {
3443 let image = Image::from_bytes(
3444 ImageFormat::Jpeg,
3445 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3446 );
3447
3448 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3449
3450 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3451
3452 let bytes = render_image.as_bytes(0).unwrap();
3453 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3454 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3455 }
3456
3457 #[test]
3458 fn test_svg_image_to_image_data_converts_to_bgra() {
3459 let image = Image::from_bytes(
3460 ImageFormat::Svg,
3461 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3462<rect width="1" height="1" fill="#38BDF8"/>
3463</svg>"##
3464 .to_vec(),
3465 );
3466
3467 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3468 let bytes = render_image.as_bytes(0).unwrap();
3469
3470 for pixel in bytes.chunks_exact(4) {
3471 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3472 }
3473 }
3474}
3475
3476#[cfg(test)]
3477mod atlas_tests {
3478 use super::*;
3479
3480 const TILE_SIZE: Size<DevicePixels> = Size {
3481 width: DevicePixels(1),
3482 height: DevicePixels(1),
3483 };
3484
3485 #[derive(Default)]
3486 struct RecordingAtlasBackend {
3487 insert_calls: u32,
3488 fail_next_insert: bool,
3489 removed_tiles: Vec<AtlasTile>,
3490 }
3491
3492 impl AtlasBackend for RecordingAtlasBackend {
3493 fn insert(
3494 &mut self,
3495 kind: AtlasTextureKind,
3496 size: Size<DevicePixels>,
3497 _bytes: &[u8],
3498 ) -> Result<AtlasTile> {
3499 self.insert_calls += 1;
3500 if std::mem::take(&mut self.fail_next_insert) {
3501 anyhow::bail!("backend failed");
3502 }
3503 Ok(AtlasTile {
3504 texture_id: AtlasTextureId { index: 0, kind },
3505 tile_id: TileId(self.insert_calls),
3506 padding: 0,
3507 bounds: Bounds {
3508 origin: Point::default(),
3509 size,
3510 },
3511 })
3512 }
3513
3514 fn remove(&mut self, tile: AtlasTile) {
3515 self.removed_tiles.push(tile);
3516 }
3517 }
3518
3519 fn image_key(image_id: usize) -> AtlasKey {
3520 AtlasKey::Image(RenderImageParams {
3521 image_id: crate::ImageId(image_id),
3522 frame_index: 0,
3523 })
3524 }
3525
3526 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3527 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3528 }
3529
3530 #[test]
3531 fn only_successful_inserts_are_cached() -> Result<()> {
3532 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3533 let key = image_key(1);
3534
3535 assert_eq!(
3536 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3537 None
3538 );
3539 state
3540 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3541 .expect_err("builder error should propagate");
3542 assert!(!state.contains(&key));
3543 assert_eq!(state.backend.insert_calls, 0);
3544
3545 state.backend.fail_next_insert = true;
3546 state
3547 .get_or_insert_with(key.clone(), &mut build_tile)
3548 .expect_err("backend error should propagate");
3549 assert!(!state.contains(&key));
3550 assert_eq!(state.backend.insert_calls, 1);
3551
3552 let tile = state
3553 .get_or_insert_with(key.clone(), &mut build_tile)?
3554 .context("builder should produce a tile")?;
3555 assert_eq!(tile.texture_id.kind, key.texture_kind());
3556 assert_eq!(
3557 state.get_or_insert_with(key.clone(), &mut || {
3558 anyhow::bail!("cache hit must not call the builder")
3559 })?,
3560 Some(tile)
3561 );
3562 assert!(state.contains(&key));
3563 assert_eq!(state.backend.insert_calls, 2);
3564 Ok(())
3565 }
3566
3567 #[test]
3568 fn remove_and_clear_invalidate_keys() -> Result<()> {
3569 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3570 let key = image_key(1);
3571 let other_key = image_key(2);
3572 let tile = state
3573 .get_or_insert_with(key.clone(), &mut build_tile)?
3574 .context("builder should produce a tile")?;
3575 state
3576 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3577 .context("builder should produce another tile")?;
3578
3579 state.remove(&key);
3580 state.remove(&key);
3581 assert!(!state.contains(&key));
3582 assert!(state.contains(&other_key));
3583 assert_eq!(state.backend.removed_tiles, vec![tile]);
3584
3585 let mut reset_calls = 0;
3586 state.clear(|_| reset_calls += 1);
3587 assert_eq!(reset_calls, 1);
3588 assert!(!state.contains(&other_key));
3589 assert_eq!(state.backend.removed_tiles, vec![tile]);
3590 Ok(())
3591 }
3592}
3593
3594#[cfg(test)]
3595mod frame_signal_tests {
3596 use super::*;
3597
3598 #[cfg(feature = "profiler")]
3599 #[test]
3600 fn coalesced_signals_retain_the_earliest_until_drained() {
3601 for (first_source, later_source) in [
3602 (
3603 FrameRequestSource::NativeCallback,
3604 FrameRequestSource::LocalSchedule,
3605 ),
3606 (
3607 FrameRequestSource::LocalSchedule,
3608 FrameRequestSource::NativeCallback,
3609 ),
3610 ] {
3611 let signal = PlatformFrameSignal::new();
3612 let first =
3613 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3614 assert_eq!(signal.take(), None);
3615 signal.record(first + Duration::from_millis(16), later_source);
3616 signal.record(first, first_source);
3617 signal.record(first + Duration::from_millis(32), later_source);
3618 assert_eq!(signal.take(), Some((first, first_source)));
3619 assert_eq!(signal.take(), None);
3620 let next = first + Duration::from_millis(48);
3621 signal.record(next, later_source);
3622 assert_eq!(signal.take(), Some((next, later_source)));
3623 }
3624 }
3625
3626 #[cfg(not(feature = "profiler"))]
3627 #[test]
3628 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3629 let captured = PlatformFrameSignal::capture(|| {
3630 panic!("profiler-disabled builds must not evaluate the capture closure");
3631 });
3632 assert_eq!(captured, None);
3633 let signal = PlatformFrameSignal::new();
3634 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3635 assert_eq!(signal.take(), None);
3636 }
3637}
3638
3639#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3640mod tests {
3641 use super::*;
3642 use std::collections::HashSet;
3643
3644 #[test]
3645 fn test_refresh_interval_from_hz() {
3646 assert_eq!(
3647 refresh_interval_from_hz(50.0),
3648 Some(Duration::from_millis(20))
3649 );
3650 assert_eq!(
3651 refresh_interval_from_hz(120.0),
3652 Some(Duration::from_secs(1) / 120)
3653 );
3654 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3655 assert_eq!(refresh_interval_from_hz(unknown), None);
3656 }
3657 }
3658
3659 #[test]
3660 fn test_window_button_layout_parse_standard() {
3661 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3662 assert_eq!(
3663 layout.left,
3664 [
3665 Some(WindowButton::Close),
3666 Some(WindowButton::Minimize),
3667 None
3668 ]
3669 );
3670 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3671 }
3672
3673 #[test]
3674 fn test_window_button_layout_parse_right_only() {
3675 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3676 assert_eq!(layout.left, [None, None, None]);
3677 assert_eq!(
3678 layout.right,
3679 [
3680 Some(WindowButton::Minimize),
3681 Some(WindowButton::Maximize),
3682 Some(WindowButton::Close)
3683 ]
3684 );
3685 }
3686
3687 #[test]
3688 fn test_window_button_layout_parse_left_only() {
3689 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3690 assert_eq!(
3691 layout.left,
3692 [
3693 Some(WindowButton::Close),
3694 Some(WindowButton::Minimize),
3695 Some(WindowButton::Maximize)
3696 ]
3697 );
3698 assert_eq!(layout.right, [None, None, None]);
3699 }
3700
3701 #[test]
3702 fn test_window_button_layout_parse_with_whitespace() {
3703 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3704 assert_eq!(
3705 layout.left,
3706 [
3707 Some(WindowButton::Close),
3708 Some(WindowButton::Minimize),
3709 None
3710 ]
3711 );
3712 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3713 }
3714
3715 #[test]
3716 fn test_window_button_layout_parse_empty() {
3717 let layout = WindowButtonLayout::parse("").unwrap();
3718 assert_eq!(layout.left, [None, None, None]);
3719 assert_eq!(layout.right, [None, None, None]);
3720 }
3721
3722 #[test]
3723 fn test_window_button_layout_parse_intentionally_empty() {
3724 let layout = WindowButtonLayout::parse(":").unwrap();
3725 assert_eq!(layout.left, [None, None, None]);
3726 assert_eq!(layout.right, [None, None, None]);
3727 }
3728
3729 #[test]
3730 fn test_window_button_layout_parse_invalid_buttons() {
3731 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3732 assert_eq!(
3733 layout.left,
3734 [
3735 Some(WindowButton::Close),
3736 Some(WindowButton::Minimize),
3737 None
3738 ]
3739 );
3740 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3741 }
3742
3743 #[test]
3744 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3745 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3746 assert_eq!(
3747 layout.right,
3748 [
3749 Some(WindowButton::Close),
3750 Some(WindowButton::Minimize),
3751 None
3752 ]
3753 );
3754 assert_eq!(layout.format(), ":close,minimize");
3755 }
3756
3757 #[test]
3758 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3759 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3760 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3761 assert_eq!(
3762 layout.right,
3763 [
3764 Some(WindowButton::Maximize),
3765 Some(WindowButton::Minimize),
3766 None
3767 ]
3768 );
3769
3770 let button_ids: Vec<_> = layout
3771 .left
3772 .iter()
3773 .chain(layout.right.iter())
3774 .flatten()
3775 .map(WindowButton::id)
3776 .collect();
3777 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3778 assert_eq!(unique_button_ids.len(), button_ids.len());
3779 assert_eq!(layout.format(), "close:maximize,minimize");
3780 }
3781
3782 #[test]
3783 fn test_window_button_layout_parse_gnome_style() {
3784 let layout = WindowButtonLayout::parse("close").unwrap();
3785 assert_eq!(layout.left, [None, None, None]);
3786 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3787 }
3788
3789 #[test]
3790 fn test_window_button_layout_parse_elementary_style() {
3791 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3792 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3793 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3794 }
3795
3796 #[test]
3797 fn test_window_button_layout_round_trip() {
3798 let cases = [
3799 "close:minimize,maximize",
3800 "minimize,maximize,close:",
3801 ":close",
3802 "close:",
3803 "close:maximize",
3804 ":",
3805 ];
3806
3807 for case in cases {
3808 let layout = WindowButtonLayout::parse(case).unwrap();
3809 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3810 }
3811 }
3812
3813 #[test]
3814 fn test_window_button_layout_linux_default() {
3815 let layout = WindowButtonLayout::linux_default();
3816 assert_eq!(layout.left, [None, None, None]);
3817 assert_eq!(
3818 layout.right,
3819 [
3820 Some(WindowButton::Minimize),
3821 Some(WindowButton::Maximize),
3822 Some(WindowButton::Close)
3823 ]
3824 );
3825
3826 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3827 assert_eq!(round_tripped, layout);
3828 }
3829
3830 #[test]
3831 fn test_window_button_layout_parse_all_invalid() {
3832 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3833 }
3834}