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, Default, Clone, Copy, PartialEq, Eq)]
129pub enum ActivationPolicy {
130 #[default]
132 Regular,
133 Accessory,
135}
136
137#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
138pub(crate) use test::*;
139
140#[cfg(any(test, feature = "test-support"))]
141pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
142
143#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
144pub use threaded_dispatcher::ThreadedDispatcher;
145
146#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
147pub use visual_test::VisualTestPlatform;
148
149pub struct ActivityGuard {
151 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
152}
153
154impl ActivityGuard {
155 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
157 Self {
158 _release: gpui_util::defer(Box::new(release)),
159 }
160 }
161
162 pub fn noop() -> Self {
164 Self::new(|| {})
165 }
166}
167
168#[cfg(any(target_os = "linux", target_os = "freebsd"))]
172#[inline]
173pub fn guess_compositor() -> &'static str {
174 if std::env::var_os("ZED_HEADLESS").is_some() {
175 return "Headless";
176 }
177 GraphicalEnvironment::detect().guess_compositor()
178}
179
180#[cfg(any(target_os = "linux", target_os = "freebsd"))]
189#[derive(Clone, Debug, Default)]
190pub struct GraphicalEnvironment {
191 pub wayland_display: Option<OsString>,
193 pub x11_display: Option<OsString>,
195 pub xdg_runtime_dir: Option<OsString>,
197 pub activation_token: Option<String>,
203}
204
205#[cfg(target_os = "windows")]
211#[derive(Clone, Debug, Default)]
212pub struct GraphicalEnvironment {
213 pub session_id: Option<u32>,
216}
217
218#[cfg(target_os = "windows")]
219impl GraphicalEnvironment {
220 pub fn detect() -> Self {
222 let mut session_id = 0;
223 let result = unsafe {
225 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
226 windows::Win32::System::Threading::GetCurrentProcessId(),
227 &mut session_id,
228 )
229 };
230 Self {
231 session_id: result.is_ok().then_some(session_id),
232 }
233 }
234
235 pub fn apply_to(&self, _command: &mut std::process::Command) {}
238}
239
240#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
242#[derive(Clone, Debug, Default)]
243pub struct GraphicalEnvironment;
244
245#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
246impl GraphicalEnvironment {
247 pub fn detect() -> Self {
249 Self
250 }
251
252 pub fn apply_to(&self, _command: &mut std::process::Command) {}
254}
255
256#[derive(Clone, Debug)]
258pub enum WindowingRequest {
259 Headless,
261 Windowed(GraphicalEnvironment),
263}
264
265#[cfg(any(target_os = "linux", target_os = "freebsd"))]
266impl GraphicalEnvironment {
267 pub fn detect() -> Self {
272 Self {
273 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
274 x11_display: std::env::var_os("DISPLAY"),
275 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
276 activation_token: None,
277 }
278 }
279
280 pub fn apply_to(&self, command: &mut std::process::Command) {
284 for (name, value) in [
285 ("WAYLAND_DISPLAY", &self.wayland_display),
286 ("DISPLAY", &self.x11_display),
287 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
288 ] {
289 match value {
290 Some(value) => command.env(name, value),
291 None => command.env_remove(name),
292 };
293 }
294 }
295
296 pub fn guess_compositor(&self) -> &'static str {
300 let is_set =
301 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
302 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
303 "Wayland"
304 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
305 "X11"
306 } else {
307 "Headless"
308 }
309 }
310}
311
312#[cfg(any(target_os = "linux", target_os = "freebsd"))]
313bitflags::bitflags! {
314 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
316 pub struct WindowingModes: u8 {
317 const WAYLAND = 1 << 0;
319 const X11 = 1 << 1;
321 const HEADLESS = 1 << 2;
323 }
324}
325
326#[expect(missing_docs)]
327pub trait Platform: 'static {
328 fn background_executor(&self) -> BackgroundExecutor;
329 fn foreground_executor(&self) -> ForegroundExecutor;
330 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
331
332 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
333 fn quit(&self);
334 fn set_initial_windowing(&self, _request: WindowingRequest) {}
339 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
340 Task::ready(Err(anyhow::anyhow!(
341 "this platform cannot switch between headless and windowed modes"
342 )))
343 }
344 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
345 fn activate(&self, ignoring_other_apps: bool);
346 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
348 fn hide(&self);
349 fn hide_other_apps(&self);
350 fn unhide_other_apps(&self);
351
352 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
353 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
354 fn active_window(&self) -> Option<AnyWindowHandle>;
355 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
356 None
357 }
358
359 fn is_screen_capture_supported(&self) -> bool {
360 false
361 }
362
363 fn screen_capture_sources(
364 &self,
365 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
366 let (sources_tx, sources_rx) = oneshot::channel();
367 sources_tx
368 .send(Err(anyhow::anyhow!(
369 "gpui was compiled without the screen-capture feature"
370 )))
371 .ok();
372 sources_rx
373 }
374
375 fn open_window(
376 &self,
377 handle: AnyWindowHandle,
378 options: WindowParams,
379 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
380
381 fn window_appearance(&self) -> WindowAppearance;
383
384 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
393
394 fn button_layout(&self) -> Option<WindowButtonLayout> {
396 None
397 }
398
399 fn open_url(&self, url: &str);
400 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
401 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
402
403 fn prompt_for_paths(
404 &self,
405 options: PathPromptOptions,
406 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
407 fn prompt_for_new_path(
408 &self,
409 directory: &Path,
410 suggested_name: Option<&str>,
411 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
412 fn can_select_mixed_files_and_dirs(&self) -> bool;
413 fn reveal_path(&self, path: &Path);
414 fn open_with_system(&self, path: &Path);
415
416 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
417 fn on_reopen(&self, callback: Box<dyn FnMut()>);
418 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
420 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
422
423 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
433
434 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
439
440 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
444 None
445 }
446
447 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
448 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
449 None
450 }
451
452 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
453 fn perform_dock_menu_action(&self, _action: usize) {}
454 fn add_recent_document(&self, _path: &Path) {}
455 fn update_jump_list(
456 &self,
457 _menus: Vec<MenuItem>,
458 _entries: Vec<SmallVec<[PathBuf; 2]>>,
459 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
460 Task::ready(Vec::new())
461 }
462 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
463 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
464 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
465
466 fn thermal_state(&self) -> ThermalState;
467 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
468 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
469
470 fn set_app_identity(&self, identifier: &str, name: &str) {
477 _ = (identifier, name);
478 }
479
480 fn show_system_notification(&self, notification: SystemNotification) {
487 _ = notification;
488 }
489
490 fn dismiss_system_notification(&self, tag: &str) {
495 _ = tag;
496 }
497
498 fn on_system_notification_response(
504 &self,
505 callback: Box<dyn FnMut(SystemNotificationResponse)>,
506 ) {
507 _ = callback;
508 }
509
510 fn compositor_name(&self) -> &'static str {
511 ""
512 }
513 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
516 None
517 }
518 fn app_path(&self) -> Result<PathBuf>;
519 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
520
521 fn set_cursor_style(&self, style: CursorStyle);
522
523 fn hide_cursor_until_mouse_moves(&self);
526
527 fn is_cursor_visible(&self) -> bool;
529
530 fn should_auto_hide_scrollbars(&self) -> bool;
531
532 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
533 fn write_to_clipboard(&self, item: ClipboardItem);
534
535 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
543 Task::ready(Ok(self.read_from_clipboard()))
544 }
545
546 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
547 fn read_from_primary(&self) -> Option<ClipboardItem>;
548 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
549 fn write_to_primary(&self, item: ClipboardItem);
550
551 #[cfg(target_os = "macos")]
552 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
553 #[cfg(target_os = "macos")]
554 fn write_to_find_pasteboard(&self, item: ClipboardItem);
555
556 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
557 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
558 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
559
560 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
561 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
562 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
563}
564
565pub trait PlatformDisplay: Debug {
567 fn id(&self) -> DisplayId;
569
570 fn uuid(&self) -> Result<Uuid>;
573
574 fn bounds(&self) -> Bounds<Pixels>;
576
577 fn visible_bounds(&self) -> Bounds<Pixels> {
581 self.bounds()
582 }
583
584 fn default_bounds(&self) -> Bounds<Pixels> {
586 let bounds = self.bounds();
587 let center = bounds.center();
588 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
589
590 let offset = clipped_window_size / 2.0;
591 let origin = point(center.x - offset.width, center.y - offset.height);
592 Bounds::new(origin, clipped_window_size)
593 }
594}
595
596#[derive(Clone, Debug, PartialEq, Eq)]
599pub struct SystemNotification {
600 pub tag: SharedString,
604 pub title: SharedString,
606 pub body: SharedString,
608 pub actions: Vec<SystemNotificationAction>,
611}
612
613#[derive(Clone, Debug, PartialEq, Eq, Hash)]
615pub struct SystemNotificationAction {
616 pub id: SharedString,
619 pub label: SharedString,
621}
622
623#[derive(Clone, Debug, PartialEq, Eq)]
625pub struct SystemNotificationResponse {
626 pub tag: SharedString,
628 pub action_id: Option<SharedString>,
631}
632
633#[derive(Debug, Clone, Copy, PartialEq, Eq)]
635pub enum ThermalState {
636 Nominal,
638 Fair,
640 Serious,
642 Critical,
644}
645
646#[derive(Clone)]
648pub struct SourceMetadata {
649 pub id: u64,
651 pub label: Option<SharedString>,
653 pub is_main: Option<bool>,
655 pub resolution: Size<DevicePixels>,
657}
658
659pub trait ScreenCaptureSource {
661 fn metadata(&self) -> Result<SourceMetadata>;
663
664 fn stream(
667 &self,
668 foreground_executor: &ForegroundExecutor,
669 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
670 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
671}
672
673pub trait ScreenCaptureStream {
675 fn metadata(&self) -> Result<SourceMetadata>;
677}
678
679pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
681
682#[derive(PartialEq, Eq, Hash, Copy, Clone)]
684pub struct DisplayId(pub(crate) u64);
685
686impl DisplayId {
687 pub fn new(id: u64) -> Self {
689 Self(id)
690 }
691}
692
693impl From<u64> for DisplayId {
694 fn from(id: u64) -> Self {
695 Self(id)
696 }
697}
698
699impl From<DisplayId> for u64 {
700 fn from(id: DisplayId) -> Self {
701 id.0
702 }
703}
704
705impl Debug for DisplayId {
706 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707 write!(f, "DisplayId({})", self.0)
708 }
709}
710
711#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub enum ResizeEdge {
714 Top,
716 TopRight,
718 Right,
720 BottomRight,
722 Bottom,
724 BottomLeft,
726 Left,
728 TopLeft,
730}
731
732#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
734pub enum WindowDecorations {
735 #[default]
736 Server,
738 Client,
740}
741
742#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
744pub enum Decorations {
745 #[default]
747 Server,
748 Client {
750 tiling: Tiling,
752 },
753}
754
755#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
757pub struct WindowControls {
758 pub fullscreen: bool,
760 pub maximize: bool,
762 pub minimize: bool,
764 pub window_menu: bool,
766}
767
768impl Default for WindowControls {
769 fn default() -> Self {
770 Self {
772 fullscreen: true,
773 maximize: true,
774 minimize: true,
775 window_menu: true,
776 }
777 }
778}
779
780#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
782pub enum WindowButton {
783 Minimize,
785 Maximize,
787 Close,
789}
790
791impl WindowButton {
792 pub fn id(&self) -> &'static str {
794 match self {
795 WindowButton::Minimize => "minimize",
796 WindowButton::Maximize => "maximize",
797 WindowButton::Close => "close",
798 }
799 }
800
801 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
802 fn index(&self) -> usize {
803 match self {
804 WindowButton::Minimize => 0,
805 WindowButton::Maximize => 1,
806 WindowButton::Close => 2,
807 }
808 }
809}
810
811pub const MAX_BUTTONS_PER_SIDE: usize = 3;
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
819pub struct WindowButtonLayout {
820 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
822 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
824}
825
826#[cfg(any(target_os = "linux", target_os = "freebsd"))]
827impl WindowButtonLayout {
828 pub fn linux_default() -> Self {
830 Self {
831 left: [None; MAX_BUTTONS_PER_SIDE],
832 right: [
833 Some(WindowButton::Minimize),
834 Some(WindowButton::Maximize),
835 Some(WindowButton::Close),
836 ],
837 }
838 }
839
840 pub fn parse(layout_string: &str) -> Result<Self> {
842 fn parse_side(
843 s: &str,
844 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
845 unrecognized: &mut Vec<String>,
846 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
847 let mut result = [None; MAX_BUTTONS_PER_SIDE];
848 let mut i = 0;
849 for name in s.split(',') {
850 let trimmed = name.trim();
851 if trimmed.is_empty() {
852 continue;
853 }
854 let button = match trimmed {
855 "minimize" => Some(WindowButton::Minimize),
856 "maximize" => Some(WindowButton::Maximize),
857 "close" => Some(WindowButton::Close),
858 other => {
859 unrecognized.push(other.to_string());
860 None
861 }
862 };
863 if let Some(button) = button {
864 if seen_buttons[button.index()] {
865 continue;
866 }
867 if let Some(slot) = result.get_mut(i) {
868 *slot = Some(button);
869 seen_buttons[button.index()] = true;
870 i += 1;
871 }
872 }
873 }
874 result
875 }
876
877 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
878 let mut unrecognized = Vec::new();
879 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
880 let layout = Self {
881 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
882 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
883 };
884
885 if !unrecognized.is_empty()
886 && layout.left.iter().all(Option::is_none)
887 && layout.right.iter().all(Option::is_none)
888 {
889 bail!(
890 "button layout string {:?} contains no valid buttons (unrecognized: {})",
891 layout_string,
892 unrecognized.join(", ")
893 );
894 }
895
896 Ok(layout)
897 }
898
899 #[cfg(test)]
901 pub fn format(&self) -> String {
902 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
903 buttons
904 .iter()
905 .flatten()
906 .map(|button| match button {
907 WindowButton::Minimize => "minimize",
908 WindowButton::Maximize => "maximize",
909 WindowButton::Close => "close",
910 })
911 .collect::<Vec<_>>()
912 .join(",")
913 }
914
915 format!("{}:{}", format_side(&self.left), format_side(&self.right))
916 }
917}
918
919#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
921pub struct Tiling {
922 pub top: bool,
924 pub left: bool,
926 pub right: bool,
928 pub bottom: bool,
930}
931
932impl Tiling {
933 pub fn tiled() -> Self {
935 Self {
936 top: true,
937 left: true,
938 right: true,
939 bottom: true,
940 }
941 }
942
943 pub fn is_tiled(&self) -> bool {
945 self.top || self.left || self.right || self.bottom
946 }
947}
948
949pub struct A11yCallbacks {
951 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
953 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
955 pub deactivation: Box<dyn Fn() + Send + 'static>,
957}
958
959#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
961pub enum FrameRequestSource {
962 #[default]
964 NativeCallback,
965 LocalSchedule,
967}
968
969#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
970#[expect(missing_docs)]
971pub struct RequestFrameOptions {
972 pub require_presentation: bool,
974 pub force_render: bool,
976 pub signal_at: Option<Instant>,
982 pub signal_source: FrameRequestSource,
984}
985
986pub struct PlatformFrameSignal {
995 #[cfg(feature = "profiler")]
996 origin: Instant,
997 #[cfg(feature = "profiler")]
998 first_signal: std::sync::atomic::AtomicU64,
999}
1000
1001impl Default for PlatformFrameSignal {
1002 fn default() -> Self {
1003 Self::new()
1004 }
1005}
1006
1007impl PlatformFrameSignal {
1008 pub fn new() -> Self {
1010 Self {
1011 #[cfg(feature = "profiler")]
1012 origin: Instant::now(),
1013 #[cfg(feature = "profiler")]
1014 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1015 }
1016 }
1017
1018 #[inline]
1022 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1023 if cfg!(feature = "profiler") {
1024 Some(capture())
1025 } else {
1026 None
1027 }
1028 }
1029
1030 #[inline]
1032 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1033 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1034 #[cfg(feature = "profiler")]
1035 {
1036 let nanoseconds = at
1037 .saturating_duration_since(self.origin)
1038 .as_nanos()
1039 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1040 let source_bit = match source {
1041 FrameRequestSource::NativeCallback => 0,
1042 FrameRequestSource::LocalSchedule => 1,
1043 };
1044 self.first_signal.fetch_min(
1045 (nanoseconds << 1) | source_bit,
1046 std::sync::atomic::Ordering::Relaxed,
1047 );
1048 }
1049 }
1050
1051 #[inline]
1053 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1054 #[cfg(feature = "profiler")]
1055 {
1056 let signal = self
1057 .first_signal
1058 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1059 (signal != u64::MAX).then(|| {
1060 let source = match signal & 1 {
1061 0 => FrameRequestSource::NativeCallback,
1062 _ => FrameRequestSource::LocalSchedule,
1063 };
1064 (self.origin + Duration::from_nanos(signal >> 1), source)
1065 })
1066 }
1067 #[cfg(not(feature = "profiler"))]
1068 {
1069 None
1070 }
1071 }
1072}
1073
1074#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1087pub enum AppLifecyclePhase {
1088 Active,
1090 Inactive,
1092 Background,
1095 Foreground,
1097}
1098
1099#[derive(Debug, Clone, Default, PartialEq)]
1106pub struct WindowInsets {
1107 pub safe_area: Edges<Pixels>,
1112 pub ime: Edges<Pixels>,
1116}
1117
1118impl WindowInsets {
1119 pub fn effective(&self) -> Edges<Pixels> {
1121 Edges {
1122 top: self.safe_area.top.max(self.ime.top),
1123 right: self.safe_area.right.max(self.ime.right),
1124 bottom: self.safe_area.bottom.max(self.ime.bottom),
1125 left: self.safe_area.left.max(self.ime.left),
1126 }
1127 }
1128}
1129
1130#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1132pub enum TextInputStateChange {
1133 FocusGained,
1135 FocusLost,
1137 SelectionChanged,
1139 ContentChanged,
1141}
1142
1143#[expect(missing_docs)]
1144pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1145 fn bounds(&self) -> Bounds<Pixels>;
1146 fn is_maximized(&self) -> bool;
1147 fn window_bounds(&self) -> WindowBounds;
1148 fn content_size(&self) -> Size<Pixels>;
1149 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1155 Bounds::new(Point::default(), self.content_size())
1156 }
1157 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1162 fn prepare_frame(&self) -> bool {
1167 false
1168 }
1169 fn resize(&mut self, size: Size<Pixels>);
1170 fn scale_factor(&self) -> f32;
1171 fn appearance(&self) -> WindowAppearance;
1172 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1173 fn mouse_position(&self) -> Point<Pixels>;
1174 fn modifiers(&self) -> Modifiers;
1175 fn capslock(&self) -> Capslock;
1176 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1177 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1178 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1183 fn prompt(
1184 &self,
1185 level: PromptLevel,
1186 msg: &str,
1187 detail: Option<&str>,
1188 answers: &[PromptButton],
1189 ) -> Option<oneshot::Receiver<usize>>;
1190 fn activate(&self);
1191 fn request_attention(&self) {}
1193 fn is_active(&self) -> bool;
1194 fn visibility(&self) -> WindowVisibility;
1197 fn is_hovered(&self) -> bool;
1198 fn background_appearance(&self) -> WindowBackgroundAppearance;
1199 fn set_title(&mut self, title: &str);
1200 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1201 fn minimize(&self);
1202 fn zoom(&self);
1203 fn toggle_fullscreen(&self);
1204 fn is_fullscreen(&self) -> bool;
1205 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1206 None
1207 }
1208 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1209 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1210 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1211 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1215 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1216 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1217 fn on_moved(&self, callback: Box<dyn FnMut()>);
1218 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1219 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1220 fn on_close(&self, callback: Box<dyn FnOnce()>);
1221 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1222 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1223 fn draw(&self, scene: &Scene);
1224 fn schedule_frame(&self) {}
1225 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1226 fn is_subpixel_rendering_supported(&self) -> bool;
1227
1228 fn get_title(&self) -> String {
1230 String::new()
1231 }
1232 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1233 None
1234 }
1235 fn tab_bar_visible(&self) -> bool {
1236 false
1237 }
1238 fn set_edited(&mut self, _edited: bool) {}
1239 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1240 fn toggle_simple_fullscreen(&self) {}
1241 fn is_simple_fullscreen(&self) -> bool {
1242 false
1243 }
1244 #[cfg(target_os = "macos")]
1245 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1246 fn show_character_palette(&self) {}
1247 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1248 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1249 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1250 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1251 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1252 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1253 fn merge_all_windows(&self) {}
1254 fn move_tab_to_new_window(&self) {}
1255 fn toggle_window_tab_overview(&self) {}
1256 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1257
1258 fn native_window_state(&self) -> Option<Vec<u8>> {
1259 None
1260 }
1261 fn restore_native_window_state(&self, _state: &[u8]) {}
1262
1263 #[cfg(target_os = "windows")]
1264 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1265
1266 fn inner_window_bounds(&self) -> WindowBounds {
1268 self.window_bounds()
1269 }
1270 fn request_decorations(&self, _decorations: WindowDecorations) {}
1271 fn show_window_menu(&self, _position: Point<Pixels>) {}
1272 fn start_window_move(&self) {}
1273 fn can_start_external_drag(&self) -> bool {
1274 false
1275 }
1276 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1277 false
1278 }
1279 fn start_window_resize(&self, _edge: ResizeEdge) {}
1280 fn set_exclusive_zone(&self, _zone: Pixels) {}
1281 #[cfg(all(target_os = "linux", feature = "wayland"))]
1282 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1283 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1284 fn window_decorations(&self) -> Decorations {
1285 Decorations::Server
1286 }
1287 fn set_app_id(&mut self, _app_id: &str) {}
1288 fn map_window(&mut self) -> anyhow::Result<()> {
1289 Ok(())
1290 }
1291 fn window_controls(&self) -> WindowControls {
1292 WindowControls::default()
1293 }
1294 fn set_client_inset(&self, _inset: Pixels) {}
1295 fn gpu_specs(&self) -> Option<GpuSpecs>;
1296
1297 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1298
1299 fn insets(&self) -> WindowInsets {
1304 WindowInsets::default()
1305 }
1306
1307 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1313
1314 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1317
1318 fn set_back_enabled(&self, _enabled: bool) {}
1321
1322 fn show_soft_keyboard(&self) {}
1324
1325 fn hide_soft_keyboard(&self) {}
1327
1328 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1330
1331 fn play_system_bell(&self) {}
1332
1333 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1335
1336 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1338
1339 fn a11y_update_window_bounds(&self) {}
1341
1342 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1343 fn as_test(&mut self) -> Option<&mut TestWindow> {
1344 None
1345 }
1346
1347 #[cfg(any(test, feature = "test-support"))]
1351 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1352 anyhow::bail!("render_to_image not implemented for this platform")
1353 }
1354}
1355
1356#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1358pub trait PlatformHeadlessRenderer {
1359 fn render_scene_to_image(
1361 &mut self,
1362 scene: &Scene,
1363 size: Size<DevicePixels>,
1364 ) -> Result<RgbaImage>;
1365
1366 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1372
1373 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1375}
1376
1377#[doc(hidden)]
1380pub type RunnableVariant = Runnable<RunnableMeta>;
1381
1382#[doc(hidden)]
1383pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1384
1385#[doc(hidden)]
1386pub enum TasksIncluded {
1387 OnlyCompleted,
1388 CompletedAndRunning,
1389}
1390
1391#[doc(hidden)]
1394pub trait PlatformDispatcher: Send + Sync {
1395 fn is_main_thread(&self) -> bool;
1396 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1397 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1398 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1399
1400 fn dispatch_on_main_thread_when_idle(
1401 &self,
1402 runnable: RunnableVariant,
1403 timeout: Option<Duration>,
1404 ) {
1405 let _ = timeout;
1406 self.dispatch_on_main_thread(runnable, Priority::Low);
1407 }
1408
1409 fn idle_time_remaining(&self) -> Option<Duration> {
1410 None
1411 }
1412
1413 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1414
1415 fn now(&self) -> Instant {
1416 Instant::now()
1417 }
1418
1419 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1420 gpui_util::defer(Box::new(|| {}))
1421 }
1422
1423 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1424 ActivityGuard::noop()
1425 }
1426
1427 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1428 fn as_test(&self) -> Option<&TestDispatcher> {
1429 None
1430 }
1431
1432 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1435 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1436 None
1437 }
1438}
1439
1440#[expect(missing_docs)]
1441pub trait PlatformTextSystem: Send + Sync {
1442 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1443 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1445 fn all_font_names(&self) -> Vec<String>;
1447 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1449 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1451 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1453 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1455 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1457 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1459 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1461 fn rasterize_glyph(
1463 &self,
1464 params: &RenderGlyphParams,
1465 raster_bounds: Bounds<DevicePixels>,
1466 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1467 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1469 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1471 -> TextRenderingMode;
1472 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1474 0
1475 }
1476}
1477
1478#[expect(missing_docs)]
1479pub struct NoopTextSystem;
1480
1481#[expect(missing_docs)]
1482impl NoopTextSystem {
1483 #[allow(dead_code)]
1484 pub fn new() -> Self {
1485 Self
1486 }
1487}
1488
1489impl PlatformTextSystem for NoopTextSystem {
1490 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1491 Ok(())
1492 }
1493
1494 fn all_font_names(&self) -> Vec<String> {
1495 Vec::new()
1496 }
1497
1498 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1499 Ok(FontId(1))
1500 }
1501
1502 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1503 FontMetrics {
1504 units_per_em: 1000,
1505 ascent: 1025.0,
1506 descent: -275.0,
1507 line_gap: 0.0,
1508 underline_position: -95.0,
1509 underline_thickness: 60.0,
1510 cap_height: 698.0,
1511 x_height: 516.0,
1512 bounding_box: Bounds {
1513 origin: Point {
1514 x: -260.0,
1515 y: -245.0,
1516 },
1517 size: Size {
1518 width: 1501.0,
1519 height: 1364.0,
1520 },
1521 },
1522 }
1523 }
1524
1525 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1526 Ok(Bounds {
1527 origin: Point { x: 54.0, y: 0.0 },
1528 size: size(392.0, 528.0),
1529 })
1530 }
1531
1532 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1533 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1534 }
1535
1536 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1537 Some(GlyphId(ch.len_utf16() as u32))
1538 }
1539
1540 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1541 Ok(Default::default())
1542 }
1543
1544 fn rasterize_glyph(
1545 &self,
1546 _params: &RenderGlyphParams,
1547 raster_bounds: Bounds<DevicePixels>,
1548 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1549 Ok((raster_bounds.size, Vec::new()))
1550 }
1551
1552 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1553 let mut position = px(0.);
1554 let metrics = self.font_metrics(FontId(0));
1555 let em_width = font_size
1556 * self
1557 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1558 .unwrap()
1559 .width
1560 / metrics.units_per_em as f32;
1561 let mut glyphs = Vec::new();
1562 for (ix, c) in text.char_indices() {
1563 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1564 glyphs.push(ShapedGlyph {
1565 id: glyph,
1566 position: point(position, px(0.)),
1567 index: ix,
1568 is_emoji: glyph.0 == 2,
1569 });
1570 if glyph.0 == 2 {
1571 position += em_width * 2.0;
1572 } else {
1573 position += em_width;
1574 }
1575 } else {
1576 position += em_width
1577 }
1578 }
1579 let mut runs = Vec::default();
1580 if !glyphs.is_empty() {
1581 runs.push(ShapedRun {
1582 font_id: FontId(0),
1583 glyphs,
1584 });
1585 } else {
1586 position = px(0.);
1587 }
1588
1589 LineLayout {
1590 font_size,
1591 width: position,
1592 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1593 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1594 runs,
1595 len: text.len(),
1596 }
1597 }
1598
1599 fn recommended_rendering_mode(
1600 &self,
1601 _font_id: FontId,
1602 _font_size: Pixels,
1603 ) -> TextRenderingMode {
1604 TextRenderingMode::Grayscale
1605 }
1606}
1607
1608#[allow(dead_code)]
1613pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1614 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1615 [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], ];
1629
1630 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1631 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1632
1633 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1634 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1635
1636 [
1637 ratios[0] * NORM13,
1638 ratios[1] * NORM24,
1639 ratios[2] * NORM13,
1640 ratios[3] * NORM24,
1641 ]
1642}
1643
1644#[derive(PartialEq, Eq, Hash, Clone)]
1645#[expect(missing_docs)]
1646pub enum AtlasKey {
1647 Glyph(RenderGlyphParams),
1648 Svg(RenderSvgParams),
1649 Image(RenderImageParams),
1650}
1651
1652impl AtlasKey {
1653 pub fn texture_kind(&self) -> AtlasTextureKind {
1655 match self {
1656 AtlasKey::Glyph(params) => {
1657 if params.is_emoji {
1658 AtlasTextureKind::Polychrome
1659 } else if params.subpixel_rendering {
1660 AtlasTextureKind::Subpixel
1661 } else {
1662 AtlasTextureKind::Monochrome
1663 }
1664 }
1665 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1666 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1667 }
1668 }
1669}
1670
1671impl From<RenderGlyphParams> for AtlasKey {
1672 fn from(params: RenderGlyphParams) -> Self {
1673 Self::Glyph(params)
1674 }
1675}
1676
1677impl From<RenderSvgParams> for AtlasKey {
1678 fn from(params: RenderSvgParams) -> Self {
1679 Self::Svg(params)
1680 }
1681}
1682
1683impl From<RenderImageParams> for AtlasKey {
1684 fn from(params: RenderImageParams) -> Self {
1685 Self::Image(params)
1686 }
1687}
1688
1689#[expect(missing_docs)]
1690pub trait PlatformAtlas {
1691 fn get_or_insert_with<'a>(
1693 &self,
1694 key: AtlasKey,
1695 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1696 ) -> Result<Option<AtlasTile>>;
1697 fn remove(&self, key: &AtlasKey);
1698
1699 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1700 fn contains(&self, _key: &AtlasKey) -> bool {
1701 false
1702 }
1703}
1704
1705#[doc(hidden)]
1706pub trait AtlasBackend {
1707 fn insert(
1708 &mut self,
1709 kind: AtlasTextureKind,
1710 size: Size<DevicePixels>,
1711 bytes: &[u8],
1712 ) -> Result<AtlasTile>;
1713
1714 fn remove(&mut self, tile: AtlasTile);
1715}
1716
1717#[doc(hidden)]
1718pub struct AtlasState<Backend> {
1719 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1720 pub backend: Backend,
1721}
1722
1723impl<Backend> AtlasState<Backend> {
1724 pub fn new(backend: Backend) -> Self {
1725 Self {
1726 tiles_by_key: FxHashMap::default(),
1727 backend,
1728 }
1729 }
1730
1731 pub fn contains(&self, key: &AtlasKey) -> bool {
1732 self.tiles_by_key.contains_key(key)
1733 }
1734
1735 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1736 self.tiles_by_key.clear();
1737 reset_backend(&mut self.backend);
1738 }
1739}
1740
1741impl<Backend: Default> Default for AtlasState<Backend> {
1742 fn default() -> Self {
1743 Self::new(Backend::default())
1744 }
1745}
1746
1747impl<Backend: AtlasBackend> AtlasState<Backend> {
1748 pub fn get_or_insert_with<'a>(
1749 &mut self,
1750 key: AtlasKey,
1751 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1752 ) -> Result<Option<AtlasTile>> {
1753 match self.tiles_by_key.entry(key) {
1754 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1755 Entry::Vacant(entry) => {
1756 profiling::scope!("new tile");
1757 let Some((size, bytes)) = build()? else {
1758 return Ok(None);
1759 };
1760 let tile = self
1761 .backend
1762 .insert(entry.key().texture_kind(), size, &bytes)?;
1763 entry.insert(tile);
1764 Ok(Some(tile))
1765 }
1766 }
1767 }
1768
1769 pub fn remove(&mut self, key: &AtlasKey) {
1770 if let Some(tile) = self.tiles_by_key.remove(key) {
1771 self.backend.remove(tile);
1772 }
1773 }
1774}
1775
1776#[derive(Default)]
1780pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1781
1782#[doc(hidden)]
1783#[derive(Default)]
1784pub struct HeadlessAtlasBackend {
1785 next_id: u32,
1786}
1787
1788impl AtlasBackend for HeadlessAtlasBackend {
1789 fn insert(
1790 &mut self,
1791 kind: AtlasTextureKind,
1792 size: Size<DevicePixels>,
1793 _bytes: &[u8],
1794 ) -> Result<AtlasTile> {
1795 self.next_id += 1;
1796 let texture_id = self.next_id;
1797 self.next_id += 1;
1798 let tile_id = self.next_id;
1799 Ok(AtlasTile {
1800 texture_id: AtlasTextureId {
1801 index: texture_id,
1802 kind,
1803 },
1804 tile_id: TileId(tile_id),
1805 padding: 0,
1806 bounds: Bounds {
1807 origin: Point::default(),
1808 size,
1809 },
1810 })
1811 }
1812
1813 fn remove(&mut self, _tile: AtlasTile) {}
1814}
1815
1816impl PlatformAtlas for HeadlessAtlas {
1817 fn get_or_insert_with<'a>(
1818 &self,
1819 key: AtlasKey,
1820 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1821 ) -> Result<Option<AtlasTile>> {
1822 self.0.lock().get_or_insert_with(key, build)
1823 }
1824
1825 fn remove(&self, key: &AtlasKey) {
1826 self.0.lock().remove(key);
1827 }
1828
1829 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1830 fn contains(&self, key: &AtlasKey) -> bool {
1831 self.0.lock().contains(key)
1832 }
1833}
1834
1835#[doc(hidden)]
1836pub struct AtlasTextureList<T> {
1837 pub textures: Vec<Option<T>>,
1838 pub free_list: Vec<usize>,
1839}
1840
1841impl<T> Default for AtlasTextureList<T> {
1842 fn default() -> Self {
1843 Self {
1844 textures: Vec::default(),
1845 free_list: Vec::default(),
1846 }
1847 }
1848}
1849
1850impl<T> ops::Index<usize> for AtlasTextureList<T> {
1851 type Output = Option<T>;
1852
1853 fn index(&self, index: usize) -> &Self::Output {
1854 &self.textures[index]
1855 }
1856}
1857
1858impl<T> AtlasTextureList<T> {
1859 #[allow(unused)]
1860 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1861 self.free_list.clear();
1862 self.textures.drain(..)
1863 }
1864
1865 #[allow(dead_code)]
1866 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1867 self.textures.iter_mut().flatten()
1868 }
1869}
1870
1871#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1872#[repr(C)]
1873#[expect(missing_docs)]
1874pub struct AtlasTile {
1875 pub texture_id: AtlasTextureId,
1877 pub tile_id: TileId,
1879 pub padding: u32,
1881 pub bounds: Bounds<DevicePixels>,
1883}
1884
1885#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1886#[repr(C)]
1887#[expect(missing_docs)]
1888pub struct AtlasTextureId {
1889 pub index: u32,
1892 pub kind: AtlasTextureKind,
1894}
1895
1896#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1897#[repr(C)]
1898#[cfg_attr(
1899 all(
1900 any(target_os = "linux", target_os = "freebsd"),
1901 not(any(feature = "x11", feature = "wayland"))
1902 ),
1903 allow(dead_code)
1904)]
1905#[expect(missing_docs)]
1906pub enum AtlasTextureKind {
1907 Monochrome = 0,
1908 Polychrome = 1,
1909 Subpixel = 2,
1910}
1911
1912#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1913#[repr(C)]
1914#[expect(missing_docs)]
1915pub struct TileId(pub u32);
1916
1917impl From<etagere::AllocId> for TileId {
1918 fn from(id: etagere::AllocId) -> Self {
1919 Self(id.serialize())
1920 }
1921}
1922
1923impl From<TileId> for etagere::AllocId {
1924 fn from(id: TileId) -> Self {
1925 Self::deserialize(id.0)
1926 }
1927}
1928
1929#[expect(missing_docs)]
1930pub struct PlatformInputHandler {
1931 cx: AsyncWindowContext,
1932 handler: Box<dyn InputHandler>,
1933}
1934
1935#[expect(missing_docs)]
1936#[cfg_attr(
1937 all(
1938 any(target_os = "linux", target_os = "freebsd"),
1939 not(any(feature = "x11", feature = "wayland"))
1940 ),
1941 allow(dead_code)
1942)]
1943impl PlatformInputHandler {
1944 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1945 Self { cx, handler }
1946 }
1947
1948 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1949 self.cx
1950 .update(|window, cx| {
1951 self.handler
1952 .selected_text_range(ignore_disabled_input, window, cx)
1953 })
1954 .ok()
1955 .flatten()
1956 }
1957
1958 #[cfg_attr(target_os = "windows", allow(dead_code))]
1959 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1960 self.cx
1961 .update(|window, cx| self.handler.marked_text_range(window, cx))
1962 .ok()
1963 .flatten()
1964 }
1965
1966 #[cfg_attr(
1967 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1968 allow(dead_code)
1969 )]
1970 pub fn text_for_range(
1971 &mut self,
1972 range_utf16: Range<usize>,
1973 adjusted: &mut Option<Range<usize>>,
1974 ) -> Option<String> {
1975 self.cx
1976 .update(|window, cx| {
1977 self.handler
1978 .text_for_range(range_utf16, adjusted, window, cx)
1979 })
1980 .ok()
1981 .flatten()
1982 }
1983
1984 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1985 self.cx
1986 .update(|window, cx| {
1987 self.handler
1988 .replace_text_in_range(replacement_range, text, window, cx);
1989 })
1990 .ok();
1991 }
1992
1993 pub fn replace_and_mark_text_in_range(
1994 &mut self,
1995 range_utf16: Option<Range<usize>>,
1996 new_text: &str,
1997 new_selected_range: Option<Range<usize>>,
1998 ) {
1999 self.cx
2000 .update(|window, cx| {
2001 self.handler.replace_and_mark_text_in_range(
2002 range_utf16,
2003 new_text,
2004 new_selected_range,
2005 window,
2006 cx,
2007 )
2008 })
2009 .ok();
2010 }
2011
2012 #[cfg_attr(target_os = "windows", allow(dead_code))]
2013 pub fn unmark_text(&mut self) {
2014 self.cx
2015 .update(|window, cx| self.handler.unmark_text(window, cx))
2016 .ok();
2017 }
2018
2019 pub fn paste(&mut self, item: ClipboardItem) {
2020 self.cx
2021 .update(|window, cx| self.handler.paste(item, window, cx))
2022 .ok();
2023 }
2024
2025 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2026 self.cx
2027 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2028 .ok()
2029 .flatten()
2030 }
2031
2032 #[allow(dead_code)]
2033 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2034 self.handler.apple_press_and_hold_enabled()
2035 }
2036
2037 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2038 self.handler.replace_text_in_range(None, input, window, cx);
2039 }
2040
2041 pub fn compute_ime_candidate_bounds(
2042 marked_range: Option<Range<usize>>,
2043 selection: &UTF16Selection,
2044 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2045 ) -> Option<Bounds<Pixels>> {
2046 if let Some(marked_range) = marked_range {
2047 let mut line_start = marked_range.start;
2049
2050 let caret = selection.range.end;
2054 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2055 for i in (marked_range.start..caret).rev() {
2056 if let Some(b) = bounds_for_range(i..i) {
2057 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2058 line_start = i + 1;
2059 break;
2060 }
2061 }
2062 }
2063 }
2064 bounds_for_range(line_start..line_start)
2065 } else {
2066 let offset = if selection.reversed {
2068 selection.range.start
2069 } else {
2070 selection.range.end
2071 };
2072 bounds_for_range(offset..offset)
2073 }
2074 }
2075
2076 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2077 let marked_range = self.handler.marked_text_range(window, cx);
2078 let selection = self.handler.selected_text_range(true, window, cx)?;
2079 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2080 self.handler.bounds_for_range(range, window, cx)
2081 })
2082 }
2083
2084 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2085 let marked_range = self.marked_text_range();
2086 let selection = self.selected_text_range(true)?;
2087 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2088 self.bounds_for_range(range)
2089 })
2090 }
2091
2092 #[allow(unused)]
2093 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2094 self.cx
2095 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2096 .ok()
2097 .flatten()
2098 }
2099
2100 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2102 self.cx
2103 .update(|window, cx| {
2104 self.handler
2105 .set_selected_text_range(range_utf16, window, cx)
2106 })
2107 .ok();
2108 }
2109
2110 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2112 self.cx
2113 .update(|window, cx| self.handler.element_bounds(window, cx))
2114 .ok()
2115 .flatten()
2116 }
2117
2118 pub fn text_length_utf16(&mut self) -> Option<usize> {
2120 self.cx
2121 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2122 .ok()
2123 .flatten()
2124 }
2125
2126 #[allow(dead_code)]
2127 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2128 self.handler.accepts_text_input(window, cx)
2129 }
2130
2131 #[allow(dead_code)]
2132 pub fn query_accepts_text_input(&mut self) -> bool {
2133 self.cx
2134 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2135 .unwrap_or(true)
2136 }
2137
2138 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2144 self.cx
2145 .update(|window, cx| {
2146 !window.has_pending_keystrokes()
2148 && self.handler.prefers_ime_for_printable_keys(window, cx)
2149 })
2150 .unwrap_or(false)
2151 }
2152
2153 pub fn text_input_configuration(
2155 &mut self,
2156 window: &mut Window,
2157 cx: &mut App,
2158 ) -> TextInputConfiguration {
2159 self.handler.text_input_configuration(window, cx)
2160 }
2161
2162 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2164 self.cx
2165 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2166 .ok()
2167 .flatten()
2168 }
2169}
2170
2171#[derive(Debug)]
2174pub struct UTF16Selection {
2175 pub range: Range<usize>,
2178 pub reversed: bool,
2181}
2182
2183pub trait InputHandler: 'static {
2188 fn selected_text_range(
2193 &mut self,
2194 ignore_disabled_input: bool,
2195 window: &mut Window,
2196 cx: &mut App,
2197 ) -> Option<UTF16Selection>;
2198
2199 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2204
2205 fn text_for_range(
2210 &mut self,
2211 range_utf16: Range<usize>,
2212 adjusted_range: &mut Option<Range<usize>>,
2213 window: &mut Window,
2214 cx: &mut App,
2215 ) -> Option<String>;
2216
2217 fn replace_text_in_range(
2222 &mut self,
2223 replacement_range: Option<Range<usize>>,
2224 text: &str,
2225 window: &mut Window,
2226 cx: &mut App,
2227 );
2228
2229 fn replace_and_mark_text_in_range(
2236 &mut self,
2237 range_utf16: Option<Range<usize>>,
2238 new_text: &str,
2239 new_selected_range: Option<Range<usize>>,
2240 window: &mut Window,
2241 cx: &mut App,
2242 );
2243
2244 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2247
2248 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2255 if let Some(text) = item.text() {
2256 self.replace_text_in_range(None, &text, window, cx);
2257 }
2258 }
2259
2260 fn bounds_for_range(
2265 &mut self,
2266 range_utf16: Range<usize>,
2267 window: &mut Window,
2268 cx: &mut App,
2269 ) -> Option<Bounds<Pixels>>;
2270
2271 fn character_index_for_point(
2275 &mut self,
2276 point: Point<Pixels>,
2277 window: &mut Window,
2278 cx: &mut App,
2279 ) -> Option<usize>;
2280
2281 fn set_selected_text_range(
2291 &mut self,
2292 _range_utf16: Range<usize>,
2293 _window: &mut Window,
2294 _cx: &mut App,
2295 ) {
2296 }
2297
2298 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2305 None
2306 }
2307
2308 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2310 None
2311 }
2312
2313 #[allow(dead_code)]
2318 fn apple_press_and_hold_enabled(&mut self) -> bool {
2319 true
2320 }
2321
2322 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2324 true
2325 }
2326
2327 fn text_input_editable_range(
2338 &mut self,
2339 _window: &mut Window,
2340 _cx: &mut App,
2341 ) -> Option<Range<usize>> {
2342 None
2343 }
2344
2345 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2354 false
2355 }
2356
2357 fn text_input_configuration(
2363 &mut self,
2364 _window: &mut Window,
2365 _cx: &mut App,
2366 ) -> TextInputConfiguration {
2367 TextInputConfiguration::default()
2368 }
2369}
2370
2371#[derive(Clone, Debug, Default, PartialEq, Eq)]
2381pub struct TextInputConfiguration {
2382 pub autocorrect: bool,
2384 pub autocapitalize: Autocapitalize,
2386 pub suggestions: bool,
2388 pub input_action: TextInputAction,
2390}
2391
2392#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2394pub enum Autocapitalize {
2395 #[default]
2397 None,
2398 Words,
2400 Sentences,
2402 Characters,
2404}
2405
2406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2416pub enum TextInputAction {
2417 #[default]
2419 Unspecified,
2420 Enter,
2422 Done,
2424 Go,
2426 Next,
2428 Previous,
2430 Search,
2432 Send,
2434}
2435
2436#[derive(Debug)]
2438pub struct WindowOptions {
2439 pub window_bounds: Option<WindowBounds>,
2443
2444 pub titlebar: Option<TitlebarOptions>,
2446
2447 pub focus: bool,
2449
2450 pub show: bool,
2452
2453 pub kind: WindowKind,
2455
2456 pub is_movable: bool,
2460
2461 pub app_owns_titlebar_drag: bool,
2473
2474 pub inactive_frame_interval: Option<Duration>,
2478
2479 pub is_resizable: bool,
2481
2482 pub is_minimizable: bool,
2484
2485 pub display_id: Option<DisplayId>,
2488
2489 pub window_background: WindowBackgroundAppearance,
2491
2492 pub app_id: Option<String>,
2494
2495 pub window_min_size: Option<Size<Pixels>>,
2497
2498 pub window_decorations: Option<WindowDecorations>,
2501
2502 pub icon: Option<Arc<image::RgbaImage>>,
2504
2505 pub tabbing_identifier: Option<String>,
2507}
2508
2509#[derive(Debug)]
2511#[cfg_attr(
2512 all(
2513 any(target_os = "linux", target_os = "freebsd"),
2514 not(any(feature = "x11", feature = "wayland"))
2515 ),
2516 allow(dead_code)
2517)]
2518#[allow(missing_docs)]
2519pub struct WindowParams {
2520 pub bounds: Bounds<Pixels>,
2521
2522 #[cfg_attr(feature = "wayland", allow(dead_code))]
2524 pub titlebar: Option<TitlebarOptions>,
2525
2526 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2528 pub kind: WindowKind,
2529
2530 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2532 pub is_movable: bool,
2533
2534 #[cfg_attr(
2536 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2537 allow(dead_code)
2538 )]
2539 pub app_owns_titlebar_drag: bool,
2540
2541 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2543 pub is_resizable: bool,
2544
2545 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2547 pub is_minimizable: bool,
2548
2549 #[cfg_attr(
2550 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2551 allow(dead_code)
2552 )]
2553 pub focus: bool,
2554
2555 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2556 pub show: bool,
2557
2558 #[cfg_attr(feature = "wayland", allow(dead_code))]
2560 pub icon: Option<Arc<image::RgbaImage>>,
2561
2562 #[cfg_attr(feature = "wayland", allow(dead_code))]
2563 pub display_id: Option<DisplayId>,
2564
2565 #[cfg_attr(feature = "wayland", allow(dead_code))]
2566 pub app_id: Option<String>,
2567
2568 pub window_min_size: Option<Size<Pixels>>,
2569
2570 #[cfg(target_os = "macos")]
2571 pub tabbing_identifier: Option<String>,
2572}
2573
2574#[derive(Debug, Copy, Clone, PartialEq)]
2576pub enum WindowBounds {
2577 Windowed(Bounds<Pixels>),
2579 Maximized(Bounds<Pixels>),
2582 Fullscreen(Bounds<Pixels>),
2585}
2586
2587impl Default for WindowBounds {
2588 fn default() -> Self {
2589 WindowBounds::Windowed(Bounds::default())
2590 }
2591}
2592
2593impl WindowBounds {
2594 pub fn get_bounds(&self) -> Bounds<Pixels> {
2596 match self {
2597 WindowBounds::Windowed(bounds) => *bounds,
2598 WindowBounds::Maximized(bounds) => *bounds,
2599 WindowBounds::Fullscreen(bounds) => *bounds,
2600 }
2601 }
2602
2603 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2605 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2606 }
2607}
2608
2609impl Default for WindowOptions {
2610 fn default() -> Self {
2611 Self {
2612 window_bounds: None,
2613 titlebar: Some(TitlebarOptions {
2614 title: Default::default(),
2615 appears_transparent: Default::default(),
2616 traffic_light_position: Default::default(),
2617 }),
2618 focus: true,
2619 show: true,
2620 kind: WindowKind::Normal,
2621 is_movable: true,
2622 app_owns_titlebar_drag: false,
2623 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2624 is_resizable: true,
2625 is_minimizable: true,
2626 display_id: None,
2627 window_background: WindowBackgroundAppearance::default(),
2628 icon: None,
2629 app_id: None,
2630 window_min_size: None,
2631 window_decorations: None,
2632 tabbing_identifier: None,
2633 }
2634 }
2635}
2636
2637#[derive(Debug, Default)]
2639pub struct TitlebarOptions {
2640 pub title: Option<SharedString>,
2642
2643 pub appears_transparent: bool,
2646
2647 pub traffic_light_position: Option<Point<Pixels>>,
2649}
2650
2651#[derive(Clone, Debug, PartialEq, Eq)]
2653pub enum WindowKind {
2654 Normal,
2656
2657 PopUp,
2660
2661 AnchoredPopup(popup::PopupOptions),
2668
2669 Floating,
2671
2672 #[cfg(all(target_os = "linux", feature = "wayland"))]
2675 LayerShell(layer_shell::LayerShellOptions),
2676
2677 Dialog,
2680}
2681
2682#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2687pub enum WindowAppearance {
2688 #[default]
2692 Light,
2693
2694 VibrantLight,
2698
2699 Dark,
2703
2704 VibrantDark,
2708}
2709
2710#[derive(Copy, Clone, Debug, Default, PartialEq)]
2713pub enum WindowBackgroundAppearance {
2714 #[default]
2722 Opaque,
2723 Transparent,
2725 Blurred,
2729 MicaBackdrop,
2731 MicaAltBackdrop,
2733}
2734
2735#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2737pub enum TextRenderingMode {
2738 #[default]
2740 PlatformDefault,
2741 Subpixel,
2743 Grayscale,
2745}
2746
2747#[derive(Clone, Debug)]
2749pub struct PathPromptOptions {
2750 pub files: bool,
2752 pub directories: bool,
2754 pub multiple: bool,
2756 pub prompt: Option<SharedString>,
2758}
2759
2760#[derive(Copy, Clone, Debug, PartialEq)]
2762pub enum PromptLevel {
2763 Info,
2765
2766 Warning,
2768
2769 Critical,
2771}
2772
2773#[derive(Clone, Debug, PartialEq)]
2775pub enum PromptButton {
2776 Ok(SharedString),
2778 Cancel(SharedString),
2780 Other(SharedString),
2782}
2783
2784impl PromptButton {
2785 pub fn new(label: impl Into<SharedString>) -> Self {
2787 PromptButton::Other(label.into())
2788 }
2789
2790 pub fn ok(label: impl Into<SharedString>) -> Self {
2792 PromptButton::Ok(label.into())
2793 }
2794
2795 pub fn cancel(label: impl Into<SharedString>) -> Self {
2797 PromptButton::Cancel(label.into())
2798 }
2799
2800 #[allow(dead_code)]
2802 pub fn is_cancel(&self) -> bool {
2803 matches!(self, PromptButton::Cancel(_))
2804 }
2805
2806 pub fn label(&self) -> &SharedString {
2808 match self {
2809 PromptButton::Ok(label) => label,
2810 PromptButton::Cancel(label) => label,
2811 PromptButton::Other(label) => label,
2812 }
2813 }
2814}
2815
2816impl From<&str> for PromptButton {
2817 fn from(value: &str) -> Self {
2818 match value.to_lowercase().as_str() {
2819 "ok" => PromptButton::Ok("OK".into()),
2820 "cancel" => PromptButton::Cancel("Cancel".into()),
2821 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2822 }
2823 }
2824}
2825
2826#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2828pub enum CursorStyle {
2829 #[default]
2831 Arrow,
2832
2833 IBeam,
2836
2837 Crosshair,
2840
2841 ClosedHand,
2844
2845 OpenHand,
2848
2849 PointingHand,
2852
2853 ResizeLeft,
2856
2857 ResizeRight,
2860
2861 ResizeLeftRight,
2864
2865 ResizeUp,
2868
2869 ResizeDown,
2872
2873 ResizeUpDown,
2876
2877 ResizeUpLeftDownRight,
2880
2881 ResizeUpRightDownLeft,
2884
2885 ResizeColumn,
2888
2889 ResizeRow,
2892
2893 IBeamCursorForVerticalLayout,
2896
2897 OperationNotAllowed,
2900
2901 DragLink,
2904
2905 DragCopy,
2908
2909 ContextualMenu,
2912}
2913
2914#[derive(Clone, Debug, Eq, PartialEq)]
2916pub struct ClipboardItem {
2917 pub entries: Vec<ClipboardEntry>,
2919}
2920
2921#[derive(Clone, Debug, PartialEq, Eq)]
2926pub enum ClipboardReadError {
2927 Unavailable,
2931 Denied(String),
2934 UnsupportedContent,
2937}
2938
2939impl std::fmt::Display for ClipboardReadError {
2940 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2941 match self {
2942 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
2943 Self::Denied(message) => {
2944 write!(formatter, "clipboard access was denied: {message}")
2945 }
2946 Self::UnsupportedContent => {
2947 formatter.write_str("the clipboard contents are unsupported")
2948 }
2949 }
2950 }
2951}
2952
2953impl std::error::Error for ClipboardReadError {}
2954
2955#[derive(Clone, Debug, Eq, PartialEq)]
2957pub enum ClipboardEntry {
2958 String(ClipboardString),
2960 Image(Image),
2962 ExternalPaths(crate::ExternalPaths),
2964}
2965
2966impl ClipboardItem {
2967 pub fn new_string(text: String) -> Self {
2969 Self {
2970 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2971 }
2972 }
2973
2974 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2976 Self {
2977 entries: vec![ClipboardEntry::String(ClipboardString {
2978 text,
2979 metadata: Some(metadata),
2980 })],
2981 }
2982 }
2983
2984 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2986 Self {
2987 entries: vec![ClipboardEntry::String(
2988 ClipboardString::new(text).with_json_metadata(metadata),
2989 )],
2990 }
2991 }
2992
2993 pub fn new_image(image: &Image) -> Self {
2995 Self {
2996 entries: vec![ClipboardEntry::Image(image.clone())],
2997 }
2998 }
2999
3000 pub fn text(&self) -> Option<String> {
3003 let mut answer = String::new();
3004
3005 for entry in self.entries.iter() {
3006 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3007 answer.push_str(text);
3008 }
3009 }
3010
3011 if answer.is_empty() {
3012 for entry in self.entries.iter() {
3013 if let ClipboardEntry::ExternalPaths(paths) = entry {
3014 for path in &paths.0 {
3015 use std::fmt::Write as _;
3016 _ = write!(answer, "{}", path.display());
3017 }
3018 }
3019 }
3020 }
3021
3022 if !answer.is_empty() {
3023 Some(answer)
3024 } else {
3025 None
3026 }
3027 }
3028
3029 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3031 pub fn metadata(&self) -> Option<&String> {
3032 match self.entries().first() {
3033 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3034 clipboard_string.metadata.as_ref()
3035 }
3036 _ => None,
3037 }
3038 }
3039
3040 pub fn entries(&self) -> &[ClipboardEntry] {
3042 &self.entries
3043 }
3044
3045 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3047 self.entries.into_iter()
3048 }
3049}
3050
3051impl From<ClipboardString> for ClipboardEntry {
3052 fn from(value: ClipboardString) -> Self {
3053 Self::String(value)
3054 }
3055}
3056
3057impl From<String> for ClipboardEntry {
3058 fn from(value: String) -> Self {
3059 Self::from(ClipboardString::from(value))
3060 }
3061}
3062
3063impl From<Image> for ClipboardEntry {
3064 fn from(value: Image) -> Self {
3065 Self::Image(value)
3066 }
3067}
3068
3069impl From<ClipboardEntry> for ClipboardItem {
3070 fn from(value: ClipboardEntry) -> Self {
3071 Self {
3072 entries: vec![value],
3073 }
3074 }
3075}
3076
3077impl From<String> for ClipboardItem {
3078 fn from(value: String) -> Self {
3079 Self::from(ClipboardEntry::from(value))
3080 }
3081}
3082
3083impl From<Image> for ClipboardItem {
3084 fn from(value: Image) -> Self {
3085 Self::from(ClipboardEntry::from(value))
3086 }
3087}
3088
3089#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3091pub enum ImageFormat {
3092 Png,
3097 Jpeg,
3099 Webp,
3101 Gif,
3103 Svg,
3105 Bmp,
3107 Tiff,
3109 Ico,
3111 Pnm,
3113}
3114
3115impl ImageFormat {
3116 pub const fn mime_type(self) -> &'static str {
3118 match self {
3119 ImageFormat::Png => "image/png",
3120 ImageFormat::Jpeg => "image/jpeg",
3121 ImageFormat::Webp => "image/webp",
3122 ImageFormat::Gif => "image/gif",
3123 ImageFormat::Svg => "image/svg+xml",
3124 ImageFormat::Bmp => "image/bmp",
3125 ImageFormat::Tiff => "image/tiff",
3126 ImageFormat::Ico => "image/ico",
3127 ImageFormat::Pnm => "image/x-portable-anymap",
3128 }
3129 }
3130
3131 pub const fn extension(self) -> &'static str {
3133 match self {
3134 ImageFormat::Png => "png",
3135 ImageFormat::Jpeg => "jpg",
3136 ImageFormat::Webp => "webp",
3137 ImageFormat::Gif => "gif",
3138 ImageFormat::Svg => "svg",
3139 ImageFormat::Bmp => "bmp",
3140 ImageFormat::Tiff => "tiff",
3141 ImageFormat::Ico => "ico",
3142 ImageFormat::Pnm => "pnm",
3143 }
3144 }
3145
3146 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3148 use strum::IntoEnumIterator;
3149 Self::iter()
3150 .find(|format| format.mime_type() == mime_type)
3151 .or_else(|| Self::from_mime_type_alias(mime_type))
3152 }
3153
3154 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3158 match mime_type {
3159 "image/jpg" => Some(Self::Jpeg),
3160 "image/tif" => Some(Self::Tiff),
3161 _ => None,
3162 }
3163 }
3164}
3165
3166#[derive(Clone, Debug, PartialEq, Eq)]
3168pub struct Image {
3169 pub format: ImageFormat,
3171 pub bytes: Vec<u8>,
3173 pub id: u64,
3175}
3176
3177pub(crate) fn decode_static_image(
3178 bytes: &[u8],
3179 format: image::ImageFormat,
3180) -> Result<SmallVec<[Frame; 1]>> {
3181 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3182 .into_decoder()
3183 .context("creating image decoder")?;
3184 decode_static_image_from_decoder(decoder)
3185}
3186
3187pub(crate) fn decode_static_image_from_decoder(
3188 mut decoder: impl image::ImageDecoder,
3189) -> Result<SmallVec<[Frame; 1]>> {
3190 let orientation = decoder
3191 .orientation()
3192 .context("reading decoder's orientation")?;
3193 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3194 image.apply_orientation(orientation);
3195
3196 let mut data = image.into_rgba8();
3197 for pixel in data.chunks_exact_mut(4) {
3198 pixel.swap(0, 2);
3199 }
3200
3201 Ok(SmallVec::from_elem(Frame::new(data), 1))
3202}
3203
3204impl Hash for Image {
3205 fn hash<H: Hasher>(&self, state: &mut H) {
3206 state.write_u64(self.id);
3207 }
3208}
3209
3210impl Image {
3211 pub fn empty() -> Self {
3213 Self::from_bytes(ImageFormat::Png, Vec::new())
3214 }
3215
3216 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3218 Self {
3219 id: hash(&bytes),
3220 format,
3221 bytes,
3222 }
3223 }
3224
3225 pub fn id(&self) -> u64 {
3227 self.id
3228 }
3229
3230 pub fn use_render_image(
3232 self: Arc<Self>,
3233 window: &mut Window,
3234 cx: &mut App,
3235 ) -> Option<Arc<RenderImage>> {
3236 ImageSource::Image(self)
3237 .use_data(None, window, cx)
3238 .and_then(|result| result.ok())
3239 }
3240
3241 pub fn get_render_image(
3243 self: Arc<Self>,
3244 window: &mut Window,
3245 cx: &mut App,
3246 ) -> Option<Arc<RenderImage>> {
3247 ImageSource::Image(self)
3248 .get_data(None, window, cx)
3249 .and_then(|result| result.ok())
3250 }
3251
3252 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3254 ImageSource::Image(self).remove_asset(cx);
3255 }
3256
3257 #[cfg(any(test, feature = "test-support"))]
3260 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3261 ImageSource::Image(self.clone()).is_asset_cached(cx)
3262 }
3263
3264 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3266 let frames = match self.format {
3267 ImageFormat::Gif => {
3268 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3269 let mut frames = SmallVec::new();
3270
3271 for frame in decoder.into_frames() {
3272 match frame {
3273 Ok(mut frame) => {
3274 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3276 pixel.swap(0, 2);
3277 }
3278 frames.push(frame);
3279 }
3280 Err(err) => {
3281 log::debug!("Skipping GIF frame due to decode error: {err}");
3282 }
3283 }
3284 }
3285
3286 if frames.is_empty() {
3287 anyhow::bail!("GIF could not be decoded: all frames failed");
3288 }
3289
3290 frames
3291 }
3292 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3293 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3294 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3295 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3296 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3297 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3298 ImageFormat::Svg => {
3299 return svg_renderer
3300 .render_single_frame(&self.bytes, 1.0)
3301 .map_err(Into::into);
3302 }
3303 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3304 };
3305
3306 Ok(Arc::new(RenderImage::new(frames)))
3307 }
3308
3309 pub fn format(&self) -> ImageFormat {
3311 self.format
3312 }
3313
3314 pub fn bytes(&self) -> &[u8] {
3316 self.bytes.as_slice()
3317 }
3318}
3319
3320#[derive(Clone, Debug, Eq, PartialEq)]
3322pub struct ClipboardString {
3323 pub text: String,
3325 pub metadata: Option<String>,
3327}
3328
3329impl ClipboardString {
3330 pub fn new(text: String) -> Self {
3332 Self {
3333 text,
3334 metadata: None,
3335 }
3336 }
3337
3338 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3341 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3342 self
3343 }
3344
3345 pub fn text(&self) -> &String {
3347 &self.text
3348 }
3349
3350 pub fn into_text(self) -> String {
3352 self.text
3353 }
3354
3355 pub fn metadata_json<T>(&self) -> Option<T>
3357 where
3358 T: for<'a> Deserialize<'a>,
3359 {
3360 self.metadata
3361 .as_ref()
3362 .and_then(|m| serde_json::from_str(m).ok())
3363 }
3364
3365 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3366 pub fn text_hash(text: &str) -> u64 {
3368 let mut hasher = SeaHasher::new();
3369 text.hash(&mut hasher);
3370 hasher.finish()
3371 }
3372}
3373
3374impl From<String> for ClipboardString {
3375 fn from(value: String) -> Self {
3376 Self {
3377 text: value,
3378 metadata: None,
3379 }
3380 }
3381}
3382
3383#[cfg(test)]
3384mod image_tests {
3385 use super::*;
3386 use std::sync::Arc;
3387
3388 #[test]
3389 fn test_image_to_image_data_applies_exif_orientation() {
3390 let image = Image::from_bytes(
3391 ImageFormat::Jpeg,
3392 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3393 );
3394
3395 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3396
3397 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3398
3399 let bytes = render_image.as_bytes(0).unwrap();
3400 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3401 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3402 }
3403
3404 #[test]
3405 fn test_svg_image_to_image_data_converts_to_bgra() {
3406 let image = Image::from_bytes(
3407 ImageFormat::Svg,
3408 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3409<rect width="1" height="1" fill="#38BDF8"/>
3410</svg>"##
3411 .to_vec(),
3412 );
3413
3414 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3415 let bytes = render_image.as_bytes(0).unwrap();
3416
3417 for pixel in bytes.chunks_exact(4) {
3418 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3419 }
3420 }
3421}
3422
3423#[cfg(test)]
3424mod atlas_tests {
3425 use super::*;
3426
3427 const TILE_SIZE: Size<DevicePixels> = Size {
3428 width: DevicePixels(1),
3429 height: DevicePixels(1),
3430 };
3431
3432 #[derive(Default)]
3433 struct RecordingAtlasBackend {
3434 insert_calls: u32,
3435 fail_next_insert: bool,
3436 removed_tiles: Vec<AtlasTile>,
3437 }
3438
3439 impl AtlasBackend for RecordingAtlasBackend {
3440 fn insert(
3441 &mut self,
3442 kind: AtlasTextureKind,
3443 size: Size<DevicePixels>,
3444 _bytes: &[u8],
3445 ) -> Result<AtlasTile> {
3446 self.insert_calls += 1;
3447 if std::mem::take(&mut self.fail_next_insert) {
3448 anyhow::bail!("backend failed");
3449 }
3450 Ok(AtlasTile {
3451 texture_id: AtlasTextureId { index: 0, kind },
3452 tile_id: TileId(self.insert_calls),
3453 padding: 0,
3454 bounds: Bounds {
3455 origin: Point::default(),
3456 size,
3457 },
3458 })
3459 }
3460
3461 fn remove(&mut self, tile: AtlasTile) {
3462 self.removed_tiles.push(tile);
3463 }
3464 }
3465
3466 fn image_key(image_id: usize) -> AtlasKey {
3467 AtlasKey::Image(RenderImageParams {
3468 image_id: crate::ImageId(image_id),
3469 frame_index: 0,
3470 })
3471 }
3472
3473 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3474 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3475 }
3476
3477 #[test]
3478 fn only_successful_inserts_are_cached() -> Result<()> {
3479 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3480 let key = image_key(1);
3481
3482 assert_eq!(
3483 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3484 None
3485 );
3486 state
3487 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3488 .expect_err("builder error should propagate");
3489 assert!(!state.contains(&key));
3490 assert_eq!(state.backend.insert_calls, 0);
3491
3492 state.backend.fail_next_insert = true;
3493 state
3494 .get_or_insert_with(key.clone(), &mut build_tile)
3495 .expect_err("backend error should propagate");
3496 assert!(!state.contains(&key));
3497 assert_eq!(state.backend.insert_calls, 1);
3498
3499 let tile = state
3500 .get_or_insert_with(key.clone(), &mut build_tile)?
3501 .context("builder should produce a tile")?;
3502 assert_eq!(tile.texture_id.kind, key.texture_kind());
3503 assert_eq!(
3504 state.get_or_insert_with(key.clone(), &mut || {
3505 anyhow::bail!("cache hit must not call the builder")
3506 })?,
3507 Some(tile)
3508 );
3509 assert!(state.contains(&key));
3510 assert_eq!(state.backend.insert_calls, 2);
3511 Ok(())
3512 }
3513
3514 #[test]
3515 fn remove_and_clear_invalidate_keys() -> Result<()> {
3516 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3517 let key = image_key(1);
3518 let other_key = image_key(2);
3519 let tile = state
3520 .get_or_insert_with(key.clone(), &mut build_tile)?
3521 .context("builder should produce a tile")?;
3522 state
3523 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3524 .context("builder should produce another tile")?;
3525
3526 state.remove(&key);
3527 state.remove(&key);
3528 assert!(!state.contains(&key));
3529 assert!(state.contains(&other_key));
3530 assert_eq!(state.backend.removed_tiles, vec![tile]);
3531
3532 let mut reset_calls = 0;
3533 state.clear(|_| reset_calls += 1);
3534 assert_eq!(reset_calls, 1);
3535 assert!(!state.contains(&other_key));
3536 assert_eq!(state.backend.removed_tiles, vec![tile]);
3537 Ok(())
3538 }
3539}
3540
3541#[cfg(test)]
3542mod frame_signal_tests {
3543 use super::*;
3544
3545 #[cfg(feature = "profiler")]
3546 #[test]
3547 fn coalesced_signals_retain_the_earliest_until_drained() {
3548 for (first_source, later_source) in [
3549 (
3550 FrameRequestSource::NativeCallback,
3551 FrameRequestSource::LocalSchedule,
3552 ),
3553 (
3554 FrameRequestSource::LocalSchedule,
3555 FrameRequestSource::NativeCallback,
3556 ),
3557 ] {
3558 let signal = PlatformFrameSignal::new();
3559 let first =
3560 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3561 assert_eq!(signal.take(), None);
3562 signal.record(first + Duration::from_millis(16), later_source);
3563 signal.record(first, first_source);
3564 signal.record(first + Duration::from_millis(32), later_source);
3565 assert_eq!(signal.take(), Some((first, first_source)));
3566 assert_eq!(signal.take(), None);
3567 let next = first + Duration::from_millis(48);
3568 signal.record(next, later_source);
3569 assert_eq!(signal.take(), Some((next, later_source)));
3570 }
3571 }
3572
3573 #[cfg(not(feature = "profiler"))]
3574 #[test]
3575 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3576 let captured = PlatformFrameSignal::capture(|| {
3577 panic!("profiler-disabled builds must not evaluate the capture closure");
3578 });
3579 assert_eq!(captured, None);
3580 let signal = PlatformFrameSignal::new();
3581 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3582 assert_eq!(signal.take(), None);
3583 }
3584}
3585
3586#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3587mod tests {
3588 use super::*;
3589 use std::collections::HashSet;
3590
3591 #[test]
3592 fn test_window_button_layout_parse_standard() {
3593 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3594 assert_eq!(
3595 layout.left,
3596 [
3597 Some(WindowButton::Close),
3598 Some(WindowButton::Minimize),
3599 None
3600 ]
3601 );
3602 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3603 }
3604
3605 #[test]
3606 fn test_window_button_layout_parse_right_only() {
3607 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3608 assert_eq!(layout.left, [None, None, None]);
3609 assert_eq!(
3610 layout.right,
3611 [
3612 Some(WindowButton::Minimize),
3613 Some(WindowButton::Maximize),
3614 Some(WindowButton::Close)
3615 ]
3616 );
3617 }
3618
3619 #[test]
3620 fn test_window_button_layout_parse_left_only() {
3621 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3622 assert_eq!(
3623 layout.left,
3624 [
3625 Some(WindowButton::Close),
3626 Some(WindowButton::Minimize),
3627 Some(WindowButton::Maximize)
3628 ]
3629 );
3630 assert_eq!(layout.right, [None, None, None]);
3631 }
3632
3633 #[test]
3634 fn test_window_button_layout_parse_with_whitespace() {
3635 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3636 assert_eq!(
3637 layout.left,
3638 [
3639 Some(WindowButton::Close),
3640 Some(WindowButton::Minimize),
3641 None
3642 ]
3643 );
3644 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3645 }
3646
3647 #[test]
3648 fn test_window_button_layout_parse_empty() {
3649 let layout = WindowButtonLayout::parse("").unwrap();
3650 assert_eq!(layout.left, [None, None, None]);
3651 assert_eq!(layout.right, [None, None, None]);
3652 }
3653
3654 #[test]
3655 fn test_window_button_layout_parse_intentionally_empty() {
3656 let layout = WindowButtonLayout::parse(":").unwrap();
3657 assert_eq!(layout.left, [None, None, None]);
3658 assert_eq!(layout.right, [None, None, None]);
3659 }
3660
3661 #[test]
3662 fn test_window_button_layout_parse_invalid_buttons() {
3663 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3664 assert_eq!(
3665 layout.left,
3666 [
3667 Some(WindowButton::Close),
3668 Some(WindowButton::Minimize),
3669 None
3670 ]
3671 );
3672 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3673 }
3674
3675 #[test]
3676 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3677 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3678 assert_eq!(
3679 layout.right,
3680 [
3681 Some(WindowButton::Close),
3682 Some(WindowButton::Minimize),
3683 None
3684 ]
3685 );
3686 assert_eq!(layout.format(), ":close,minimize");
3687 }
3688
3689 #[test]
3690 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3691 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3692 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3693 assert_eq!(
3694 layout.right,
3695 [
3696 Some(WindowButton::Maximize),
3697 Some(WindowButton::Minimize),
3698 None
3699 ]
3700 );
3701
3702 let button_ids: Vec<_> = layout
3703 .left
3704 .iter()
3705 .chain(layout.right.iter())
3706 .flatten()
3707 .map(WindowButton::id)
3708 .collect();
3709 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
3710 assert_eq!(unique_button_ids.len(), button_ids.len());
3711 assert_eq!(layout.format(), "close:maximize,minimize");
3712 }
3713
3714 #[test]
3715 fn test_window_button_layout_parse_gnome_style() {
3716 let layout = WindowButtonLayout::parse("close").unwrap();
3717 assert_eq!(layout.left, [None, None, None]);
3718 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
3719 }
3720
3721 #[test]
3722 fn test_window_button_layout_parse_elementary_style() {
3723 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
3724 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3725 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3726 }
3727
3728 #[test]
3729 fn test_window_button_layout_round_trip() {
3730 let cases = [
3731 "close:minimize,maximize",
3732 "minimize,maximize,close:",
3733 ":close",
3734 "close:",
3735 "close:maximize",
3736 ":",
3737 ];
3738
3739 for case in cases {
3740 let layout = WindowButtonLayout::parse(case).unwrap();
3741 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
3742 }
3743 }
3744
3745 #[test]
3746 fn test_window_button_layout_linux_default() {
3747 let layout = WindowButtonLayout::linux_default();
3748 assert_eq!(layout.left, [None, None, None]);
3749 assert_eq!(
3750 layout.right,
3751 [
3752 Some(WindowButton::Minimize),
3753 Some(WindowButton::Maximize),
3754 Some(WindowButton::Close)
3755 ]
3756 );
3757
3758 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
3759 assert_eq!(round_tripped, layout);
3760 }
3761
3762 #[test]
3763 fn test_window_button_layout_parse_all_invalid() {
3764 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
3765 }
3766}