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("APP_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 fn should_auto_hide_scrollbars(&self) -> bool;
216
217 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
218 fn write_to_clipboard(&self, item: ClipboardItem);
219
220 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
221 fn read_from_primary(&self) -> Option<ClipboardItem>;
222 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
223 fn write_to_primary(&self, item: ClipboardItem);
224
225 #[cfg(target_os = "macos")]
226 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
227 #[cfg(target_os = "macos")]
228 fn write_to_find_pasteboard(&self, item: ClipboardItem);
229
230 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
231 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
232 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
233
234 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
235 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
236 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
237}
238
239pub trait PlatformDisplay: Debug {
241 fn id(&self) -> DisplayId;
243
244 fn uuid(&self) -> Result<Uuid>;
247
248 fn bounds(&self) -> Bounds<Pixels>;
250
251 fn visible_bounds(&self) -> Bounds<Pixels> {
255 self.bounds()
256 }
257
258 fn default_bounds(&self) -> Bounds<Pixels> {
260 let bounds = self.bounds();
261 let center = bounds.center();
262 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
263
264 let offset = clipped_window_size / 2.0;
265 let origin = point(center.x - offset.width, center.y - offset.height);
266 Bounds::new(origin, clipped_window_size)
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum ThermalState {
273 Nominal,
275 Fair,
277 Serious,
279 Critical,
281}
282
283#[derive(Clone)]
285pub struct SourceMetadata {
286 pub id: u64,
288 pub label: Option<SharedString>,
290 pub is_main: Option<bool>,
292 pub resolution: Size<DevicePixels>,
294}
295
296pub trait ScreenCaptureSource {
298 fn metadata(&self) -> Result<SourceMetadata>;
300
301 fn stream(
304 &self,
305 foreground_executor: &ForegroundExecutor,
306 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
307 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
308}
309
310pub trait ScreenCaptureStream {
312 fn metadata(&self) -> Result<SourceMetadata>;
314}
315
316pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
318
319#[derive(PartialEq, Eq, Hash, Copy, Clone)]
321pub struct DisplayId(pub(crate) u32);
322
323impl DisplayId {
324 pub fn new(id: u32) -> Self {
326 Self(id)
327 }
328}
329
330impl From<u32> for DisplayId {
331 fn from(id: u32) -> Self {
332 Self(id)
333 }
334}
335
336impl From<DisplayId> for u32 {
337 fn from(id: DisplayId) -> Self {
338 id.0
339 }
340}
341
342impl Debug for DisplayId {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 write!(f, "DisplayId({})", self.0)
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub enum ResizeEdge {
351 Top,
353 TopRight,
355 Right,
357 BottomRight,
359 Bottom,
361 BottomLeft,
363 Left,
365 TopLeft,
367}
368
369#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
371pub enum WindowDecorations {
372 #[default]
373 Server,
375 Client,
377}
378
379#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
381pub enum Decorations {
382 #[default]
384 Server,
385 Client {
387 tiling: Tiling,
389 },
390}
391
392#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
394pub struct WindowControls {
395 pub fullscreen: bool,
397 pub maximize: bool,
399 pub minimize: bool,
401 pub window_menu: bool,
403}
404
405impl Default for WindowControls {
406 fn default() -> Self {
407 Self {
409 fullscreen: true,
410 maximize: true,
411 minimize: true,
412 window_menu: true,
413 }
414 }
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
419pub enum WindowButton {
420 Minimize,
422 Maximize,
424 Close,
426}
427
428impl WindowButton {
429 pub fn id(&self) -> &'static str {
431 match self {
432 WindowButton::Minimize => "minimize",
433 WindowButton::Maximize => "maximize",
434 WindowButton::Close => "close",
435 }
436 }
437
438 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
439 fn index(&self) -> usize {
440 match self {
441 WindowButton::Minimize => 0,
442 WindowButton::Maximize => 1,
443 WindowButton::Close => 2,
444 }
445 }
446}
447
448pub const MAX_BUTTONS_PER_SIDE: usize = 3;
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub struct WindowButtonLayout {
457 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
459 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
461}
462
463#[cfg(any(target_os = "linux", target_os = "freebsd"))]
464impl WindowButtonLayout {
465 pub fn linux_default() -> Self {
467 Self {
468 left: [None; MAX_BUTTONS_PER_SIDE],
469 right: [
470 Some(WindowButton::Minimize),
471 Some(WindowButton::Maximize),
472 Some(WindowButton::Close),
473 ],
474 }
475 }
476
477 pub fn parse(layout_string: &str) -> Result<Self> {
479 fn parse_side(
480 s: &str,
481 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
482 unrecognized: &mut Vec<String>,
483 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
484 let mut result = [None; MAX_BUTTONS_PER_SIDE];
485 let mut i = 0;
486 for name in s.split(',') {
487 let trimmed = name.trim();
488 if trimmed.is_empty() {
489 continue;
490 }
491 let button = match trimmed {
492 "minimize" => Some(WindowButton::Minimize),
493 "maximize" => Some(WindowButton::Maximize),
494 "close" => Some(WindowButton::Close),
495 other => {
496 unrecognized.push(other.to_string());
497 None
498 }
499 };
500 if let Some(button) = button {
501 if seen_buttons[button.index()] {
502 continue;
503 }
504 if let Some(slot) = result.get_mut(i) {
505 *slot = Some(button);
506 seen_buttons[button.index()] = true;
507 i += 1;
508 }
509 }
510 }
511 result
512 }
513
514 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
515 let mut unrecognized = Vec::new();
516 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
517 let layout = Self {
518 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
519 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
520 };
521
522 if !unrecognized.is_empty()
523 && layout.left.iter().all(Option::is_none)
524 && layout.right.iter().all(Option::is_none)
525 {
526 bail!(
527 "button layout string {:?} contains no valid buttons (unrecognized: {})",
528 layout_string,
529 unrecognized.join(", ")
530 );
531 }
532
533 Ok(layout)
534 }
535
536 #[cfg(test)]
538 pub fn format(&self) -> String {
539 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
540 buttons
541 .iter()
542 .flatten()
543 .map(|button| match button {
544 WindowButton::Minimize => "minimize",
545 WindowButton::Maximize => "maximize",
546 WindowButton::Close => "close",
547 })
548 .collect::<Vec<_>>()
549 .join(",")
550 }
551
552 format!("{}:{}", format_side(&self.left), format_side(&self.right))
553 }
554}
555
556#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
558pub struct Tiling {
559 pub top: bool,
561 pub left: bool,
563 pub right: bool,
565 pub bottom: bool,
567}
568
569impl Tiling {
570 pub fn tiled() -> Self {
572 Self {
573 top: true,
574 left: true,
575 right: true,
576 bottom: true,
577 }
578 }
579
580 pub fn is_tiled(&self) -> bool {
582 self.top || self.left || self.right || self.bottom
583 }
584}
585
586#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
587#[expect(missing_docs)]
588pub struct RequestFrameOptions {
589 pub require_presentation: bool,
591 pub force_render: bool,
593}
594
595#[expect(missing_docs)]
596pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
597 fn bounds(&self) -> Bounds<Pixels>;
598 fn is_maximized(&self) -> bool;
599 fn window_bounds(&self) -> WindowBounds;
600 fn content_size(&self) -> Size<Pixels>;
601 fn resize(&mut self, size: Size<Pixels>);
602 fn scale_factor(&self) -> f32;
603 fn appearance(&self) -> WindowAppearance;
604 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
605 fn mouse_position(&self) -> Point<Pixels>;
606 fn modifiers(&self) -> Modifiers;
607 fn capslock(&self) -> Capslock;
608 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
609 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
610 fn prompt(
611 &self,
612 level: PromptLevel,
613 msg: &str,
614 detail: Option<&str>,
615 answers: &[PromptButton],
616 ) -> Option<oneshot::Receiver<usize>>;
617 fn activate(&self);
618 fn is_active(&self) -> bool;
619 fn is_hovered(&self) -> bool;
620 fn background_appearance(&self) -> WindowBackgroundAppearance;
621 fn set_title(&mut self, title: &str);
622 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
623 fn minimize(&self);
624 fn zoom(&self);
625 fn toggle_fullscreen(&self);
626 fn is_fullscreen(&self) -> bool;
627 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
628 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
629 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
630 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
631 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
632 fn on_moved(&self, callback: Box<dyn FnMut()>);
633 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
634 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
635 fn on_close(&self, callback: Box<dyn FnOnce()>);
636 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
637 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
638 fn draw(&self, scene: &Scene);
639 fn completed_frame(&self) {}
640 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
641 fn is_subpixel_rendering_supported(&self) -> bool;
642
643 fn get_title(&self) -> String {
645 String::new()
646 }
647 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
648 None
649 }
650 fn tab_bar_visible(&self) -> bool {
651 false
652 }
653 fn set_edited(&mut self, _edited: bool) {}
654 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
655 fn show_character_palette(&self) {}
656 fn titlebar_double_click(&self) {}
657 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
658 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
659 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
660 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
661 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
662 fn merge_all_windows(&self) {}
663 fn move_tab_to_new_window(&self) {}
664 fn toggle_window_tab_overview(&self) {}
665 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
666
667 #[cfg(target_os = "windows")]
668 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
669
670 fn inner_window_bounds(&self) -> WindowBounds {
672 self.window_bounds()
673 }
674 fn request_decorations(&self, _decorations: WindowDecorations) {}
675 fn show_window_menu(&self, _position: Point<Pixels>) {}
676 fn start_window_move(&self) {}
677 fn start_window_resize(&self, _edge: ResizeEdge) {}
678 fn window_decorations(&self) -> Decorations {
679 Decorations::Server
680 }
681 fn set_app_id(&mut self, _app_id: &str) {}
682 fn map_window(&mut self) -> anyhow::Result<()> {
683 Ok(())
684 }
685 fn window_controls(&self) -> WindowControls {
686 WindowControls::default()
687 }
688 fn set_client_inset(&self, _inset: Pixels) {}
689 fn gpu_specs(&self) -> Option<GpuSpecs>;
690
691 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
692
693 fn play_system_bell(&self) {}
694
695 #[cfg(any(test, feature = "test-support"))]
696 fn as_test(&mut self) -> Option<&mut TestWindow> {
697 None
698 }
699
700 #[cfg(any(test, feature = "test-support"))]
704 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
705 anyhow::bail!("render_to_image not implemented for this platform")
706 }
707}
708
709#[cfg(any(test, feature = "test-support"))]
711pub trait PlatformHeadlessRenderer {
712 fn render_scene_to_image(
714 &mut self,
715 scene: &Scene,
716 size: Size<DevicePixels>,
717 ) -> Result<RgbaImage>;
718
719 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
721}
722
723#[doc(hidden)]
726pub type RunnableVariant = Runnable<RunnableMeta>;
727
728#[doc(hidden)]
729pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
730
731#[doc(hidden)]
734pub trait PlatformDispatcher: Send + Sync {
735 fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
736 fn get_current_thread_timings(&self) -> ThreadTaskTimings;
737 fn is_main_thread(&self) -> bool;
738 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
739 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
740 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
741
742 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
743
744 fn now(&self) -> Instant {
745 Instant::now()
746 }
747
748 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
749 gpui_util::defer(Box::new(|| {}))
750 }
751
752 #[cfg(any(test, feature = "test-support"))]
753 fn as_test(&self) -> Option<&TestDispatcher> {
754 None
755 }
756}
757
758#[expect(missing_docs)]
759pub trait PlatformTextSystem: Send + Sync {
760 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
761 fn all_font_names(&self) -> Vec<String>;
763 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
765 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
767 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
769 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
771 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
773 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
775 fn rasterize_glyph(
777 &self,
778 params: &RenderGlyphParams,
779 raster_bounds: Bounds<DevicePixels>,
780 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
781 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
783 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
785 -> TextRenderingMode;
786 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
788 0
789 }
790}
791
792#[expect(missing_docs)]
793pub struct NoopTextSystem;
794
795#[expect(missing_docs)]
796impl NoopTextSystem {
797 #[allow(dead_code)]
798 pub fn new() -> Self {
799 Self
800 }
801}
802
803impl PlatformTextSystem for NoopTextSystem {
804 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
805 Ok(())
806 }
807
808 fn all_font_names(&self) -> Vec<String> {
809 Vec::new()
810 }
811
812 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
813 Ok(FontId(1))
814 }
815
816 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
817 FontMetrics {
818 units_per_em: 1000,
819 ascent: 1025.0,
820 descent: -275.0,
821 line_gap: 0.0,
822 underline_position: -95.0,
823 underline_thickness: 60.0,
824 cap_height: 698.0,
825 x_height: 516.0,
826 bounding_box: Bounds {
827 origin: Point {
828 x: -260.0,
829 y: -245.0,
830 },
831 size: Size {
832 width: 1501.0,
833 height: 1364.0,
834 },
835 },
836 }
837 }
838
839 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
840 Ok(Bounds {
841 origin: Point { x: 54.0, y: 0.0 },
842 size: size(392.0, 528.0),
843 })
844 }
845
846 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
847 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
848 }
849
850 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
851 Some(GlyphId(ch.len_utf16() as u32))
852 }
853
854 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
855 Ok(Default::default())
856 }
857
858 fn rasterize_glyph(
859 &self,
860 _params: &RenderGlyphParams,
861 raster_bounds: Bounds<DevicePixels>,
862 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
863 Ok((raster_bounds.size, Vec::new()))
864 }
865
866 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
867 let mut position = px(0.);
868 let metrics = self.font_metrics(FontId(0));
869 let em_width = font_size
870 * self
871 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
872 .unwrap()
873 .width
874 / metrics.units_per_em as f32;
875 let mut glyphs = Vec::new();
876 for (ix, c) in text.char_indices() {
877 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
878 glyphs.push(ShapedGlyph {
879 id: glyph,
880 position: point(position, px(0.)),
881 index: ix,
882 is_emoji: glyph.0 == 2,
883 });
884 if glyph.0 == 2 {
885 position += em_width * 2.0;
886 } else {
887 position += em_width;
888 }
889 } else {
890 position += em_width
891 }
892 }
893 let mut runs = Vec::default();
894 if !glyphs.is_empty() {
895 runs.push(ShapedRun {
896 font_id: FontId(0),
897 glyphs,
898 });
899 } else {
900 position = px(0.);
901 }
902
903 LineLayout {
904 font_size,
905 width: position,
906 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
907 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
908 runs,
909 len: text.len(),
910 }
911 }
912
913 fn recommended_rendering_mode(
914 &self,
915 _font_id: FontId,
916 _font_size: Pixels,
917 ) -> TextRenderingMode {
918 TextRenderingMode::Grayscale
919 }
920}
921
922#[allow(dead_code)]
927pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
928 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
929 [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], ];
943
944 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
945 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
946
947 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
948 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
949
950 [
951 ratios[0] * NORM13,
952 ratios[1] * NORM24,
953 ratios[2] * NORM13,
954 ratios[3] * NORM24,
955 ]
956}
957
958#[derive(PartialEq, Eq, Hash, Clone)]
959#[expect(missing_docs)]
960pub enum AtlasKey {
961 Glyph(RenderGlyphParams),
962 Svg(RenderSvgParams),
963 Image(RenderImageParams),
964}
965
966impl AtlasKey {
967 #[cfg_attr(
968 all(
969 any(target_os = "linux", target_os = "freebsd"),
970 not(any(feature = "x11", feature = "wayland"))
971 ),
972 allow(dead_code)
973 )]
974 pub fn texture_kind(&self) -> AtlasTextureKind {
976 match self {
977 AtlasKey::Glyph(params) => {
978 if params.is_emoji {
979 AtlasTextureKind::Polychrome
980 } else if params.subpixel_rendering {
981 AtlasTextureKind::Subpixel
982 } else {
983 AtlasTextureKind::Monochrome
984 }
985 }
986 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
987 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
988 }
989 }
990}
991
992impl From<RenderGlyphParams> for AtlasKey {
993 fn from(params: RenderGlyphParams) -> Self {
994 Self::Glyph(params)
995 }
996}
997
998impl From<RenderSvgParams> for AtlasKey {
999 fn from(params: RenderSvgParams) -> Self {
1000 Self::Svg(params)
1001 }
1002}
1003
1004impl From<RenderImageParams> for AtlasKey {
1005 fn from(params: RenderImageParams) -> Self {
1006 Self::Image(params)
1007 }
1008}
1009
1010#[expect(missing_docs)]
1011pub trait PlatformAtlas {
1012 fn get_or_insert_with<'a>(
1013 &self,
1014 key: &AtlasKey,
1015 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1016 ) -> Result<Option<AtlasTile>>;
1017 fn remove(&self, key: &AtlasKey);
1018}
1019
1020#[doc(hidden)]
1021pub struct AtlasTextureList<T> {
1022 pub textures: Vec<Option<T>>,
1023 pub free_list: Vec<usize>,
1024}
1025
1026impl<T> Default for AtlasTextureList<T> {
1027 fn default() -> Self {
1028 Self {
1029 textures: Vec::default(),
1030 free_list: Vec::default(),
1031 }
1032 }
1033}
1034
1035impl<T> ops::Index<usize> for AtlasTextureList<T> {
1036 type Output = Option<T>;
1037
1038 fn index(&self, index: usize) -> &Self::Output {
1039 &self.textures[index]
1040 }
1041}
1042
1043impl<T> AtlasTextureList<T> {
1044 #[allow(unused)]
1045 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1046 self.free_list.clear();
1047 self.textures.drain(..)
1048 }
1049
1050 #[allow(dead_code)]
1051 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1052 self.textures.iter_mut().flatten()
1053 }
1054}
1055
1056#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1057#[repr(C)]
1058#[expect(missing_docs)]
1059pub struct AtlasTile {
1060 pub texture_id: AtlasTextureId,
1062 pub tile_id: TileId,
1064 pub padding: u32,
1066 pub bounds: Bounds<DevicePixels>,
1068}
1069
1070#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1071#[repr(C)]
1072#[expect(missing_docs)]
1073pub struct AtlasTextureId {
1074 pub index: u32,
1077 pub kind: AtlasTextureKind,
1079}
1080
1081#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1082#[repr(C)]
1083#[cfg_attr(
1084 all(
1085 any(target_os = "linux", target_os = "freebsd"),
1086 not(any(feature = "x11", feature = "wayland"))
1087 ),
1088 allow(dead_code)
1089)]
1090#[expect(missing_docs)]
1091pub enum AtlasTextureKind {
1092 Monochrome = 0,
1093 Polychrome = 1,
1094 Subpixel = 2,
1095}
1096
1097#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1098#[repr(C)]
1099#[expect(missing_docs)]
1100pub struct TileId(pub u32);
1101
1102impl From<etagere::AllocId> for TileId {
1103 fn from(id: etagere::AllocId) -> Self {
1104 Self(id.serialize())
1105 }
1106}
1107
1108impl From<TileId> for etagere::AllocId {
1109 fn from(id: TileId) -> Self {
1110 Self::deserialize(id.0)
1111 }
1112}
1113
1114#[expect(missing_docs)]
1115pub struct PlatformInputHandler {
1116 cx: AsyncWindowContext,
1117 handler: Box<dyn InputHandler>,
1118}
1119
1120#[expect(missing_docs)]
1121#[cfg_attr(
1122 all(
1123 any(target_os = "linux", target_os = "freebsd"),
1124 not(any(feature = "x11", feature = "wayland"))
1125 ),
1126 allow(dead_code)
1127)]
1128impl PlatformInputHandler {
1129 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1130 Self { cx, handler }
1131 }
1132
1133 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1134 self.cx
1135 .update(|window, cx| {
1136 self.handler
1137 .selected_text_range(ignore_disabled_input, window, cx)
1138 })
1139 .ok()
1140 .flatten()
1141 }
1142
1143 #[cfg_attr(target_os = "windows", allow(dead_code))]
1144 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1145 self.cx
1146 .update(|window, cx| self.handler.marked_text_range(window, cx))
1147 .ok()
1148 .flatten()
1149 }
1150
1151 #[cfg_attr(
1152 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1153 allow(dead_code)
1154 )]
1155 pub fn text_for_range(
1156 &mut self,
1157 range_utf16: Range<usize>,
1158 adjusted: &mut Option<Range<usize>>,
1159 ) -> Option<String> {
1160 self.cx
1161 .update(|window, cx| {
1162 self.handler
1163 .text_for_range(range_utf16, adjusted, window, cx)
1164 })
1165 .ok()
1166 .flatten()
1167 }
1168
1169 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1170 self.cx
1171 .update(|window, cx| {
1172 self.handler
1173 .replace_text_in_range(replacement_range, text, window, cx);
1174 })
1175 .ok();
1176 }
1177
1178 pub fn replace_and_mark_text_in_range(
1179 &mut self,
1180 range_utf16: Option<Range<usize>>,
1181 new_text: &str,
1182 new_selected_range: Option<Range<usize>>,
1183 ) {
1184 self.cx
1185 .update(|window, cx| {
1186 self.handler.replace_and_mark_text_in_range(
1187 range_utf16,
1188 new_text,
1189 new_selected_range,
1190 window,
1191 cx,
1192 )
1193 })
1194 .ok();
1195 }
1196
1197 #[cfg_attr(target_os = "windows", allow(dead_code))]
1198 pub fn unmark_text(&mut self) {
1199 self.cx
1200 .update(|window, cx| self.handler.unmark_text(window, cx))
1201 .ok();
1202 }
1203
1204 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1205 self.cx
1206 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1207 .ok()
1208 .flatten()
1209 }
1210
1211 #[allow(dead_code)]
1212 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1213 self.handler.apple_press_and_hold_enabled()
1214 }
1215
1216 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1217 self.handler.replace_text_in_range(None, input, window, cx);
1218 }
1219
1220 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1221 let selection = self.handler.selected_text_range(true, window, cx)?;
1222 self.handler.bounds_for_range(
1223 if selection.reversed {
1224 selection.range.start..selection.range.start
1225 } else {
1226 selection.range.end..selection.range.end
1227 },
1228 window,
1229 cx,
1230 )
1231 }
1232
1233 #[allow(unused)]
1234 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1235 self.cx
1236 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1237 .ok()
1238 .flatten()
1239 }
1240
1241 #[allow(dead_code)]
1242 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1243 self.handler.accepts_text_input(window, cx)
1244 }
1245
1246 #[allow(dead_code)]
1247 pub fn query_accepts_text_input(&mut self) -> bool {
1248 self.cx
1249 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1250 .unwrap_or(true)
1251 }
1252
1253 #[allow(dead_code)]
1254 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1255 self.cx
1256 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1257 .unwrap_or(false)
1258 }
1259}
1260
1261#[derive(Debug)]
1264pub struct UTF16Selection {
1265 pub range: Range<usize>,
1268 pub reversed: bool,
1271}
1272
1273pub trait InputHandler: 'static {
1278 fn selected_text_range(
1283 &mut self,
1284 ignore_disabled_input: bool,
1285 window: &mut Window,
1286 cx: &mut App,
1287 ) -> Option<UTF16Selection>;
1288
1289 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1294
1295 fn text_for_range(
1300 &mut self,
1301 range_utf16: Range<usize>,
1302 adjusted_range: &mut Option<Range<usize>>,
1303 window: &mut Window,
1304 cx: &mut App,
1305 ) -> Option<String>;
1306
1307 fn replace_text_in_range(
1312 &mut self,
1313 replacement_range: Option<Range<usize>>,
1314 text: &str,
1315 window: &mut Window,
1316 cx: &mut App,
1317 );
1318
1319 fn replace_and_mark_text_in_range(
1326 &mut self,
1327 range_utf16: Option<Range<usize>>,
1328 new_text: &str,
1329 new_selected_range: Option<Range<usize>>,
1330 window: &mut Window,
1331 cx: &mut App,
1332 );
1333
1334 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1337
1338 fn bounds_for_range(
1343 &mut self,
1344 range_utf16: Range<usize>,
1345 window: &mut Window,
1346 cx: &mut App,
1347 ) -> Option<Bounds<Pixels>>;
1348
1349 fn character_index_for_point(
1353 &mut self,
1354 point: Point<Pixels>,
1355 window: &mut Window,
1356 cx: &mut App,
1357 ) -> Option<usize>;
1358
1359 #[allow(dead_code)]
1364 fn apple_press_and_hold_enabled(&mut self) -> bool {
1365 true
1366 }
1367
1368 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1370 true
1371 }
1372
1373 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1382 false
1383 }
1384}
1385
1386#[derive(Debug)]
1388pub struct WindowOptions {
1389 pub window_bounds: Option<WindowBounds>,
1393
1394 pub titlebar: Option<TitlebarOptions>,
1396
1397 pub focus: bool,
1399
1400 pub show: bool,
1402
1403 pub kind: WindowKind,
1405
1406 pub is_movable: bool,
1408
1409 pub is_resizable: bool,
1411
1412 pub is_minimizable: bool,
1414
1415 pub display_id: Option<DisplayId>,
1418
1419 pub window_background: WindowBackgroundAppearance,
1421
1422 pub app_id: Option<String>,
1424
1425 pub window_min_size: Option<Size<Pixels>>,
1427
1428 pub window_decorations: Option<WindowDecorations>,
1431
1432 pub icon: Option<Arc<image::RgbaImage>>,
1434
1435 pub tabbing_identifier: Option<String>,
1437}
1438
1439#[derive(Debug)]
1441#[cfg_attr(
1442 all(
1443 any(target_os = "linux", target_os = "freebsd"),
1444 not(any(feature = "x11", feature = "wayland"))
1445 ),
1446 allow(dead_code)
1447)]
1448#[allow(missing_docs)]
1449pub struct WindowParams {
1450 pub bounds: Bounds<Pixels>,
1451
1452 #[cfg_attr(feature = "wayland", allow(dead_code))]
1454 pub titlebar: Option<TitlebarOptions>,
1455
1456 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1458 pub kind: WindowKind,
1459
1460 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1462 pub is_movable: bool,
1463
1464 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1466 pub is_resizable: bool,
1467
1468 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1470 pub is_minimizable: bool,
1471
1472 #[cfg_attr(
1473 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1474 allow(dead_code)
1475 )]
1476 pub focus: bool,
1477
1478 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1479 pub show: bool,
1480
1481 #[cfg_attr(feature = "wayland", allow(dead_code))]
1483 pub icon: Option<Arc<image::RgbaImage>>,
1484
1485 #[cfg_attr(feature = "wayland", allow(dead_code))]
1486 pub display_id: Option<DisplayId>,
1487
1488 pub window_min_size: Option<Size<Pixels>>,
1489 #[cfg(target_os = "macos")]
1490 pub tabbing_identifier: Option<String>,
1491}
1492
1493#[derive(Debug, Copy, Clone, PartialEq)]
1495pub enum WindowBounds {
1496 Windowed(Bounds<Pixels>),
1498 Maximized(Bounds<Pixels>),
1501 Fullscreen(Bounds<Pixels>),
1504}
1505
1506impl Default for WindowBounds {
1507 fn default() -> Self {
1508 WindowBounds::Windowed(Bounds::default())
1509 }
1510}
1511
1512impl WindowBounds {
1513 pub fn get_bounds(&self) -> Bounds<Pixels> {
1515 match self {
1516 WindowBounds::Windowed(bounds) => *bounds,
1517 WindowBounds::Maximized(bounds) => *bounds,
1518 WindowBounds::Fullscreen(bounds) => *bounds,
1519 }
1520 }
1521
1522 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1524 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1525 }
1526}
1527
1528impl Default for WindowOptions {
1529 fn default() -> Self {
1530 Self {
1531 window_bounds: None,
1532 titlebar: Some(TitlebarOptions {
1533 title: Default::default(),
1534 appears_transparent: Default::default(),
1535 traffic_light_position: Default::default(),
1536 }),
1537 focus: true,
1538 show: true,
1539 kind: WindowKind::Normal,
1540 is_movable: true,
1541 is_resizable: true,
1542 is_minimizable: true,
1543 display_id: None,
1544 window_background: WindowBackgroundAppearance::default(),
1545 icon: None,
1546 app_id: None,
1547 window_min_size: None,
1548 window_decorations: None,
1549 tabbing_identifier: None,
1550 }
1551 }
1552}
1553
1554#[derive(Debug, Default)]
1556pub struct TitlebarOptions {
1557 pub title: Option<SharedString>,
1559
1560 pub appears_transparent: bool,
1563
1564 pub traffic_light_position: Option<Point<Pixels>>,
1566}
1567
1568#[derive(Clone, Debug, PartialEq, Eq)]
1570pub enum WindowKind {
1571 Normal,
1573
1574 PopUp,
1577
1578 Floating,
1580
1581 #[cfg(all(target_os = "linux", feature = "wayland"))]
1584 LayerShell(layer_shell::LayerShellOptions),
1585
1586 Dialog,
1589}
1590
1591#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1596pub enum WindowAppearance {
1597 #[default]
1601 Light,
1602
1603 VibrantLight,
1607
1608 Dark,
1612
1613 VibrantDark,
1617}
1618
1619#[derive(Copy, Clone, Debug, Default, PartialEq)]
1622pub enum WindowBackgroundAppearance {
1623 #[default]
1631 Opaque,
1632 Transparent,
1634 Blurred,
1638 MicaBackdrop,
1640 MicaAltBackdrop,
1642}
1643
1644#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1646pub enum TextRenderingMode {
1647 #[default]
1649 PlatformDefault,
1650 Subpixel,
1652 Grayscale,
1654}
1655
1656#[derive(Clone, Debug)]
1658pub struct PathPromptOptions {
1659 pub files: bool,
1661 pub directories: bool,
1663 pub multiple: bool,
1665 pub prompt: Option<SharedString>,
1667}
1668
1669#[derive(Copy, Clone, Debug, PartialEq)]
1671pub enum PromptLevel {
1672 Info,
1674
1675 Warning,
1677
1678 Critical,
1680}
1681
1682#[derive(Clone, Debug, PartialEq)]
1684pub enum PromptButton {
1685 Ok(SharedString),
1687 Cancel(SharedString),
1689 Other(SharedString),
1691}
1692
1693impl PromptButton {
1694 pub fn new(label: impl Into<SharedString>) -> Self {
1696 PromptButton::Other(label.into())
1697 }
1698
1699 pub fn ok(label: impl Into<SharedString>) -> Self {
1701 PromptButton::Ok(label.into())
1702 }
1703
1704 pub fn cancel(label: impl Into<SharedString>) -> Self {
1706 PromptButton::Cancel(label.into())
1707 }
1708
1709 #[allow(dead_code)]
1711 pub fn is_cancel(&self) -> bool {
1712 matches!(self, PromptButton::Cancel(_))
1713 }
1714
1715 pub fn label(&self) -> &SharedString {
1717 match self {
1718 PromptButton::Ok(label) => label,
1719 PromptButton::Cancel(label) => label,
1720 PromptButton::Other(label) => label,
1721 }
1722 }
1723}
1724
1725impl From<&str> for PromptButton {
1726 fn from(value: &str) -> Self {
1727 match value.to_lowercase().as_str() {
1728 "ok" => PromptButton::Ok("Ok".into()),
1729 "cancel" => PromptButton::Cancel("Cancel".into()),
1730 _ => PromptButton::Other(SharedString::from(value.to_owned())),
1731 }
1732 }
1733}
1734
1735#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1737pub enum CursorStyle {
1738 #[default]
1740 Arrow,
1741
1742 IBeam,
1745
1746 Crosshair,
1749
1750 ClosedHand,
1753
1754 OpenHand,
1757
1758 PointingHand,
1761
1762 ResizeLeft,
1765
1766 ResizeRight,
1769
1770 ResizeLeftRight,
1773
1774 ResizeUp,
1777
1778 ResizeDown,
1781
1782 ResizeUpDown,
1785
1786 ResizeUpLeftDownRight,
1789
1790 ResizeUpRightDownLeft,
1793
1794 ResizeColumn,
1797
1798 ResizeRow,
1801
1802 IBeamCursorForVerticalLayout,
1805
1806 OperationNotAllowed,
1809
1810 DragLink,
1813
1814 DragCopy,
1817
1818 ContextualMenu,
1821
1822 None,
1824}
1825
1826#[derive(Clone, Debug, Eq, PartialEq)]
1828pub struct ClipboardItem {
1829 pub entries: Vec<ClipboardEntry>,
1831}
1832
1833#[derive(Clone, Debug, Eq, PartialEq)]
1835pub enum ClipboardEntry {
1836 String(ClipboardString),
1838 Image(Image),
1840 ExternalPaths(crate::ExternalPaths),
1842}
1843
1844impl ClipboardItem {
1845 pub fn new_string(text: String) -> Self {
1847 Self {
1848 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
1849 }
1850 }
1851
1852 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
1854 Self {
1855 entries: vec![ClipboardEntry::String(ClipboardString {
1856 text,
1857 metadata: Some(metadata),
1858 })],
1859 }
1860 }
1861
1862 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
1864 Self {
1865 entries: vec![ClipboardEntry::String(
1866 ClipboardString::new(text).with_json_metadata(metadata),
1867 )],
1868 }
1869 }
1870
1871 pub fn new_image(image: &Image) -> Self {
1873 Self {
1874 entries: vec![ClipboardEntry::Image(image.clone())],
1875 }
1876 }
1877
1878 pub fn text(&self) -> Option<String> {
1881 let mut answer = String::new();
1882
1883 for entry in self.entries.iter() {
1884 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
1885 answer.push_str(text);
1886 }
1887 }
1888
1889 if answer.is_empty() {
1890 for entry in self.entries.iter() {
1891 if let ClipboardEntry::ExternalPaths(paths) = entry {
1892 for path in &paths.0 {
1893 use std::fmt::Write as _;
1894 _ = write!(answer, "{}", path.display());
1895 }
1896 }
1897 }
1898 }
1899
1900 if !answer.is_empty() {
1901 Some(answer)
1902 } else {
1903 None
1904 }
1905 }
1906
1907 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
1909 pub fn metadata(&self) -> Option<&String> {
1910 match self.entries().first() {
1911 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
1912 clipboard_string.metadata.as_ref()
1913 }
1914 _ => None,
1915 }
1916 }
1917
1918 pub fn entries(&self) -> &[ClipboardEntry] {
1920 &self.entries
1921 }
1922
1923 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
1925 self.entries.into_iter()
1926 }
1927}
1928
1929impl From<ClipboardString> for ClipboardEntry {
1930 fn from(value: ClipboardString) -> Self {
1931 Self::String(value)
1932 }
1933}
1934
1935impl From<String> for ClipboardEntry {
1936 fn from(value: String) -> Self {
1937 Self::from(ClipboardString::from(value))
1938 }
1939}
1940
1941impl From<Image> for ClipboardEntry {
1942 fn from(value: Image) -> Self {
1943 Self::Image(value)
1944 }
1945}
1946
1947impl From<ClipboardEntry> for ClipboardItem {
1948 fn from(value: ClipboardEntry) -> Self {
1949 Self {
1950 entries: vec![value],
1951 }
1952 }
1953}
1954
1955impl From<String> for ClipboardItem {
1956 fn from(value: String) -> Self {
1957 Self::from(ClipboardEntry::from(value))
1958 }
1959}
1960
1961impl From<Image> for ClipboardItem {
1962 fn from(value: Image) -> Self {
1963 Self::from(ClipboardEntry::from(value))
1964 }
1965}
1966
1967#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
1969pub enum ImageFormat {
1970 Png,
1975 Jpeg,
1977 Webp,
1979 Gif,
1981 Svg,
1983 Bmp,
1985 Tiff,
1987 Ico,
1989 Pnm,
1991}
1992
1993impl ImageFormat {
1994 pub const fn mime_type(self) -> &'static str {
1996 match self {
1997 ImageFormat::Png => "image/png",
1998 ImageFormat::Jpeg => "image/jpeg",
1999 ImageFormat::Webp => "image/webp",
2000 ImageFormat::Gif => "image/gif",
2001 ImageFormat::Svg => "image/svg+xml",
2002 ImageFormat::Bmp => "image/bmp",
2003 ImageFormat::Tiff => "image/tiff",
2004 ImageFormat::Ico => "image/ico",
2005 ImageFormat::Pnm => "image/x-portable-anymap",
2006 }
2007 }
2008
2009 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2011 use strum::IntoEnumIterator;
2012 Self::iter()
2013 .find(|format| format.mime_type() == mime_type)
2014 .or_else(|| Self::from_mime_type_alias(mime_type))
2015 }
2016
2017 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2021 match mime_type {
2022 "image/jpg" => Some(Self::Jpeg),
2023 "image/tif" => Some(Self::Tiff),
2024 _ => None,
2025 }
2026 }
2027}
2028
2029#[derive(Clone, Debug, PartialEq, Eq)]
2031pub struct Image {
2032 pub format: ImageFormat,
2034 pub bytes: Vec<u8>,
2036 pub id: u64,
2038}
2039
2040impl Hash for Image {
2041 fn hash<H: Hasher>(&self, state: &mut H) {
2042 state.write_u64(self.id);
2043 }
2044}
2045
2046impl Image {
2047 pub fn empty() -> Self {
2049 Self::from_bytes(ImageFormat::Png, Vec::new())
2050 }
2051
2052 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2054 Self {
2055 id: hash(&bytes),
2056 format,
2057 bytes,
2058 }
2059 }
2060
2061 pub fn id(&self) -> u64 {
2063 self.id
2064 }
2065
2066 pub fn use_render_image(
2068 self: Arc<Self>,
2069 window: &mut Window,
2070 cx: &mut App,
2071 ) -> Option<Arc<RenderImage>> {
2072 ImageSource::Image(self)
2073 .use_data(None, window, cx)
2074 .and_then(|result| result.ok())
2075 }
2076
2077 pub fn get_render_image(
2079 self: Arc<Self>,
2080 window: &mut Window,
2081 cx: &mut App,
2082 ) -> Option<Arc<RenderImage>> {
2083 ImageSource::Image(self)
2084 .get_data(None, window, cx)
2085 .and_then(|result| result.ok())
2086 }
2087
2088 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2090 ImageSource::Image(self).remove_asset(cx);
2091 }
2092
2093 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2095 fn frames_for_image(
2096 bytes: &[u8],
2097 format: image::ImageFormat,
2098 ) -> Result<SmallVec<[Frame; 1]>> {
2099 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2100
2101 for pixel in data.chunks_exact_mut(4) {
2103 pixel.swap(0, 2);
2104 }
2105
2106 Ok(SmallVec::from_elem(Frame::new(data), 1))
2107 }
2108
2109 let frames = match self.format {
2110 ImageFormat::Gif => {
2111 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2112 let mut frames = SmallVec::new();
2113
2114 for frame in decoder.into_frames() {
2115 match frame {
2116 Ok(mut frame) => {
2117 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2119 pixel.swap(0, 2);
2120 }
2121 frames.push(frame);
2122 }
2123 Err(err) => {
2124 log::debug!("Skipping GIF frame due to decode error: {err}");
2125 }
2126 }
2127 }
2128
2129 if frames.is_empty() {
2130 anyhow::bail!("GIF could not be decoded: all frames failed");
2131 }
2132
2133 frames
2134 }
2135 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2136 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2137 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2138 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2139 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2140 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2141 ImageFormat::Svg => {
2142 return svg_renderer
2143 .render_single_frame(&self.bytes, 1.0)
2144 .map_err(Into::into);
2145 }
2146 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2147 };
2148
2149 Ok(Arc::new(RenderImage::new(frames)))
2150 }
2151
2152 pub fn format(&self) -> ImageFormat {
2154 self.format
2155 }
2156
2157 pub fn bytes(&self) -> &[u8] {
2159 self.bytes.as_slice()
2160 }
2161}
2162
2163#[derive(Clone, Debug, Eq, PartialEq)]
2165pub struct ClipboardString {
2166 pub text: String,
2168 pub metadata: Option<String>,
2170}
2171
2172impl ClipboardString {
2173 pub fn new(text: String) -> Self {
2175 Self {
2176 text,
2177 metadata: None,
2178 }
2179 }
2180
2181 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2184 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2185 self
2186 }
2187
2188 pub fn text(&self) -> &String {
2190 &self.text
2191 }
2192
2193 pub fn into_text(self) -> String {
2195 self.text
2196 }
2197
2198 pub fn metadata_json<T>(&self) -> Option<T>
2200 where
2201 T: for<'a> Deserialize<'a>,
2202 {
2203 self.metadata
2204 .as_ref()
2205 .and_then(|m| serde_json::from_str(m).ok())
2206 }
2207
2208 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2209 pub fn text_hash(text: &str) -> u64 {
2211 let mut hasher = SeaHasher::new();
2212 text.hash(&mut hasher);
2213 hasher.finish()
2214 }
2215}
2216
2217impl From<String> for ClipboardString {
2218 fn from(value: String) -> Self {
2219 Self {
2220 text: value,
2221 metadata: None,
2222 }
2223 }
2224}
2225
2226#[cfg(test)]
2227mod image_tests {
2228 use super::*;
2229 use std::sync::Arc;
2230
2231 #[test]
2232 fn test_svg_image_to_image_data_converts_to_bgra() {
2233 let image = Image::from_bytes(
2234 ImageFormat::Svg,
2235 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2236<rect width="1" height="1" fill="#38BDF8"/>
2237</svg>"##
2238 .to_vec(),
2239 );
2240
2241 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2242 let bytes = render_image.as_bytes(0).unwrap();
2243
2244 for pixel in bytes.chunks_exact(4) {
2245 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2246 }
2247 }
2248}
2249
2250#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2251mod tests {
2252 use super::*;
2253 use std::collections::HashSet;
2254
2255 #[test]
2256 fn test_window_button_layout_parse_standard() {
2257 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2258 assert_eq!(
2259 layout.left,
2260 [
2261 Some(WindowButton::Close),
2262 Some(WindowButton::Minimize),
2263 None
2264 ]
2265 );
2266 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2267 }
2268
2269 #[test]
2270 fn test_window_button_layout_parse_right_only() {
2271 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2272 assert_eq!(layout.left, [None, None, None]);
2273 assert_eq!(
2274 layout.right,
2275 [
2276 Some(WindowButton::Minimize),
2277 Some(WindowButton::Maximize),
2278 Some(WindowButton::Close)
2279 ]
2280 );
2281 }
2282
2283 #[test]
2284 fn test_window_button_layout_parse_left_only() {
2285 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2286 assert_eq!(
2287 layout.left,
2288 [
2289 Some(WindowButton::Close),
2290 Some(WindowButton::Minimize),
2291 Some(WindowButton::Maximize)
2292 ]
2293 );
2294 assert_eq!(layout.right, [None, None, None]);
2295 }
2296
2297 #[test]
2298 fn test_window_button_layout_parse_with_whitespace() {
2299 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2300 assert_eq!(
2301 layout.left,
2302 [
2303 Some(WindowButton::Close),
2304 Some(WindowButton::Minimize),
2305 None
2306 ]
2307 );
2308 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2309 }
2310
2311 #[test]
2312 fn test_window_button_layout_parse_empty() {
2313 let layout = WindowButtonLayout::parse("").unwrap();
2314 assert_eq!(layout.left, [None, None, None]);
2315 assert_eq!(layout.right, [None, None, None]);
2316 }
2317
2318 #[test]
2319 fn test_window_button_layout_parse_intentionally_empty() {
2320 let layout = WindowButtonLayout::parse(":").unwrap();
2321 assert_eq!(layout.left, [None, None, None]);
2322 assert_eq!(layout.right, [None, None, None]);
2323 }
2324
2325 #[test]
2326 fn test_window_button_layout_parse_invalid_buttons() {
2327 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2328 assert_eq!(
2329 layout.left,
2330 [
2331 Some(WindowButton::Close),
2332 Some(WindowButton::Minimize),
2333 None
2334 ]
2335 );
2336 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2337 }
2338
2339 #[test]
2340 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2341 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2342 assert_eq!(
2343 layout.right,
2344 [
2345 Some(WindowButton::Close),
2346 Some(WindowButton::Minimize),
2347 None
2348 ]
2349 );
2350 assert_eq!(layout.format(), ":close,minimize");
2351 }
2352
2353 #[test]
2354 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2355 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2356 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2357 assert_eq!(
2358 layout.right,
2359 [
2360 Some(WindowButton::Maximize),
2361 Some(WindowButton::Minimize),
2362 None
2363 ]
2364 );
2365
2366 let button_ids: Vec<_> = layout
2367 .left
2368 .iter()
2369 .chain(layout.right.iter())
2370 .flatten()
2371 .map(WindowButton::id)
2372 .collect();
2373 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2374 assert_eq!(unique_button_ids.len(), button_ids.len());
2375 assert_eq!(layout.format(), "close:maximize,minimize");
2376 }
2377
2378 #[test]
2379 fn test_window_button_layout_parse_gnome_style() {
2380 let layout = WindowButtonLayout::parse("close").unwrap();
2381 assert_eq!(layout.left, [None, None, None]);
2382 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2383 }
2384
2385 #[test]
2386 fn test_window_button_layout_parse_elementary_style() {
2387 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2388 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2389 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2390 }
2391
2392 #[test]
2393 fn test_window_button_layout_round_trip() {
2394 let cases = [
2395 "close:minimize,maximize",
2396 "minimize,maximize,close:",
2397 ":close",
2398 "close:",
2399 "close:maximize",
2400 ":",
2401 ];
2402
2403 for case in cases {
2404 let layout = WindowButtonLayout::parse(case).unwrap();
2405 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2406 }
2407 }
2408
2409 #[test]
2410 fn test_window_button_layout_linux_default() {
2411 let layout = WindowButtonLayout::linux_default();
2412 assert_eq!(layout.left, [None, None, None]);
2413 assert_eq!(
2414 layout.right,
2415 [
2416 Some(WindowButton::Minimize),
2417 Some(WindowButton::Maximize),
2418 Some(WindowButton::Close)
2419 ]
2420 );
2421
2422 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2423 assert_eq!(round_tripped, layout);
2424 }
2425
2426 #[test]
2427 fn test_window_button_layout_parse_all_invalid() {
2428 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2429 }
2430}