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, 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 open_gpui_scheduler::Instant;
49pub use open_gpui_scheduler::RunnableMeta;
50use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
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
594pub struct A11yCallbacks {
596 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
598 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
600 pub deactivation: Box<dyn Fn() + Send + 'static>,
602}
603
604#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
605#[expect(missing_docs)]
606pub struct RequestFrameOptions {
607 pub require_presentation: bool,
609 pub force_render: bool,
611}
612
613#[expect(missing_docs)]
614pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
615 fn bounds(&self) -> Bounds<Pixels>;
616 fn is_maximized(&self) -> bool;
617 fn window_bounds(&self) -> WindowBounds;
618 fn content_size(&self) -> Size<Pixels>;
619 fn resize(&mut self, size: Size<Pixels>);
620 fn scale_factor(&self) -> f32;
621 fn appearance(&self) -> WindowAppearance;
622 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
623 fn mouse_position(&self) -> Point<Pixels>;
624 fn modifiers(&self) -> Modifiers;
625 fn capslock(&self) -> Capslock;
626 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
627 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
628 fn prompt(
629 &self,
630 level: PromptLevel,
631 msg: &str,
632 detail: Option<&str>,
633 answers: &[PromptButton],
634 ) -> Option<oneshot::Receiver<usize>>;
635 fn activate(&self);
636 fn is_active(&self) -> bool;
637 fn is_hovered(&self) -> bool;
638 fn background_appearance(&self) -> WindowBackgroundAppearance;
639 fn set_title(&mut self, title: &str);
640 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
641 fn minimize(&self);
642 fn zoom(&self);
643 fn toggle_fullscreen(&self);
644 fn is_fullscreen(&self) -> bool;
645 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
646 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
647 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
648 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
649 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
650 fn on_moved(&self, callback: Box<dyn FnMut()>);
651 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
652 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
653 fn on_close(&self, callback: Box<dyn FnOnce()>);
654 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
655 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
656 fn draw(&self, scene: &Scene);
657 fn completed_frame(&self) {}
658 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
659 fn is_subpixel_rendering_supported(&self) -> bool;
660
661 fn get_title(&self) -> String {
663 String::new()
664 }
665 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
666 None
667 }
668 fn tab_bar_visible(&self) -> bool {
669 false
670 }
671 fn set_edited(&mut self, _edited: bool) {}
672 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
673 #[cfg(target_os = "macos")]
674 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
675 fn show_character_palette(&self) {}
676 fn titlebar_double_click(&self) {}
677 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
678 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
679 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
680 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
681 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
682 fn merge_all_windows(&self) {}
683 fn move_tab_to_new_window(&self) {}
684 fn toggle_window_tab_overview(&self) {}
685 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
686
687 #[cfg(target_os = "windows")]
688 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
689
690 fn inner_window_bounds(&self) -> WindowBounds {
692 self.window_bounds()
693 }
694 fn request_decorations(&self, _decorations: WindowDecorations) {}
695 fn show_window_menu(&self, _position: Point<Pixels>) {}
696 fn start_window_move(&self) {}
697 fn start_window_resize(&self, _edge: ResizeEdge) {}
698 fn window_decorations(&self) -> Decorations {
699 Decorations::Server
700 }
701 fn set_app_id(&mut self, _app_id: &str) {}
702 fn map_window(&mut self) -> anyhow::Result<()> {
703 Ok(())
704 }
705 fn window_controls(&self) -> WindowControls {
706 WindowControls::default()
707 }
708 fn set_client_inset(&self, _inset: Pixels) {}
709 fn gpu_specs(&self) -> Option<GpuSpecs>;
710
711 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
712
713 fn play_system_bell(&self) {}
714
715 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
717
718 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
720
721 fn a11y_update_window_bounds(&self) {}
723
724 #[cfg(any(test, feature = "test-support"))]
725 fn as_test(&mut self) -> Option<&mut TestWindow> {
726 None
727 }
728
729 #[cfg(any(test, feature = "test-support"))]
733 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
734 anyhow::bail!("render_to_image not implemented for this platform")
735 }
736}
737
738#[cfg(any(test, feature = "test-support"))]
740pub trait PlatformHeadlessRenderer {
741 fn render_scene_to_image(
743 &mut self,
744 scene: &Scene,
745 size: Size<DevicePixels>,
746 ) -> Result<RgbaImage>;
747
748 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
750}
751
752#[doc(hidden)]
755pub type RunnableVariant = Runnable<RunnableMeta>;
756
757#[doc(hidden)]
758pub type TimerResolutionGuard = open_gpui_core_util::Deferred<Box<dyn FnOnce() + Send>>;
759
760#[doc(hidden)]
761pub enum TasksIncluded {
762 OnlyCompleted,
763 CompletedAndRunning,
764}
765
766#[doc(hidden)]
769pub trait PlatformDispatcher: Send + Sync {
770 fn is_main_thread(&self) -> bool;
771 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
772 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
773 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
774
775 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
776
777 fn now(&self) -> Instant {
778 Instant::now()
779 }
780
781 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
782 open_gpui_core_util::defer(Box::new(|| {}))
783 }
784
785 #[cfg(any(test, feature = "test-support"))]
786 fn as_test(&self) -> Option<&TestDispatcher> {
787 None
788 }
789}
790
791#[expect(missing_docs)]
792pub trait PlatformTextSystem: Send + Sync {
793 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
794 fn all_font_names(&self) -> Vec<String>;
796 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
798 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
800 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
802 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
804 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
806 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
808 fn rasterize_glyph(
810 &self,
811 params: &RenderGlyphParams,
812 raster_bounds: Bounds<DevicePixels>,
813 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
814 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
816 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
818 -> TextRenderingMode;
819 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
821 0
822 }
823}
824
825#[expect(missing_docs)]
826pub struct NoopTextSystem;
827
828#[expect(missing_docs)]
829impl NoopTextSystem {
830 #[allow(dead_code)]
831 pub fn new() -> Self {
832 Self
833 }
834}
835
836impl PlatformTextSystem for NoopTextSystem {
837 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
838 Ok(())
839 }
840
841 fn all_font_names(&self) -> Vec<String> {
842 Vec::new()
843 }
844
845 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
846 Ok(FontId(1))
847 }
848
849 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
850 FontMetrics {
851 units_per_em: 1000,
852 ascent: 1025.0,
853 descent: -275.0,
854 line_gap: 0.0,
855 underline_position: -95.0,
856 underline_thickness: 60.0,
857 cap_height: 698.0,
858 x_height: 516.0,
859 bounding_box: Bounds {
860 origin: Point {
861 x: -260.0,
862 y: -245.0,
863 },
864 size: Size {
865 width: 1501.0,
866 height: 1364.0,
867 },
868 },
869 }
870 }
871
872 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
873 Ok(Bounds {
874 origin: Point { x: 54.0, y: 0.0 },
875 size: size(392.0, 528.0),
876 })
877 }
878
879 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
880 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
881 }
882
883 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
884 Some(GlyphId(ch.len_utf16() as u32))
885 }
886
887 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
888 Ok(Default::default())
889 }
890
891 fn rasterize_glyph(
892 &self,
893 _params: &RenderGlyphParams,
894 raster_bounds: Bounds<DevicePixels>,
895 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
896 Ok((raster_bounds.size, Vec::new()))
897 }
898
899 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
900 let mut position = px(0.);
901 let metrics = self.font_metrics(FontId(0));
902 let em_width = font_size
903 * self
904 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
905 .unwrap()
906 .width
907 / metrics.units_per_em as f32;
908 let mut glyphs = Vec::new();
909 for (ix, c) in text.char_indices() {
910 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
911 glyphs.push(ShapedGlyph {
912 id: glyph,
913 position: point(position, px(0.)),
914 index: ix,
915 is_emoji: glyph.0 == 2,
916 });
917 if glyph.0 == 2 {
918 position += em_width * 2.0;
919 } else {
920 position += em_width;
921 }
922 } else {
923 position += em_width
924 }
925 }
926 let mut runs = Vec::default();
927 if !glyphs.is_empty() {
928 runs.push(ShapedRun {
929 font_id: FontId(0),
930 glyphs,
931 });
932 } else {
933 position = px(0.);
934 }
935
936 LineLayout {
937 font_size,
938 width: position,
939 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
940 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
941 runs,
942 len: text.len(),
943 }
944 }
945
946 fn recommended_rendering_mode(
947 &self,
948 _font_id: FontId,
949 _font_size: Pixels,
950 ) -> TextRenderingMode {
951 TextRenderingMode::Grayscale
952 }
953}
954
955#[allow(dead_code)]
960pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
961 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
962 [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], ];
976
977 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
978 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
979
980 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
981 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
982
983 [
984 ratios[0] * NORM13,
985 ratios[1] * NORM24,
986 ratios[2] * NORM13,
987 ratios[3] * NORM24,
988 ]
989}
990
991#[derive(PartialEq, Eq, Hash, Clone)]
992#[expect(missing_docs)]
993pub enum AtlasKey {
994 Glyph(RenderGlyphParams),
995 Svg(RenderSvgParams),
996 Image(RenderImageParams),
997}
998
999impl AtlasKey {
1000 #[cfg_attr(
1001 all(
1002 any(target_os = "linux", target_os = "freebsd"),
1003 not(any(feature = "x11", feature = "wayland"))
1004 ),
1005 allow(dead_code)
1006 )]
1007 pub fn texture_kind(&self) -> AtlasTextureKind {
1009 match self {
1010 AtlasKey::Glyph(params) => {
1011 if params.is_emoji {
1012 AtlasTextureKind::Polychrome
1013 } else if params.subpixel_rendering {
1014 AtlasTextureKind::Subpixel
1015 } else {
1016 AtlasTextureKind::Monochrome
1017 }
1018 }
1019 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1020 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1021 }
1022 }
1023}
1024
1025impl From<RenderGlyphParams> for AtlasKey {
1026 fn from(params: RenderGlyphParams) -> Self {
1027 Self::Glyph(params)
1028 }
1029}
1030
1031impl From<RenderSvgParams> for AtlasKey {
1032 fn from(params: RenderSvgParams) -> Self {
1033 Self::Svg(params)
1034 }
1035}
1036
1037impl From<RenderImageParams> for AtlasKey {
1038 fn from(params: RenderImageParams) -> Self {
1039 Self::Image(params)
1040 }
1041}
1042
1043#[expect(missing_docs)]
1044pub trait PlatformAtlas {
1045 fn get_or_insert_with<'a>(
1046 &self,
1047 key: &AtlasKey,
1048 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1049 ) -> Result<Option<AtlasTile>>;
1050 fn remove(&self, key: &AtlasKey);
1051}
1052
1053#[doc(hidden)]
1054pub struct AtlasTextureList<T> {
1055 pub textures: Vec<Option<T>>,
1056 pub free_list: Vec<usize>,
1057}
1058
1059impl<T> Default for AtlasTextureList<T> {
1060 fn default() -> Self {
1061 Self {
1062 textures: Vec::default(),
1063 free_list: Vec::default(),
1064 }
1065 }
1066}
1067
1068impl<T> ops::Index<usize> for AtlasTextureList<T> {
1069 type Output = Option<T>;
1070
1071 fn index(&self, index: usize) -> &Self::Output {
1072 &self.textures[index]
1073 }
1074}
1075
1076impl<T> AtlasTextureList<T> {
1077 #[allow(unused)]
1078 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1079 self.free_list.clear();
1080 self.textures.drain(..)
1081 }
1082
1083 #[allow(dead_code)]
1084 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1085 self.textures.iter_mut().flatten()
1086 }
1087}
1088
1089#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1090#[repr(C)]
1091#[expect(missing_docs)]
1092pub struct AtlasTile {
1093 pub texture_id: AtlasTextureId,
1095 pub tile_id: TileId,
1097 pub padding: u32,
1099 pub bounds: Bounds<DevicePixels>,
1101}
1102
1103#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1104#[repr(C)]
1105#[expect(missing_docs)]
1106pub struct AtlasTextureId {
1107 pub index: u32,
1110 pub kind: AtlasTextureKind,
1112}
1113
1114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1115#[repr(C)]
1116#[cfg_attr(
1117 all(
1118 any(target_os = "linux", target_os = "freebsd"),
1119 not(any(feature = "x11", feature = "wayland"))
1120 ),
1121 allow(dead_code)
1122)]
1123#[expect(missing_docs)]
1124pub enum AtlasTextureKind {
1125 Monochrome = 0,
1126 Polychrome = 1,
1127 Subpixel = 2,
1128}
1129
1130#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1131#[repr(C)]
1132#[expect(missing_docs)]
1133pub struct TileId(pub u32);
1134
1135impl From<etagere::AllocId> for TileId {
1136 fn from(id: etagere::AllocId) -> Self {
1137 Self(id.serialize())
1138 }
1139}
1140
1141impl From<TileId> for etagere::AllocId {
1142 fn from(id: TileId) -> Self {
1143 Self::deserialize(id.0)
1144 }
1145}
1146
1147#[expect(missing_docs)]
1148pub struct PlatformInputHandler {
1149 cx: AsyncWindowContext,
1150 handler: Box<dyn InputHandler>,
1151}
1152
1153#[expect(missing_docs)]
1154#[cfg_attr(
1155 all(
1156 any(target_os = "linux", target_os = "freebsd"),
1157 not(any(feature = "x11", feature = "wayland"))
1158 ),
1159 allow(dead_code)
1160)]
1161impl PlatformInputHandler {
1162 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1163 Self { cx, handler }
1164 }
1165
1166 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1167 self.cx
1168 .update(|window, cx| {
1169 self.handler
1170 .selected_text_range(ignore_disabled_input, window, cx)
1171 })
1172 .ok()
1173 .flatten()
1174 }
1175
1176 #[cfg_attr(target_os = "windows", allow(dead_code))]
1177 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1178 self.cx
1179 .update(|window, cx| self.handler.marked_text_range(window, cx))
1180 .ok()
1181 .flatten()
1182 }
1183
1184 #[cfg_attr(
1185 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1186 allow(dead_code)
1187 )]
1188 pub fn text_for_range(
1189 &mut self,
1190 range_utf16: Range<usize>,
1191 adjusted: &mut Option<Range<usize>>,
1192 ) -> Option<String> {
1193 self.cx
1194 .update(|window, cx| {
1195 self.handler
1196 .text_for_range(range_utf16, adjusted, window, cx)
1197 })
1198 .ok()
1199 .flatten()
1200 }
1201
1202 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1203 self.cx
1204 .update(|window, cx| {
1205 self.handler
1206 .replace_text_in_range(replacement_range, text, window, cx);
1207 })
1208 .ok();
1209 }
1210
1211 pub fn replace_and_mark_text_in_range(
1212 &mut self,
1213 range_utf16: Option<Range<usize>>,
1214 new_text: &str,
1215 new_selected_range: Option<Range<usize>>,
1216 ) {
1217 self.cx
1218 .update(|window, cx| {
1219 self.handler.replace_and_mark_text_in_range(
1220 range_utf16,
1221 new_text,
1222 new_selected_range,
1223 window,
1224 cx,
1225 )
1226 })
1227 .ok();
1228 }
1229
1230 #[cfg_attr(target_os = "windows", allow(dead_code))]
1231 pub fn unmark_text(&mut self) {
1232 self.cx
1233 .update(|window, cx| self.handler.unmark_text(window, cx))
1234 .ok();
1235 }
1236
1237 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1238 self.cx
1239 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1240 .ok()
1241 .flatten()
1242 }
1243
1244 #[allow(dead_code)]
1245 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1246 self.handler.apple_press_and_hold_enabled()
1247 }
1248
1249 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1250 self.handler.replace_text_in_range(None, input, window, cx);
1251 }
1252
1253 pub fn compute_ime_candidate_bounds(
1254 marked_range: Option<Range<usize>>,
1255 selection: &UTF16Selection,
1256 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1257 ) -> Option<Bounds<Pixels>> {
1258 if let Some(marked_range) = marked_range {
1259 let mut line_start = marked_range.start;
1261
1262 let caret = selection.range.end;
1266 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1267 for i in (marked_range.start..caret).rev() {
1268 if let Some(b) = bounds_for_range(i..i) {
1269 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1270 line_start = i + 1;
1271 break;
1272 }
1273 }
1274 }
1275 }
1276 bounds_for_range(line_start..line_start)
1277 } else {
1278 let offset = if selection.reversed {
1280 selection.range.start
1281 } else {
1282 selection.range.end
1283 };
1284 bounds_for_range(offset..offset)
1285 }
1286 }
1287
1288 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1289 let marked_range = self.handler.marked_text_range(window, cx);
1290 let selection = self.handler.selected_text_range(true, window, cx)?;
1291 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1292 self.handler.bounds_for_range(range, window, cx)
1293 })
1294 }
1295
1296 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1297 let marked_range = self.marked_text_range();
1298 let selection = self.selected_text_range(true)?;
1299 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1300 self.bounds_for_range(range)
1301 })
1302 }
1303
1304 #[allow(unused)]
1305 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1306 self.cx
1307 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1308 .ok()
1309 .flatten()
1310 }
1311
1312 #[allow(dead_code)]
1313 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1314 self.handler.accepts_text_input(window, cx)
1315 }
1316
1317 #[allow(dead_code)]
1318 pub fn query_accepts_text_input(&mut self) -> bool {
1319 self.cx
1320 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1321 .unwrap_or(true)
1322 }
1323
1324 #[allow(dead_code)]
1325 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1326 self.cx
1327 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1328 .unwrap_or(false)
1329 }
1330}
1331
1332#[derive(Debug)]
1335pub struct UTF16Selection {
1336 pub range: Range<usize>,
1339 pub reversed: bool,
1342}
1343
1344pub trait InputHandler: 'static {
1349 fn selected_text_range(
1354 &mut self,
1355 ignore_disabled_input: bool,
1356 window: &mut Window,
1357 cx: &mut App,
1358 ) -> Option<UTF16Selection>;
1359
1360 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1365
1366 fn text_for_range(
1371 &mut self,
1372 range_utf16: Range<usize>,
1373 adjusted_range: &mut Option<Range<usize>>,
1374 window: &mut Window,
1375 cx: &mut App,
1376 ) -> Option<String>;
1377
1378 fn replace_text_in_range(
1383 &mut self,
1384 replacement_range: Option<Range<usize>>,
1385 text: &str,
1386 window: &mut Window,
1387 cx: &mut App,
1388 );
1389
1390 fn replace_and_mark_text_in_range(
1397 &mut self,
1398 range_utf16: Option<Range<usize>>,
1399 new_text: &str,
1400 new_selected_range: Option<Range<usize>>,
1401 window: &mut Window,
1402 cx: &mut App,
1403 );
1404
1405 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1408
1409 fn bounds_for_range(
1414 &mut self,
1415 range_utf16: Range<usize>,
1416 window: &mut Window,
1417 cx: &mut App,
1418 ) -> Option<Bounds<Pixels>>;
1419
1420 fn character_index_for_point(
1424 &mut self,
1425 point: Point<Pixels>,
1426 window: &mut Window,
1427 cx: &mut App,
1428 ) -> Option<usize>;
1429
1430 #[allow(dead_code)]
1435 fn apple_press_and_hold_enabled(&mut self) -> bool {
1436 true
1437 }
1438
1439 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1441 true
1442 }
1443
1444 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1453 false
1454 }
1455}
1456
1457#[derive(Debug)]
1459pub struct WindowOptions {
1460 pub window_bounds: Option<WindowBounds>,
1464
1465 pub titlebar: Option<TitlebarOptions>,
1467
1468 pub focus: bool,
1470
1471 pub show: bool,
1473
1474 pub kind: WindowKind,
1476
1477 pub is_movable: bool,
1479
1480 pub is_resizable: bool,
1482
1483 pub is_minimizable: bool,
1485
1486 pub display_id: Option<DisplayId>,
1489
1490 pub window_background: WindowBackgroundAppearance,
1492
1493 pub app_id: Option<String>,
1495
1496 pub window_min_size: Option<Size<Pixels>>,
1498
1499 pub window_decorations: Option<WindowDecorations>,
1502
1503 pub icon: Option<Arc<image::RgbaImage>>,
1505
1506 pub tabbing_identifier: Option<String>,
1508}
1509
1510#[derive(Debug)]
1512#[cfg_attr(
1513 all(
1514 any(target_os = "linux", target_os = "freebsd"),
1515 not(any(feature = "x11", feature = "wayland"))
1516 ),
1517 allow(dead_code)
1518)]
1519#[allow(missing_docs)]
1520pub struct WindowParams {
1521 pub bounds: Bounds<Pixels>,
1522
1523 #[cfg_attr(feature = "wayland", allow(dead_code))]
1525 pub titlebar: Option<TitlebarOptions>,
1526
1527 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1529 pub kind: WindowKind,
1530
1531 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1533 pub is_movable: bool,
1534
1535 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1537 pub is_resizable: bool,
1538
1539 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1541 pub is_minimizable: bool,
1542
1543 #[cfg_attr(
1544 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1545 allow(dead_code)
1546 )]
1547 pub focus: bool,
1548
1549 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1550 pub show: bool,
1551
1552 #[cfg_attr(feature = "wayland", allow(dead_code))]
1554 pub icon: Option<Arc<image::RgbaImage>>,
1555
1556 #[cfg_attr(feature = "wayland", allow(dead_code))]
1557 pub display_id: Option<DisplayId>,
1558
1559 pub window_min_size: Option<Size<Pixels>>,
1560 #[cfg(target_os = "macos")]
1561 pub tabbing_identifier: Option<String>,
1562}
1563
1564#[derive(Debug, Copy, Clone, PartialEq)]
1566pub enum WindowBounds {
1567 Windowed(Bounds<Pixels>),
1569 Maximized(Bounds<Pixels>),
1572 Fullscreen(Bounds<Pixels>),
1575}
1576
1577impl Default for WindowBounds {
1578 fn default() -> Self {
1579 WindowBounds::Windowed(Bounds::default())
1580 }
1581}
1582
1583impl WindowBounds {
1584 pub fn get_bounds(&self) -> Bounds<Pixels> {
1586 match self {
1587 WindowBounds::Windowed(bounds) => *bounds,
1588 WindowBounds::Maximized(bounds) => *bounds,
1589 WindowBounds::Fullscreen(bounds) => *bounds,
1590 }
1591 }
1592
1593 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1595 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1596 }
1597}
1598
1599impl Default for WindowOptions {
1600 fn default() -> Self {
1601 Self {
1602 window_bounds: None,
1603 titlebar: Some(TitlebarOptions {
1604 title: Default::default(),
1605 appears_transparent: Default::default(),
1606 traffic_light_position: Default::default(),
1607 }),
1608 focus: true,
1609 show: true,
1610 kind: WindowKind::Normal,
1611 is_movable: true,
1612 is_resizable: true,
1613 is_minimizable: true,
1614 display_id: None,
1615 window_background: WindowBackgroundAppearance::default(),
1616 icon: None,
1617 app_id: None,
1618 window_min_size: None,
1619 window_decorations: None,
1620 tabbing_identifier: None,
1621 }
1622 }
1623}
1624
1625#[derive(Debug, Default)]
1627pub struct TitlebarOptions {
1628 pub title: Option<SharedString>,
1630
1631 pub appears_transparent: bool,
1634
1635 pub traffic_light_position: Option<Point<Pixels>>,
1637}
1638
1639#[derive(Clone, Debug, PartialEq, Eq)]
1641pub enum WindowKind {
1642 Normal,
1644
1645 PopUp,
1648
1649 Floating,
1651
1652 #[cfg(all(target_os = "linux", feature = "wayland"))]
1655 LayerShell(layer_shell::LayerShellOptions),
1656
1657 Dialog,
1660}
1661
1662#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1667pub enum WindowAppearance {
1668 #[default]
1672 Light,
1673
1674 VibrantLight,
1678
1679 Dark,
1683
1684 VibrantDark,
1688}
1689
1690#[derive(Copy, Clone, Debug, Default, PartialEq)]
1693pub enum WindowBackgroundAppearance {
1694 #[default]
1702 Opaque,
1703 Transparent,
1705 Blurred,
1709 MicaBackdrop,
1711 MicaAltBackdrop,
1713}
1714
1715#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1717pub enum TextRenderingMode {
1718 #[default]
1720 PlatformDefault,
1721 Subpixel,
1723 Grayscale,
1725}
1726
1727#[derive(Clone, Debug)]
1729pub struct PathPromptOptions {
1730 pub files: bool,
1732 pub directories: bool,
1734 pub multiple: bool,
1736 pub prompt: Option<SharedString>,
1738}
1739
1740#[derive(Copy, Clone, Debug, PartialEq)]
1742pub enum PromptLevel {
1743 Info,
1745
1746 Warning,
1748
1749 Critical,
1751}
1752
1753#[derive(Clone, Debug, PartialEq)]
1755pub enum PromptButton {
1756 Ok(SharedString),
1758 Cancel(SharedString),
1760 Other(SharedString),
1762}
1763
1764impl PromptButton {
1765 pub fn new(label: impl Into<SharedString>) -> Self {
1767 PromptButton::Other(label.into())
1768 }
1769
1770 pub fn ok(label: impl Into<SharedString>) -> Self {
1772 PromptButton::Ok(label.into())
1773 }
1774
1775 pub fn cancel(label: impl Into<SharedString>) -> Self {
1777 PromptButton::Cancel(label.into())
1778 }
1779
1780 #[allow(dead_code)]
1782 pub fn is_cancel(&self) -> bool {
1783 matches!(self, PromptButton::Cancel(_))
1784 }
1785
1786 pub fn label(&self) -> &SharedString {
1788 match self {
1789 PromptButton::Ok(label) => label,
1790 PromptButton::Cancel(label) => label,
1791 PromptButton::Other(label) => label,
1792 }
1793 }
1794}
1795
1796impl From<&str> for PromptButton {
1797 fn from(value: &str) -> Self {
1798 match value.to_lowercase().as_str() {
1799 "ok" => PromptButton::Ok("Ok".into()),
1800 "cancel" => PromptButton::Cancel("Cancel".into()),
1801 _ => PromptButton::Other(SharedString::from(value.to_owned())),
1802 }
1803 }
1804}
1805
1806#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1808pub enum CursorStyle {
1809 #[default]
1811 Arrow,
1812
1813 IBeam,
1816
1817 Crosshair,
1820
1821 ClosedHand,
1824
1825 OpenHand,
1828
1829 PointingHand,
1832
1833 ResizeLeft,
1836
1837 ResizeRight,
1840
1841 ResizeLeftRight,
1844
1845 ResizeUp,
1848
1849 ResizeDown,
1852
1853 ResizeUpDown,
1856
1857 ResizeUpLeftDownRight,
1860
1861 ResizeUpRightDownLeft,
1864
1865 ResizeColumn,
1868
1869 ResizeRow,
1872
1873 IBeamCursorForVerticalLayout,
1876
1877 OperationNotAllowed,
1880
1881 DragLink,
1884
1885 DragCopy,
1888
1889 ContextualMenu,
1892}
1893
1894#[derive(Clone, Debug, Eq, PartialEq)]
1896pub struct ClipboardItem {
1897 pub entries: Vec<ClipboardEntry>,
1899}
1900
1901#[derive(Clone, Debug, Eq, PartialEq)]
1903pub enum ClipboardEntry {
1904 String(ClipboardString),
1906 Image(Image),
1908 ExternalPaths(crate::ExternalPaths),
1910}
1911
1912impl ClipboardItem {
1913 pub fn new_string(text: String) -> Self {
1915 Self {
1916 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
1917 }
1918 }
1919
1920 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
1922 Self {
1923 entries: vec![ClipboardEntry::String(ClipboardString {
1924 text,
1925 metadata: Some(metadata),
1926 })],
1927 }
1928 }
1929
1930 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
1932 Self {
1933 entries: vec![ClipboardEntry::String(
1934 ClipboardString::new(text).with_json_metadata(metadata),
1935 )],
1936 }
1937 }
1938
1939 pub fn new_image(image: &Image) -> Self {
1941 Self {
1942 entries: vec![ClipboardEntry::Image(image.clone())],
1943 }
1944 }
1945
1946 pub fn text(&self) -> Option<String> {
1949 let mut answer = String::new();
1950
1951 for entry in self.entries.iter() {
1952 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
1953 answer.push_str(text);
1954 }
1955 }
1956
1957 if answer.is_empty() {
1958 for entry in self.entries.iter() {
1959 if let ClipboardEntry::ExternalPaths(paths) = entry {
1960 for path in &paths.0 {
1961 use std::fmt::Write as _;
1962 _ = write!(answer, "{}", path.display());
1963 }
1964 }
1965 }
1966 }
1967
1968 if !answer.is_empty() {
1969 Some(answer)
1970 } else {
1971 None
1972 }
1973 }
1974
1975 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
1977 pub fn metadata(&self) -> Option<&String> {
1978 match self.entries().first() {
1979 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
1980 clipboard_string.metadata.as_ref()
1981 }
1982 _ => None,
1983 }
1984 }
1985
1986 pub fn entries(&self) -> &[ClipboardEntry] {
1988 &self.entries
1989 }
1990
1991 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
1993 self.entries.into_iter()
1994 }
1995}
1996
1997impl From<ClipboardString> for ClipboardEntry {
1998 fn from(value: ClipboardString) -> Self {
1999 Self::String(value)
2000 }
2001}
2002
2003impl From<String> for ClipboardEntry {
2004 fn from(value: String) -> Self {
2005 Self::from(ClipboardString::from(value))
2006 }
2007}
2008
2009impl From<Image> for ClipboardEntry {
2010 fn from(value: Image) -> Self {
2011 Self::Image(value)
2012 }
2013}
2014
2015impl From<ClipboardEntry> for ClipboardItem {
2016 fn from(value: ClipboardEntry) -> Self {
2017 Self {
2018 entries: vec![value],
2019 }
2020 }
2021}
2022
2023impl From<String> for ClipboardItem {
2024 fn from(value: String) -> Self {
2025 Self::from(ClipboardEntry::from(value))
2026 }
2027}
2028
2029impl From<Image> for ClipboardItem {
2030 fn from(value: Image) -> Self {
2031 Self::from(ClipboardEntry::from(value))
2032 }
2033}
2034
2035#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2037pub enum ImageFormat {
2038 Png,
2043 Jpeg,
2045 Webp,
2047 Gif,
2049 Svg,
2051 Bmp,
2053 Tiff,
2055 Ico,
2057 Pnm,
2059}
2060
2061impl ImageFormat {
2062 pub const fn mime_type(self) -> &'static str {
2064 match self {
2065 ImageFormat::Png => "image/png",
2066 ImageFormat::Jpeg => "image/jpeg",
2067 ImageFormat::Webp => "image/webp",
2068 ImageFormat::Gif => "image/gif",
2069 ImageFormat::Svg => "image/svg+xml",
2070 ImageFormat::Bmp => "image/bmp",
2071 ImageFormat::Tiff => "image/tiff",
2072 ImageFormat::Ico => "image/ico",
2073 ImageFormat::Pnm => "image/x-portable-anymap",
2074 }
2075 }
2076
2077 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2079 use strum::IntoEnumIterator;
2080 Self::iter()
2081 .find(|format| format.mime_type() == mime_type)
2082 .or_else(|| Self::from_mime_type_alias(mime_type))
2083 }
2084
2085 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2089 match mime_type {
2090 "image/jpg" => Some(Self::Jpeg),
2091 "image/tif" => Some(Self::Tiff),
2092 _ => None,
2093 }
2094 }
2095}
2096
2097#[derive(Clone, Debug, PartialEq, Eq)]
2099pub struct Image {
2100 pub format: ImageFormat,
2102 pub bytes: Vec<u8>,
2104 pub id: u64,
2106}
2107
2108impl Hash for Image {
2109 fn hash<H: Hasher>(&self, state: &mut H) {
2110 state.write_u64(self.id);
2111 }
2112}
2113
2114impl Image {
2115 pub fn empty() -> Self {
2117 Self::from_bytes(ImageFormat::Png, Vec::new())
2118 }
2119
2120 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2122 Self {
2123 id: hash(&bytes),
2124 format,
2125 bytes,
2126 }
2127 }
2128
2129 pub fn id(&self) -> u64 {
2131 self.id
2132 }
2133
2134 pub fn use_render_image(
2136 self: Arc<Self>,
2137 window: &mut Window,
2138 cx: &mut App,
2139 ) -> Option<Arc<RenderImage>> {
2140 ImageSource::Image(self)
2141 .use_data(None, window, cx)
2142 .and_then(|result| result.ok())
2143 }
2144
2145 pub fn get_render_image(
2147 self: Arc<Self>,
2148 window: &mut Window,
2149 cx: &mut App,
2150 ) -> Option<Arc<RenderImage>> {
2151 ImageSource::Image(self)
2152 .get_data(None, window, cx)
2153 .and_then(|result| result.ok())
2154 }
2155
2156 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2158 ImageSource::Image(self).remove_asset(cx);
2159 }
2160
2161 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2163 fn frames_for_image(
2164 bytes: &[u8],
2165 format: image::ImageFormat,
2166 ) -> Result<SmallVec<[Frame; 1]>> {
2167 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2168
2169 for pixel in data.chunks_exact_mut(4) {
2171 pixel.swap(0, 2);
2172 }
2173
2174 Ok(SmallVec::from_elem(Frame::new(data), 1))
2175 }
2176
2177 let frames = match self.format {
2178 ImageFormat::Gif => {
2179 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2180 let mut frames = SmallVec::new();
2181
2182 for frame in decoder.into_frames() {
2183 match frame {
2184 Ok(mut frame) => {
2185 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2187 pixel.swap(0, 2);
2188 }
2189 frames.push(frame);
2190 }
2191 Err(err) => {
2192 log::debug!("Skipping GIF frame due to decode error: {err}");
2193 }
2194 }
2195 }
2196
2197 if frames.is_empty() {
2198 anyhow::bail!("GIF could not be decoded: all frames failed");
2199 }
2200
2201 frames
2202 }
2203 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2204 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2205 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2206 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2207 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2208 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2209 ImageFormat::Svg => {
2210 return svg_renderer
2211 .render_single_frame(&self.bytes, 1.0)
2212 .map_err(Into::into);
2213 }
2214 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2215 };
2216
2217 Ok(Arc::new(RenderImage::new(frames)))
2218 }
2219
2220 pub fn format(&self) -> ImageFormat {
2222 self.format
2223 }
2224
2225 pub fn bytes(&self) -> &[u8] {
2227 self.bytes.as_slice()
2228 }
2229}
2230
2231#[derive(Clone, Debug, Eq, PartialEq)]
2233pub struct ClipboardString {
2234 pub text: String,
2236 pub metadata: Option<String>,
2238}
2239
2240impl ClipboardString {
2241 pub fn new(text: String) -> Self {
2243 Self {
2244 text,
2245 metadata: None,
2246 }
2247 }
2248
2249 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2252 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2253 self
2254 }
2255
2256 pub fn text(&self) -> &String {
2258 &self.text
2259 }
2260
2261 pub fn into_text(self) -> String {
2263 self.text
2264 }
2265
2266 pub fn metadata_json<T>(&self) -> Option<T>
2268 where
2269 T: for<'a> Deserialize<'a>,
2270 {
2271 self.metadata
2272 .as_ref()
2273 .and_then(|m| serde_json::from_str(m).ok())
2274 }
2275
2276 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2277 pub fn text_hash(text: &str) -> u64 {
2279 let mut hasher = SeaHasher::new();
2280 text.hash(&mut hasher);
2281 hasher.finish()
2282 }
2283}
2284
2285impl From<String> for ClipboardString {
2286 fn from(value: String) -> Self {
2287 Self {
2288 text: value,
2289 metadata: None,
2290 }
2291 }
2292}
2293
2294#[cfg(test)]
2295mod image_tests {
2296 use super::*;
2297 use std::sync::Arc;
2298
2299 #[test]
2300 fn test_svg_image_to_image_data_converts_to_bgra() {
2301 let image = Image::from_bytes(
2302 ImageFormat::Svg,
2303 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2304<rect width="1" height="1" fill="#38BDF8"/>
2305</svg>"##
2306 .to_vec(),
2307 );
2308
2309 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2310 let bytes = render_image.as_bytes(0).unwrap();
2311
2312 for pixel in bytes.chunks_exact(4) {
2313 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2314 }
2315 }
2316}
2317
2318#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2319mod tests {
2320 use super::*;
2321 use std::collections::HashSet;
2322
2323 #[test]
2324 fn test_window_button_layout_parse_standard() {
2325 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2326 assert_eq!(
2327 layout.left,
2328 [
2329 Some(WindowButton::Close),
2330 Some(WindowButton::Minimize),
2331 None
2332 ]
2333 );
2334 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2335 }
2336
2337 #[test]
2338 fn test_window_button_layout_parse_right_only() {
2339 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2340 assert_eq!(layout.left, [None, None, None]);
2341 assert_eq!(
2342 layout.right,
2343 [
2344 Some(WindowButton::Minimize),
2345 Some(WindowButton::Maximize),
2346 Some(WindowButton::Close)
2347 ]
2348 );
2349 }
2350
2351 #[test]
2352 fn test_window_button_layout_parse_left_only() {
2353 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2354 assert_eq!(
2355 layout.left,
2356 [
2357 Some(WindowButton::Close),
2358 Some(WindowButton::Minimize),
2359 Some(WindowButton::Maximize)
2360 ]
2361 );
2362 assert_eq!(layout.right, [None, None, None]);
2363 }
2364
2365 #[test]
2366 fn test_window_button_layout_parse_with_whitespace() {
2367 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2368 assert_eq!(
2369 layout.left,
2370 [
2371 Some(WindowButton::Close),
2372 Some(WindowButton::Minimize),
2373 None
2374 ]
2375 );
2376 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2377 }
2378
2379 #[test]
2380 fn test_window_button_layout_parse_empty() {
2381 let layout = WindowButtonLayout::parse("").unwrap();
2382 assert_eq!(layout.left, [None, None, None]);
2383 assert_eq!(layout.right, [None, None, None]);
2384 }
2385
2386 #[test]
2387 fn test_window_button_layout_parse_intentionally_empty() {
2388 let layout = WindowButtonLayout::parse(":").unwrap();
2389 assert_eq!(layout.left, [None, None, None]);
2390 assert_eq!(layout.right, [None, None, None]);
2391 }
2392
2393 #[test]
2394 fn test_window_button_layout_parse_invalid_buttons() {
2395 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2396 assert_eq!(
2397 layout.left,
2398 [
2399 Some(WindowButton::Close),
2400 Some(WindowButton::Minimize),
2401 None
2402 ]
2403 );
2404 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2405 }
2406
2407 #[test]
2408 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2409 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2410 assert_eq!(
2411 layout.right,
2412 [
2413 Some(WindowButton::Close),
2414 Some(WindowButton::Minimize),
2415 None
2416 ]
2417 );
2418 assert_eq!(layout.format(), ":close,minimize");
2419 }
2420
2421 #[test]
2422 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2423 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2424 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2425 assert_eq!(
2426 layout.right,
2427 [
2428 Some(WindowButton::Maximize),
2429 Some(WindowButton::Minimize),
2430 None
2431 ]
2432 );
2433
2434 let button_ids: Vec<_> = layout
2435 .left
2436 .iter()
2437 .chain(layout.right.iter())
2438 .flatten()
2439 .map(WindowButton::id)
2440 .collect();
2441 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2442 assert_eq!(unique_button_ids.len(), button_ids.len());
2443 assert_eq!(layout.format(), "close:maximize,minimize");
2444 }
2445
2446 #[test]
2447 fn test_window_button_layout_parse_gnome_style() {
2448 let layout = WindowButtonLayout::parse("close").unwrap();
2449 assert_eq!(layout.left, [None, None, None]);
2450 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2451 }
2452
2453 #[test]
2454 fn test_window_button_layout_parse_elementary_style() {
2455 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2456 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2457 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2458 }
2459
2460 #[test]
2461 fn test_window_button_layout_round_trip() {
2462 let cases = [
2463 "close:minimize,maximize",
2464 "minimize,maximize,close:",
2465 ":close",
2466 "close:",
2467 "close:maximize",
2468 ":",
2469 ];
2470
2471 for case in cases {
2472 let layout = WindowButtonLayout::parse(case).unwrap();
2473 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2474 }
2475 }
2476
2477 #[test]
2478 fn test_window_button_layout_linux_default() {
2479 let layout = WindowButtonLayout::linux_default();
2480 assert_eq!(layout.left, [None, None, None]);
2481 assert_eq!(
2482 layout.right,
2483 [
2484 Some(WindowButton::Minimize),
2485 Some(WindowButton::Maximize),
2486 Some(WindowButton::Close)
2487 ]
2488 );
2489
2490 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2491 assert_eq!(round_tripped, layout);
2492 }
2493
2494 #[test]
2495 fn test_window_button_layout_parse_all_invalid() {
2496 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2497 }
2498}