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
9#[cfg(any(test, feature = "test-support"))]
10mod test;
11
12#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
13mod visual_test;
14
15#[cfg(all(
16 feature = "screen-capture",
17 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
18))]
19pub mod scap_screen_capture;
20
21#[cfg(all(
22 any(target_os = "windows", target_os = "linux"),
23 feature = "screen-capture"
24))]
25pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
26#[cfg(not(feature = "screen-capture"))]
27pub(crate) type PlatformScreenCaptureFrame = ();
28#[cfg(all(target_os = "macos", feature = "screen-capture"))]
29pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
30
31use crate::scheduler::Instant;
32pub use crate::scheduler::RunnableMeta;
33use crate::{
34 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
35 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
36 ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels,
37 PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
38 RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
39 SystemWindowTab, Task, ThreadTaskTimings, Tray, TrayIconEvent, TrayMenuItem, Window,
40 WindowControlArea, hash, point, px, size,
41};
42use anyhow::Result;
43#[cfg(any(target_os = "linux", target_os = "freebsd"))]
44use anyhow::bail;
45use async_task::Runnable;
46use futures::channel::oneshot;
47#[cfg(any(test, feature = "test-support"))]
48use image::RgbaImage;
49use image::codecs::gif::GifDecoder;
50use image::{AnimationDecoder as _, Frame};
51use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
52use schemars::JsonSchema;
53use seahash::SeaHasher;
54use serde::{Deserialize, Serialize};
55use smallvec::SmallVec;
56use std::borrow::Cow;
57use std::hash::{Hash, Hasher};
58use std::io::Cursor;
59use std::ops;
60use std::time::Duration;
61use std::{
62 fmt::{self, Debug},
63 ops::Range,
64 path::{Path, PathBuf},
65 rc::Rc,
66 sync::Arc,
67};
68use strum::EnumIter;
69use uuid::Uuid;
70
71pub use app_menu::*;
72pub use keyboard::*;
73pub use keystroke::*;
74
75#[cfg(any(test, feature = "test-support"))]
76pub(crate) use test::*;
77
78#[cfg(any(test, feature = "test-support"))]
79pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
80
81#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
82pub use visual_test::VisualTestPlatform;
83
84#[cfg(any(target_os = "linux", target_os = "freebsd"))]
88#[inline]
89pub fn guess_compositor() -> &'static str {
90 if std::env::var_os("ZED_HEADLESS").is_some() {
91 return "Headless";
92 }
93
94 #[cfg(feature = "wayland")]
95 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
96 #[cfg(not(feature = "wayland"))]
97 let wayland_display: Option<std::ffi::OsString> = None;
98
99 #[cfg(feature = "x11")]
100 let x11_display = std::env::var_os("DISPLAY");
101 #[cfg(not(feature = "x11"))]
102 let x11_display: Option<std::ffi::OsString> = None;
103
104 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
105 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
106
107 if use_wayland {
108 "Wayland"
109 } else if use_x11 {
110 "X11"
111 } else {
112 "Headless"
113 }
114}
115
116#[expect(missing_docs)]
119pub trait Platform: 'static {
120 fn background_executor(&self) -> BackgroundExecutor;
121 fn foreground_executor(&self) -> ForegroundExecutor;
122 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
123
124 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
125 fn quit(&self);
126 fn restart(&self, binary_path: Option<PathBuf>);
127 fn activate(&self, ignoring_other_apps: bool);
128 fn hide(&self);
129 fn hide_other_apps(&self);
130 fn unhide_other_apps(&self);
131
132 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
133 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
134 fn active_window(&self) -> Option<AnyWindowHandle>;
135 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
136 None
137 }
138
139 fn is_screen_capture_supported(&self) -> bool {
140 false
141 }
142
143 fn screen_capture_sources(
144 &self,
145 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
146 let (sources_tx, sources_rx) = oneshot::channel();
147 sources_tx
148 .send(Err(anyhow::anyhow!(
149 "gpui was compiled without the screen-capture feature"
150 )))
151 .ok();
152 sources_rx
153 }
154
155 fn open_window(
156 &self,
157 handle: AnyWindowHandle,
158 options: WindowParams,
159 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
160
161 fn window_appearance(&self) -> WindowAppearance;
163
164 fn button_layout(&self) -> Option<WindowButtonLayout> {
166 None
167 }
168
169 fn open_url(&self, url: &str);
170 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
171 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
172
173 fn prompt_for_paths(
174 &self,
175 options: PathPromptOptions,
176 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
177 fn prompt_for_new_path(
178 &self,
179 directory: &Path,
180 suggested_name: Option<&str>,
181 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
182 fn can_select_mixed_files_and_dirs(&self) -> bool;
183 fn reveal_path(&self, path: &Path);
184 fn open_with_system(&self, path: &Path);
185
186 fn on_quit(&self, callback: Box<dyn FnMut()>);
187 fn on_reopen(&self, callback: Box<dyn FnMut()>);
188
189 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
190 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
191 None
192 }
193
194 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
195 fn perform_dock_menu_action(&self, _action: usize) {}
196 fn add_recent_document(&self, _path: &Path) {}
197 fn update_jump_list(
198 &self,
199 _menus: Vec<MenuItem>,
200 _entries: Vec<SmallVec<[PathBuf; 2]>>,
201 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
202 Task::ready(Vec::new())
203 }
204
205 fn set_tray_icon(&self, _icon: Option<&[u8]>) {}
208 fn set_tray_menu(&self, _menu: Vec<TrayMenuItem>) {}
210 fn set_tray_tooltip(&self, _tooltip: &str) {}
212 fn set_tray_panel_mode(&self, _enabled: bool) {}
214 fn get_tray_icon_bounds(&self) -> Option<Bounds<Pixels>> {
216 None
217 }
218 fn on_tray_icon_event(&self, _callback: Box<dyn FnMut(TrayIconEvent)>) {}
220 fn on_tray_menu_action(&self, _callback: Box<dyn FnMut(SharedString)>) {}
222
223 fn set_tray(&self, _tray: Tray, _menu: Option<Vec<MenuItem>>, _keymap: &Keymap) {}
225
226 fn set_keep_alive_without_windows(&self, _keep_alive: bool) {}
227
228 fn register_global_hotkey(&self, _id: u32, _keystroke: &Keystroke) -> Result<()> {
230 Err(anyhow::anyhow!(
231 "Global hotkeys not supported on this platform"
232 ))
233 }
234
235 fn unregister_global_hotkey(&self, _id: u32) {}
237
238 fn on_global_hotkey(&self, _callback: Box<dyn FnMut(u32)>) {}
240
241 fn show_notification(&self, _title: &str, _body: &str) -> Result<()> {
243 Err(anyhow::anyhow!(
244 "Notifications not supported on this platform"
245 ))
246 }
247
248 fn set_auto_launch(&self, _app_id: &str, _enabled: bool) -> Result<()> {
250 Err(anyhow::anyhow!(
251 "Auto launch not supported on this platform"
252 ))
253 }
254
255 fn is_auto_launch_enabled(&self, _app_id: &str) -> bool {
257 false
258 }
259
260 fn focused_window_info(&self) -> Option<FocusedWindowInfo> {
262 None
263 }
264
265 fn accessibility_status(&self) -> PermissionStatus {
267 PermissionStatus::Granted
268 }
269
270 fn request_accessibility_permission(&self) {}
272
273 fn microphone_status(&self) -> PermissionStatus {
275 PermissionStatus::Granted
276 }
277
278 fn request_microphone_permission(&self, callback: Box<dyn FnOnce(bool)>) {
280 callback(true);
281 }
282
283 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
284 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
285 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
286
287 fn thermal_state(&self) -> ThermalState;
288 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
289
290 fn compositor_name(&self) -> &'static str {
291 ""
292 }
293 fn app_path(&self) -> Result<PathBuf>;
294 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
295
296 fn set_cursor_style(&self, style: CursorStyle);
297
298 fn hide_cursor_until_mouse_moves(&self);
301
302 fn is_cursor_visible(&self) -> bool;
304
305 fn should_auto_hide_scrollbars(&self) -> bool;
306
307 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
308 fn write_to_clipboard(&self, item: ClipboardItem);
309
310 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
311 fn read_from_primary(&self) -> Option<ClipboardItem>;
312 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
313 fn write_to_primary(&self, item: ClipboardItem);
314
315 #[cfg(target_os = "macos")]
316 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
317 #[cfg(target_os = "macos")]
318 fn write_to_find_pasteboard(&self, item: ClipboardItem);
319
320 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
321 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
322 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
323
324 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
325 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
326 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
327
328 fn on_system_power_event(&self, _callback: Box<dyn FnMut(SystemPowerEvent)>) {}
330 fn start_power_save_blocker(&self, _kind: PowerSaveBlockerKind) -> Option<u32> {
332 None
333 }
334 fn stop_power_save_blocker(&self, _id: u32) {}
336 fn system_idle_time(&self) -> Option<Duration> {
338 None
339 }
340 fn network_status(&self) -> NetworkStatus {
342 NetworkStatus::Online
343 }
344 fn on_network_status_change(&self, _callback: Box<dyn FnMut(NetworkStatus)>) {}
346 fn on_media_key_event(&self, _callback: Box<dyn FnMut(MediaKeyEvent)>) {}
348 fn request_user_attention(&self, _attention_type: AttentionType) {}
350 fn cancel_user_attention(&self) {}
352 fn set_dock_badge(&self, _label: Option<&str>) {}
354 fn show_context_menu(
356 &self,
357 _position: Point<Pixels>,
358 _items: Vec<TrayMenuItem>,
359 _callback: Box<dyn FnMut(SharedString)>,
360 ) {
361 }
362 fn show_dialog(&self, _options: DialogOptions) -> oneshot::Receiver<usize> {
364 let (tx, rx) = oneshot::channel();
365 tx.send(0).ok();
366 rx
367 }
368 fn os_info(&self) -> OsInfo {
370 OsInfo {
371 name: std::env::consts::OS.into(),
372 arch: std::env::consts::ARCH.into(),
373 version: String::new().into(),
374 locale: String::new().into(),
375 hostname: String::new().into(),
376 }
377 }
378 fn biometric_status(&self) -> BiometricStatus {
380 BiometricStatus::Unavailable
381 }
382 fn authenticate_biometric(&self, _reason: &str, callback: Box<dyn FnOnce(bool) + Send>) {
384 callback(false);
385 }
386}
387
388pub trait PlatformDisplay: Debug {
390 fn id(&self) -> DisplayId;
392
393 fn uuid(&self) -> Result<Uuid>;
395
396 fn bounds(&self) -> Bounds<Pixels>;
398
399 fn visible_bounds(&self) -> Bounds<Pixels> {
403 self.bounds()
404 }
405
406 fn default_bounds(&self) -> Bounds<Pixels> {
408 let bounds = self.bounds();
409 let center = bounds.center();
410 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
411
412 let offset = clipped_window_size / 2.0;
413 let origin = point(center.x - offset.width, center.y - offset.height);
414 Bounds::new(origin, clipped_window_size)
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum ThermalState {
421 Nominal,
423 Fair,
425 Serious,
427 Critical,
429}
430
431#[derive(Clone)]
433pub struct SourceMetadata {
434 pub id: u64,
436 pub label: Option<SharedString>,
438 pub is_main: Option<bool>,
440 pub resolution: Size<DevicePixels>,
442}
443
444pub trait ScreenCaptureSource {
446 fn metadata(&self) -> Result<SourceMetadata>;
448
449 fn stream(
451 &self,
452 foreground_executor: &ForegroundExecutor,
453 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
454 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
455}
456
457pub trait ScreenCaptureStream {
459 fn metadata(&self) -> Result<SourceMetadata>;
461}
462
463pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
465
466#[derive(PartialEq, Eq, Hash, Copy, Clone)]
468pub struct DisplayId(pub(crate) u64);
469
470impl DisplayId {
471 pub fn new(id: u64) -> Self {
473 Self(id)
474 }
475}
476
477impl From<u64> for DisplayId {
478 fn from(id: u64) -> Self {
479 Self(id)
480 }
481}
482
483impl From<DisplayId> for u64 {
484 fn from(id: DisplayId) -> Self {
485 id.0
486 }
487}
488
489impl Debug for DisplayId {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 write!(f, "DisplayId({})", self.0)
492 }
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub enum ResizeEdge {
498 Top,
500 TopRight,
502 Right,
504 BottomRight,
506 Bottom,
508 BottomLeft,
510 Left,
512 TopLeft,
514}
515
516#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
518pub enum WindowDecorations {
519 #[default]
520 Server,
522 Client,
524}
525
526#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
528pub enum Decorations {
529 #[default]
531 Server,
532 Client {
534 tiling: Tiling,
536 },
537}
538
539#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
541pub struct WindowControls {
542 pub fullscreen: bool,
544 pub maximize: bool,
546 pub minimize: bool,
548 pub window_menu: bool,
550}
551
552impl Default for WindowControls {
553 fn default() -> Self {
554 Self {
556 fullscreen: true,
557 maximize: true,
558 minimize: true,
559 window_menu: true,
560 }
561 }
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
566pub enum WindowButton {
567 Minimize,
569 Maximize,
571 Close,
573}
574
575impl WindowButton {
576 pub fn id(&self) -> &'static str {
578 match self {
579 WindowButton::Minimize => "minimize",
580 WindowButton::Maximize => "maximize",
581 WindowButton::Close => "close",
582 }
583 }
584
585 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
586 fn index(&self) -> usize {
587 match self {
588 WindowButton::Minimize => 0,
589 WindowButton::Maximize => 1,
590 WindowButton::Close => 2,
591 }
592 }
593}
594
595pub const MAX_BUTTONS_PER_SIDE: usize = 3;
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
603pub struct WindowButtonLayout {
604 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
606 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
608}
609
610#[cfg(any(target_os = "linux", target_os = "freebsd"))]
611impl WindowButtonLayout {
612 pub fn linux_default() -> Self {
614 Self {
615 left: [None; MAX_BUTTONS_PER_SIDE],
616 right: [
617 Some(WindowButton::Minimize),
618 Some(WindowButton::Maximize),
619 Some(WindowButton::Close),
620 ],
621 }
622 }
623
624 pub fn parse(layout_string: &str) -> Result<Self> {
626 fn parse_side(
627 s: &str,
628 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
629 unrecognized: &mut Vec<String>,
630 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
631 let mut result = [None; MAX_BUTTONS_PER_SIDE];
632 let mut i = 0;
633 for name in s.split(',') {
634 let trimmed = name.trim();
635 if trimmed.is_empty() {
636 continue;
637 }
638 let button = match trimmed {
639 "minimize" => Some(WindowButton::Minimize),
640 "maximize" => Some(WindowButton::Maximize),
641 "close" => Some(WindowButton::Close),
642 other => {
643 unrecognized.push(other.to_string());
644 None
645 }
646 };
647 if let Some(button) = button {
648 if seen_buttons[button.index()] {
649 continue;
650 }
651 if let Some(slot) = result.get_mut(i) {
652 *slot = Some(button);
653 seen_buttons[button.index()] = true;
654 i += 1;
655 }
656 }
657 }
658 result
659 }
660
661 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
662 let mut unrecognized = Vec::new();
663 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
664 let layout = Self {
665 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
666 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
667 };
668
669 if !unrecognized.is_empty()
670 && layout.left.iter().all(Option::is_none)
671 && layout.right.iter().all(Option::is_none)
672 {
673 bail!(
674 "button layout string {:?} contains no valid buttons (unrecognized: {})",
675 layout_string,
676 unrecognized.join(", ")
677 );
678 }
679
680 Ok(layout)
681 }
682
683 #[cfg(test)]
685 pub fn format(&self) -> String {
686 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
687 buttons
688 .iter()
689 .flatten()
690 .map(|button| match button {
691 WindowButton::Minimize => "minimize",
692 WindowButton::Maximize => "maximize",
693 WindowButton::Close => "close",
694 })
695 .collect::<Vec<_>>()
696 .join(",")
697 }
698
699 format!("{}:{}", format_side(&self.left), format_side(&self.right))
700 }
701}
702
703#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
705pub struct Tiling {
706 pub top: bool,
708 pub left: bool,
710 pub right: bool,
712 pub bottom: bool,
714}
715
716impl Tiling {
717 pub fn tiled() -> Self {
719 Self {
720 top: true,
721 left: true,
722 right: true,
723 bottom: true,
724 }
725 }
726
727 pub fn is_tiled(&self) -> bool {
729 self.top || self.left || self.right || self.bottom
730 }
731}
732
733#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
734#[expect(missing_docs)]
735pub struct RequestFrameOptions {
736 pub require_presentation: bool,
738 pub force_render: bool,
740}
741
742#[expect(missing_docs)]
745pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
746 fn bounds(&self) -> Bounds<Pixels>;
747 fn is_maximized(&self) -> bool;
748 fn window_bounds(&self) -> WindowBounds;
749 fn content_size(&self) -> Size<Pixels>;
750 fn resize(&mut self, size: Size<Pixels>);
751 fn set_position(&mut self, position: Point<Pixels>);
753 fn scale_factor(&self) -> f32;
754 fn appearance(&self) -> WindowAppearance;
755 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
756 fn mouse_position(&self) -> Point<Pixels>;
757 fn modifiers(&self) -> Modifiers;
758 fn capslock(&self) -> Capslock;
759 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
760 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
761 fn prompt(
762 &self,
763 level: PromptLevel,
764 msg: &str,
765 detail: Option<&str>,
766 answers: &[PromptButton],
767 ) -> Option<oneshot::Receiver<usize>>;
768 fn activate(&self);
769 fn is_active(&self) -> bool;
770 fn is_hovered(&self) -> bool;
771 fn background_appearance(&self) -> WindowBackgroundAppearance;
772 fn set_title(&mut self, title: &str);
773 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
774 fn minimize(&self);
775 fn hide(&self);
776 fn zoom(&self);
777 fn toggle_fullscreen(&self);
778 fn is_fullscreen(&self) -> bool;
779 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
780 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
781 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
782 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
783 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
784 fn on_moved(&self, callback: Box<dyn FnMut()>);
785 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
786 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
787 fn on_close(&self, callback: Box<dyn FnOnce()>);
788 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
789 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
790 fn draw(&self, scene: &Scene);
791 fn completed_frame(&self) {}
792 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
793 fn is_subpixel_rendering_supported(&self) -> bool;
794
795 fn get_title(&self) -> String {
797 String::new()
798 }
799 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
800 None
801 }
802 fn tab_bar_visible(&self) -> bool {
803 false
804 }
805 fn set_edited(&mut self, _edited: bool) {}
806 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
807 fn show_character_palette(&self) {}
808 fn titlebar_double_click(&self) {}
809 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
810 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
811 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
812 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
813 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
814 fn merge_all_windows(&self) {}
815 fn move_tab_to_new_window(&self) {}
816 fn toggle_window_tab_overview(&self) {}
817 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
818
819 #[cfg(target_os = "windows")]
820 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
821
822 fn inner_window_bounds(&self) -> WindowBounds {
824 self.window_bounds()
825 }
826 fn request_decorations(&self, _decorations: WindowDecorations) {}
827 fn show_window_menu(&self, _position: Point<Pixels>) {}
828 fn start_window_move(&self) {}
829 fn start_window_resize(&self, _edge: ResizeEdge) {}
830 fn window_decorations(&self) -> Decorations {
831 Decorations::Server
832 }
833 fn set_app_id(&mut self, _app_id: &str) {}
834 fn map_window(&mut self) -> anyhow::Result<()> {
835 Ok(())
836 }
837 fn window_controls(&self) -> WindowControls {
838 WindowControls::default()
839 }
840 fn set_client_inset(&self, _inset: Pixels) {}
841 fn gpu_specs(&self) -> Option<GpuSpecs>;
842
843 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
844
845 fn play_system_bell(&self) {}
846
847 fn show(&self) {}
849 fn is_visible(&self) -> bool {
851 true
852 }
853 fn set_progress_bar(&self, _state: ProgressBarState) {}
855
856 fn set_mouse_passthrough(&self, _passthrough: bool) {}
858
859 fn window_extended_style(&self) -> u32 {
861 0
862 }
863
864 fn set_window_extended_style(&self, _style: u32) {}
867
868 fn set_titlebar_visible(&self, _visible: bool) {}
870
871 #[cfg(any(test, feature = "test-support"))]
872 fn as_test(&mut self) -> Option<&mut TestWindow> {
873 None
874 }
875
876 #[cfg(any(test, feature = "test-support"))]
879 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
880 anyhow::bail!("render_to_image not implemented for this platform")
881 }
882}
883
884#[cfg(any(test, feature = "test-support"))]
886pub trait PlatformHeadlessRenderer {
887 fn render_scene_to_image(
889 &mut self,
890 scene: &Scene,
891 size: Size<DevicePixels>,
892 ) -> Result<RgbaImage>;
893
894 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
896}
897
898#[doc(hidden)]
900pub type RunnableVariant = Runnable<RunnableMeta>;
901
902#[doc(hidden)]
903pub type TimerResolutionGuard = crate::rgpui_util::Deferred<Box<dyn FnOnce() + Send>>;
904
905#[doc(hidden)]
908pub trait PlatformDispatcher: Send + Sync {
909 fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
910 fn get_current_thread_timings(&self) -> ThreadTaskTimings;
911 fn is_main_thread(&self) -> bool;
912 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
913 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
914 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
915
916 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
917
918 fn now(&self) -> Instant {
919 Instant::now()
920 }
921
922 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
923 crate::defer(Box::new(|| {}))
924 }
925
926 #[cfg(any(test, feature = "test-support"))]
927 fn as_test(&self) -> Option<&TestDispatcher> {
928 None
929 }
930}
931
932#[expect(missing_docs)]
934pub trait PlatformTextSystem: Send + Sync {
935 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
936 fn all_font_names(&self) -> Vec<String>;
938 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
940 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
942 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
944 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
946 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
948 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
950 fn rasterize_glyph(
952 &self,
953 params: &RenderGlyphParams,
954 raster_bounds: Bounds<DevicePixels>,
955 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
956 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
958 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
960 -> TextRenderingMode;
961 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
963 0
964 }
965}
966
967#[expect(missing_docs)]
968pub struct NoopTextSystem;
969
970#[expect(missing_docs)]
971impl NoopTextSystem {
972 #[allow(dead_code)]
973 pub fn new() -> Self {
974 Self
975 }
976}
977
978impl PlatformTextSystem for NoopTextSystem {
979 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
980 Ok(())
981 }
982
983 fn all_font_names(&self) -> Vec<String> {
984 Vec::new()
985 }
986
987 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
988 Ok(FontId(1))
989 }
990
991 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
992 FontMetrics {
993 units_per_em: 1000,
994 ascent: 1025.0,
995 descent: -275.0,
996 line_gap: 0.0,
997 underline_position: -95.0,
998 underline_thickness: 60.0,
999 cap_height: 698.0,
1000 x_height: 516.0,
1001 bounding_box: Bounds {
1002 origin: Point {
1003 x: -260.0,
1004 y: -245.0,
1005 },
1006 size: Size {
1007 width: 1501.0,
1008 height: 1364.0,
1009 },
1010 },
1011 }
1012 }
1013
1014 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1015 Ok(Bounds {
1016 origin: Point { x: 54.0, y: 0.0 },
1017 size: size(392.0, 528.0),
1018 })
1019 }
1020
1021 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1022 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1023 }
1024
1025 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1026 Some(GlyphId(ch.len_utf16() as u32))
1027 }
1028
1029 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1030 Ok(Default::default())
1031 }
1032
1033 fn rasterize_glyph(
1034 &self,
1035 _params: &RenderGlyphParams,
1036 raster_bounds: Bounds<DevicePixels>,
1037 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1038 Ok((raster_bounds.size, Vec::new()))
1039 }
1040
1041 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1042 let mut position = px(0.);
1043 let metrics = self.font_metrics(FontId(0));
1044 let em_width = font_size
1045 * self
1046 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1047 .unwrap()
1048 .width
1049 / metrics.units_per_em as f32;
1050 let mut glyphs = Vec::new();
1051 for (ix, c) in text.char_indices() {
1052 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1053 glyphs.push(ShapedGlyph {
1054 id: glyph,
1055 position: point(position, px(0.)),
1056 index: ix,
1057 is_emoji: glyph.0 == 2,
1058 });
1059 if glyph.0 == 2 {
1060 position += em_width * 2.0;
1061 } else {
1062 position += em_width;
1063 }
1064 } else {
1065 position += em_width
1066 }
1067 }
1068 let mut runs = Vec::default();
1069 if !glyphs.is_empty() {
1070 runs.push(ShapedRun {
1071 font_id: FontId(0),
1072 glyphs,
1073 });
1074 } else {
1075 position = px(0.);
1076 }
1077
1078 LineLayout {
1079 font_size,
1080 width: position,
1081 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1082 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1083 runs,
1084 len: text.len(),
1085 }
1086 }
1087
1088 fn recommended_rendering_mode(
1089 &self,
1090 _font_id: FontId,
1091 _font_size: Pixels,
1092 ) -> TextRenderingMode {
1093 TextRenderingMode::Grayscale
1094 }
1095}
1096
1097#[allow(dead_code)]
1102pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1103 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1104 [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], ];
1118
1119 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1120 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1121
1122 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1123 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1124
1125 [
1126 ratios[0] * NORM13,
1127 ratios[1] * NORM24,
1128 ratios[2] * NORM13,
1129 ratios[3] * NORM24,
1130 ]
1131}
1132
1133#[derive(PartialEq, Eq, Hash, Clone)]
1134#[expect(missing_docs)]
1135pub enum AtlasKey {
1136 Glyph(RenderGlyphParams),
1137 Svg(RenderSvgParams),
1138 Image(RenderImageParams),
1139}
1140
1141impl AtlasKey {
1142 #[cfg_attr(
1143 all(
1144 any(target_os = "linux", target_os = "freebsd"),
1145 not(any(feature = "x11", feature = "wayland"))
1146 ),
1147 allow(dead_code)
1148 )]
1149 pub fn texture_kind(&self) -> AtlasTextureKind {
1151 match self {
1152 AtlasKey::Glyph(params) => {
1153 if params.is_emoji {
1154 AtlasTextureKind::Polychrome
1155 } else if params.subpixel_rendering {
1156 AtlasTextureKind::Subpixel
1157 } else {
1158 AtlasTextureKind::Monochrome
1159 }
1160 }
1161 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1162 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1163 }
1164 }
1165}
1166
1167impl From<RenderGlyphParams> for AtlasKey {
1168 fn from(params: RenderGlyphParams) -> Self {
1169 Self::Glyph(params)
1170 }
1171}
1172
1173impl From<RenderSvgParams> for AtlasKey {
1174 fn from(params: RenderSvgParams) -> Self {
1175 Self::Svg(params)
1176 }
1177}
1178
1179impl From<RenderImageParams> for AtlasKey {
1180 fn from(params: RenderImageParams) -> Self {
1181 Self::Image(params)
1182 }
1183}
1184
1185#[expect(missing_docs)]
1187pub trait PlatformAtlas {
1188 fn get_or_insert_with<'a>(
1189 &self,
1190 key: &AtlasKey,
1191 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1192 ) -> Result<Option<AtlasTile>>;
1193 fn remove(&self, key: &AtlasKey);
1194}
1195
1196#[doc(hidden)]
1197pub struct AtlasTextureList<T> {
1198 pub textures: Vec<Option<T>>,
1199 pub free_list: Vec<usize>,
1200}
1201
1202impl<T> Default for AtlasTextureList<T> {
1203 fn default() -> Self {
1204 Self {
1205 textures: Vec::default(),
1206 free_list: Vec::default(),
1207 }
1208 }
1209}
1210
1211impl<T> ops::Index<usize> for AtlasTextureList<T> {
1212 type Output = Option<T>;
1213
1214 fn index(&self, index: usize) -> &Self::Output {
1215 &self.textures[index]
1216 }
1217}
1218
1219impl<T> AtlasTextureList<T> {
1220 #[allow(unused)]
1221 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1222 self.free_list.clear();
1223 self.textures.drain(..)
1224 }
1225
1226 #[allow(dead_code)]
1227 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1228 self.textures.iter_mut().flatten()
1229 }
1230}
1231
1232#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1233#[repr(C)]
1234#[expect(missing_docs)]
1235pub struct AtlasTile {
1236 pub texture_id: AtlasTextureId,
1238 pub tile_id: TileId,
1240 pub padding: u32,
1242 pub bounds: Bounds<DevicePixels>,
1244}
1245
1246#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1247#[repr(C)]
1248#[expect(missing_docs)]
1249pub struct AtlasTextureId {
1250 pub index: u32,
1253 pub kind: AtlasTextureKind,
1255}
1256
1257#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1258#[repr(C)]
1259#[cfg_attr(
1260 all(
1261 any(target_os = "linux", target_os = "freebsd"),
1262 not(any(feature = "x11", feature = "wayland"))
1263 ),
1264 allow(dead_code)
1265)]
1266#[expect(missing_docs)]
1267pub enum AtlasTextureKind {
1268 Monochrome = 0,
1269 Polychrome = 1,
1270 Subpixel = 2,
1271}
1272
1273#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1274#[repr(C)]
1275#[expect(missing_docs)]
1276pub struct TileId(pub u32);
1277
1278impl From<etagere::AllocId> for TileId {
1279 fn from(id: etagere::AllocId) -> Self {
1280 Self(id.serialize())
1281 }
1282}
1283
1284impl From<TileId> for etagere::AllocId {
1285 fn from(id: TileId) -> Self {
1286 Self::deserialize(id.0)
1287 }
1288}
1289
1290#[expect(missing_docs)]
1291pub struct PlatformInputHandler {
1292 cx: AsyncWindowContext,
1293 handler: Box<dyn InputHandler>,
1294}
1295
1296#[expect(missing_docs)]
1297#[cfg_attr(
1298 all(
1299 any(target_os = "linux", target_os = "freebsd"),
1300 not(any(feature = "x11", feature = "wayland"))
1301 ),
1302 allow(dead_code)
1303)]
1304impl PlatformInputHandler {
1305 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1306 Self { cx, handler }
1307 }
1308
1309 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1310 self.cx
1311 .update(|window, cx| {
1312 self.handler
1313 .selected_text_range(ignore_disabled_input, window, cx)
1314 })
1315 .ok()
1316 .flatten()
1317 }
1318
1319 #[cfg_attr(target_os = "windows", allow(dead_code))]
1320 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1321 self.cx
1322 .update(|window, cx| self.handler.marked_text_range(window, cx))
1323 .ok()
1324 .flatten()
1325 }
1326
1327 #[cfg_attr(
1328 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1329 allow(dead_code)
1330 )]
1331 pub fn text_for_range(
1332 &mut self,
1333 range_utf16: Range<usize>,
1334 adjusted: &mut Option<Range<usize>>,
1335 ) -> Option<String> {
1336 self.cx
1337 .update(|window, cx| {
1338 self.handler
1339 .text_for_range(range_utf16, adjusted, window, cx)
1340 })
1341 .ok()
1342 .flatten()
1343 }
1344
1345 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1346 self.cx
1347 .update(|window, cx| {
1348 self.handler
1349 .replace_text_in_range(replacement_range, text, window, cx);
1350 })
1351 .ok();
1352 }
1353
1354 pub fn replace_and_mark_text_in_range(
1355 &mut self,
1356 range_utf16: Option<Range<usize>>,
1357 new_text: &str,
1358 new_selected_range: Option<Range<usize>>,
1359 ) {
1360 self.cx
1361 .update(|window, cx| {
1362 self.handler.replace_and_mark_text_in_range(
1363 range_utf16,
1364 new_text,
1365 new_selected_range,
1366 window,
1367 cx,
1368 )
1369 })
1370 .ok();
1371 }
1372
1373 #[cfg_attr(target_os = "windows", allow(dead_code))]
1374 pub fn unmark_text(&mut self) {
1375 self.cx
1376 .update(|window, cx| self.handler.unmark_text(window, cx))
1377 .ok();
1378 }
1379
1380 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1381 self.cx
1382 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1383 .ok()
1384 .flatten()
1385 }
1386
1387 #[allow(dead_code)]
1388 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1389 self.handler.apple_press_and_hold_enabled()
1390 }
1391
1392 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1393 self.handler.replace_text_in_range(None, input, window, cx);
1394 }
1395
1396 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1397 let selection = self.handler.selected_text_range(true, window, cx)?;
1398 self.handler.bounds_for_range(
1399 if selection.reversed {
1400 selection.range.start..selection.range.start
1401 } else {
1402 selection.range.end..selection.range.end
1403 },
1404 window,
1405 cx,
1406 )
1407 }
1408
1409 #[allow(unused)]
1410 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1411 self.cx
1412 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1413 .ok()
1414 .flatten()
1415 }
1416
1417 #[allow(dead_code)]
1418 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1419 self.handler.accepts_text_input(window, cx)
1420 }
1421
1422 #[allow(dead_code)]
1423 pub fn query_accepts_text_input(&mut self) -> bool {
1424 self.cx
1425 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1426 .unwrap_or(true)
1427 }
1428
1429 #[allow(dead_code)]
1430 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1431 self.cx
1432 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1433 .unwrap_or(false)
1434 }
1435}
1436
1437#[derive(Debug)]
1440pub struct UTF16Selection {
1441 pub range: Range<usize>,
1443 pub reversed: bool,
1445}
1446
1447pub trait InputHandler: 'static {
1452 fn selected_text_range(
1457 &mut self,
1458 ignore_disabled_input: bool,
1459 window: &mut Window,
1460 cx: &mut App,
1461 ) -> Option<UTF16Selection>;
1462
1463 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1468
1469 fn text_for_range(
1474 &mut self,
1475 range_utf16: Range<usize>,
1476 adjusted_range: &mut Option<Range<usize>>,
1477 window: &mut Window,
1478 cx: &mut App,
1479 ) -> Option<String>;
1480
1481 fn replace_text_in_range(
1486 &mut self,
1487 replacement_range: Option<Range<usize>>,
1488 text: &str,
1489 window: &mut Window,
1490 cx: &mut App,
1491 );
1492
1493 fn replace_and_mark_text_in_range(
1499 &mut self,
1500 range_utf16: Option<Range<usize>>,
1501 new_text: &str,
1502 new_selected_range: Option<Range<usize>>,
1503 window: &mut Window,
1504 cx: &mut App,
1505 );
1506
1507 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1510
1511 fn bounds_for_range(
1516 &mut self,
1517 range_utf16: Range<usize>,
1518 window: &mut Window,
1519 cx: &mut App,
1520 ) -> Option<Bounds<Pixels>>;
1521
1522 fn character_index_for_point(
1526 &mut self,
1527 point: Point<Pixels>,
1528 window: &mut Window,
1529 cx: &mut App,
1530 ) -> Option<usize>;
1531
1532 #[allow(dead_code)]
1536 fn apple_press_and_hold_enabled(&mut self) -> bool {
1537 true
1538 }
1539
1540 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1542 true
1543 }
1544
1545 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1553 false
1554 }
1555}
1556
1557#[derive(Debug)]
1559pub struct WindowOptions {
1560 pub window_bounds: Option<WindowBounds>,
1564
1565 pub titlebar: Option<TitlebarOptions>,
1567
1568 pub focus: bool,
1570
1571 pub show: bool,
1573
1574 pub kind: WindowKind,
1576
1577 pub is_movable: bool,
1579
1580 pub is_resizable: bool,
1582
1583 pub is_minimizable: bool,
1585
1586 pub display_id: Option<DisplayId>,
1588
1589 pub window_background: WindowBackgroundAppearance,
1591
1592 pub app_id: Option<String>,
1594
1595 pub window_min_size: Option<Size<Pixels>>,
1597
1598 pub window_decorations: Option<WindowDecorations>,
1601
1602 pub icon: Option<Arc<image::RgbaImage>>,
1604
1605 pub tabbing_identifier: Option<String>,
1608
1609 pub mouse_passthrough: bool,
1611}
1612
1613#[derive(Debug)]
1615#[cfg_attr(
1616 all(
1617 any(target_os = "linux", target_os = "freebsd"),
1618 not(any(feature = "x11", feature = "wayland"))
1619 ),
1620 allow(dead_code)
1621)]
1622#[allow(missing_docs)]
1623pub struct WindowParams {
1624 pub bounds: Bounds<Pixels>,
1625
1626 #[cfg_attr(feature = "wayland", allow(dead_code))]
1628 pub titlebar: Option<TitlebarOptions>,
1629
1630 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1632 pub kind: WindowKind,
1633
1634 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1636 pub is_movable: bool,
1637
1638 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1640 pub is_resizable: bool,
1641
1642 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1644 pub is_minimizable: bool,
1645
1646 #[cfg_attr(
1647 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1648 allow(dead_code)
1649 )]
1650 pub focus: bool,
1651
1652 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1653 pub show: bool,
1654
1655 #[cfg_attr(feature = "wayland", allow(dead_code))]
1657 pub icon: Option<Arc<image::RgbaImage>>,
1658
1659 #[cfg_attr(feature = "wayland", allow(dead_code))]
1660 pub display_id: Option<DisplayId>,
1661
1662 pub window_min_size: Option<Size<Pixels>>,
1663 #[cfg(target_os = "macos")]
1664 pub tabbing_identifier: Option<String>,
1665 #[cfg_attr(
1666 any(target_os = "linux", target_os = "freebsd", target_os = "macos"),
1667 allow(dead_code)
1668 )]
1669 pub window_decorations: WindowDecorations,
1670
1671 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1673 pub mouse_passthrough: bool,
1674}
1675
1676#[derive(Debug, Copy, Clone, PartialEq)]
1678pub enum WindowBounds {
1679 Windowed(Bounds<Pixels>),
1681 Maximized(Bounds<Pixels>),
1683 Fullscreen(Bounds<Pixels>),
1685}
1686
1687impl Default for WindowBounds {
1688 fn default() -> Self {
1689 WindowBounds::Windowed(Bounds::default())
1690 }
1691}
1692
1693impl WindowBounds {
1694 pub fn get_bounds(&self) -> Bounds<Pixels> {
1696 match self {
1697 WindowBounds::Windowed(bounds) => *bounds,
1698 WindowBounds::Maximized(bounds) => *bounds,
1699 WindowBounds::Fullscreen(bounds) => *bounds,
1700 }
1701 }
1702
1703 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1705 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1706 }
1707}
1708
1709impl Default for WindowOptions {
1710 fn default() -> Self {
1711 Self {
1712 window_bounds: None,
1713 titlebar: Some(TitlebarOptions {
1714 title: Default::default(),
1715 appears_transparent: Default::default(),
1716 traffic_light_position: Default::default(),
1717 }),
1718 focus: true,
1719 show: true,
1720 kind: WindowKind::Normal,
1721 is_movable: true,
1722 is_resizable: true,
1723 is_minimizable: true,
1724 display_id: None,
1725 window_background: WindowBackgroundAppearance::default(),
1726 icon: None,
1727 app_id: None,
1728 window_min_size: None,
1729 window_decorations: None,
1730 tabbing_identifier: None,
1731 mouse_passthrough: false,
1732 }
1733 }
1734}
1735
1736#[derive(Debug, Default)]
1738pub struct TitlebarOptions {
1739 pub title: Option<SharedString>,
1741
1742 pub appears_transparent: bool,
1745
1746 pub traffic_light_position: Option<Point<Pixels>>,
1748}
1749
1750#[derive(Clone, Debug, PartialEq)]
1752pub enum WindowKind {
1753 Normal,
1755
1756 PopUp,
1759
1760 Floating,
1762
1763 #[cfg(all(target_os = "linux", feature = "wayland"))]
1766 LayerShell(layer_shell::LayerShellOptions),
1767
1768 Dialog,
1771
1772 Overlay,
1777}
1778
1779#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1783pub enum WindowAppearance {
1784 #[default]
1788 Light,
1789
1790 VibrantLight,
1794
1795 Dark,
1799
1800 VibrantDark,
1804}
1805
1806#[derive(Copy, Clone, Debug, Default, PartialEq)]
1808pub enum WindowBackgroundAppearance {
1809 #[default]
1815 Opaque,
1816 Transparent,
1818 Blurred,
1822 MicaBackdrop,
1824 MicaAltBackdrop,
1826}
1827
1828#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1830pub enum TextRenderingMode {
1831 #[default]
1833 PlatformDefault,
1834 Subpixel,
1836 Grayscale,
1838}
1839
1840#[derive(Clone, Debug)]
1842pub struct PathPromptOptions {
1843 pub files: bool,
1845 pub directories: bool,
1847 pub multiple: bool,
1849 pub prompt: Option<SharedString>,
1851}
1852
1853#[derive(Copy, Clone, Debug, PartialEq)]
1855pub enum PromptLevel {
1856 Info,
1858
1859 Warning,
1861
1862 Critical,
1864}
1865
1866#[derive(Clone, Debug, PartialEq)]
1868pub enum PromptButton {
1869 Ok(SharedString),
1871 Cancel(SharedString),
1873 Other(SharedString),
1875}
1876
1877impl PromptButton {
1878 pub fn new(label: impl Into<SharedString>) -> Self {
1880 PromptButton::Other(label.into())
1881 }
1882
1883 pub fn ok(label: impl Into<SharedString>) -> Self {
1885 PromptButton::Ok(label.into())
1886 }
1887
1888 pub fn cancel(label: impl Into<SharedString>) -> Self {
1890 PromptButton::Cancel(label.into())
1891 }
1892
1893 #[allow(dead_code)]
1895 pub fn is_cancel(&self) -> bool {
1896 matches!(self, PromptButton::Cancel(_))
1897 }
1898
1899 pub fn label(&self) -> &SharedString {
1901 match self {
1902 PromptButton::Ok(label) => label,
1903 PromptButton::Cancel(label) => label,
1904 PromptButton::Other(label) => label,
1905 }
1906 }
1907}
1908
1909impl From<&str> for PromptButton {
1910 fn from(value: &str) -> Self {
1911 match value.to_lowercase().as_str() {
1912 "ok" => PromptButton::Ok("Ok".into()),
1913 "cancel" => PromptButton::Cancel("Cancel".into()),
1914 _ => PromptButton::Other(SharedString::from(value.to_owned())),
1915 }
1916 }
1917}
1918
1919#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1921pub enum CursorStyle {
1922 #[default]
1924 Arrow,
1925
1926 IBeam,
1929
1930 Crosshair,
1933
1934 ClosedHand,
1937
1938 OpenHand,
1941
1942 PointingHand,
1945
1946 ResizeLeft,
1949
1950 ResizeRight,
1953
1954 ResizeLeftRight,
1957
1958 ResizeUp,
1961
1962 ResizeDown,
1965
1966 ResizeUpDown,
1969
1970 ResizeUpLeftDownRight,
1973
1974 ResizeUpRightDownLeft,
1977
1978 ResizeColumn,
1981
1982 ResizeRow,
1985
1986 IBeamCursorForVerticalLayout,
1989
1990 OperationNotAllowed,
1993
1994 DragLink,
1997
1998 DragCopy,
2001
2002 ContextualMenu,
2005}
2006
2007#[derive(Clone, Debug, Eq, PartialEq)]
2009pub struct ClipboardItem {
2010 pub entries: Vec<ClipboardEntry>,
2012}
2013
2014#[derive(Clone, Debug, Eq, PartialEq)]
2016pub enum ClipboardEntry {
2017 String(ClipboardString),
2019 Image(Image),
2021 ExternalPaths(crate::ExternalPaths),
2023}
2024
2025impl ClipboardItem {
2026 pub fn new_string(text: String) -> Self {
2028 Self {
2029 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2030 }
2031 }
2032
2033 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2035 Self {
2036 entries: vec![ClipboardEntry::String(ClipboardString {
2037 text,
2038 metadata: Some(metadata),
2039 })],
2040 }
2041 }
2042
2043 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2045 Self {
2046 entries: vec![ClipboardEntry::String(
2047 ClipboardString::new(text).with_json_metadata(metadata),
2048 )],
2049 }
2050 }
2051
2052 pub fn new_image(image: &Image) -> Self {
2054 Self {
2055 entries: vec![ClipboardEntry::Image(image.clone())],
2056 }
2057 }
2058
2059 pub fn text(&self) -> Option<String> {
2062 let mut answer = String::new();
2063
2064 for entry in self.entries.iter() {
2065 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2066 answer.push_str(text);
2067 }
2068 }
2069
2070 if answer.is_empty() {
2071 for entry in self.entries.iter() {
2072 if let ClipboardEntry::ExternalPaths(paths) = entry {
2073 for path in &paths.0 {
2074 use std::fmt::Write as _;
2075 _ = write!(answer, "{}", path.display());
2076 }
2077 }
2078 }
2079 }
2080
2081 if !answer.is_empty() {
2082 Some(answer)
2083 } else {
2084 None
2085 }
2086 }
2087
2088 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2090 pub fn metadata(&self) -> Option<&String> {
2091 match self.entries().first() {
2092 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2093 clipboard_string.metadata.as_ref()
2094 }
2095 _ => None,
2096 }
2097 }
2098
2099 pub fn entries(&self) -> &[ClipboardEntry] {
2101 &self.entries
2102 }
2103
2104 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2106 self.entries.into_iter()
2107 }
2108}
2109
2110impl From<ClipboardString> for ClipboardEntry {
2111 fn from(value: ClipboardString) -> Self {
2112 Self::String(value)
2113 }
2114}
2115
2116impl From<String> for ClipboardEntry {
2117 fn from(value: String) -> Self {
2118 Self::from(ClipboardString::from(value))
2119 }
2120}
2121
2122impl From<Image> for ClipboardEntry {
2123 fn from(value: Image) -> Self {
2124 Self::Image(value)
2125 }
2126}
2127
2128impl From<ClipboardEntry> for ClipboardItem {
2129 fn from(value: ClipboardEntry) -> Self {
2130 Self {
2131 entries: vec![value],
2132 }
2133 }
2134}
2135
2136impl From<String> for ClipboardItem {
2137 fn from(value: String) -> Self {
2138 Self::from(ClipboardEntry::from(value))
2139 }
2140}
2141
2142impl From<Image> for ClipboardItem {
2143 fn from(value: Image) -> Self {
2144 Self::from(ClipboardEntry::from(value))
2145 }
2146}
2147
2148#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2150pub enum ImageFormat {
2151 Png,
2156 Jpeg,
2158 Webp,
2160 Gif,
2162 Svg,
2164 Bmp,
2166 Tiff,
2168 Ico,
2170 Pnm,
2172}
2173
2174impl ImageFormat {
2175 pub const fn mime_type(self) -> &'static str {
2177 match self {
2178 ImageFormat::Png => "image/png",
2179 ImageFormat::Jpeg => "image/jpeg",
2180 ImageFormat::Webp => "image/webp",
2181 ImageFormat::Gif => "image/gif",
2182 ImageFormat::Svg => "image/svg+xml",
2183 ImageFormat::Bmp => "image/bmp",
2184 ImageFormat::Tiff => "image/tiff",
2185 ImageFormat::Ico => "image/ico",
2186 ImageFormat::Pnm => "image/x-portable-anymap",
2187 }
2188 }
2189
2190 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2192 use strum::IntoEnumIterator;
2193 Self::iter()
2194 .find(|format| format.mime_type() == mime_type)
2195 .or_else(|| Self::from_mime_type_alias(mime_type))
2196 }
2197
2198 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2202 match mime_type {
2203 "image/jpg" => Some(Self::Jpeg),
2204 "image/tif" => Some(Self::Tiff),
2205 _ => None,
2206 }
2207 }
2208}
2209
2210#[derive(Clone, Debug, PartialEq, Eq)]
2212pub struct Image {
2213 pub format: ImageFormat,
2215 pub bytes: Vec<u8>,
2217 pub id: u64,
2219}
2220
2221impl Hash for Image {
2222 fn hash<H: Hasher>(&self, state: &mut H) {
2223 state.write_u64(self.id);
2224 }
2225}
2226
2227impl Image {
2228 pub fn empty() -> Self {
2230 Self::from_bytes(ImageFormat::Png, Vec::new())
2231 }
2232
2233 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2235 Self {
2236 id: hash(&bytes),
2237 format,
2238 bytes,
2239 }
2240 }
2241
2242 pub fn id(&self) -> u64 {
2244 self.id
2245 }
2246
2247 pub fn use_render_image(
2249 self: Arc<Self>,
2250 window: &mut Window,
2251 cx: &mut App,
2252 ) -> Option<Arc<RenderImage>> {
2253 ImageSource::Image(self)
2254 .use_data(None, window, cx)
2255 .and_then(|result| result.ok())
2256 }
2257
2258 pub fn get_render_image(
2260 self: Arc<Self>,
2261 window: &mut Window,
2262 cx: &mut App,
2263 ) -> Option<Arc<RenderImage>> {
2264 ImageSource::Image(self)
2265 .get_data(None, window, cx)
2266 .and_then(|result| result.ok())
2267 }
2268
2269 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2271 ImageSource::Image(self).remove_asset(cx);
2272 }
2273
2274 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2276 fn frames_for_image(
2277 bytes: &[u8],
2278 format: image::ImageFormat,
2279 ) -> Result<SmallVec<[Frame; 1]>> {
2280 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2281
2282 for pixel in data.chunks_exact_mut(4) {
2284 pixel.swap(0, 2);
2285 }
2286
2287 Ok(SmallVec::from_elem(Frame::new(data), 1))
2288 }
2289
2290 let frames = match self.format {
2291 ImageFormat::Gif => {
2292 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2293 let mut frames = SmallVec::new();
2294
2295 for frame in decoder.into_frames() {
2296 match frame {
2297 Ok(mut frame) => {
2298 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2300 pixel.swap(0, 2);
2301 }
2302 frames.push(frame);
2303 }
2304 Err(err) => {
2305 log::debug!("Skipping GIF frame due to decode error: {err}");
2306 }
2307 }
2308 }
2309
2310 if frames.is_empty() {
2311 anyhow::bail!("GIF could not be decoded: all frames failed");
2312 }
2313
2314 frames
2315 }
2316 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2317 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2318 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2319 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2320 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2321 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2322 ImageFormat::Svg => {
2323 return svg_renderer
2324 .render_single_frame(&self.bytes, 1.0)
2325 .map_err(Into::into);
2326 }
2327 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2328 };
2329
2330 Ok(Arc::new(RenderImage::new(frames)))
2331 }
2332
2333 pub fn format(&self) -> ImageFormat {
2335 self.format
2336 }
2337
2338 pub fn bytes(&self) -> &[u8] {
2340 self.bytes.as_slice()
2341 }
2342}
2343
2344#[derive(Clone, Debug, Eq, PartialEq)]
2346pub struct ClipboardString {
2347 pub text: String,
2349 pub metadata: Option<String>,
2351}
2352
2353impl ClipboardString {
2354 pub fn new(text: String) -> Self {
2356 Self {
2357 text,
2358 metadata: None,
2359 }
2360 }
2361
2362 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2365 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2366 self
2367 }
2368
2369 pub fn text(&self) -> &String {
2371 &self.text
2372 }
2373
2374 pub fn into_text(self) -> String {
2376 self.text
2377 }
2378
2379 pub fn metadata_json<T>(&self) -> Option<T>
2381 where
2382 T: for<'a> Deserialize<'a>,
2383 {
2384 self.metadata
2385 .as_ref()
2386 .and_then(|m| serde_json::from_str(m).ok())
2387 }
2388
2389 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2390 pub fn text_hash(text: &str) -> u64 {
2392 let mut hasher = SeaHasher::new();
2393 text.hash(&mut hasher);
2394 hasher.finish()
2395 }
2396}
2397
2398impl From<String> for ClipboardString {
2399 fn from(value: String) -> Self {
2400 Self {
2401 text: value,
2402 metadata: None,
2403 }
2404 }
2405}
2406
2407#[derive(Debug, Clone, PartialEq)]
2409pub enum WindowPosition {
2410 Center,
2412 CenterOnDisplay(DisplayId),
2414 TrayCenter(Bounds<Pixels>),
2416 TopRight {
2418 margin: Pixels,
2420 },
2421 BottomRight {
2423 margin: Pixels,
2425 },
2426 TopLeft {
2428 margin: Pixels,
2430 },
2431 BottomLeft {
2433 margin: Pixels,
2435 },
2436}
2437
2438#[derive(Debug, Clone)]
2440pub struct FocusedWindowInfo {
2441 pub app_name: String,
2443 pub window_title: String,
2445 pub bundle_id: Option<String>,
2447 pub pid: Option<u32>,
2449}
2450
2451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2453pub enum PermissionType {
2454 Accessibility,
2456 ScreenCapture,
2458 InputMonitoring,
2460}
2461
2462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2464pub enum PermissionStatus {
2465 Granted,
2467 Denied,
2469 NotDetermined,
2471}
2472
2473#[derive(Debug, Clone)]
2475pub struct GlobalHotKeyEvent {
2476 pub id: u32,
2478}
2479
2480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2482pub enum SystemPowerEvent {
2483 Suspend,
2485 Resume,
2487 LockScreen,
2489 UnlockScreen,
2491 Shutdown,
2493}
2494
2495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2497pub enum PowerSaveBlockerKind {
2498 PreventAppSuspension,
2500 PreventDisplaySleep,
2502}
2503
2504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2506pub enum NetworkStatus {
2507 Online,
2509 Offline,
2511}
2512
2513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2515pub enum MediaKeyEvent {
2516 Play,
2518 Pause,
2520 PlayPause,
2522 Stop,
2524 NextTrack,
2526 PreviousTrack,
2528}
2529
2530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2532pub enum AttentionType {
2533 Informational,
2535 Critical,
2537}
2538
2539#[derive(Debug, Clone, Copy, PartialEq)]
2541pub enum ProgressBarState {
2542 None,
2544 Indeterminate,
2546 Normal(f64),
2548 Error(f64),
2550 Paused(f64),
2552}
2553
2554#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2556pub enum DialogKind {
2557 Info,
2559 Warning,
2561 Error,
2563}
2564
2565#[derive(Debug, Clone)]
2567pub struct DialogOptions {
2568 pub kind: DialogKind,
2570 pub title: SharedString,
2572 pub message: SharedString,
2574 pub detail: Option<SharedString>,
2576 pub buttons: Vec<SharedString>,
2578}
2579
2580#[derive(Debug, Clone)]
2582pub struct OsInfo {
2583 pub name: SharedString,
2585 pub version: SharedString,
2587 pub arch: SharedString,
2589 pub locale: SharedString,
2591 pub hostname: SharedString,
2593}
2594
2595#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2597pub enum BiometricKind {
2598 TouchId,
2600 WindowsHello,
2602 Fingerprint,
2604}
2605
2606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2608pub enum BiometricStatus {
2609 Available(BiometricKind),
2611 Unavailable,
2613}
2614
2615#[cfg(test)]
2616mod image_tests {
2617 use super::*;
2618 use std::sync::Arc;
2619
2620 #[test]
2621 fn test_svg_image_to_image_data_converts_to_bgra() {
2622 let image = Image::from_bytes(
2623 ImageFormat::Svg,
2624 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2625<rect width="1" height="1" fill="#38BDF8"/>
2626</svg>"##
2627 .to_vec(),
2628 );
2629
2630 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2631 let bytes = render_image.as_bytes(0).unwrap();
2632
2633 for pixel in bytes.chunks_exact(4) {
2634 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2635 }
2636 }
2637}
2638
2639#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2640mod tests {
2641 use super::*;
2642 use std::collections::HashSet;
2643
2644 #[test]
2645 fn test_window_button_layout_parse_standard() {
2646 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2647 assert_eq!(
2648 layout.left,
2649 [
2650 Some(WindowButton::Close),
2651 Some(WindowButton::Minimize),
2652 None
2653 ]
2654 );
2655 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2656 }
2657
2658 #[test]
2659 fn test_window_button_layout_parse_right_only() {
2660 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2661 assert_eq!(layout.left, [None, None, None]);
2662 assert_eq!(
2663 layout.right,
2664 [
2665 Some(WindowButton::Minimize),
2666 Some(WindowButton::Maximize),
2667 Some(WindowButton::Close)
2668 ]
2669 );
2670 }
2671
2672 #[test]
2673 fn test_window_button_layout_parse_left_only() {
2674 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2675 assert_eq!(
2676 layout.left,
2677 [
2678 Some(WindowButton::Close),
2679 Some(WindowButton::Minimize),
2680 Some(WindowButton::Maximize)
2681 ]
2682 );
2683 assert_eq!(layout.right, [None, None, None]);
2684 }
2685
2686 #[test]
2687 fn test_window_button_layout_parse_with_whitespace() {
2688 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2689 assert_eq!(
2690 layout.left,
2691 [
2692 Some(WindowButton::Close),
2693 Some(WindowButton::Minimize),
2694 None
2695 ]
2696 );
2697 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2698 }
2699
2700 #[test]
2701 fn test_window_button_layout_parse_empty() {
2702 let layout = WindowButtonLayout::parse("").unwrap();
2703 assert_eq!(layout.left, [None, None, None]);
2704 assert_eq!(layout.right, [None, None, None]);
2705 }
2706
2707 #[test]
2708 fn test_window_button_layout_parse_intentionally_empty() {
2709 let layout = WindowButtonLayout::parse(":").unwrap();
2710 assert_eq!(layout.left, [None, None, None]);
2711 assert_eq!(layout.right, [None, None, None]);
2712 }
2713
2714 #[test]
2715 fn test_window_button_layout_parse_invalid_buttons() {
2716 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2717 assert_eq!(
2718 layout.left,
2719 [
2720 Some(WindowButton::Close),
2721 Some(WindowButton::Minimize),
2722 None
2723 ]
2724 );
2725 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2726 }
2727
2728 #[test]
2729 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2730 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2731 assert_eq!(
2732 layout.right,
2733 [
2734 Some(WindowButton::Close),
2735 Some(WindowButton::Minimize),
2736 None
2737 ]
2738 );
2739 assert_eq!(layout.format(), ":close,minimize");
2740 }
2741
2742 #[test]
2743 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2744 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2745 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2746 assert_eq!(
2747 layout.right,
2748 [
2749 Some(WindowButton::Maximize),
2750 Some(WindowButton::Minimize),
2751 None
2752 ]
2753 );
2754
2755 let button_ids: Vec<_> = layout
2756 .left
2757 .iter()
2758 .chain(layout.right.iter())
2759 .flatten()
2760 .map(WindowButton::id)
2761 .collect();
2762 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2763 assert_eq!(unique_button_ids.len(), button_ids.len());
2764 assert_eq!(layout.format(), "close:maximize,minimize");
2765 }
2766
2767 #[test]
2768 fn test_window_button_layout_parse_gnome_style() {
2769 let layout = WindowButtonLayout::parse("close").unwrap();
2770 assert_eq!(layout.left, [None, None, None]);
2771 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2772 }
2773
2774 #[test]
2775 fn test_window_button_layout_parse_elementary_style() {
2776 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2777 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2778 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2779 }
2780
2781 #[test]
2782 fn test_window_button_layout_round_trip() {
2783 let cases = [
2784 "close:minimize,maximize",
2785 "minimize,maximize,close:",
2786 ":close",
2787 "close:",
2788 "close:maximize",
2789 ":",
2790 ];
2791
2792 for case in cases {
2793 let layout = WindowButtonLayout::parse(case).unwrap();
2794 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2795 }
2796 }
2797
2798 #[test]
2799 fn test_window_button_layout_linux_default() {
2800 let layout = WindowButtonLayout::linux_default();
2801 assert_eq!(layout.left, [None, None, None]);
2802 assert_eq!(
2803 layout.right,
2804 [
2805 Some(WindowButton::Minimize),
2806 Some(WindowButton::Maximize),
2807 Some(WindowButton::Close)
2808 ]
2809 );
2810
2811 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2812 assert_eq!(round_tripped, layout);
2813 }
2814
2815 #[test]
2816 fn test_window_button_layout_parse_all_invalid() {
2817 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2818 }
2819}