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 = PlatformPixelBuffer;
30#[cfg(target_os = "macos")]
32pub type PlatformPixelBuffer = objc2_core_foundation::CFRetained<objc2_core_video::CVPixelBuffer>;
33
34use crate::{
35 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
36 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
37 ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, MouseButton,
38 Pixels, PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
39 RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
40 SystemWindowTab, Task, Window, 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 open_gpui_scheduler::Instant;
52pub use open_gpui_scheduler::RunnableMeta;
53use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
54use schemars::JsonSchema;
55use seahash::SeaHasher;
56use serde::{Deserialize, Serialize};
57use smallvec::SmallVec;
58use std::borrow::Cow;
59use std::hash::{Hash, Hasher};
60use std::io::Cursor;
61use std::ops;
62use std::time::Duration;
63use std::{
64 fmt::{self, Debug},
65 ops::Range,
66 path::{Path, PathBuf},
67 rc::Rc,
68 sync::Arc,
69};
70use strum::EnumIter;
71use uuid::Uuid;
72
73pub use app_menu::*;
74pub use keyboard::*;
75pub use keystroke::*;
76
77#[cfg(any(test, feature = "test-support"))]
78pub(crate) use test::*;
79
80#[cfg(any(test, feature = "test-support"))]
81pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
82
83#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
84pub use visual_test::VisualTestPlatform;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub struct PlatformViewportCapabilities {
89 pub platform_viewport_windows: bool,
91 pub global_window_bounds: bool,
93 pub window_stack: bool,
95 pub display_work_area: bool,
97 pub dpi_scale: bool,
99 pub live_window_move: bool,
101 pub no_input_windows: bool,
103 pub hovered_window_ignores_no_input: bool,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
109pub struct PlatformViewportFlagCapabilities {
110 pub no_focus_on_appearing_windows: bool,
112 pub no_focus_on_click_windows: bool,
114 pub alpha_windows: bool,
116 pub topmost_windows: bool,
118 pub no_taskbar_windows: bool,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum PlatformHoveredWindow {
125 #[default]
127 Unavailable,
128 NoWindow,
130 Window(AnyWindowHandle),
132}
133
134impl PlatformHoveredWindow {
135 pub fn from_window(window: Option<AnyWindowHandle>) -> Self {
137 window.map_or(Self::NoWindow, Self::Window)
138 }
139
140 pub fn window(self) -> Option<AnyWindowHandle> {
142 match self {
143 Self::Window(window) => Some(window),
144 Self::Unavailable | Self::NoWindow => None,
145 }
146 }
147
148 pub fn is_available(self) -> bool {
150 !matches!(self, Self::Unavailable)
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
156pub enum PlatformFocusedWindow {
157 #[default]
159 Unavailable,
160 NoWindow,
162 Window(AnyWindowHandle),
164}
165
166impl PlatformFocusedWindow {
167 pub fn from_window(window: Option<AnyWindowHandle>) -> Self {
169 window.map_or(Self::NoWindow, Self::Window)
170 }
171
172 pub fn window(self) -> Option<AnyWindowHandle> {
174 match self {
175 Self::Window(window) => Some(window),
176 Self::Unavailable | Self::NoWindow => None,
177 }
178 }
179
180 pub fn is_available(self) -> bool {
182 !matches!(self, Self::Unavailable)
183 }
184}
185
186#[cfg(any(target_os = "linux", target_os = "freebsd"))]
190#[inline]
191pub fn guess_compositor() -> &'static str {
192 if std::env::var_os("ZED_HEADLESS").is_some() {
193 return "Headless";
194 }
195
196 #[cfg(feature = "wayland")]
197 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
198 #[cfg(not(feature = "wayland"))]
199 let wayland_display: Option<std::ffi::OsString> = None;
200
201 #[cfg(feature = "x11")]
202 let x11_display = std::env::var_os("DISPLAY");
203 #[cfg(not(feature = "x11"))]
204 let x11_display: Option<std::ffi::OsString> = None;
205
206 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
207 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
208
209 if use_wayland {
210 "Wayland"
211 } else if use_x11 {
212 "X11"
213 } else {
214 "Headless"
215 }
216}
217
218#[expect(missing_docs)]
219pub trait Platform: 'static {
220 fn background_executor(&self) -> BackgroundExecutor;
221 fn foreground_executor(&self) -> ForegroundExecutor;
222 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
223
224 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
225 fn quit(&self);
226 fn restart(&self, binary_path: Option<PathBuf>);
227 fn activate(&self, ignoring_other_apps: bool);
228 fn hide(&self);
229 fn hide_other_apps(&self);
230 fn unhide_other_apps(&self);
231
232 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
233 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
234 fn hovered_window(&self) -> PlatformHoveredWindow {
235 PlatformHoveredWindow::Unavailable
236 }
237 fn active_window(&self) -> Option<AnyWindowHandle>;
238 fn focused_window(&self) -> PlatformFocusedWindow {
239 PlatformFocusedWindow::Unavailable
240 }
241 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
242 None
243 }
244 fn viewport_capabilities(&self) -> PlatformViewportCapabilities {
245 PlatformViewportCapabilities::default()
246 }
247 fn viewport_flag_capabilities(&self) -> PlatformViewportFlagCapabilities {
248 PlatformViewportFlagCapabilities::default()
249 }
250 fn mouse_button_is_pressed(&self, _button: MouseButton) -> Option<bool> {
251 None
252 }
253
254 fn is_screen_capture_supported(&self) -> bool {
255 false
256 }
257
258 fn screen_capture_sources(
259 &self,
260 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
261 let (sources_tx, sources_rx) = oneshot::channel();
262 sources_tx
263 .send(Err(anyhow::anyhow!(
264 "gpui was compiled without the screen-capture feature"
265 )))
266 .ok();
267 sources_rx
268 }
269
270 fn open_window(
271 &self,
272 handle: AnyWindowHandle,
273 options: WindowParams,
274 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
275
276 fn window_appearance(&self) -> WindowAppearance;
278
279 fn button_layout(&self) -> Option<WindowButtonLayout> {
281 None
282 }
283
284 fn open_url(&self, url: &str);
285 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
286 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
287
288 fn prompt_for_paths(
289 &self,
290 options: PathPromptOptions,
291 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
292 fn prompt_for_new_path(
293 &self,
294 directory: &Path,
295 suggested_name: Option<&str>,
296 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
297 fn can_select_mixed_files_and_dirs(&self) -> bool;
298 fn reveal_path(&self, path: &Path);
299 fn open_with_system(&self, path: &Path);
300
301 fn on_quit(&self, callback: Box<dyn FnMut()>);
302 fn on_reopen(&self, callback: Box<dyn FnMut()>);
303 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
304
305 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
306 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
307 None
308 }
309
310 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
311 fn perform_dock_menu_action(&self, _action: usize) {}
312 fn add_recent_document(&self, _path: &Path) {}
313 fn update_jump_list(
314 &self,
315 _menus: Vec<MenuItem>,
316 _entries: Vec<SmallVec<[PathBuf; 2]>>,
317 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
318 Task::ready(Vec::new())
319 }
320 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
321 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
322 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
323
324 fn thermal_state(&self) -> ThermalState;
325 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
326
327 fn compositor_name(&self) -> &'static str {
328 ""
329 }
330 fn app_path(&self) -> Result<PathBuf>;
331 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
332
333 fn hide_cursor_until_mouse_moves(&self);
336
337 fn is_cursor_visible(&self) -> bool;
339
340 fn should_auto_hide_scrollbars(&self) -> bool;
341
342 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
343 fn write_to_clipboard(&self, item: ClipboardItem);
344
345 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
346 fn read_from_primary(&self) -> Option<ClipboardItem>;
347 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
348 fn write_to_primary(&self, item: ClipboardItem);
349
350 #[cfg(target_os = "macos")]
351 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
352 #[cfg(target_os = "macos")]
353 fn write_to_find_pasteboard(&self, item: ClipboardItem);
354
355 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
356 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
357 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
358
359 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
360 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
361 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
362}
363
364pub trait PlatformDisplay: Debug {
366 fn id(&self) -> DisplayId;
368
369 fn uuid(&self) -> Result<Uuid>;
372
373 fn bounds(&self) -> Bounds<Pixels>;
375
376 fn visible_bounds(&self) -> Bounds<Pixels> {
380 self.bounds()
381 }
382
383 fn default_bounds(&self) -> Bounds<Pixels> {
385 let bounds = self.bounds();
386 let center = bounds.center();
387 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
388
389 let offset = clipped_window_size / 2.0;
390 let origin = point(center.x - offset.width, center.y - offset.height);
391 Bounds::new(origin, clipped_window_size)
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum ThermalState {
398 Nominal,
400 Fair,
402 Serious,
404 Critical,
406}
407
408#[derive(Clone)]
410pub struct SourceMetadata {
411 pub id: u64,
413 pub label: Option<SharedString>,
415 pub is_main: Option<bool>,
417 pub resolution: Size<DevicePixels>,
419}
420
421pub trait ScreenCaptureSource {
423 fn metadata(&self) -> Result<SourceMetadata>;
425
426 fn stream(
429 &self,
430 foreground_executor: &ForegroundExecutor,
431 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
432 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
433}
434
435pub trait ScreenCaptureStream {
437 fn metadata(&self) -> Result<SourceMetadata>;
439}
440
441pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
443
444#[derive(PartialEq, Eq, Hash, Copy, Clone)]
446pub struct DisplayId(pub(crate) u64);
447
448impl DisplayId {
449 pub fn new(id: u64) -> Self {
451 Self(id)
452 }
453}
454
455impl From<u64> for DisplayId {
456 fn from(id: u64) -> Self {
457 Self(id)
458 }
459}
460
461impl From<DisplayId> for u64 {
462 fn from(id: DisplayId) -> Self {
463 id.0
464 }
465}
466
467impl Debug for DisplayId {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 write!(f, "DisplayId({})", self.0)
470 }
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum ResizeEdge {
476 Top,
478 TopRight,
480 Right,
482 BottomRight,
484 Bottom,
486 BottomLeft,
488 Left,
490 TopLeft,
492}
493
494#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
496pub enum WindowDecorations {
497 #[default]
498 Server,
500 Client,
502}
503
504#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
506pub enum Decorations {
507 #[default]
509 Server,
510 Client {
512 tiling: Tiling,
514 },
515}
516
517#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
519pub struct WindowControls {
520 pub fullscreen: bool,
522 pub maximize: bool,
524 pub minimize: bool,
526 pub window_menu: bool,
528}
529
530impl Default for WindowControls {
531 fn default() -> Self {
532 Self {
534 fullscreen: true,
535 maximize: true,
536 minimize: true,
537 window_menu: true,
538 }
539 }
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544pub enum WindowButton {
545 Minimize,
547 Maximize,
549 Close,
551}
552
553impl WindowButton {
554 pub fn id(&self) -> &'static str {
556 match self {
557 WindowButton::Minimize => "minimize",
558 WindowButton::Maximize => "maximize",
559 WindowButton::Close => "close",
560 }
561 }
562
563 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
564 fn index(&self) -> usize {
565 match self {
566 WindowButton::Minimize => 0,
567 WindowButton::Maximize => 1,
568 WindowButton::Close => 2,
569 }
570 }
571}
572
573pub const MAX_BUTTONS_PER_SIDE: usize = 3;
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub struct WindowButtonLayout {
582 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
584 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
586}
587
588#[cfg(any(target_os = "linux", target_os = "freebsd"))]
589impl WindowButtonLayout {
590 pub fn linux_default() -> Self {
592 Self {
593 left: [None; MAX_BUTTONS_PER_SIDE],
594 right: [
595 Some(WindowButton::Minimize),
596 Some(WindowButton::Maximize),
597 Some(WindowButton::Close),
598 ],
599 }
600 }
601
602 pub fn parse(layout_string: &str) -> Result<Self> {
604 fn parse_side(
605 s: &str,
606 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
607 unrecognized: &mut Vec<String>,
608 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
609 let mut result = [None; MAX_BUTTONS_PER_SIDE];
610 let mut i = 0;
611 for name in s.split(',') {
612 let trimmed = name.trim();
613 if trimmed.is_empty() {
614 continue;
615 }
616 let button = match trimmed {
617 "minimize" => Some(WindowButton::Minimize),
618 "maximize" => Some(WindowButton::Maximize),
619 "close" => Some(WindowButton::Close),
620 other => {
621 unrecognized.push(other.to_string());
622 None
623 }
624 };
625 if let Some(button) = button {
626 if seen_buttons[button.index()] {
627 continue;
628 }
629 if let Some(slot) = result.get_mut(i) {
630 *slot = Some(button);
631 seen_buttons[button.index()] = true;
632 i += 1;
633 }
634 }
635 }
636 result
637 }
638
639 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
640 let mut unrecognized = Vec::new();
641 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
642 let layout = Self {
643 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
644 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
645 };
646
647 if !unrecognized.is_empty()
648 && layout.left.iter().all(Option::is_none)
649 && layout.right.iter().all(Option::is_none)
650 {
651 bail!(
652 "button layout string {:?} contains no valid buttons (unrecognized: {})",
653 layout_string,
654 unrecognized.join(", ")
655 );
656 }
657
658 Ok(layout)
659 }
660
661 #[cfg(test)]
663 pub fn format(&self) -> String {
664 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
665 buttons
666 .iter()
667 .flatten()
668 .map(|button| match button {
669 WindowButton::Minimize => "minimize",
670 WindowButton::Maximize => "maximize",
671 WindowButton::Close => "close",
672 })
673 .collect::<Vec<_>>()
674 .join(",")
675 }
676
677 format!("{}:{}", format_side(&self.left), format_side(&self.right))
678 }
679}
680
681#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
683pub struct Tiling {
684 pub top: bool,
686 pub left: bool,
688 pub right: bool,
690 pub bottom: bool,
692}
693
694impl Tiling {
695 pub fn tiled() -> Self {
697 Self {
698 top: true,
699 left: true,
700 right: true,
701 bottom: true,
702 }
703 }
704
705 pub fn is_tiled(&self) -> bool {
707 self.top || self.left || self.right || self.bottom
708 }
709}
710
711pub struct A11yCallbacks {
713 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
715 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
717 pub deactivation: Box<dyn Fn() + Send + 'static>,
719}
720
721#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
722#[expect(missing_docs)]
723pub struct RequestFrameOptions {
724 pub require_presentation: bool,
726 pub force_render: bool,
728}
729
730#[expect(missing_docs)]
731pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
732 fn bounds(&self) -> Bounds<Pixels>;
733 fn is_maximized(&self) -> bool;
734 fn is_minimized(&self) -> bool {
735 false
736 }
737 fn window_bounds(&self) -> WindowBounds;
738 fn content_size(&self) -> Size<Pixels>;
739 fn resize(&mut self, size: Size<Pixels>);
740 fn scale_factor(&self) -> f32;
741 fn appearance(&self) -> WindowAppearance;
742 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
743 fn mouse_position(&self) -> Point<Pixels>;
744 fn set_cursor_style(&self, style: CursorStyle);
745 fn modifiers(&self) -> Modifiers;
746 fn capslock(&self) -> Capslock;
747 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
748 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
749 fn prompt(
750 &self,
751 level: PromptLevel,
752 msg: &str,
753 detail: Option<&str>,
754 answers: &[PromptButton],
755 ) -> Option<oneshot::Receiver<usize>>;
756 fn activate(&self);
757 fn is_active(&self) -> bool;
758 fn is_hovered(&self) -> bool;
759 fn accepts_pointer_input(&self) -> bool {
760 true
761 }
762 fn set_accepts_pointer_input(&mut self, _accepts_pointer_input: bool) -> bool {
763 false
764 }
765 fn background_appearance(&self) -> WindowBackgroundAppearance;
766 fn set_title(&mut self, title: &str);
767 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
768 fn minimize(&self);
769 fn zoom(&self);
770 fn toggle_fullscreen(&self);
771 fn is_fullscreen(&self) -> bool;
772 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
773 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
774 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
775 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
776 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
777 fn on_moved(&self, callback: Box<dyn FnMut()>);
778 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
779 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
780 fn on_close(&self, callback: Box<dyn FnOnce()>);
781 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
782 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
783 fn draw(&self, scene: &Scene);
784 fn completed_frame(&self) {}
785 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
786 fn is_subpixel_rendering_supported(&self) -> bool;
787
788 fn get_title(&self) -> String {
790 String::new()
791 }
792 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
793 None
794 }
795 fn tab_bar_visible(&self) -> bool {
796 false
797 }
798 fn set_edited(&mut self, _edited: bool) {}
799 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
800 #[cfg(target_os = "macos")]
801 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
802 fn show_character_palette(&self) {}
803 fn titlebar_double_click(&self) {}
804 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
805 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
806 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
807 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
808 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
809 fn merge_all_windows(&self) {}
810 fn move_tab_to_new_window(&self) {}
811 fn toggle_window_tab_overview(&self) {}
812 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
813
814 #[cfg(target_os = "windows")]
815 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
816
817 fn inner_window_bounds(&self) -> WindowBounds {
819 self.window_bounds()
820 }
821 fn request_decorations(&self, _decorations: WindowDecorations) {}
822 fn show_window_menu(&self, _position: Point<Pixels>) {}
823 fn start_window_move(&self) {}
824 fn start_window_resize(&self, _edge: ResizeEdge) {}
825 fn window_decorations(&self) -> Decorations {
826 Decorations::Server
827 }
828 fn set_app_id(&mut self, _app_id: &str) {}
829 fn map_window(&mut self) -> anyhow::Result<()> {
830 Ok(())
831 }
832 fn window_controls(&self) -> WindowControls {
833 WindowControls::default()
834 }
835 fn set_client_inset(&self, _inset: Pixels) {}
836 fn gpu_specs(&self) -> Option<GpuSpecs>;
837
838 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
839
840 fn play_system_bell(&self) {}
841
842 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
844
845 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
847
848 fn a11y_update_window_bounds(&self) {}
850
851 #[cfg(any(test, feature = "test-support"))]
852 fn as_test(&mut self) -> Option<&mut TestWindow> {
853 None
854 }
855
856 #[cfg(any(test, feature = "test-support"))]
860 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
861 anyhow::bail!("render_to_image not implemented for this platform")
862 }
863}
864
865#[cfg(any(test, feature = "test-support"))]
867pub trait PlatformHeadlessRenderer {
868 fn render_scene_to_image(
870 &mut self,
871 scene: &Scene,
872 size: Size<DevicePixels>,
873 ) -> Result<RgbaImage>;
874
875 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
877}
878
879#[doc(hidden)]
882pub type RunnableVariant = Runnable<RunnableMeta>;
883
884#[doc(hidden)]
885pub type TimerResolutionGuard = open_gpui_core_util::Deferred<Box<dyn FnOnce() + Send>>;
886
887#[doc(hidden)]
888pub enum TasksIncluded {
889 OnlyCompleted,
890 CompletedAndRunning,
891}
892
893#[doc(hidden)]
896pub trait PlatformDispatcher: Send + Sync {
897 fn is_main_thread(&self) -> bool;
898 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
899 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
900 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
901
902 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
903
904 fn now(&self) -> Instant {
905 Instant::now()
906 }
907
908 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
909 open_gpui_core_util::defer(Box::new(|| {}))
910 }
911
912 #[cfg(any(test, feature = "test-support"))]
913 fn as_test(&self) -> Option<&TestDispatcher> {
914 None
915 }
916}
917
918#[expect(missing_docs)]
919pub trait PlatformTextSystem: Send + Sync {
920 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
921 fn all_font_names(&self) -> Vec<String>;
923 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
925 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
927 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
929 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
931 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
933 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
935 fn rasterize_glyph(
937 &self,
938 params: &RenderGlyphParams,
939 raster_bounds: Bounds<DevicePixels>,
940 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
941 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
943 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
945 -> TextRenderingMode;
946 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
948 0
949 }
950}
951
952#[expect(missing_docs)]
953pub struct NoopTextSystem;
954
955#[expect(missing_docs)]
956impl NoopTextSystem {
957 #[allow(dead_code)]
958 pub fn new() -> Self {
959 Self
960 }
961}
962
963impl PlatformTextSystem for NoopTextSystem {
964 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
965 Ok(())
966 }
967
968 fn all_font_names(&self) -> Vec<String> {
969 Vec::new()
970 }
971
972 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
973 Ok(FontId(1))
974 }
975
976 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
977 FontMetrics {
978 units_per_em: 1000,
979 ascent: 1025.0,
980 descent: -275.0,
981 line_gap: 0.0,
982 underline_position: -95.0,
983 underline_thickness: 60.0,
984 cap_height: 698.0,
985 x_height: 516.0,
986 bounding_box: Bounds {
987 origin: Point {
988 x: -260.0,
989 y: -245.0,
990 },
991 size: Size {
992 width: 1501.0,
993 height: 1364.0,
994 },
995 },
996 }
997 }
998
999 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1000 Ok(Bounds {
1001 origin: Point { x: 54.0, y: 0.0 },
1002 size: size(392.0, 528.0),
1003 })
1004 }
1005
1006 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1007 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1008 }
1009
1010 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1011 Some(GlyphId(ch.len_utf16() as u32))
1012 }
1013
1014 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1015 Ok(Default::default())
1016 }
1017
1018 fn rasterize_glyph(
1019 &self,
1020 _params: &RenderGlyphParams,
1021 raster_bounds: Bounds<DevicePixels>,
1022 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1023 Ok((raster_bounds.size, Vec::new()))
1024 }
1025
1026 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1027 let mut position = px(0.);
1028 let metrics = self.font_metrics(FontId(0));
1029 let em_width = font_size
1030 * self
1031 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1032 .unwrap()
1033 .width
1034 / metrics.units_per_em as f32;
1035 let mut glyphs = Vec::new();
1036 for (ix, c) in text.char_indices() {
1037 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1038 glyphs.push(ShapedGlyph {
1039 id: glyph,
1040 position: point(position, px(0.)),
1041 index: ix,
1042 is_emoji: glyph.0 == 2,
1043 });
1044 if glyph.0 == 2 {
1045 position += em_width * 2.0;
1046 } else {
1047 position += em_width;
1048 }
1049 } else {
1050 position += em_width
1051 }
1052 }
1053 let mut runs = Vec::default();
1054 if !glyphs.is_empty() {
1055 runs.push(ShapedRun {
1056 font_id: FontId(0),
1057 glyphs,
1058 });
1059 } else {
1060 position = px(0.);
1061 }
1062
1063 LineLayout {
1064 font_size,
1065 width: position,
1066 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1067 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1068 runs,
1069 len: text.len(),
1070 }
1071 }
1072
1073 fn recommended_rendering_mode(
1074 &self,
1075 _font_id: FontId,
1076 _font_size: Pixels,
1077 ) -> TextRenderingMode {
1078 TextRenderingMode::Grayscale
1079 }
1080}
1081
1082#[allow(dead_code)]
1087pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1088 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1089 [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], ];
1103
1104 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1105 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1106
1107 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1108 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1109
1110 [
1111 ratios[0] * NORM13,
1112 ratios[1] * NORM24,
1113 ratios[2] * NORM13,
1114 ratios[3] * NORM24,
1115 ]
1116}
1117
1118#[derive(PartialEq, Eq, Hash, Clone)]
1119#[expect(missing_docs)]
1120pub enum AtlasKey {
1121 Glyph(RenderGlyphParams),
1122 Svg(RenderSvgParams),
1123 Image(RenderImageParams),
1124}
1125
1126impl AtlasKey {
1127 #[cfg_attr(
1128 all(
1129 any(target_os = "linux", target_os = "freebsd"),
1130 not(any(feature = "x11", feature = "wayland"))
1131 ),
1132 allow(dead_code)
1133 )]
1134 pub fn texture_kind(&self) -> AtlasTextureKind {
1136 match self {
1137 AtlasKey::Glyph(params) => {
1138 if params.is_emoji {
1139 AtlasTextureKind::Polychrome
1140 } else if params.subpixel_rendering {
1141 AtlasTextureKind::Subpixel
1142 } else {
1143 AtlasTextureKind::Monochrome
1144 }
1145 }
1146 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1147 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1148 }
1149 }
1150}
1151
1152impl From<RenderGlyphParams> for AtlasKey {
1153 fn from(params: RenderGlyphParams) -> Self {
1154 Self::Glyph(params)
1155 }
1156}
1157
1158impl From<RenderSvgParams> for AtlasKey {
1159 fn from(params: RenderSvgParams) -> Self {
1160 Self::Svg(params)
1161 }
1162}
1163
1164impl From<RenderImageParams> for AtlasKey {
1165 fn from(params: RenderImageParams) -> Self {
1166 Self::Image(params)
1167 }
1168}
1169
1170#[expect(missing_docs)]
1171pub trait PlatformAtlas {
1172 fn get_or_insert_with<'a>(
1173 &self,
1174 key: &AtlasKey,
1175 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1176 ) -> Result<Option<AtlasTile>>;
1177 fn remove(&self, key: &AtlasKey);
1178}
1179
1180#[doc(hidden)]
1181pub struct AtlasTextureList<T> {
1182 pub textures: Vec<Option<T>>,
1183 pub free_list: Vec<usize>,
1184}
1185
1186impl<T> Default for AtlasTextureList<T> {
1187 fn default() -> Self {
1188 Self {
1189 textures: Vec::default(),
1190 free_list: Vec::default(),
1191 }
1192 }
1193}
1194
1195impl<T> ops::Index<usize> for AtlasTextureList<T> {
1196 type Output = Option<T>;
1197
1198 fn index(&self, index: usize) -> &Self::Output {
1199 &self.textures[index]
1200 }
1201}
1202
1203impl<T> AtlasTextureList<T> {
1204 #[allow(unused)]
1205 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1206 self.free_list.clear();
1207 self.textures.drain(..)
1208 }
1209
1210 #[allow(dead_code)]
1211 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1212 self.textures.iter_mut().flatten()
1213 }
1214}
1215
1216#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1217#[repr(C)]
1218#[expect(missing_docs)]
1219pub struct AtlasTile {
1220 pub texture_id: AtlasTextureId,
1222 pub tile_id: TileId,
1224 pub padding: u32,
1226 pub bounds: Bounds<DevicePixels>,
1228}
1229
1230#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1231#[repr(C)]
1232#[expect(missing_docs)]
1233pub struct AtlasTextureId {
1234 pub index: u32,
1237 pub kind: AtlasTextureKind,
1239}
1240
1241#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1242#[repr(C)]
1243#[cfg_attr(
1244 all(
1245 any(target_os = "linux", target_os = "freebsd"),
1246 not(any(feature = "x11", feature = "wayland"))
1247 ),
1248 allow(dead_code)
1249)]
1250#[expect(missing_docs)]
1251pub enum AtlasTextureKind {
1252 Monochrome = 0,
1253 Polychrome = 1,
1254 Subpixel = 2,
1255}
1256
1257#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1258#[repr(C)]
1259#[expect(missing_docs)]
1260pub struct TileId(pub u32);
1261
1262impl From<etagere::AllocId> for TileId {
1263 fn from(id: etagere::AllocId) -> Self {
1264 Self(id.serialize())
1265 }
1266}
1267
1268impl From<TileId> for etagere::AllocId {
1269 fn from(id: TileId) -> Self {
1270 Self::deserialize(id.0)
1271 }
1272}
1273
1274#[expect(missing_docs)]
1275pub struct PlatformInputHandler {
1276 cx: AsyncWindowContext,
1277 handler: Box<dyn InputHandler>,
1278}
1279
1280#[expect(missing_docs)]
1281#[cfg_attr(
1282 all(
1283 any(target_os = "linux", target_os = "freebsd"),
1284 not(any(feature = "x11", feature = "wayland"))
1285 ),
1286 allow(dead_code)
1287)]
1288impl PlatformInputHandler {
1289 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1290 Self { cx, handler }
1291 }
1292
1293 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1294 self.cx
1295 .update(|window, cx| {
1296 self.handler
1297 .selected_text_range(ignore_disabled_input, window, cx)
1298 })
1299 .ok()
1300 .flatten()
1301 }
1302
1303 #[cfg_attr(target_os = "windows", allow(dead_code))]
1304 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1305 self.cx
1306 .update(|window, cx| self.handler.marked_text_range(window, cx))
1307 .ok()
1308 .flatten()
1309 }
1310
1311 #[cfg_attr(
1312 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1313 allow(dead_code)
1314 )]
1315 pub fn text_for_range(
1316 &mut self,
1317 range_utf16: Range<usize>,
1318 adjusted: &mut Option<Range<usize>>,
1319 ) -> Option<String> {
1320 self.cx
1321 .update(|window, cx| {
1322 self.handler
1323 .text_for_range(range_utf16, adjusted, window, cx)
1324 })
1325 .ok()
1326 .flatten()
1327 }
1328
1329 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1330 self.cx
1331 .update(|window, cx| {
1332 self.handler
1333 .replace_text_in_range(replacement_range, text, window, cx);
1334 })
1335 .ok();
1336 }
1337
1338 pub fn replace_and_mark_text_in_range(
1339 &mut self,
1340 range_utf16: Option<Range<usize>>,
1341 new_text: &str,
1342 new_selected_range: Option<Range<usize>>,
1343 ) {
1344 self.cx
1345 .update(|window, cx| {
1346 self.handler.replace_and_mark_text_in_range(
1347 range_utf16,
1348 new_text,
1349 new_selected_range,
1350 window,
1351 cx,
1352 )
1353 })
1354 .ok();
1355 }
1356
1357 #[cfg_attr(target_os = "windows", allow(dead_code))]
1358 pub fn unmark_text(&mut self) {
1359 self.cx
1360 .update(|window, cx| self.handler.unmark_text(window, cx))
1361 .ok();
1362 }
1363
1364 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1365 self.cx
1366 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1367 .ok()
1368 .flatten()
1369 }
1370
1371 #[allow(dead_code)]
1372 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1373 self.handler.apple_press_and_hold_enabled()
1374 }
1375
1376 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1377 self.handler.replace_text_in_range(None, input, window, cx);
1378 }
1379
1380 pub fn compute_ime_candidate_bounds(
1381 marked_range: Option<Range<usize>>,
1382 selection: &UTF16Selection,
1383 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1384 ) -> Option<Bounds<Pixels>> {
1385 if let Some(marked_range) = marked_range {
1386 let mut line_start = marked_range.start;
1388
1389 let caret = selection.range.end;
1393 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1394 for i in (marked_range.start..caret).rev() {
1395 if let Some(b) = bounds_for_range(i..i) {
1396 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1397 line_start = i + 1;
1398 break;
1399 }
1400 }
1401 }
1402 }
1403 bounds_for_range(line_start..line_start)
1404 } else {
1405 let offset = if selection.reversed {
1407 selection.range.start
1408 } else {
1409 selection.range.end
1410 };
1411 bounds_for_range(offset..offset)
1412 }
1413 }
1414
1415 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1416 let marked_range = self.handler.marked_text_range(window, cx);
1417 let selection = self.handler.selected_text_range(true, window, cx)?;
1418 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1419 self.handler.bounds_for_range(range, window, cx)
1420 })
1421 }
1422
1423 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1424 let marked_range = self.marked_text_range();
1425 let selection = self.selected_text_range(true)?;
1426 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1427 self.bounds_for_range(range)
1428 })
1429 }
1430
1431 #[allow(unused)]
1432 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1433 self.cx
1434 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1435 .ok()
1436 .flatten()
1437 }
1438
1439 #[allow(dead_code)]
1440 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1441 self.handler.accepts_text_input(window, cx)
1442 }
1443
1444 #[allow(dead_code)]
1445 pub fn query_accepts_text_input(&mut self) -> bool {
1446 self.cx
1447 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1448 .unwrap_or(true)
1449 }
1450
1451 #[allow(dead_code)]
1452 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1453 self.cx
1454 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1455 .unwrap_or(false)
1456 }
1457}
1458
1459#[derive(Debug)]
1462pub struct UTF16Selection {
1463 pub range: Range<usize>,
1466 pub reversed: bool,
1469}
1470
1471pub trait InputHandler: 'static {
1476 fn selected_text_range(
1481 &mut self,
1482 ignore_disabled_input: bool,
1483 window: &mut Window,
1484 cx: &mut App,
1485 ) -> Option<UTF16Selection>;
1486
1487 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1492
1493 fn text_for_range(
1498 &mut self,
1499 range_utf16: Range<usize>,
1500 adjusted_range: &mut Option<Range<usize>>,
1501 window: &mut Window,
1502 cx: &mut App,
1503 ) -> Option<String>;
1504
1505 fn replace_text_in_range(
1510 &mut self,
1511 replacement_range: Option<Range<usize>>,
1512 text: &str,
1513 window: &mut Window,
1514 cx: &mut App,
1515 );
1516
1517 fn replace_and_mark_text_in_range(
1524 &mut self,
1525 range_utf16: Option<Range<usize>>,
1526 new_text: &str,
1527 new_selected_range: Option<Range<usize>>,
1528 window: &mut Window,
1529 cx: &mut App,
1530 );
1531
1532 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1535
1536 fn bounds_for_range(
1541 &mut self,
1542 range_utf16: Range<usize>,
1543 window: &mut Window,
1544 cx: &mut App,
1545 ) -> Option<Bounds<Pixels>>;
1546
1547 fn character_index_for_point(
1551 &mut self,
1552 point: Point<Pixels>,
1553 window: &mut Window,
1554 cx: &mut App,
1555 ) -> Option<usize>;
1556
1557 #[allow(dead_code)]
1562 fn apple_press_and_hold_enabled(&mut self) -> bool {
1563 true
1564 }
1565
1566 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1568 true
1569 }
1570
1571 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1580 false
1581 }
1582}
1583
1584#[derive(Debug)]
1586pub struct WindowOptions {
1587 pub window_bounds: Option<WindowBounds>,
1591
1592 pub titlebar: Option<TitlebarOptions>,
1594
1595 pub focus: bool,
1597
1598 pub show: bool,
1600
1601 pub kind: WindowKind,
1603
1604 pub is_movable: bool,
1606
1607 pub is_resizable: bool,
1609
1610 pub is_minimizable: bool,
1612
1613 pub accepts_pointer_input: bool,
1616
1617 pub display_id: Option<DisplayId>,
1620
1621 pub window_background: WindowBackgroundAppearance,
1623
1624 pub app_id: Option<String>,
1626
1627 pub window_min_size: Option<Size<Pixels>>,
1629
1630 pub window_decorations: Option<WindowDecorations>,
1633
1634 pub icon: Option<Arc<image::RgbaImage>>,
1636
1637 pub tabbing_identifier: Option<String>,
1639}
1640
1641#[derive(Debug)]
1643#[cfg_attr(
1644 all(
1645 any(target_os = "linux", target_os = "freebsd"),
1646 not(any(feature = "x11", feature = "wayland"))
1647 ),
1648 allow(dead_code)
1649)]
1650#[allow(missing_docs)]
1651pub struct WindowParams {
1652 pub bounds: Bounds<Pixels>,
1653
1654 #[cfg_attr(feature = "wayland", allow(dead_code))]
1656 pub titlebar: Option<TitlebarOptions>,
1657
1658 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1660 pub kind: WindowKind,
1661
1662 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1664 pub is_movable: bool,
1665
1666 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1668 pub is_resizable: bool,
1669
1670 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1672 pub is_minimizable: bool,
1673
1674 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1675 pub accepts_pointer_input: bool,
1676
1677 #[cfg_attr(
1678 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1679 allow(dead_code)
1680 )]
1681 pub focus: bool,
1682
1683 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1684 pub show: bool,
1685
1686 #[cfg_attr(feature = "wayland", allow(dead_code))]
1688 pub icon: Option<Arc<image::RgbaImage>>,
1689
1690 #[cfg_attr(feature = "wayland", allow(dead_code))]
1691 pub display_id: Option<DisplayId>,
1692
1693 pub window_min_size: Option<Size<Pixels>>,
1694 #[cfg(target_os = "macos")]
1695 pub tabbing_identifier: Option<String>,
1696}
1697
1698#[derive(Debug, Copy, Clone, PartialEq)]
1700pub enum WindowBounds {
1701 Windowed(Bounds<Pixels>),
1703 Maximized(Bounds<Pixels>),
1706 Fullscreen(Bounds<Pixels>),
1709}
1710
1711impl Default for WindowBounds {
1712 fn default() -> Self {
1713 WindowBounds::Windowed(Bounds::default())
1714 }
1715}
1716
1717impl WindowBounds {
1718 pub fn get_bounds(&self) -> Bounds<Pixels> {
1720 match self {
1721 WindowBounds::Windowed(bounds) => *bounds,
1722 WindowBounds::Maximized(bounds) => *bounds,
1723 WindowBounds::Fullscreen(bounds) => *bounds,
1724 }
1725 }
1726
1727 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1729 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1730 }
1731}
1732
1733impl Default for WindowOptions {
1734 fn default() -> Self {
1735 Self {
1736 window_bounds: None,
1737 titlebar: Some(TitlebarOptions {
1738 title: Default::default(),
1739 appears_transparent: Default::default(),
1740 traffic_light_position: Default::default(),
1741 }),
1742 focus: true,
1743 show: true,
1744 kind: WindowKind::Normal,
1745 is_movable: true,
1746 is_resizable: true,
1747 is_minimizable: true,
1748 accepts_pointer_input: true,
1749 display_id: None,
1750 window_background: WindowBackgroundAppearance::default(),
1751 icon: None,
1752 app_id: None,
1753 window_min_size: None,
1754 window_decorations: None,
1755 tabbing_identifier: None,
1756 }
1757 }
1758}
1759
1760#[derive(Debug, Default)]
1762pub struct TitlebarOptions {
1763 pub title: Option<SharedString>,
1765
1766 pub appears_transparent: bool,
1769
1770 pub traffic_light_position: Option<Point<Pixels>>,
1772}
1773
1774#[derive(Clone, Debug, PartialEq, Eq)]
1776pub enum WindowKind {
1777 Normal,
1779
1780 PopUp,
1783
1784 Floating,
1786
1787 #[cfg(all(target_os = "linux", feature = "wayland"))]
1790 LayerShell(layer_shell::LayerShellOptions),
1791
1792 Dialog,
1795}
1796
1797#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1802pub enum WindowAppearance {
1803 #[default]
1807 Light,
1808
1809 VibrantLight,
1813
1814 Dark,
1818
1819 VibrantDark,
1823}
1824
1825#[derive(Copy, Clone, Debug, Default, PartialEq)]
1828pub enum WindowBackgroundAppearance {
1829 #[default]
1837 Opaque,
1838 Transparent,
1840 Blurred,
1844 MicaBackdrop,
1846 MicaAltBackdrop,
1848}
1849
1850#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1852pub enum TextRenderingMode {
1853 #[default]
1855 PlatformDefault,
1856 Subpixel,
1858 Grayscale,
1860}
1861
1862#[derive(Clone, Debug)]
1864pub struct PathPromptOptions {
1865 pub files: bool,
1867 pub directories: bool,
1869 pub multiple: bool,
1871 pub prompt: Option<SharedString>,
1873}
1874
1875#[derive(Copy, Clone, Debug, PartialEq)]
1877pub enum PromptLevel {
1878 Info,
1880
1881 Warning,
1883
1884 Critical,
1886}
1887
1888#[derive(Clone, Debug, PartialEq)]
1890pub enum PromptButton {
1891 Ok(SharedString),
1893 Cancel(SharedString),
1895 Other(SharedString),
1897}
1898
1899impl PromptButton {
1900 pub fn new(label: impl Into<SharedString>) -> Self {
1902 PromptButton::Other(label.into())
1903 }
1904
1905 pub fn ok(label: impl Into<SharedString>) -> Self {
1907 PromptButton::Ok(label.into())
1908 }
1909
1910 pub fn cancel(label: impl Into<SharedString>) -> Self {
1912 PromptButton::Cancel(label.into())
1913 }
1914
1915 #[allow(dead_code)]
1917 pub fn is_cancel(&self) -> bool {
1918 matches!(self, PromptButton::Cancel(_))
1919 }
1920
1921 pub fn label(&self) -> &SharedString {
1923 match self {
1924 PromptButton::Ok(label) => label,
1925 PromptButton::Cancel(label) => label,
1926 PromptButton::Other(label) => label,
1927 }
1928 }
1929}
1930
1931impl From<&str> for PromptButton {
1932 fn from(value: &str) -> Self {
1933 match value.to_lowercase().as_str() {
1934 "ok" => PromptButton::Ok("Ok".into()),
1935 "cancel" => PromptButton::Cancel("Cancel".into()),
1936 _ => PromptButton::Other(SharedString::from(value.to_owned())),
1937 }
1938 }
1939}
1940
1941#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1943pub enum CursorStyle {
1944 #[default]
1946 Arrow,
1947
1948 IBeam,
1951
1952 Crosshair,
1955
1956 ClosedHand,
1959
1960 OpenHand,
1963
1964 PointingHand,
1967
1968 ResizeLeft,
1971
1972 ResizeRight,
1975
1976 ResizeLeftRight,
1979
1980 ResizeUp,
1983
1984 ResizeDown,
1987
1988 ResizeUpDown,
1991
1992 ResizeUpLeftDownRight,
1995
1996 ResizeUpRightDownLeft,
1999
2000 ResizeColumn,
2003
2004 ResizeRow,
2007
2008 IBeamCursorForVerticalLayout,
2011
2012 OperationNotAllowed,
2015
2016 DragLink,
2019
2020 DragCopy,
2023
2024 ContextualMenu,
2027}
2028
2029#[derive(Clone, Debug, Eq, PartialEq)]
2031pub struct ClipboardItem {
2032 pub entries: Vec<ClipboardEntry>,
2034}
2035
2036#[derive(Clone, Debug, Eq, PartialEq)]
2038pub enum ClipboardEntry {
2039 String(ClipboardString),
2041 Image(Image),
2043 ExternalPaths(crate::ExternalPaths),
2045}
2046
2047impl ClipboardItem {
2048 pub fn new_string(text: String) -> Self {
2050 Self {
2051 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2052 }
2053 }
2054
2055 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2057 Self {
2058 entries: vec![ClipboardEntry::String(ClipboardString {
2059 text,
2060 metadata: Some(metadata),
2061 })],
2062 }
2063 }
2064
2065 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2067 Self {
2068 entries: vec![ClipboardEntry::String(
2069 ClipboardString::new(text).with_json_metadata(metadata),
2070 )],
2071 }
2072 }
2073
2074 pub fn new_image(image: &Image) -> Self {
2076 Self {
2077 entries: vec![ClipboardEntry::Image(image.clone())],
2078 }
2079 }
2080
2081 pub fn text(&self) -> Option<String> {
2084 let mut answer = String::new();
2085
2086 for entry in self.entries.iter() {
2087 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2088 answer.push_str(text);
2089 }
2090 }
2091
2092 if answer.is_empty() {
2093 for entry in self.entries.iter() {
2094 if let ClipboardEntry::ExternalPaths(paths) = entry {
2095 for path in &paths.0 {
2096 use std::fmt::Write as _;
2097 _ = write!(answer, "{}", path.display());
2098 }
2099 }
2100 }
2101 }
2102
2103 if !answer.is_empty() {
2104 Some(answer)
2105 } else {
2106 None
2107 }
2108 }
2109
2110 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2112 pub fn metadata(&self) -> Option<&String> {
2113 match self.entries().first() {
2114 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2115 clipboard_string.metadata.as_ref()
2116 }
2117 _ => None,
2118 }
2119 }
2120
2121 pub fn entries(&self) -> &[ClipboardEntry] {
2123 &self.entries
2124 }
2125
2126 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2128 self.entries.into_iter()
2129 }
2130}
2131
2132impl From<ClipboardString> for ClipboardEntry {
2133 fn from(value: ClipboardString) -> Self {
2134 Self::String(value)
2135 }
2136}
2137
2138impl From<String> for ClipboardEntry {
2139 fn from(value: String) -> Self {
2140 Self::from(ClipboardString::from(value))
2141 }
2142}
2143
2144impl From<Image> for ClipboardEntry {
2145 fn from(value: Image) -> Self {
2146 Self::Image(value)
2147 }
2148}
2149
2150impl From<ClipboardEntry> for ClipboardItem {
2151 fn from(value: ClipboardEntry) -> Self {
2152 Self {
2153 entries: vec![value],
2154 }
2155 }
2156}
2157
2158impl From<String> for ClipboardItem {
2159 fn from(value: String) -> Self {
2160 Self::from(ClipboardEntry::from(value))
2161 }
2162}
2163
2164impl From<Image> for ClipboardItem {
2165 fn from(value: Image) -> Self {
2166 Self::from(ClipboardEntry::from(value))
2167 }
2168}
2169
2170#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2172pub enum ImageFormat {
2173 Png,
2178 Jpeg,
2180 Webp,
2182 Gif,
2184 Svg,
2186 Bmp,
2188 Tiff,
2190 Ico,
2192 Pnm,
2194}
2195
2196impl ImageFormat {
2197 pub const fn mime_type(self) -> &'static str {
2199 match self {
2200 ImageFormat::Png => "image/png",
2201 ImageFormat::Jpeg => "image/jpeg",
2202 ImageFormat::Webp => "image/webp",
2203 ImageFormat::Gif => "image/gif",
2204 ImageFormat::Svg => "image/svg+xml",
2205 ImageFormat::Bmp => "image/bmp",
2206 ImageFormat::Tiff => "image/tiff",
2207 ImageFormat::Ico => "image/ico",
2208 ImageFormat::Pnm => "image/x-portable-anymap",
2209 }
2210 }
2211
2212 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2214 use strum::IntoEnumIterator;
2215 Self::iter()
2216 .find(|format| format.mime_type() == mime_type)
2217 .or_else(|| Self::from_mime_type_alias(mime_type))
2218 }
2219
2220 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2224 match mime_type {
2225 "image/jpg" => Some(Self::Jpeg),
2226 "image/tif" => Some(Self::Tiff),
2227 _ => None,
2228 }
2229 }
2230}
2231
2232#[derive(Clone, Debug, PartialEq, Eq)]
2234pub struct Image {
2235 pub format: ImageFormat,
2237 pub bytes: Vec<u8>,
2239 pub id: u64,
2241}
2242
2243impl Hash for Image {
2244 fn hash<H: Hasher>(&self, state: &mut H) {
2245 state.write_u64(self.id);
2246 }
2247}
2248
2249impl Image {
2250 pub fn empty() -> Self {
2252 Self::from_bytes(ImageFormat::Png, Vec::new())
2253 }
2254
2255 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2257 Self {
2258 id: hash(&bytes),
2259 format,
2260 bytes,
2261 }
2262 }
2263
2264 pub fn id(&self) -> u64 {
2266 self.id
2267 }
2268
2269 pub fn use_render_image(
2271 self: Arc<Self>,
2272 window: &mut Window,
2273 cx: &mut App,
2274 ) -> Option<Arc<RenderImage>> {
2275 ImageSource::Image(self)
2276 .use_data(None, window, cx)
2277 .and_then(|result| result.ok())
2278 }
2279
2280 pub fn get_render_image(
2282 self: Arc<Self>,
2283 window: &mut Window,
2284 cx: &mut App,
2285 ) -> Option<Arc<RenderImage>> {
2286 ImageSource::Image(self)
2287 .get_data(None, window, cx)
2288 .and_then(|result| result.ok())
2289 }
2290
2291 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2293 ImageSource::Image(self).remove_asset(cx);
2294 }
2295
2296 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2298 fn frames_for_image(
2299 bytes: &[u8],
2300 format: image::ImageFormat,
2301 ) -> Result<SmallVec<[Frame; 1]>> {
2302 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2303
2304 for pixel in data.chunks_exact_mut(4) {
2306 pixel.swap(0, 2);
2307 }
2308
2309 Ok(SmallVec::from_elem(Frame::new(data), 1))
2310 }
2311
2312 let frames = match self.format {
2313 ImageFormat::Gif => {
2314 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2315 let mut frames = SmallVec::new();
2316
2317 for frame in decoder.into_frames() {
2318 match frame {
2319 Ok(mut frame) => {
2320 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2322 pixel.swap(0, 2);
2323 }
2324 frames.push(frame);
2325 }
2326 Err(err) => {
2327 log::debug!("Skipping GIF frame due to decode error: {err}");
2328 }
2329 }
2330 }
2331
2332 if frames.is_empty() {
2333 anyhow::bail!("GIF could not be decoded: all frames failed");
2334 }
2335
2336 frames
2337 }
2338 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2339 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2340 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2341 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2342 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2343 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2344 ImageFormat::Svg => {
2345 return svg_renderer
2346 .render_single_frame(&self.bytes, 1.0)
2347 .map_err(Into::into);
2348 }
2349 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2350 };
2351
2352 Ok(Arc::new(RenderImage::new(frames)))
2353 }
2354
2355 pub fn format(&self) -> ImageFormat {
2357 self.format
2358 }
2359
2360 pub fn bytes(&self) -> &[u8] {
2362 self.bytes.as_slice()
2363 }
2364}
2365
2366#[derive(Clone, Debug, Eq, PartialEq)]
2368pub struct ClipboardString {
2369 pub text: String,
2371 pub metadata: Option<String>,
2373}
2374
2375impl ClipboardString {
2376 pub fn new(text: String) -> Self {
2378 Self {
2379 text,
2380 metadata: None,
2381 }
2382 }
2383
2384 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2387 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2388 self
2389 }
2390
2391 pub fn text(&self) -> &String {
2393 &self.text
2394 }
2395
2396 pub fn into_text(self) -> String {
2398 self.text
2399 }
2400
2401 pub fn metadata_json<T>(&self) -> Option<T>
2403 where
2404 T: for<'a> Deserialize<'a>,
2405 {
2406 self.metadata
2407 .as_ref()
2408 .and_then(|m| serde_json::from_str(m).ok())
2409 }
2410
2411 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2412 pub fn text_hash(text: &str) -> u64 {
2414 let mut hasher = SeaHasher::new();
2415 text.hash(&mut hasher);
2416 hasher.finish()
2417 }
2418}
2419
2420impl From<String> for ClipboardString {
2421 fn from(value: String) -> Self {
2422 Self {
2423 text: value,
2424 metadata: None,
2425 }
2426 }
2427}
2428
2429#[cfg(test)]
2430mod image_tests {
2431 use super::*;
2432 use std::sync::Arc;
2433
2434 #[test]
2435 fn test_svg_image_to_image_data_converts_to_bgra() {
2436 let image = Image::from_bytes(
2437 ImageFormat::Svg,
2438 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2439<rect width="1" height="1" fill="#38BDF8"/>
2440</svg>"##
2441 .to_vec(),
2442 );
2443
2444 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2445 let bytes = render_image.as_bytes(0).unwrap();
2446
2447 for pixel in bytes.chunks_exact(4) {
2448 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2449 }
2450 }
2451}
2452
2453#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2454mod tests {
2455 use super::*;
2456 use std::collections::HashSet;
2457
2458 #[test]
2459 fn test_window_button_layout_parse_standard() {
2460 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2461 assert_eq!(
2462 layout.left,
2463 [
2464 Some(WindowButton::Close),
2465 Some(WindowButton::Minimize),
2466 None
2467 ]
2468 );
2469 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2470 }
2471
2472 #[test]
2473 fn test_window_button_layout_parse_right_only() {
2474 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2475 assert_eq!(layout.left, [None, None, None]);
2476 assert_eq!(
2477 layout.right,
2478 [
2479 Some(WindowButton::Minimize),
2480 Some(WindowButton::Maximize),
2481 Some(WindowButton::Close)
2482 ]
2483 );
2484 }
2485
2486 #[test]
2487 fn test_window_button_layout_parse_left_only() {
2488 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2489 assert_eq!(
2490 layout.left,
2491 [
2492 Some(WindowButton::Close),
2493 Some(WindowButton::Minimize),
2494 Some(WindowButton::Maximize)
2495 ]
2496 );
2497 assert_eq!(layout.right, [None, None, None]);
2498 }
2499
2500 #[test]
2501 fn test_window_button_layout_parse_with_whitespace() {
2502 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2503 assert_eq!(
2504 layout.left,
2505 [
2506 Some(WindowButton::Close),
2507 Some(WindowButton::Minimize),
2508 None
2509 ]
2510 );
2511 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2512 }
2513
2514 #[test]
2515 fn test_window_button_layout_parse_empty() {
2516 let layout = WindowButtonLayout::parse("").unwrap();
2517 assert_eq!(layout.left, [None, None, None]);
2518 assert_eq!(layout.right, [None, None, None]);
2519 }
2520
2521 #[test]
2522 fn test_window_button_layout_parse_intentionally_empty() {
2523 let layout = WindowButtonLayout::parse(":").unwrap();
2524 assert_eq!(layout.left, [None, None, None]);
2525 assert_eq!(layout.right, [None, None, None]);
2526 }
2527
2528 #[test]
2529 fn test_window_button_layout_parse_invalid_buttons() {
2530 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2531 assert_eq!(
2532 layout.left,
2533 [
2534 Some(WindowButton::Close),
2535 Some(WindowButton::Minimize),
2536 None
2537 ]
2538 );
2539 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2540 }
2541
2542 #[test]
2543 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2544 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2545 assert_eq!(
2546 layout.right,
2547 [
2548 Some(WindowButton::Close),
2549 Some(WindowButton::Minimize),
2550 None
2551 ]
2552 );
2553 assert_eq!(layout.format(), ":close,minimize");
2554 }
2555
2556 #[test]
2557 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2558 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2559 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2560 assert_eq!(
2561 layout.right,
2562 [
2563 Some(WindowButton::Maximize),
2564 Some(WindowButton::Minimize),
2565 None
2566 ]
2567 );
2568
2569 let button_ids: Vec<_> = layout
2570 .left
2571 .iter()
2572 .chain(layout.right.iter())
2573 .flatten()
2574 .map(WindowButton::id)
2575 .collect();
2576 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2577 assert_eq!(unique_button_ids.len(), button_ids.len());
2578 assert_eq!(layout.format(), "close:maximize,minimize");
2579 }
2580
2581 #[test]
2582 fn test_window_button_layout_parse_gnome_style() {
2583 let layout = WindowButtonLayout::parse("close").unwrap();
2584 assert_eq!(layout.left, [None, None, None]);
2585 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2586 }
2587
2588 #[test]
2589 fn test_window_button_layout_parse_elementary_style() {
2590 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2591 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2592 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2593 }
2594
2595 #[test]
2596 fn test_window_button_layout_round_trip() {
2597 let cases = [
2598 "close:minimize,maximize",
2599 "minimize,maximize,close:",
2600 ":close",
2601 "close:",
2602 "close:maximize",
2603 ":",
2604 ];
2605
2606 for case in cases {
2607 let layout = WindowButtonLayout::parse(case).unwrap();
2608 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2609 }
2610 }
2611
2612 #[test]
2613 fn test_window_button_layout_linux_default() {
2614 let layout = WindowButtonLayout::linux_default();
2615 assert_eq!(layout.left, [None, None, None]);
2616 assert_eq!(
2617 layout.right,
2618 [
2619 Some(WindowButton::Minimize),
2620 Some(WindowButton::Maximize),
2621 Some(WindowButton::Close)
2622 ]
2623 );
2624
2625 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2626 assert_eq!(round_tripped, layout);
2627 }
2628
2629 #[test]
2630 fn test_window_button_layout_parse_all_invalid() {
2631 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2632 }
2633}