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::{
32 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
33 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
34 ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels,
35 PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
36 RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
37 SystemWindowTab, Task, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size,
38};
39use anyhow::Result;
40#[cfg(any(target_os = "linux", target_os = "freebsd"))]
41use anyhow::bail;
42use async_task::Runnable;
43use futures::channel::oneshot;
44#[cfg(any(test, feature = "test-support"))]
45use image::RgbaImage;
46use image::codecs::gif::GifDecoder;
47use image::{AnimationDecoder as _, Frame};
48use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
49use scheduler::Instant;
50pub use scheduler::RunnableMeta;
51use schemars::JsonSchema;
52use seahash::SeaHasher;
53use serde::{Deserialize, Serialize};
54use smallvec::SmallVec;
55use std::borrow::Cow;
56use std::hash::{Hash, Hasher};
57use std::io::Cursor;
58use std::ops;
59use std::time::Duration;
60use std::{
61 fmt::{self, Debug},
62 ops::Range,
63 path::{Path, PathBuf},
64 rc::Rc,
65 sync::Arc,
66};
67use strum::EnumIter;
68use uuid::Uuid;
69
70pub use app_menu::*;
71pub use keyboard::*;
72pub use keystroke::*;
73
74#[cfg(any(test, feature = "test-support"))]
75pub(crate) use test::*;
76
77#[cfg(any(test, feature = "test-support"))]
78pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
79
80#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
81pub use visual_test::VisualTestPlatform;
82
83#[cfg(any(target_os = "linux", target_os = "freebsd"))]
87#[inline]
88pub fn guess_compositor() -> &'static str {
89 if std::env::var_os("ZED_HEADLESS").is_some() {
90 return "Headless";
91 }
92
93 #[cfg(feature = "wayland")]
94 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
95 #[cfg(not(feature = "wayland"))]
96 let wayland_display: Option<std::ffi::OsString> = None;
97
98 #[cfg(feature = "x11")]
99 let x11_display = std::env::var_os("DISPLAY");
100 #[cfg(not(feature = "x11"))]
101 let x11_display: Option<std::ffi::OsString> = None;
102
103 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
104 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
105
106 if use_wayland {
107 "Wayland"
108 } else if use_x11 {
109 "X11"
110 } else {
111 "Headless"
112 }
113}
114
115#[expect(missing_docs)]
116pub trait Platform: 'static {
117 fn background_executor(&self) -> BackgroundExecutor;
118 fn foreground_executor(&self) -> ForegroundExecutor;
119 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
120
121 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
122 fn quit(&self);
123 fn restart(&self, binary_path: Option<PathBuf>);
124 fn activate(&self, ignoring_other_apps: bool);
125 fn hide(&self);
126 fn hide_other_apps(&self);
127 fn unhide_other_apps(&self);
128
129 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
130 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
131 fn active_window(&self) -> Option<AnyWindowHandle>;
132 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
133 None
134 }
135
136 fn is_screen_capture_supported(&self) -> bool {
137 false
138 }
139
140 fn screen_capture_sources(
141 &self,
142 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
143 let (sources_tx, sources_rx) = oneshot::channel();
144 sources_tx
145 .send(Err(anyhow::anyhow!(
146 "gpui was compiled without the screen-capture feature"
147 )))
148 .ok();
149 sources_rx
150 }
151
152 fn open_window(
153 &self,
154 handle: AnyWindowHandle,
155 options: WindowParams,
156 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
157
158 fn window_appearance(&self) -> WindowAppearance;
160
161 fn button_layout(&self) -> Option<WindowButtonLayout> {
163 None
164 }
165
166 fn open_url(&self, url: &str);
167 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
168 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
169
170 fn prompt_for_paths(
171 &self,
172 options: PathPromptOptions,
173 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
174 fn prompt_for_new_path(
175 &self,
176 directory: &Path,
177 suggested_name: Option<&str>,
178 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
179 fn can_select_mixed_files_and_dirs(&self) -> bool;
180 fn reveal_path(&self, path: &Path);
181 fn open_with_system(&self, path: &Path);
182
183 fn on_quit(&self, callback: Box<dyn FnMut()>);
184 fn on_reopen(&self, callback: Box<dyn FnMut()>);
185
186 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
187 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
188 None
189 }
190
191 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
192 fn perform_dock_menu_action(&self, _action: usize) {}
193 fn add_recent_document(&self, _path: &Path) {}
194 fn update_jump_list(
195 &self,
196 _menus: Vec<MenuItem>,
197 _entries: Vec<SmallVec<[PathBuf; 2]>>,
198 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
199 Task::ready(Vec::new())
200 }
201 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
202 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
203 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
204
205 fn thermal_state(&self) -> ThermalState;
206 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
207
208 fn compositor_name(&self) -> &'static str {
209 ""
210 }
211 fn app_path(&self) -> Result<PathBuf>;
212 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
213
214 fn set_cursor_style(&self, style: CursorStyle);
215
216 fn hide_cursor_until_mouse_moves(&self);
219
220 fn is_cursor_visible(&self) -> bool;
222
223 fn should_auto_hide_scrollbars(&self) -> bool;
224
225 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
226 fn write_to_clipboard(&self, item: ClipboardItem);
227
228 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
229 fn read_from_primary(&self) -> Option<ClipboardItem>;
230 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
231 fn write_to_primary(&self, item: ClipboardItem);
232
233 #[cfg(target_os = "macos")]
234 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
235 #[cfg(target_os = "macos")]
236 fn write_to_find_pasteboard(&self, item: ClipboardItem);
237
238 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
239 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
240 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
241
242 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
243 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
244 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
245}
246
247pub trait PlatformDisplay: Debug {
249 fn id(&self) -> DisplayId;
251
252 fn uuid(&self) -> Result<Uuid>;
255
256 fn bounds(&self) -> Bounds<Pixels>;
258
259 fn visible_bounds(&self) -> Bounds<Pixels> {
263 self.bounds()
264 }
265
266 fn default_bounds(&self) -> Bounds<Pixels> {
268 let bounds = self.bounds();
269 let center = bounds.center();
270 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
271
272 let offset = clipped_window_size / 2.0;
273 let origin = point(center.x - offset.width, center.y - offset.height);
274 Bounds::new(origin, clipped_window_size)
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub enum ThermalState {
281 Nominal,
283 Fair,
285 Serious,
287 Critical,
289}
290
291#[derive(Clone)]
293pub struct SourceMetadata {
294 pub id: u64,
296 pub label: Option<SharedString>,
298 pub is_main: Option<bool>,
300 pub resolution: Size<DevicePixels>,
302}
303
304pub trait ScreenCaptureSource {
306 fn metadata(&self) -> Result<SourceMetadata>;
308
309 fn stream(
312 &self,
313 foreground_executor: &ForegroundExecutor,
314 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
315 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
316}
317
318pub trait ScreenCaptureStream {
320 fn metadata(&self) -> Result<SourceMetadata>;
322}
323
324pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
326
327#[derive(PartialEq, Eq, Hash, Copy, Clone)]
329pub struct DisplayId(pub(crate) u64);
330
331impl DisplayId {
332 pub fn new(id: u64) -> Self {
334 Self(id)
335 }
336}
337
338impl From<u64> for DisplayId {
339 fn from(id: u64) -> Self {
340 Self(id)
341 }
342}
343
344impl From<DisplayId> for u64 {
345 fn from(id: DisplayId) -> Self {
346 id.0
347 }
348}
349
350impl Debug for DisplayId {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 write!(f, "DisplayId({})", self.0)
353 }
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum ResizeEdge {
359 Top,
361 TopRight,
363 Right,
365 BottomRight,
367 Bottom,
369 BottomLeft,
371 Left,
373 TopLeft,
375}
376
377#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
379pub enum WindowDecorations {
380 #[default]
381 Server,
383 Client,
385}
386
387#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
389pub enum Decorations {
390 #[default]
392 Server,
393 Client {
395 tiling: Tiling,
397 },
398}
399
400#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
402pub struct WindowControls {
403 pub fullscreen: bool,
405 pub maximize: bool,
407 pub minimize: bool,
409 pub window_menu: bool,
411}
412
413impl Default for WindowControls {
414 fn default() -> Self {
415 Self {
417 fullscreen: true,
418 maximize: true,
419 minimize: true,
420 window_menu: true,
421 }
422 }
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
427pub enum WindowButton {
428 Minimize,
430 Maximize,
432 Close,
434}
435
436impl WindowButton {
437 pub fn id(&self) -> &'static str {
439 match self {
440 WindowButton::Minimize => "minimize",
441 WindowButton::Maximize => "maximize",
442 WindowButton::Close => "close",
443 }
444 }
445
446 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
447 fn index(&self) -> usize {
448 match self {
449 WindowButton::Minimize => 0,
450 WindowButton::Maximize => 1,
451 WindowButton::Close => 2,
452 }
453 }
454}
455
456pub const MAX_BUTTONS_PER_SIDE: usize = 3;
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub struct WindowButtonLayout {
465 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
467 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
469}
470
471#[cfg(any(target_os = "linux", target_os = "freebsd"))]
472impl WindowButtonLayout {
473 pub fn linux_default() -> Self {
475 Self {
476 left: [None; MAX_BUTTONS_PER_SIDE],
477 right: [
478 Some(WindowButton::Minimize),
479 Some(WindowButton::Maximize),
480 Some(WindowButton::Close),
481 ],
482 }
483 }
484
485 pub fn parse(layout_string: &str) -> Result<Self> {
487 fn parse_side(
488 s: &str,
489 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
490 unrecognized: &mut Vec<String>,
491 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
492 let mut result = [None; MAX_BUTTONS_PER_SIDE];
493 let mut i = 0;
494 for name in s.split(',') {
495 let trimmed = name.trim();
496 if trimmed.is_empty() {
497 continue;
498 }
499 let button = match trimmed {
500 "minimize" => Some(WindowButton::Minimize),
501 "maximize" => Some(WindowButton::Maximize),
502 "close" => Some(WindowButton::Close),
503 other => {
504 unrecognized.push(other.to_string());
505 None
506 }
507 };
508 if let Some(button) = button {
509 if seen_buttons[button.index()] {
510 continue;
511 }
512 if let Some(slot) = result.get_mut(i) {
513 *slot = Some(button);
514 seen_buttons[button.index()] = true;
515 i += 1;
516 }
517 }
518 }
519 result
520 }
521
522 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
523 let mut unrecognized = Vec::new();
524 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
525 let layout = Self {
526 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
527 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
528 };
529
530 if !unrecognized.is_empty()
531 && layout.left.iter().all(Option::is_none)
532 && layout.right.iter().all(Option::is_none)
533 {
534 bail!(
535 "button layout string {:?} contains no valid buttons (unrecognized: {})",
536 layout_string,
537 unrecognized.join(", ")
538 );
539 }
540
541 Ok(layout)
542 }
543
544 #[cfg(test)]
546 pub fn format(&self) -> String {
547 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
548 buttons
549 .iter()
550 .flatten()
551 .map(|button| match button {
552 WindowButton::Minimize => "minimize",
553 WindowButton::Maximize => "maximize",
554 WindowButton::Close => "close",
555 })
556 .collect::<Vec<_>>()
557 .join(",")
558 }
559
560 format!("{}:{}", format_side(&self.left), format_side(&self.right))
561 }
562}
563
564#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
566pub struct Tiling {
567 pub top: bool,
569 pub left: bool,
571 pub right: bool,
573 pub bottom: bool,
575}
576
577impl Tiling {
578 pub fn tiled() -> Self {
580 Self {
581 top: true,
582 left: true,
583 right: true,
584 bottom: true,
585 }
586 }
587
588 pub fn is_tiled(&self) -> bool {
590 self.top || self.left || self.right || self.bottom
591 }
592}
593
594#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
595#[expect(missing_docs)]
596pub struct RequestFrameOptions {
597 pub require_presentation: bool,
599 pub force_render: bool,
601}
602
603#[expect(missing_docs)]
604pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
605 fn bounds(&self) -> Bounds<Pixels>;
606 fn is_maximized(&self) -> bool;
607 fn window_bounds(&self) -> WindowBounds;
608 fn content_size(&self) -> Size<Pixels>;
609 fn resize(&mut self, size: Size<Pixels>);
610 fn scale_factor(&self) -> f32;
611 fn appearance(&self) -> WindowAppearance;
612 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
613 fn mouse_position(&self) -> Point<Pixels>;
614 fn modifiers(&self) -> Modifiers;
615 fn capslock(&self) -> Capslock;
616 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
617 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
618 fn prompt(
619 &self,
620 level: PromptLevel,
621 msg: &str,
622 detail: Option<&str>,
623 answers: &[PromptButton],
624 ) -> Option<oneshot::Receiver<usize>>;
625 fn activate(&self);
626 fn is_active(&self) -> bool;
627 fn is_hovered(&self) -> bool;
628 fn background_appearance(&self) -> WindowBackgroundAppearance;
629 fn set_title(&mut self, title: &str);
630 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
631 fn minimize(&self);
632 fn zoom(&self);
633 fn toggle_fullscreen(&self);
634 fn is_fullscreen(&self) -> bool;
635 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
636 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
637 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
638 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
639 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
640 fn on_moved(&self, callback: Box<dyn FnMut()>);
641 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
642 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
643 fn on_close(&self, callback: Box<dyn FnOnce()>);
644 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
645 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
646 fn draw(&self, scene: &Scene);
647 fn completed_frame(&self) {}
648 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
649 fn is_subpixel_rendering_supported(&self) -> bool;
650
651 fn get_title(&self) -> String {
653 String::new()
654 }
655 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
656 None
657 }
658 fn tab_bar_visible(&self) -> bool {
659 false
660 }
661 fn set_edited(&mut self, _edited: bool) {}
662 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
663 fn show_character_palette(&self) {}
664 fn titlebar_double_click(&self) {}
665 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
666 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
667 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
668 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
669 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
670 fn merge_all_windows(&self) {}
671 fn move_tab_to_new_window(&self) {}
672 fn toggle_window_tab_overview(&self) {}
673 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
674
675 #[cfg(target_os = "windows")]
676 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
677
678 fn inner_window_bounds(&self) -> WindowBounds {
680 self.window_bounds()
681 }
682 fn request_decorations(&self, _decorations: WindowDecorations) {}
683 fn show_window_menu(&self, _position: Point<Pixels>) {}
684 fn start_window_move(&self) {}
685 fn start_window_resize(&self, _edge: ResizeEdge) {}
686 fn window_decorations(&self) -> Decorations {
687 Decorations::Server
688 }
689 fn set_app_id(&mut self, _app_id: &str) {}
690 fn map_window(&mut self) -> anyhow::Result<()> {
691 Ok(())
692 }
693 fn window_controls(&self) -> WindowControls {
694 WindowControls::default()
695 }
696 fn set_client_inset(&self, _inset: Pixels) {}
697 fn gpu_specs(&self) -> Option<GpuSpecs>;
698
699 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
700
701 fn play_system_bell(&self) {}
702
703 #[cfg(any(test, feature = "test-support"))]
704 fn as_test(&mut self) -> Option<&mut TestWindow> {
705 None
706 }
707
708 #[cfg(any(test, feature = "test-support"))]
712 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
713 anyhow::bail!("render_to_image not implemented for this platform")
714 }
715}
716
717#[cfg(any(test, feature = "test-support"))]
719pub trait PlatformHeadlessRenderer {
720 fn render_scene_to_image(
722 &mut self,
723 scene: &Scene,
724 size: Size<DevicePixels>,
725 ) -> Result<RgbaImage>;
726
727 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
729}
730
731#[doc(hidden)]
734pub type RunnableVariant = Runnable<RunnableMeta>;
735
736#[doc(hidden)]
737pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
738
739#[doc(hidden)]
742pub trait PlatformDispatcher: Send + Sync {
743 fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
744 fn get_current_thread_timings(&self) -> ThreadTaskTimings;
745 fn is_main_thread(&self) -> bool;
746 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
747 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
748 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
749
750 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
751
752 fn now(&self) -> Instant {
753 Instant::now()
754 }
755
756 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
757 gpui_util::defer(Box::new(|| {}))
758 }
759
760 #[cfg(any(test, feature = "test-support"))]
761 fn as_test(&self) -> Option<&TestDispatcher> {
762 None
763 }
764}
765
766#[expect(missing_docs)]
767pub trait PlatformTextSystem: Send + Sync {
768 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
769 fn all_font_names(&self) -> Vec<String>;
771 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
773 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
775 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
777 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
779 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
781 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
783 fn rasterize_glyph(
785 &self,
786 params: &RenderGlyphParams,
787 raster_bounds: Bounds<DevicePixels>,
788 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
789 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
791 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
793 -> TextRenderingMode;
794 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
796 0
797 }
798}
799
800#[expect(missing_docs)]
801pub struct NoopTextSystem;
802
803#[expect(missing_docs)]
804impl NoopTextSystem {
805 #[allow(dead_code)]
806 pub fn new() -> Self {
807 Self
808 }
809}
810
811impl PlatformTextSystem for NoopTextSystem {
812 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
813 Ok(())
814 }
815
816 fn all_font_names(&self) -> Vec<String> {
817 Vec::new()
818 }
819
820 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
821 Ok(FontId(1))
822 }
823
824 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
825 FontMetrics {
826 units_per_em: 1000,
827 ascent: 1025.0,
828 descent: -275.0,
829 line_gap: 0.0,
830 underline_position: -95.0,
831 underline_thickness: 60.0,
832 cap_height: 698.0,
833 x_height: 516.0,
834 bounding_box: Bounds {
835 origin: Point {
836 x: -260.0,
837 y: -245.0,
838 },
839 size: Size {
840 width: 1501.0,
841 height: 1364.0,
842 },
843 },
844 }
845 }
846
847 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
848 Ok(Bounds {
849 origin: Point { x: 54.0, y: 0.0 },
850 size: size(392.0, 528.0),
851 })
852 }
853
854 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
855 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
856 }
857
858 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
859 Some(GlyphId(ch.len_utf16() as u32))
860 }
861
862 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
863 Ok(Default::default())
864 }
865
866 fn rasterize_glyph(
867 &self,
868 _params: &RenderGlyphParams,
869 raster_bounds: Bounds<DevicePixels>,
870 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
871 Ok((raster_bounds.size, Vec::new()))
872 }
873
874 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
875 let mut position = px(0.);
876 let metrics = self.font_metrics(FontId(0));
877 let em_width = font_size
878 * self
879 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
880 .unwrap()
881 .width
882 / metrics.units_per_em as f32;
883 let mut glyphs = Vec::new();
884 for (ix, c) in text.char_indices() {
885 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
886 glyphs.push(ShapedGlyph {
887 id: glyph,
888 position: point(position, px(0.)),
889 index: ix,
890 is_emoji: glyph.0 == 2,
891 });
892 if glyph.0 == 2 {
893 position += em_width * 2.0;
894 } else {
895 position += em_width;
896 }
897 } else {
898 position += em_width
899 }
900 }
901 let mut runs = Vec::default();
902 if !glyphs.is_empty() {
903 runs.push(ShapedRun {
904 font_id: FontId(0),
905 glyphs,
906 });
907 } else {
908 position = px(0.);
909 }
910
911 LineLayout {
912 font_size,
913 width: position,
914 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
915 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
916 runs,
917 len: text.len(),
918 }
919 }
920
921 fn recommended_rendering_mode(
922 &self,
923 _font_id: FontId,
924 _font_size: Pixels,
925 ) -> TextRenderingMode {
926 TextRenderingMode::Grayscale
927 }
928}
929
930#[allow(dead_code)]
935pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
936 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
937 [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], ];
951
952 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
953 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
954
955 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
956 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
957
958 [
959 ratios[0] * NORM13,
960 ratios[1] * NORM24,
961 ratios[2] * NORM13,
962 ratios[3] * NORM24,
963 ]
964}
965
966#[derive(PartialEq, Eq, Hash, Clone)]
967#[expect(missing_docs)]
968pub enum AtlasKey {
969 Glyph(RenderGlyphParams),
970 Svg(RenderSvgParams),
971 Image(RenderImageParams),
972}
973
974impl AtlasKey {
975 #[cfg_attr(
976 all(
977 any(target_os = "linux", target_os = "freebsd"),
978 not(any(feature = "x11", feature = "wayland"))
979 ),
980 allow(dead_code)
981 )]
982 pub fn texture_kind(&self) -> AtlasTextureKind {
984 match self {
985 AtlasKey::Glyph(params) => {
986 if params.is_emoji {
987 AtlasTextureKind::Polychrome
988 } else if params.subpixel_rendering {
989 AtlasTextureKind::Subpixel
990 } else {
991 AtlasTextureKind::Monochrome
992 }
993 }
994 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
995 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
996 }
997 }
998}
999
1000impl From<RenderGlyphParams> for AtlasKey {
1001 fn from(params: RenderGlyphParams) -> Self {
1002 Self::Glyph(params)
1003 }
1004}
1005
1006impl From<RenderSvgParams> for AtlasKey {
1007 fn from(params: RenderSvgParams) -> Self {
1008 Self::Svg(params)
1009 }
1010}
1011
1012impl From<RenderImageParams> for AtlasKey {
1013 fn from(params: RenderImageParams) -> Self {
1014 Self::Image(params)
1015 }
1016}
1017
1018#[expect(missing_docs)]
1019pub trait PlatformAtlas {
1020 fn get_or_insert_with<'a>(
1021 &self,
1022 key: &AtlasKey,
1023 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1024 ) -> Result<Option<AtlasTile>>;
1025 fn remove(&self, key: &AtlasKey);
1026}
1027
1028#[doc(hidden)]
1029pub struct AtlasTextureList<T> {
1030 pub textures: Vec<Option<T>>,
1031 pub free_list: Vec<usize>,
1032}
1033
1034impl<T> Default for AtlasTextureList<T> {
1035 fn default() -> Self {
1036 Self {
1037 textures: Vec::default(),
1038 free_list: Vec::default(),
1039 }
1040 }
1041}
1042
1043impl<T> ops::Index<usize> for AtlasTextureList<T> {
1044 type Output = Option<T>;
1045
1046 fn index(&self, index: usize) -> &Self::Output {
1047 &self.textures[index]
1048 }
1049}
1050
1051impl<T> AtlasTextureList<T> {
1052 #[allow(unused)]
1053 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1054 self.free_list.clear();
1055 self.textures.drain(..)
1056 }
1057
1058 #[allow(dead_code)]
1059 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1060 self.textures.iter_mut().flatten()
1061 }
1062}
1063
1064#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1065#[repr(C)]
1066#[expect(missing_docs)]
1067pub struct AtlasTile {
1068 pub texture_id: AtlasTextureId,
1070 pub tile_id: TileId,
1072 pub padding: u32,
1074 pub bounds: Bounds<DevicePixels>,
1076}
1077
1078#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1079#[repr(C)]
1080#[expect(missing_docs)]
1081pub struct AtlasTextureId {
1082 pub index: u32,
1085 pub kind: AtlasTextureKind,
1087}
1088
1089#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1090#[repr(C)]
1091#[cfg_attr(
1092 all(
1093 any(target_os = "linux", target_os = "freebsd"),
1094 not(any(feature = "x11", feature = "wayland"))
1095 ),
1096 allow(dead_code)
1097)]
1098#[expect(missing_docs)]
1099pub enum AtlasTextureKind {
1100 Monochrome = 0,
1101 Polychrome = 1,
1102 Subpixel = 2,
1103}
1104
1105#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1106#[repr(C)]
1107#[expect(missing_docs)]
1108pub struct TileId(pub u32);
1109
1110impl From<etagere::AllocId> for TileId {
1111 fn from(id: etagere::AllocId) -> Self {
1112 Self(id.serialize())
1113 }
1114}
1115
1116impl From<TileId> for etagere::AllocId {
1117 fn from(id: TileId) -> Self {
1118 Self::deserialize(id.0)
1119 }
1120}
1121
1122#[expect(missing_docs)]
1123pub struct PlatformInputHandler {
1124 cx: AsyncWindowContext,
1125 handler: Box<dyn InputHandler>,
1126}
1127
1128#[expect(missing_docs)]
1129#[cfg_attr(
1130 all(
1131 any(target_os = "linux", target_os = "freebsd"),
1132 not(any(feature = "x11", feature = "wayland"))
1133 ),
1134 allow(dead_code)
1135)]
1136impl PlatformInputHandler {
1137 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1138 Self { cx, handler }
1139 }
1140
1141 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1142 self.cx
1143 .update(|window, cx| {
1144 self.handler
1145 .selected_text_range(ignore_disabled_input, window, cx)
1146 })
1147 .ok()
1148 .flatten()
1149 }
1150
1151 #[cfg_attr(target_os = "windows", allow(dead_code))]
1152 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1153 self.cx
1154 .update(|window, cx| self.handler.marked_text_range(window, cx))
1155 .ok()
1156 .flatten()
1157 }
1158
1159 #[cfg_attr(
1160 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1161 allow(dead_code)
1162 )]
1163 pub fn text_for_range(
1164 &mut self,
1165 range_utf16: Range<usize>,
1166 adjusted: &mut Option<Range<usize>>,
1167 ) -> Option<String> {
1168 self.cx
1169 .update(|window, cx| {
1170 self.handler
1171 .text_for_range(range_utf16, adjusted, window, cx)
1172 })
1173 .ok()
1174 .flatten()
1175 }
1176
1177 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1178 self.cx
1179 .update(|window, cx| {
1180 self.handler
1181 .replace_text_in_range(replacement_range, text, window, cx);
1182 })
1183 .ok();
1184 }
1185
1186 pub fn replace_and_mark_text_in_range(
1187 &mut self,
1188 range_utf16: Option<Range<usize>>,
1189 new_text: &str,
1190 new_selected_range: Option<Range<usize>>,
1191 ) {
1192 self.cx
1193 .update(|window, cx| {
1194 self.handler.replace_and_mark_text_in_range(
1195 range_utf16,
1196 new_text,
1197 new_selected_range,
1198 window,
1199 cx,
1200 )
1201 })
1202 .ok();
1203 }
1204
1205 #[cfg_attr(target_os = "windows", allow(dead_code))]
1206 pub fn unmark_text(&mut self) {
1207 self.cx
1208 .update(|window, cx| self.handler.unmark_text(window, cx))
1209 .ok();
1210 }
1211
1212 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1213 self.cx
1214 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1215 .ok()
1216 .flatten()
1217 }
1218
1219 #[allow(dead_code)]
1220 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1221 self.handler.apple_press_and_hold_enabled()
1222 }
1223
1224 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1225 self.handler.replace_text_in_range(None, input, window, cx);
1226 }
1227
1228 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1229 let selection = self.handler.selected_text_range(true, window, cx)?;
1230 self.handler.bounds_for_range(
1231 if selection.reversed {
1232 selection.range.start..selection.range.start
1233 } else {
1234 selection.range.end..selection.range.end
1235 },
1236 window,
1237 cx,
1238 )
1239 }
1240
1241 #[allow(unused)]
1242 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1243 self.cx
1244 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1245 .ok()
1246 .flatten()
1247 }
1248
1249 #[allow(dead_code)]
1250 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1251 self.handler.accepts_text_input(window, cx)
1252 }
1253
1254 #[allow(dead_code)]
1255 pub fn query_accepts_text_input(&mut self) -> bool {
1256 self.cx
1257 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1258 .unwrap_or(true)
1259 }
1260
1261 #[allow(dead_code)]
1262 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1263 self.cx
1264 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1265 .unwrap_or(false)
1266 }
1267}
1268
1269#[derive(Debug)]
1272pub struct UTF16Selection {
1273 pub range: Range<usize>,
1276 pub reversed: bool,
1279}
1280
1281pub trait InputHandler: 'static {
1286 fn selected_text_range(
1291 &mut self,
1292 ignore_disabled_input: bool,
1293 window: &mut Window,
1294 cx: &mut App,
1295 ) -> Option<UTF16Selection>;
1296
1297 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1302
1303 fn text_for_range(
1308 &mut self,
1309 range_utf16: Range<usize>,
1310 adjusted_range: &mut Option<Range<usize>>,
1311 window: &mut Window,
1312 cx: &mut App,
1313 ) -> Option<String>;
1314
1315 fn replace_text_in_range(
1320 &mut self,
1321 replacement_range: Option<Range<usize>>,
1322 text: &str,
1323 window: &mut Window,
1324 cx: &mut App,
1325 );
1326
1327 fn replace_and_mark_text_in_range(
1334 &mut self,
1335 range_utf16: Option<Range<usize>>,
1336 new_text: &str,
1337 new_selected_range: Option<Range<usize>>,
1338 window: &mut Window,
1339 cx: &mut App,
1340 );
1341
1342 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1345
1346 fn bounds_for_range(
1351 &mut self,
1352 range_utf16: Range<usize>,
1353 window: &mut Window,
1354 cx: &mut App,
1355 ) -> Option<Bounds<Pixels>>;
1356
1357 fn character_index_for_point(
1361 &mut self,
1362 point: Point<Pixels>,
1363 window: &mut Window,
1364 cx: &mut App,
1365 ) -> Option<usize>;
1366
1367 #[allow(dead_code)]
1372 fn apple_press_and_hold_enabled(&mut self) -> bool {
1373 true
1374 }
1375
1376 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1378 true
1379 }
1380
1381 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1390 false
1391 }
1392}
1393
1394#[derive(Debug)]
1396pub struct WindowOptions {
1397 pub window_bounds: Option<WindowBounds>,
1401
1402 pub titlebar: Option<TitlebarOptions>,
1404
1405 pub focus: bool,
1407
1408 pub show: bool,
1410
1411 pub kind: WindowKind,
1413
1414 pub is_movable: bool,
1416
1417 pub is_resizable: bool,
1419
1420 pub is_minimizable: bool,
1422
1423 pub display_id: Option<DisplayId>,
1426
1427 pub window_background: WindowBackgroundAppearance,
1429
1430 pub app_id: Option<String>,
1432
1433 pub window_min_size: Option<Size<Pixels>>,
1435
1436 pub window_decorations: Option<WindowDecorations>,
1439
1440 pub icon: Option<Arc<image::RgbaImage>>,
1442
1443 pub tabbing_identifier: Option<String>,
1445}
1446
1447#[derive(Debug)]
1449#[cfg_attr(
1450 all(
1451 any(target_os = "linux", target_os = "freebsd"),
1452 not(any(feature = "x11", feature = "wayland"))
1453 ),
1454 allow(dead_code)
1455)]
1456#[allow(missing_docs)]
1457pub struct WindowParams {
1458 pub bounds: Bounds<Pixels>,
1459
1460 #[cfg_attr(feature = "wayland", allow(dead_code))]
1462 pub titlebar: Option<TitlebarOptions>,
1463
1464 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1466 pub kind: WindowKind,
1467
1468 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1470 pub is_movable: bool,
1471
1472 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1474 pub is_resizable: bool,
1475
1476 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1478 pub is_minimizable: bool,
1479
1480 #[cfg_attr(
1481 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1482 allow(dead_code)
1483 )]
1484 pub focus: bool,
1485
1486 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1487 pub show: bool,
1488
1489 #[cfg_attr(feature = "wayland", allow(dead_code))]
1491 pub icon: Option<Arc<image::RgbaImage>>,
1492
1493 #[cfg_attr(feature = "wayland", allow(dead_code))]
1494 pub display_id: Option<DisplayId>,
1495
1496 pub window_min_size: Option<Size<Pixels>>,
1497 #[cfg(target_os = "macos")]
1498 pub tabbing_identifier: Option<String>,
1499}
1500
1501#[derive(Debug, Copy, Clone, PartialEq)]
1503pub enum WindowBounds {
1504 Windowed(Bounds<Pixels>),
1506 Maximized(Bounds<Pixels>),
1509 Fullscreen(Bounds<Pixels>),
1512}
1513
1514impl Default for WindowBounds {
1515 fn default() -> Self {
1516 WindowBounds::Windowed(Bounds::default())
1517 }
1518}
1519
1520impl WindowBounds {
1521 pub fn get_bounds(&self) -> Bounds<Pixels> {
1523 match self {
1524 WindowBounds::Windowed(bounds) => *bounds,
1525 WindowBounds::Maximized(bounds) => *bounds,
1526 WindowBounds::Fullscreen(bounds) => *bounds,
1527 }
1528 }
1529
1530 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1532 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1533 }
1534}
1535
1536impl Default for WindowOptions {
1537 fn default() -> Self {
1538 Self {
1539 window_bounds: None,
1540 titlebar: Some(TitlebarOptions {
1541 title: Default::default(),
1542 appears_transparent: Default::default(),
1543 traffic_light_position: Default::default(),
1544 }),
1545 focus: true,
1546 show: true,
1547 kind: WindowKind::Normal,
1548 is_movable: true,
1549 is_resizable: true,
1550 is_minimizable: true,
1551 display_id: None,
1552 window_background: WindowBackgroundAppearance::default(),
1553 icon: None,
1554 app_id: None,
1555 window_min_size: None,
1556 window_decorations: None,
1557 tabbing_identifier: None,
1558 }
1559 }
1560}
1561
1562#[derive(Debug, Default)]
1564pub struct TitlebarOptions {
1565 pub title: Option<SharedString>,
1567
1568 pub appears_transparent: bool,
1571
1572 pub traffic_light_position: Option<Point<Pixels>>,
1574}
1575
1576#[derive(Clone, Debug, PartialEq, Eq)]
1578pub enum WindowKind {
1579 Normal,
1581
1582 PopUp,
1585
1586 Floating,
1588
1589 #[cfg(all(target_os = "linux", feature = "wayland"))]
1592 LayerShell(layer_shell::LayerShellOptions),
1593
1594 Dialog,
1597}
1598
1599#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1604pub enum WindowAppearance {
1605 #[default]
1609 Light,
1610
1611 VibrantLight,
1615
1616 Dark,
1620
1621 VibrantDark,
1625}
1626
1627#[derive(Copy, Clone, Debug, Default, PartialEq)]
1630pub enum WindowBackgroundAppearance {
1631 #[default]
1639 Opaque,
1640 Transparent,
1642 Blurred,
1646 MicaBackdrop,
1648 MicaAltBackdrop,
1650}
1651
1652#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1654pub enum TextRenderingMode {
1655 #[default]
1657 PlatformDefault,
1658 Subpixel,
1660 Grayscale,
1662}
1663
1664#[derive(Clone, Debug)]
1666pub struct PathPromptOptions {
1667 pub files: bool,
1669 pub directories: bool,
1671 pub multiple: bool,
1673 pub prompt: Option<SharedString>,
1675}
1676
1677#[derive(Copy, Clone, Debug, PartialEq)]
1679pub enum PromptLevel {
1680 Info,
1682
1683 Warning,
1685
1686 Critical,
1688}
1689
1690#[derive(Clone, Debug, PartialEq)]
1692pub enum PromptButton {
1693 Ok(SharedString),
1695 Cancel(SharedString),
1697 Other(SharedString),
1699}
1700
1701impl PromptButton {
1702 pub fn new(label: impl Into<SharedString>) -> Self {
1704 PromptButton::Other(label.into())
1705 }
1706
1707 pub fn ok(label: impl Into<SharedString>) -> Self {
1709 PromptButton::Ok(label.into())
1710 }
1711
1712 pub fn cancel(label: impl Into<SharedString>) -> Self {
1714 PromptButton::Cancel(label.into())
1715 }
1716
1717 #[allow(dead_code)]
1719 pub fn is_cancel(&self) -> bool {
1720 matches!(self, PromptButton::Cancel(_))
1721 }
1722
1723 pub fn label(&self) -> &SharedString {
1725 match self {
1726 PromptButton::Ok(label) => label,
1727 PromptButton::Cancel(label) => label,
1728 PromptButton::Other(label) => label,
1729 }
1730 }
1731}
1732
1733impl From<&str> for PromptButton {
1734 fn from(value: &str) -> Self {
1735 match value.to_lowercase().as_str() {
1736 "ok" => PromptButton::Ok("Ok".into()),
1737 "cancel" => PromptButton::Cancel("Cancel".into()),
1738 _ => PromptButton::Other(SharedString::from(value.to_owned())),
1739 }
1740 }
1741}
1742
1743#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1745pub enum CursorStyle {
1746 #[default]
1748 Arrow,
1749
1750 IBeam,
1753
1754 Crosshair,
1757
1758 ClosedHand,
1761
1762 OpenHand,
1765
1766 PointingHand,
1769
1770 ResizeLeft,
1773
1774 ResizeRight,
1777
1778 ResizeLeftRight,
1781
1782 ResizeUp,
1785
1786 ResizeDown,
1789
1790 ResizeUpDown,
1793
1794 ResizeUpLeftDownRight,
1797
1798 ResizeUpRightDownLeft,
1801
1802 ResizeColumn,
1805
1806 ResizeRow,
1809
1810 IBeamCursorForVerticalLayout,
1813
1814 OperationNotAllowed,
1817
1818 DragLink,
1821
1822 DragCopy,
1825
1826 ContextualMenu,
1829}
1830
1831#[derive(Clone, Debug, Eq, PartialEq)]
1833pub struct ClipboardItem {
1834 pub entries: Vec<ClipboardEntry>,
1836}
1837
1838#[derive(Clone, Debug, Eq, PartialEq)]
1840pub enum ClipboardEntry {
1841 String(ClipboardString),
1843 Image(Image),
1845 ExternalPaths(crate::ExternalPaths),
1847}
1848
1849impl ClipboardItem {
1850 pub fn new_string(text: String) -> Self {
1852 Self {
1853 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
1854 }
1855 }
1856
1857 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
1859 Self {
1860 entries: vec![ClipboardEntry::String(ClipboardString {
1861 text,
1862 metadata: Some(metadata),
1863 })],
1864 }
1865 }
1866
1867 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
1869 Self {
1870 entries: vec![ClipboardEntry::String(
1871 ClipboardString::new(text).with_json_metadata(metadata),
1872 )],
1873 }
1874 }
1875
1876 pub fn new_image(image: &Image) -> Self {
1878 Self {
1879 entries: vec![ClipboardEntry::Image(image.clone())],
1880 }
1881 }
1882
1883 pub fn text(&self) -> Option<String> {
1886 let mut answer = String::new();
1887
1888 for entry in self.entries.iter() {
1889 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
1890 answer.push_str(text);
1891 }
1892 }
1893
1894 if answer.is_empty() {
1895 for entry in self.entries.iter() {
1896 if let ClipboardEntry::ExternalPaths(paths) = entry {
1897 for path in &paths.0 {
1898 use std::fmt::Write as _;
1899 _ = write!(answer, "{}", path.display());
1900 }
1901 }
1902 }
1903 }
1904
1905 if !answer.is_empty() {
1906 Some(answer)
1907 } else {
1908 None
1909 }
1910 }
1911
1912 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
1914 pub fn metadata(&self) -> Option<&String> {
1915 match self.entries().first() {
1916 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
1917 clipboard_string.metadata.as_ref()
1918 }
1919 _ => None,
1920 }
1921 }
1922
1923 pub fn entries(&self) -> &[ClipboardEntry] {
1925 &self.entries
1926 }
1927
1928 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
1930 self.entries.into_iter()
1931 }
1932}
1933
1934impl From<ClipboardString> for ClipboardEntry {
1935 fn from(value: ClipboardString) -> Self {
1936 Self::String(value)
1937 }
1938}
1939
1940impl From<String> for ClipboardEntry {
1941 fn from(value: String) -> Self {
1942 Self::from(ClipboardString::from(value))
1943 }
1944}
1945
1946impl From<Image> for ClipboardEntry {
1947 fn from(value: Image) -> Self {
1948 Self::Image(value)
1949 }
1950}
1951
1952impl From<ClipboardEntry> for ClipboardItem {
1953 fn from(value: ClipboardEntry) -> Self {
1954 Self {
1955 entries: vec![value],
1956 }
1957 }
1958}
1959
1960impl From<String> for ClipboardItem {
1961 fn from(value: String) -> Self {
1962 Self::from(ClipboardEntry::from(value))
1963 }
1964}
1965
1966impl From<Image> for ClipboardItem {
1967 fn from(value: Image) -> Self {
1968 Self::from(ClipboardEntry::from(value))
1969 }
1970}
1971
1972#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
1974pub enum ImageFormat {
1975 Png,
1980 Jpeg,
1982 Webp,
1984 Gif,
1986 Svg,
1988 Bmp,
1990 Tiff,
1992 Ico,
1994 Pnm,
1996}
1997
1998impl ImageFormat {
1999 pub const fn mime_type(self) -> &'static str {
2001 match self {
2002 ImageFormat::Png => "image/png",
2003 ImageFormat::Jpeg => "image/jpeg",
2004 ImageFormat::Webp => "image/webp",
2005 ImageFormat::Gif => "image/gif",
2006 ImageFormat::Svg => "image/svg+xml",
2007 ImageFormat::Bmp => "image/bmp",
2008 ImageFormat::Tiff => "image/tiff",
2009 ImageFormat::Ico => "image/ico",
2010 ImageFormat::Pnm => "image/x-portable-anymap",
2011 }
2012 }
2013
2014 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2016 use strum::IntoEnumIterator;
2017 Self::iter()
2018 .find(|format| format.mime_type() == mime_type)
2019 .or_else(|| Self::from_mime_type_alias(mime_type))
2020 }
2021
2022 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2026 match mime_type {
2027 "image/jpg" => Some(Self::Jpeg),
2028 "image/tif" => Some(Self::Tiff),
2029 _ => None,
2030 }
2031 }
2032}
2033
2034#[derive(Clone, Debug, PartialEq, Eq)]
2036pub struct Image {
2037 pub format: ImageFormat,
2039 pub bytes: Vec<u8>,
2041 pub id: u64,
2043}
2044
2045impl Hash for Image {
2046 fn hash<H: Hasher>(&self, state: &mut H) {
2047 state.write_u64(self.id);
2048 }
2049}
2050
2051impl Image {
2052 pub fn empty() -> Self {
2054 Self::from_bytes(ImageFormat::Png, Vec::new())
2055 }
2056
2057 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2059 Self {
2060 id: hash(&bytes),
2061 format,
2062 bytes,
2063 }
2064 }
2065
2066 pub fn id(&self) -> u64 {
2068 self.id
2069 }
2070
2071 pub fn use_render_image(
2073 self: Arc<Self>,
2074 window: &mut Window,
2075 cx: &mut App,
2076 ) -> Option<Arc<RenderImage>> {
2077 ImageSource::Image(self)
2078 .use_data(None, window, cx)
2079 .and_then(|result| result.ok())
2080 }
2081
2082 pub fn get_render_image(
2084 self: Arc<Self>,
2085 window: &mut Window,
2086 cx: &mut App,
2087 ) -> Option<Arc<RenderImage>> {
2088 ImageSource::Image(self)
2089 .get_data(None, window, cx)
2090 .and_then(|result| result.ok())
2091 }
2092
2093 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2095 ImageSource::Image(self).remove_asset(cx);
2096 }
2097
2098 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2100 fn frames_for_image(
2101 bytes: &[u8],
2102 format: image::ImageFormat,
2103 ) -> Result<SmallVec<[Frame; 1]>> {
2104 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2105
2106 for pixel in data.chunks_exact_mut(4) {
2108 pixel.swap(0, 2);
2109 }
2110
2111 Ok(SmallVec::from_elem(Frame::new(data), 1))
2112 }
2113
2114 let frames = match self.format {
2115 ImageFormat::Gif => {
2116 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2117 let mut frames = SmallVec::new();
2118
2119 for frame in decoder.into_frames() {
2120 match frame {
2121 Ok(mut frame) => {
2122 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2124 pixel.swap(0, 2);
2125 }
2126 frames.push(frame);
2127 }
2128 Err(err) => {
2129 log::debug!("Skipping GIF frame due to decode error: {err}");
2130 }
2131 }
2132 }
2133
2134 if frames.is_empty() {
2135 anyhow::bail!("GIF could not be decoded: all frames failed");
2136 }
2137
2138 frames
2139 }
2140 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2141 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2142 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2143 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2144 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2145 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2146 ImageFormat::Svg => {
2147 return svg_renderer
2148 .render_single_frame(&self.bytes, 1.0)
2149 .map_err(Into::into);
2150 }
2151 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2152 };
2153
2154 Ok(Arc::new(RenderImage::new(frames)))
2155 }
2156
2157 pub fn format(&self) -> ImageFormat {
2159 self.format
2160 }
2161
2162 pub fn bytes(&self) -> &[u8] {
2164 self.bytes.as_slice()
2165 }
2166}
2167
2168#[derive(Clone, Debug, Eq, PartialEq)]
2170pub struct ClipboardString {
2171 pub text: String,
2173 pub metadata: Option<String>,
2175}
2176
2177impl ClipboardString {
2178 pub fn new(text: String) -> Self {
2180 Self {
2181 text,
2182 metadata: None,
2183 }
2184 }
2185
2186 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2189 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2190 self
2191 }
2192
2193 pub fn text(&self) -> &String {
2195 &self.text
2196 }
2197
2198 pub fn into_text(self) -> String {
2200 self.text
2201 }
2202
2203 pub fn metadata_json<T>(&self) -> Option<T>
2205 where
2206 T: for<'a> Deserialize<'a>,
2207 {
2208 self.metadata
2209 .as_ref()
2210 .and_then(|m| serde_json::from_str(m).ok())
2211 }
2212
2213 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2214 pub fn text_hash(text: &str) -> u64 {
2216 let mut hasher = SeaHasher::new();
2217 text.hash(&mut hasher);
2218 hasher.finish()
2219 }
2220}
2221
2222impl From<String> for ClipboardString {
2223 fn from(value: String) -> Self {
2224 Self {
2225 text: value,
2226 metadata: None,
2227 }
2228 }
2229}
2230
2231#[cfg(test)]
2232mod image_tests {
2233 use super::*;
2234 use std::sync::Arc;
2235
2236 #[test]
2237 fn test_svg_image_to_image_data_converts_to_bgra() {
2238 let image = Image::from_bytes(
2239 ImageFormat::Svg,
2240 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2241<rect width="1" height="1" fill="#38BDF8"/>
2242</svg>"##
2243 .to_vec(),
2244 );
2245
2246 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2247 let bytes = render_image.as_bytes(0).unwrap();
2248
2249 for pixel in bytes.chunks_exact(4) {
2250 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2251 }
2252 }
2253}
2254
2255#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2256mod tests {
2257 use super::*;
2258 use std::collections::HashSet;
2259
2260 #[test]
2261 fn test_window_button_layout_parse_standard() {
2262 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2263 assert_eq!(
2264 layout.left,
2265 [
2266 Some(WindowButton::Close),
2267 Some(WindowButton::Minimize),
2268 None
2269 ]
2270 );
2271 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2272 }
2273
2274 #[test]
2275 fn test_window_button_layout_parse_right_only() {
2276 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2277 assert_eq!(layout.left, [None, None, None]);
2278 assert_eq!(
2279 layout.right,
2280 [
2281 Some(WindowButton::Minimize),
2282 Some(WindowButton::Maximize),
2283 Some(WindowButton::Close)
2284 ]
2285 );
2286 }
2287
2288 #[test]
2289 fn test_window_button_layout_parse_left_only() {
2290 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2291 assert_eq!(
2292 layout.left,
2293 [
2294 Some(WindowButton::Close),
2295 Some(WindowButton::Minimize),
2296 Some(WindowButton::Maximize)
2297 ]
2298 );
2299 assert_eq!(layout.right, [None, None, None]);
2300 }
2301
2302 #[test]
2303 fn test_window_button_layout_parse_with_whitespace() {
2304 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2305 assert_eq!(
2306 layout.left,
2307 [
2308 Some(WindowButton::Close),
2309 Some(WindowButton::Minimize),
2310 None
2311 ]
2312 );
2313 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2314 }
2315
2316 #[test]
2317 fn test_window_button_layout_parse_empty() {
2318 let layout = WindowButtonLayout::parse("").unwrap();
2319 assert_eq!(layout.left, [None, None, None]);
2320 assert_eq!(layout.right, [None, None, None]);
2321 }
2322
2323 #[test]
2324 fn test_window_button_layout_parse_intentionally_empty() {
2325 let layout = WindowButtonLayout::parse(":").unwrap();
2326 assert_eq!(layout.left, [None, None, None]);
2327 assert_eq!(layout.right, [None, None, None]);
2328 }
2329
2330 #[test]
2331 fn test_window_button_layout_parse_invalid_buttons() {
2332 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2333 assert_eq!(
2334 layout.left,
2335 [
2336 Some(WindowButton::Close),
2337 Some(WindowButton::Minimize),
2338 None
2339 ]
2340 );
2341 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2342 }
2343
2344 #[test]
2345 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2346 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2347 assert_eq!(
2348 layout.right,
2349 [
2350 Some(WindowButton::Close),
2351 Some(WindowButton::Minimize),
2352 None
2353 ]
2354 );
2355 assert_eq!(layout.format(), ":close,minimize");
2356 }
2357
2358 #[test]
2359 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2360 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2361 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2362 assert_eq!(
2363 layout.right,
2364 [
2365 Some(WindowButton::Maximize),
2366 Some(WindowButton::Minimize),
2367 None
2368 ]
2369 );
2370
2371 let button_ids: Vec<_> = layout
2372 .left
2373 .iter()
2374 .chain(layout.right.iter())
2375 .flatten()
2376 .map(WindowButton::id)
2377 .collect();
2378 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2379 assert_eq!(unique_button_ids.len(), button_ids.len());
2380 assert_eq!(layout.format(), "close:maximize,minimize");
2381 }
2382
2383 #[test]
2384 fn test_window_button_layout_parse_gnome_style() {
2385 let layout = WindowButtonLayout::parse("close").unwrap();
2386 assert_eq!(layout.left, [None, None, None]);
2387 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2388 }
2389
2390 #[test]
2391 fn test_window_button_layout_parse_elementary_style() {
2392 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2393 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2394 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2395 }
2396
2397 #[test]
2398 fn test_window_button_layout_round_trip() {
2399 let cases = [
2400 "close:minimize,maximize",
2401 "minimize,maximize,close:",
2402 ":close",
2403 "close:",
2404 "close:maximize",
2405 ":",
2406 ];
2407
2408 for case in cases {
2409 let layout = WindowButtonLayout::parse(case).unwrap();
2410 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2411 }
2412 }
2413
2414 #[test]
2415 fn test_window_button_layout_linux_default() {
2416 let layout = WindowButtonLayout::linux_default();
2417 assert_eq!(layout.left, [None, None, None]);
2418 assert_eq!(
2419 layout.right,
2420 [
2421 Some(WindowButton::Minimize),
2422 Some(WindowButton::Maximize),
2423 Some(WindowButton::Close)
2424 ]
2425 );
2426
2427 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2428 assert_eq!(round_tripped, layout);
2429 }
2430
2431 #[test]
2432 fn test_window_button_layout_parse_all_invalid() {
2433 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2434 }
2435}