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
9pub mod popup;
11
12#[cfg(any(test, feature = "test-support"))]
13mod threaded_dispatcher;
14
15#[cfg(any(test, feature = "test-support"))]
16mod test;
17
18#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
19mod visual_test;
20
21#[cfg(all(
22 feature = "screen-capture",
23 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
24))]
25pub mod scap_screen_capture;
26
27#[cfg(all(
28 any(target_os = "windows", target_os = "linux"),
29 feature = "screen-capture"
30))]
31pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
32#[cfg(not(feature = "screen-capture"))]
33pub(crate) type PlatformScreenCaptureFrame = ();
34#[cfg(all(target_os = "macos", feature = "screen-capture"))]
35pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
36
37use crate::{
38 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
39 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
40 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
41 LineLayout, Pixels, PlatformGestures, PlatformInput, Point, Priority, RenderGlyphParams,
42 RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString,
43 Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size,
44};
45#[cfg(any(target_os = "linux", target_os = "freebsd"))]
46use anyhow::bail;
47use anyhow::{Context as _, Result};
48use async_task::Runnable;
49use futures::channel::oneshot;
50#[cfg(any(test, feature = "test-support"))]
51use image::RgbaImage;
52use image::codecs::gif::GifDecoder;
53use image::{AnimationDecoder as _, DynamicImage, Frame};
54use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
55use scheduler::Instant;
56pub use scheduler::RunnableMeta;
57use schemars::JsonSchema;
58use seahash::SeaHasher;
59use serde::{Deserialize, Serialize};
60use smallvec::SmallVec;
61use std::borrow::Cow;
62use std::hash::{Hash, Hasher};
63use std::io::Cursor;
64use std::ops;
65use std::time::Duration;
66use std::{
67 any::Any,
68 fmt::{self, Debug},
69 ops::Range,
70 path::{Path, PathBuf},
71 rc::Rc,
72 sync::Arc,
73};
74
75pub trait PlatformNativeSurface {
78 fn set_bounds(&self, bounds: Bounds<DevicePixels>) -> Result<()>;
80 fn set_visible(&self, visible: bool) -> Result<()>;
82 fn platform_handle(&self) -> Box<dyn Any>;
85}
86use strum::EnumIter;
87use uuid::Uuid;
88
89pub use app_menu::*;
90pub use keyboard::*;
91pub use keystroke::*;
92
93#[cfg(any(test, feature = "test-support"))]
94pub(crate) use test::*;
95
96#[cfg(any(test, feature = "test-support"))]
97pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
98
99#[cfg(any(test, feature = "test-support"))]
100pub use threaded_dispatcher::ThreadedDispatcher;
101
102#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
103pub use visual_test::VisualTestPlatform;
104
105#[cfg(any(target_os = "linux", target_os = "freebsd"))]
109#[inline]
110pub fn guess_compositor() -> &'static str {
111 if std::env::var_os("ZED_HEADLESS").is_some() {
112 return "Headless";
113 }
114
115 #[cfg(feature = "wayland")]
116 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
117 #[cfg(not(feature = "wayland"))]
118 let wayland_display: Option<std::ffi::OsString> = None;
119
120 #[cfg(feature = "x11")]
121 let x11_display = std::env::var_os("DISPLAY");
122 #[cfg(not(feature = "x11"))]
123 let x11_display: Option<std::ffi::OsString> = None;
124
125 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
126 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
127
128 if use_wayland {
129 "Wayland"
130 } else if use_x11 {
131 "X11"
132 } else {
133 "Headless"
134 }
135}
136
137#[expect(missing_docs)]
138pub trait Platform: 'static {
139 fn background_executor(&self) -> BackgroundExecutor;
140 fn foreground_executor(&self) -> ForegroundExecutor;
141 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
142
143 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
144 fn quit(&self);
145 fn restart(&self, binary_path: Option<PathBuf>);
146 fn activate(&self, ignoring_other_apps: bool);
147 fn hide(&self);
148 fn hide_other_apps(&self);
149 fn unhide_other_apps(&self);
150
151 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
152 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
153 fn active_window(&self) -> Option<AnyWindowHandle>;
154 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
155 None
156 }
157
158 fn is_screen_capture_supported(&self) -> bool {
159 false
160 }
161
162 fn screen_capture_sources(
163 &self,
164 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
165 let (sources_tx, sources_rx) = oneshot::channel();
166 sources_tx
167 .send(Err(anyhow::anyhow!(
168 "gpui was compiled without the screen-capture feature"
169 )))
170 .ok();
171 sources_rx
172 }
173
174 fn open_window(
175 &self,
176 handle: AnyWindowHandle,
177 options: WindowParams,
178 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
179
180 fn window_appearance(&self) -> WindowAppearance;
182
183 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
192
193 fn button_layout(&self) -> Option<WindowButtonLayout> {
195 None
196 }
197
198 fn open_url(&self, url: &str);
199 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
200 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
201
202 fn prompt_for_paths(
203 &self,
204 options: PathPromptOptions,
205 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
206 fn prompt_for_new_path(
207 &self,
208 directory: &Path,
209 suggested_name: Option<&str>,
210 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
211 fn can_select_mixed_files_and_dirs(&self) -> bool;
212 fn reveal_path(&self, path: &Path);
213 fn open_with_system(&self, path: &Path);
214
215 fn on_quit(&self, callback: Box<dyn FnMut()>);
216 fn on_reopen(&self, callback: Box<dyn FnMut()>);
217 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
218
219 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
229
230 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
235
236 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
240 None
241 }
242
243 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
244 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
245 None
246 }
247
248 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
249 fn perform_dock_menu_action(&self, _action: usize) {}
250 fn add_recent_document(&self, _path: &Path) {}
251 fn update_jump_list(
252 &self,
253 _menus: Vec<MenuItem>,
254 _entries: Vec<SmallVec<[PathBuf; 2]>>,
255 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
256 Task::ready(Vec::new())
257 }
258 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
259 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
260 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
261
262 fn thermal_state(&self) -> ThermalState;
263 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
264
265 fn set_app_identity(&self, identifier: &str, name: &str) {
272 _ = (identifier, name);
273 }
274
275 fn show_system_notification(&self, notification: SystemNotification) {
282 _ = notification;
283 }
284
285 fn dismiss_system_notification(&self, tag: &str) {
290 _ = tag;
291 }
292
293 fn on_system_notification_response(
299 &self,
300 callback: Box<dyn FnMut(SystemNotificationResponse)>,
301 ) {
302 _ = callback;
303 }
304
305 fn compositor_name(&self) -> &'static str {
306 ""
307 }
308 fn app_path(&self) -> Result<PathBuf>;
309 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
310
311 fn set_cursor_style(&self, style: CursorStyle);
312
313 fn hide_cursor_until_mouse_moves(&self);
316
317 fn is_cursor_visible(&self) -> bool;
319
320 fn should_auto_hide_scrollbars(&self) -> bool;
321
322 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
323 fn write_to_clipboard(&self, item: ClipboardItem);
324
325 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
326 fn read_from_primary(&self) -> Option<ClipboardItem>;
327 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
328 fn write_to_primary(&self, item: ClipboardItem);
329
330 #[cfg(target_os = "macos")]
331 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
332 #[cfg(target_os = "macos")]
333 fn write_to_find_pasteboard(&self, item: ClipboardItem);
334
335 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
336 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
337 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
338
339 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
340 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
341 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
342}
343
344pub trait PlatformDisplay: Debug {
346 fn id(&self) -> DisplayId;
348
349 fn uuid(&self) -> Result<Uuid>;
352
353 fn bounds(&self) -> Bounds<Pixels>;
355
356 fn visible_bounds(&self) -> Bounds<Pixels> {
360 self.bounds()
361 }
362
363 fn default_bounds(&self) -> Bounds<Pixels> {
365 let bounds = self.bounds();
366 let center = bounds.center();
367 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
368
369 let offset = clipped_window_size / 2.0;
370 let origin = point(center.x - offset.width, center.y - offset.height);
371 Bounds::new(origin, clipped_window_size)
372 }
373}
374
375#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct SystemNotification {
379 pub tag: SharedString,
383 pub title: SharedString,
385 pub body: SharedString,
387 pub actions: Vec<SystemNotificationAction>,
390}
391
392#[derive(Clone, Debug, PartialEq, Eq, Hash)]
394pub struct SystemNotificationAction {
395 pub id: SharedString,
398 pub label: SharedString,
400}
401
402#[derive(Clone, Debug, PartialEq, Eq)]
404pub struct SystemNotificationResponse {
405 pub tag: SharedString,
407 pub action_id: Option<SharedString>,
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub enum ThermalState {
415 Nominal,
417 Fair,
419 Serious,
421 Critical,
423}
424
425#[derive(Clone)]
427pub struct SourceMetadata {
428 pub id: u64,
430 pub label: Option<SharedString>,
432 pub is_main: Option<bool>,
434 pub resolution: Size<DevicePixels>,
436}
437
438pub trait ScreenCaptureSource {
440 fn metadata(&self) -> Result<SourceMetadata>;
442
443 fn stream(
446 &self,
447 foreground_executor: &ForegroundExecutor,
448 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
449 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
450}
451
452pub trait ScreenCaptureStream {
454 fn metadata(&self) -> Result<SourceMetadata>;
456}
457
458pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
460
461#[derive(PartialEq, Eq, Hash, Copy, Clone)]
463pub struct DisplayId(pub(crate) u64);
464
465impl DisplayId {
466 pub fn new(id: u64) -> Self {
468 Self(id)
469 }
470}
471
472impl From<u64> for DisplayId {
473 fn from(id: u64) -> Self {
474 Self(id)
475 }
476}
477
478impl From<DisplayId> for u64 {
479 fn from(id: DisplayId) -> Self {
480 id.0
481 }
482}
483
484impl Debug for DisplayId {
485 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486 write!(f, "DisplayId({})", self.0)
487 }
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
492pub enum ResizeEdge {
493 Top,
495 TopRight,
497 Right,
499 BottomRight,
501 Bottom,
503 BottomLeft,
505 Left,
507 TopLeft,
509}
510
511#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
513pub enum WindowDecorations {
514 #[default]
515 Server,
517 Client,
519}
520
521#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
523pub enum Decorations {
524 #[default]
526 Server,
527 Client {
529 tiling: Tiling,
531 },
532}
533
534#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
536pub struct WindowControls {
537 pub fullscreen: bool,
539 pub maximize: bool,
541 pub minimize: bool,
543 pub window_menu: bool,
545}
546
547impl Default for WindowControls {
548 fn default() -> Self {
549 Self {
551 fullscreen: true,
552 maximize: true,
553 minimize: true,
554 window_menu: true,
555 }
556 }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
561pub enum WindowButton {
562 Minimize,
564 Maximize,
566 Close,
568}
569
570impl WindowButton {
571 pub fn id(&self) -> &'static str {
573 match self {
574 WindowButton::Minimize => "minimize",
575 WindowButton::Maximize => "maximize",
576 WindowButton::Close => "close",
577 }
578 }
579
580 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
581 fn index(&self) -> usize {
582 match self {
583 WindowButton::Minimize => 0,
584 WindowButton::Maximize => 1,
585 WindowButton::Close => 2,
586 }
587 }
588}
589
590pub const MAX_BUTTONS_PER_SIDE: usize = 3;
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub struct WindowButtonLayout {
600 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
602 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
604}
605
606#[cfg(any(target_os = "linux", target_os = "freebsd"))]
607impl WindowButtonLayout {
608 pub fn linux_default() -> Self {
610 Self {
611 left: [None; MAX_BUTTONS_PER_SIDE],
612 right: [
613 Some(WindowButton::Minimize),
614 Some(WindowButton::Maximize),
615 Some(WindowButton::Close),
616 ],
617 }
618 }
619
620 pub fn parse(layout_string: &str) -> Result<Self> {
622 fn parse_side(
623 s: &str,
624 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
625 unrecognized: &mut Vec<String>,
626 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
627 let mut result = [None; MAX_BUTTONS_PER_SIDE];
628 let mut i = 0;
629 for name in s.split(',') {
630 let trimmed = name.trim();
631 if trimmed.is_empty() {
632 continue;
633 }
634 let button = match trimmed {
635 "minimize" => Some(WindowButton::Minimize),
636 "maximize" => Some(WindowButton::Maximize),
637 "close" => Some(WindowButton::Close),
638 other => {
639 unrecognized.push(other.to_string());
640 None
641 }
642 };
643 if let Some(button) = button {
644 if seen_buttons[button.index()] {
645 continue;
646 }
647 if let Some(slot) = result.get_mut(i) {
648 *slot = Some(button);
649 seen_buttons[button.index()] = true;
650 i += 1;
651 }
652 }
653 }
654 result
655 }
656
657 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
658 let mut unrecognized = Vec::new();
659 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
660 let layout = Self {
661 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
662 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
663 };
664
665 if !unrecognized.is_empty()
666 && layout.left.iter().all(Option::is_none)
667 && layout.right.iter().all(Option::is_none)
668 {
669 bail!(
670 "button layout string {:?} contains no valid buttons (unrecognized: {})",
671 layout_string,
672 unrecognized.join(", ")
673 );
674 }
675
676 Ok(layout)
677 }
678
679 #[cfg(test)]
681 pub fn format(&self) -> String {
682 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
683 buttons
684 .iter()
685 .flatten()
686 .map(|button| match button {
687 WindowButton::Minimize => "minimize",
688 WindowButton::Maximize => "maximize",
689 WindowButton::Close => "close",
690 })
691 .collect::<Vec<_>>()
692 .join(",")
693 }
694
695 format!("{}:{}", format_side(&self.left), format_side(&self.right))
696 }
697}
698
699#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
701pub struct Tiling {
702 pub top: bool,
704 pub left: bool,
706 pub right: bool,
708 pub bottom: bool,
710}
711
712impl Tiling {
713 pub fn tiled() -> Self {
715 Self {
716 top: true,
717 left: true,
718 right: true,
719 bottom: true,
720 }
721 }
722
723 pub fn is_tiled(&self) -> bool {
725 self.top || self.left || self.right || self.bottom
726 }
727}
728
729pub struct A11yCallbacks {
731 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
733 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
735 pub deactivation: Box<dyn Fn() + Send + 'static>,
737}
738
739#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
740#[expect(missing_docs)]
741pub struct RequestFrameOptions {
742 pub require_presentation: bool,
744 pub force_render: bool,
746}
747
748#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
761pub enum AppLifecyclePhase {
762 Active,
764 Inactive,
766 Background,
769 Foreground,
771}
772
773#[derive(Debug, Clone, Default, PartialEq)]
780pub struct WindowInsets {
781 pub safe_area: Edges<Pixels>,
786 pub ime: Edges<Pixels>,
790}
791
792impl WindowInsets {
793 pub fn effective(&self) -> Edges<Pixels> {
795 Edges {
796 top: self.safe_area.top.max(self.ime.top),
797 right: self.safe_area.right.max(self.ime.right),
798 bottom: self.safe_area.bottom.max(self.ime.bottom),
799 left: self.safe_area.left.max(self.ime.left),
800 }
801 }
802}
803
804#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
806pub enum TextInputStateChange {
807 FocusGained,
809 FocusLost,
811 SelectionChanged,
813 ContentChanged,
815}
816
817#[expect(missing_docs)]
818pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
819 fn bounds(&self) -> Bounds<Pixels>;
820 fn is_maximized(&self) -> bool;
821 fn window_bounds(&self) -> WindowBounds;
822 fn content_size(&self) -> Size<Pixels>;
823 fn resize(&mut self, size: Size<Pixels>);
824 fn scale_factor(&self) -> f32;
825 fn appearance(&self) -> WindowAppearance;
826 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
827 fn mouse_position(&self) -> Point<Pixels>;
828 fn modifiers(&self) -> Modifiers;
829 fn capslock(&self) -> Capslock;
830 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
831 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
832 fn prompt(
833 &self,
834 level: PromptLevel,
835 msg: &str,
836 detail: Option<&str>,
837 answers: &[PromptButton],
838 ) -> Option<oneshot::Receiver<usize>>;
839 fn activate(&self);
840 fn request_attention(&self) {}
842 fn is_active(&self) -> bool;
843 fn is_hovered(&self) -> bool;
844 fn background_appearance(&self) -> WindowBackgroundAppearance;
845 fn set_title(&mut self, title: &str);
846 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
847 fn minimize(&self);
848 fn zoom(&self);
849 fn toggle_fullscreen(&self);
850 fn is_fullscreen(&self) -> bool;
851 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
852 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
853 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
854 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
855 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
856 fn on_moved(&self, callback: Box<dyn FnMut()>);
857 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
858 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
859 fn on_close(&self, callback: Box<dyn FnOnce()>);
860 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
861 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
862 fn draw(&self, scene: &Scene);
863 fn draw_layered(&self, scene: &Scene, _overlay_start: usize) {
869 self.draw(scene);
870 }
871 fn enable_scene_overlay(&self) -> anyhow::Result<()> {
876 anyhow::bail!("layered GPUI scenes are not supported on this platform")
877 }
878 fn create_native_surface(&self) -> Result<Rc<dyn PlatformNativeSurface>> {
880 anyhow::bail!("native surface portals are not supported on this platform")
881 }
882 fn completed_frame(&self) {}
883 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
884 fn is_subpixel_rendering_supported(&self) -> bool;
885
886 fn get_title(&self) -> String {
888 String::new()
889 }
890 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
891 None
892 }
893 fn tab_bar_visible(&self) -> bool {
894 false
895 }
896 fn set_edited(&mut self, _edited: bool) {}
897 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
898 #[cfg(target_os = "macos")]
899 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
900 fn show_character_palette(&self) {}
901 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
902 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
903 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
904 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
905 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
906 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
907 fn merge_all_windows(&self) {}
908 fn move_tab_to_new_window(&self) {}
909 fn toggle_window_tab_overview(&self) {}
910 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
911
912 #[cfg(target_os = "windows")]
913 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
914
915 fn inner_window_bounds(&self) -> WindowBounds {
917 self.window_bounds()
918 }
919 fn request_decorations(&self, _decorations: WindowDecorations) {}
920 fn show_window_menu(&self, _position: Point<Pixels>) {}
921 fn start_window_move(&self) {}
922 fn can_start_external_drag(&self) -> bool {
923 false
924 }
925 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
926 false
927 }
928 fn start_window_resize(&self, _edge: ResizeEdge) {}
929 fn set_exclusive_zone(&self, _zone: Pixels) {}
930 #[cfg(all(target_os = "linux", feature = "wayland"))]
931 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
932 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
933 fn window_decorations(&self) -> Decorations {
934 Decorations::Server
935 }
936 fn set_app_id(&mut self, _app_id: &str) {}
937 fn map_window(&mut self) -> anyhow::Result<()> {
938 Ok(())
939 }
940 fn window_controls(&self) -> WindowControls {
941 WindowControls::default()
942 }
943 fn set_client_inset(&self, _inset: Pixels) {}
944 fn gpu_specs(&self) -> Option<GpuSpecs>;
945
946 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
947
948 fn insets(&self) -> WindowInsets {
953 WindowInsets::default()
954 }
955
956 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
962
963 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
966
967 fn set_back_enabled(&self, _enabled: bool) {}
970
971 fn show_soft_keyboard(&self) {}
973
974 fn hide_soft_keyboard(&self) {}
976
977 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
979
980 fn play_system_bell(&self) {}
981
982 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
984
985 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
987
988 fn a11y_update_window_bounds(&self) {}
990
991 #[cfg(any(test, feature = "test-support"))]
992 fn as_test(&mut self) -> Option<&mut TestWindow> {
993 None
994 }
995
996 #[cfg(any(test, feature = "test-support"))]
1000 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1001 anyhow::bail!("render_to_image not implemented for this platform")
1002 }
1003}
1004
1005#[cfg(any(test, feature = "test-support"))]
1007pub trait PlatformHeadlessRenderer {
1008 fn render_scene_to_image(
1010 &mut self,
1011 scene: &Scene,
1012 size: Size<DevicePixels>,
1013 ) -> Result<RgbaImage>;
1014
1015 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1021
1022 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1024}
1025
1026#[doc(hidden)]
1029pub type RunnableVariant = Runnable<RunnableMeta>;
1030
1031#[doc(hidden)]
1032pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1033
1034#[doc(hidden)]
1035pub enum TasksIncluded {
1036 OnlyCompleted,
1037 CompletedAndRunning,
1038}
1039
1040#[doc(hidden)]
1043pub trait PlatformDispatcher: Send + Sync {
1044 fn is_main_thread(&self) -> bool;
1045 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1046 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1047 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1048
1049 fn dispatch_on_main_thread_when_idle(
1050 &self,
1051 runnable: RunnableVariant,
1052 timeout: Option<Duration>,
1053 ) {
1054 let _ = timeout;
1055 self.dispatch_on_main_thread(runnable, Priority::Low);
1056 }
1057
1058 fn idle_time_remaining(&self) -> Option<Duration> {
1059 None
1060 }
1061
1062 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1063
1064 fn now(&self) -> Instant {
1065 Instant::now()
1066 }
1067
1068 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1069 gpui_util::defer(Box::new(|| {}))
1070 }
1071
1072 #[cfg(any(test, feature = "test-support"))]
1073 fn as_test(&self) -> Option<&TestDispatcher> {
1074 None
1075 }
1076
1077 #[cfg(any(test, feature = "test-support"))]
1080 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1081 None
1082 }
1083}
1084
1085#[expect(missing_docs)]
1086pub trait PlatformTextSystem: Send + Sync {
1087 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1088 fn all_font_names(&self) -> Vec<String>;
1090 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1092 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1094 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1096 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1098 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1100 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1102 fn rasterize_glyph(
1104 &self,
1105 params: &RenderGlyphParams,
1106 raster_bounds: Bounds<DevicePixels>,
1107 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1108 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1110 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1112 -> TextRenderingMode;
1113 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1115 0
1116 }
1117}
1118
1119#[expect(missing_docs)]
1120pub struct NoopTextSystem;
1121
1122#[expect(missing_docs)]
1123impl NoopTextSystem {
1124 #[allow(dead_code)]
1125 pub fn new() -> Self {
1126 Self
1127 }
1128}
1129
1130impl PlatformTextSystem for NoopTextSystem {
1131 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1132 Ok(())
1133 }
1134
1135 fn all_font_names(&self) -> Vec<String> {
1136 Vec::new()
1137 }
1138
1139 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1140 Ok(FontId(1))
1141 }
1142
1143 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1144 FontMetrics {
1145 units_per_em: 1000,
1146 ascent: 1025.0,
1147 descent: -275.0,
1148 line_gap: 0.0,
1149 underline_position: -95.0,
1150 underline_thickness: 60.0,
1151 cap_height: 698.0,
1152 x_height: 516.0,
1153 bounding_box: Bounds {
1154 origin: Point {
1155 x: -260.0,
1156 y: -245.0,
1157 },
1158 size: Size {
1159 width: 1501.0,
1160 height: 1364.0,
1161 },
1162 },
1163 }
1164 }
1165
1166 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1167 Ok(Bounds {
1168 origin: Point { x: 54.0, y: 0.0 },
1169 size: size(392.0, 528.0),
1170 })
1171 }
1172
1173 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1174 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1175 }
1176
1177 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1178 Some(GlyphId(ch.len_utf16() as u32))
1179 }
1180
1181 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1182 Ok(Default::default())
1183 }
1184
1185 fn rasterize_glyph(
1186 &self,
1187 _params: &RenderGlyphParams,
1188 raster_bounds: Bounds<DevicePixels>,
1189 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1190 Ok((raster_bounds.size, Vec::new()))
1191 }
1192
1193 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1194 let mut position = px(0.);
1195 let metrics = self.font_metrics(FontId(0));
1196 let em_width = font_size
1197 * self
1198 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1199 .unwrap()
1200 .width
1201 / metrics.units_per_em as f32;
1202 let mut glyphs = Vec::new();
1203 for (ix, c) in text.char_indices() {
1204 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1205 glyphs.push(ShapedGlyph {
1206 id: glyph,
1207 position: point(position, px(0.)),
1208 index: ix,
1209 is_emoji: glyph.0 == 2,
1210 });
1211 if glyph.0 == 2 {
1212 position += em_width * 2.0;
1213 } else {
1214 position += em_width;
1215 }
1216 } else {
1217 position += em_width
1218 }
1219 }
1220 let mut runs = Vec::default();
1221 if !glyphs.is_empty() {
1222 runs.push(ShapedRun {
1223 font_id: FontId(0),
1224 glyphs,
1225 });
1226 } else {
1227 position = px(0.);
1228 }
1229
1230 LineLayout {
1231 font_size,
1232 width: position,
1233 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1234 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1235 runs,
1236 len: text.len(),
1237 }
1238 }
1239
1240 fn recommended_rendering_mode(
1241 &self,
1242 _font_id: FontId,
1243 _font_size: Pixels,
1244 ) -> TextRenderingMode {
1245 TextRenderingMode::Grayscale
1246 }
1247}
1248
1249#[allow(dead_code)]
1254pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1255 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1256 [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], ];
1270
1271 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1272 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1273
1274 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1275 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1276
1277 [
1278 ratios[0] * NORM13,
1279 ratios[1] * NORM24,
1280 ratios[2] * NORM13,
1281 ratios[3] * NORM24,
1282 ]
1283}
1284
1285#[derive(PartialEq, Eq, Hash, Clone)]
1286#[expect(missing_docs)]
1287pub enum AtlasKey {
1288 Glyph(RenderGlyphParams),
1289 Svg(RenderSvgParams),
1290 Image(RenderImageParams),
1291}
1292
1293impl AtlasKey {
1294 #[cfg_attr(
1295 all(
1296 any(target_os = "linux", target_os = "freebsd"),
1297 not(any(feature = "x11", feature = "wayland"))
1298 ),
1299 allow(dead_code)
1300 )]
1301 pub fn texture_kind(&self) -> AtlasTextureKind {
1303 match self {
1304 AtlasKey::Glyph(params) => {
1305 if params.is_emoji {
1306 AtlasTextureKind::Polychrome
1307 } else if params.subpixel_rendering {
1308 AtlasTextureKind::Subpixel
1309 } else {
1310 AtlasTextureKind::Monochrome
1311 }
1312 }
1313 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1314 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1315 }
1316 }
1317}
1318
1319impl From<RenderGlyphParams> for AtlasKey {
1320 fn from(params: RenderGlyphParams) -> Self {
1321 Self::Glyph(params)
1322 }
1323}
1324
1325impl From<RenderSvgParams> for AtlasKey {
1326 fn from(params: RenderSvgParams) -> Self {
1327 Self::Svg(params)
1328 }
1329}
1330
1331impl From<RenderImageParams> for AtlasKey {
1332 fn from(params: RenderImageParams) -> Self {
1333 Self::Image(params)
1334 }
1335}
1336
1337#[expect(missing_docs)]
1338pub trait PlatformAtlas {
1339 fn get_or_insert_with<'a>(
1340 &self,
1341 key: &AtlasKey,
1342 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1343 ) -> Result<Option<AtlasTile>>;
1344 fn remove(&self, key: &AtlasKey);
1345
1346 #[cfg(any(test, feature = "test-support"))]
1347 fn contains(&self, _key: &AtlasKey) -> bool {
1348 false
1349 }
1350}
1351
1352#[doc(hidden)]
1353pub struct AtlasTextureList<T> {
1354 pub textures: Vec<Option<T>>,
1355 pub free_list: Vec<usize>,
1356}
1357
1358impl<T> Default for AtlasTextureList<T> {
1359 fn default() -> Self {
1360 Self {
1361 textures: Vec::default(),
1362 free_list: Vec::default(),
1363 }
1364 }
1365}
1366
1367impl<T> ops::Index<usize> for AtlasTextureList<T> {
1368 type Output = Option<T>;
1369
1370 fn index(&self, index: usize) -> &Self::Output {
1371 &self.textures[index]
1372 }
1373}
1374
1375impl<T> AtlasTextureList<T> {
1376 #[allow(unused)]
1377 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1378 self.free_list.clear();
1379 self.textures.drain(..)
1380 }
1381
1382 #[allow(dead_code)]
1383 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1384 self.textures.iter_mut().flatten()
1385 }
1386}
1387
1388#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1389#[repr(C)]
1390#[expect(missing_docs)]
1391pub struct AtlasTile {
1392 pub texture_id: AtlasTextureId,
1394 pub tile_id: TileId,
1396 pub padding: u32,
1398 pub bounds: Bounds<DevicePixels>,
1400}
1401
1402#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1403#[repr(C)]
1404#[expect(missing_docs)]
1405pub struct AtlasTextureId {
1406 pub index: u32,
1409 pub kind: AtlasTextureKind,
1411}
1412
1413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1414#[repr(C)]
1415#[cfg_attr(
1416 all(
1417 any(target_os = "linux", target_os = "freebsd"),
1418 not(any(feature = "x11", feature = "wayland"))
1419 ),
1420 allow(dead_code)
1421)]
1422#[expect(missing_docs)]
1423pub enum AtlasTextureKind {
1424 Monochrome = 0,
1425 Polychrome = 1,
1426 Subpixel = 2,
1427}
1428
1429#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1430#[repr(C)]
1431#[expect(missing_docs)]
1432pub struct TileId(pub u32);
1433
1434impl From<etagere::AllocId> for TileId {
1435 fn from(id: etagere::AllocId) -> Self {
1436 Self(id.serialize())
1437 }
1438}
1439
1440impl From<TileId> for etagere::AllocId {
1441 fn from(id: TileId) -> Self {
1442 Self::deserialize(id.0)
1443 }
1444}
1445
1446#[expect(missing_docs)]
1447pub struct PlatformInputHandler {
1448 cx: AsyncWindowContext,
1449 handler: Box<dyn InputHandler>,
1450}
1451
1452#[expect(missing_docs)]
1453#[cfg_attr(
1454 all(
1455 any(target_os = "linux", target_os = "freebsd"),
1456 not(any(feature = "x11", feature = "wayland"))
1457 ),
1458 allow(dead_code)
1459)]
1460impl PlatformInputHandler {
1461 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1462 Self { cx, handler }
1463 }
1464
1465 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1466 self.cx
1467 .update(|window, cx| {
1468 self.handler
1469 .selected_text_range(ignore_disabled_input, window, cx)
1470 })
1471 .ok()
1472 .flatten()
1473 }
1474
1475 #[cfg_attr(target_os = "windows", allow(dead_code))]
1476 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1477 self.cx
1478 .update(|window, cx| self.handler.marked_text_range(window, cx))
1479 .ok()
1480 .flatten()
1481 }
1482
1483 #[cfg_attr(
1484 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1485 allow(dead_code)
1486 )]
1487 pub fn text_for_range(
1488 &mut self,
1489 range_utf16: Range<usize>,
1490 adjusted: &mut Option<Range<usize>>,
1491 ) -> Option<String> {
1492 self.cx
1493 .update(|window, cx| {
1494 self.handler
1495 .text_for_range(range_utf16, adjusted, window, cx)
1496 })
1497 .ok()
1498 .flatten()
1499 }
1500
1501 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1502 self.cx
1503 .update(|window, cx| {
1504 self.handler
1505 .replace_text_in_range(replacement_range, text, window, cx);
1506 })
1507 .ok();
1508 }
1509
1510 pub fn replace_and_mark_text_in_range(
1511 &mut self,
1512 range_utf16: Option<Range<usize>>,
1513 new_text: &str,
1514 new_selected_range: Option<Range<usize>>,
1515 ) {
1516 self.cx
1517 .update(|window, cx| {
1518 self.handler.replace_and_mark_text_in_range(
1519 range_utf16,
1520 new_text,
1521 new_selected_range,
1522 window,
1523 cx,
1524 )
1525 })
1526 .ok();
1527 }
1528
1529 #[cfg_attr(target_os = "windows", allow(dead_code))]
1530 pub fn unmark_text(&mut self) {
1531 self.cx
1532 .update(|window, cx| self.handler.unmark_text(window, cx))
1533 .ok();
1534 }
1535
1536 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1537 self.cx
1538 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1539 .ok()
1540 .flatten()
1541 }
1542
1543 #[allow(dead_code)]
1544 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1545 self.handler.apple_press_and_hold_enabled()
1546 }
1547
1548 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1549 self.handler.replace_text_in_range(None, input, window, cx);
1550 }
1551
1552 pub fn compute_ime_candidate_bounds(
1553 marked_range: Option<Range<usize>>,
1554 selection: &UTF16Selection,
1555 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1556 ) -> Option<Bounds<Pixels>> {
1557 if let Some(marked_range) = marked_range {
1558 let mut line_start = marked_range.start;
1560
1561 let caret = selection.range.end;
1565 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1566 for i in (marked_range.start..caret).rev() {
1567 if let Some(b) = bounds_for_range(i..i) {
1568 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1569 line_start = i + 1;
1570 break;
1571 }
1572 }
1573 }
1574 }
1575 bounds_for_range(line_start..line_start)
1576 } else {
1577 let offset = if selection.reversed {
1579 selection.range.start
1580 } else {
1581 selection.range.end
1582 };
1583 bounds_for_range(offset..offset)
1584 }
1585 }
1586
1587 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1588 let marked_range = self.handler.marked_text_range(window, cx);
1589 let selection = self.handler.selected_text_range(true, window, cx)?;
1590 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1591 self.handler.bounds_for_range(range, window, cx)
1592 })
1593 }
1594
1595 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1596 let marked_range = self.marked_text_range();
1597 let selection = self.selected_text_range(true)?;
1598 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1599 self.bounds_for_range(range)
1600 })
1601 }
1602
1603 #[allow(unused)]
1604 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1605 self.cx
1606 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1607 .ok()
1608 .flatten()
1609 }
1610
1611 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
1613 self.cx
1614 .update(|window, cx| {
1615 self.handler
1616 .set_selected_text_range(range_utf16, window, cx)
1617 })
1618 .ok();
1619 }
1620
1621 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
1623 self.cx
1624 .update(|window, cx| self.handler.element_bounds(window, cx))
1625 .ok()
1626 .flatten()
1627 }
1628
1629 pub fn text_length_utf16(&mut self) -> Option<usize> {
1631 self.cx
1632 .update(|window, cx| self.handler.text_length_utf16(window, cx))
1633 .ok()
1634 .flatten()
1635 }
1636
1637 #[allow(dead_code)]
1638 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1639 self.handler.accepts_text_input(window, cx)
1640 }
1641
1642 #[allow(dead_code)]
1643 pub fn query_accepts_text_input(&mut self) -> bool {
1644 self.cx
1645 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1646 .unwrap_or(true)
1647 }
1648
1649 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1655 self.cx
1656 .update(|window, cx| {
1657 !window.has_pending_keystrokes()
1659 && self.handler.prefers_ime_for_printable_keys(window, cx)
1660 })
1661 .unwrap_or(false)
1662 }
1663}
1664
1665#[derive(Debug)]
1668pub struct UTF16Selection {
1669 pub range: Range<usize>,
1672 pub reversed: bool,
1675}
1676
1677pub trait InputHandler: 'static {
1682 fn selected_text_range(
1687 &mut self,
1688 ignore_disabled_input: bool,
1689 window: &mut Window,
1690 cx: &mut App,
1691 ) -> Option<UTF16Selection>;
1692
1693 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1698
1699 fn text_for_range(
1704 &mut self,
1705 range_utf16: Range<usize>,
1706 adjusted_range: &mut Option<Range<usize>>,
1707 window: &mut Window,
1708 cx: &mut App,
1709 ) -> Option<String>;
1710
1711 fn replace_text_in_range(
1716 &mut self,
1717 replacement_range: Option<Range<usize>>,
1718 text: &str,
1719 window: &mut Window,
1720 cx: &mut App,
1721 );
1722
1723 fn replace_and_mark_text_in_range(
1730 &mut self,
1731 range_utf16: Option<Range<usize>>,
1732 new_text: &str,
1733 new_selected_range: Option<Range<usize>>,
1734 window: &mut Window,
1735 cx: &mut App,
1736 );
1737
1738 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1741
1742 fn bounds_for_range(
1747 &mut self,
1748 range_utf16: Range<usize>,
1749 window: &mut Window,
1750 cx: &mut App,
1751 ) -> Option<Bounds<Pixels>>;
1752
1753 fn character_index_for_point(
1757 &mut self,
1758 point: Point<Pixels>,
1759 window: &mut Window,
1760 cx: &mut App,
1761 ) -> Option<usize>;
1762
1763 fn set_selected_text_range(
1773 &mut self,
1774 _range_utf16: Range<usize>,
1775 _window: &mut Window,
1776 _cx: &mut App,
1777 ) {
1778 }
1779
1780 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
1787 None
1788 }
1789
1790 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
1792 None
1793 }
1794
1795 #[allow(dead_code)]
1800 fn apple_press_and_hold_enabled(&mut self) -> bool {
1801 true
1802 }
1803
1804 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1806 true
1807 }
1808
1809 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1818 false
1819 }
1820}
1821
1822#[derive(Debug)]
1824pub struct WindowOptions {
1825 pub window_bounds: Option<WindowBounds>,
1829
1830 pub titlebar: Option<TitlebarOptions>,
1832
1833 pub focus: bool,
1835
1836 pub show: bool,
1838
1839 pub kind: WindowKind,
1841
1842 pub is_movable: bool,
1846
1847 pub app_owns_titlebar_drag: bool,
1859
1860 pub is_resizable: bool,
1862
1863 pub is_minimizable: bool,
1865
1866 pub display_id: Option<DisplayId>,
1869
1870 pub window_background: WindowBackgroundAppearance,
1872
1873 pub app_id: Option<String>,
1875
1876 pub window_min_size: Option<Size<Pixels>>,
1878
1879 pub window_decorations: Option<WindowDecorations>,
1882
1883 pub icon: Option<Arc<image::RgbaImage>>,
1885
1886 pub tabbing_identifier: Option<String>,
1888}
1889
1890#[derive(Debug)]
1892#[cfg_attr(
1893 all(
1894 any(target_os = "linux", target_os = "freebsd"),
1895 not(any(feature = "x11", feature = "wayland"))
1896 ),
1897 allow(dead_code)
1898)]
1899#[allow(missing_docs)]
1900pub struct WindowParams {
1901 pub bounds: Bounds<Pixels>,
1902
1903 #[cfg_attr(feature = "wayland", allow(dead_code))]
1905 pub titlebar: Option<TitlebarOptions>,
1906
1907 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1909 pub kind: WindowKind,
1910
1911 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1913 pub is_movable: bool,
1914
1915 #[cfg_attr(
1917 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1918 allow(dead_code)
1919 )]
1920 pub app_owns_titlebar_drag: bool,
1921
1922 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1924 pub is_resizable: bool,
1925
1926 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1928 pub is_minimizable: bool,
1929
1930 #[cfg_attr(
1931 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1932 allow(dead_code)
1933 )]
1934 pub focus: bool,
1935
1936 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1937 pub show: bool,
1938
1939 #[cfg_attr(feature = "wayland", allow(dead_code))]
1941 pub icon: Option<Arc<image::RgbaImage>>,
1942
1943 #[cfg_attr(feature = "wayland", allow(dead_code))]
1944 pub display_id: Option<DisplayId>,
1945
1946 #[cfg_attr(feature = "wayland", allow(dead_code))]
1947 pub app_id: Option<String>,
1948
1949 pub window_min_size: Option<Size<Pixels>>,
1950
1951 #[cfg(target_os = "macos")]
1952 pub tabbing_identifier: Option<String>,
1953}
1954
1955#[derive(Debug, Copy, Clone, PartialEq)]
1957pub enum WindowBounds {
1958 Windowed(Bounds<Pixels>),
1960 Maximized(Bounds<Pixels>),
1963 Fullscreen(Bounds<Pixels>),
1966}
1967
1968impl Default for WindowBounds {
1969 fn default() -> Self {
1970 WindowBounds::Windowed(Bounds::default())
1971 }
1972}
1973
1974impl WindowBounds {
1975 pub fn get_bounds(&self) -> Bounds<Pixels> {
1977 match self {
1978 WindowBounds::Windowed(bounds) => *bounds,
1979 WindowBounds::Maximized(bounds) => *bounds,
1980 WindowBounds::Fullscreen(bounds) => *bounds,
1981 }
1982 }
1983
1984 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1986 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1987 }
1988}
1989
1990impl Default for WindowOptions {
1991 fn default() -> Self {
1992 Self {
1993 window_bounds: None,
1994 titlebar: Some(TitlebarOptions {
1995 title: Default::default(),
1996 appears_transparent: Default::default(),
1997 traffic_light_position: Default::default(),
1998 }),
1999 focus: true,
2000 show: true,
2001 kind: WindowKind::Normal,
2002 is_movable: true,
2003 app_owns_titlebar_drag: false,
2004 is_resizable: true,
2005 is_minimizable: true,
2006 display_id: None,
2007 window_background: WindowBackgroundAppearance::default(),
2008 icon: None,
2009 app_id: None,
2010 window_min_size: None,
2011 window_decorations: None,
2012 tabbing_identifier: None,
2013 }
2014 }
2015}
2016
2017#[derive(Debug, Default)]
2019pub struct TitlebarOptions {
2020 pub title: Option<SharedString>,
2022
2023 pub appears_transparent: bool,
2026
2027 pub traffic_light_position: Option<Point<Pixels>>,
2029}
2030
2031#[derive(Clone, Debug, PartialEq, Eq)]
2033pub enum WindowKind {
2034 Normal,
2036
2037 PopUp,
2040
2041 AnchoredPopup(popup::PopupOptions),
2048
2049 Floating,
2051
2052 #[cfg(all(target_os = "linux", feature = "wayland"))]
2055 LayerShell(layer_shell::LayerShellOptions),
2056
2057 Dialog,
2060}
2061
2062#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2067pub enum WindowAppearance {
2068 #[default]
2072 Light,
2073
2074 VibrantLight,
2078
2079 Dark,
2083
2084 VibrantDark,
2088}
2089
2090#[derive(Copy, Clone, Debug, Default, PartialEq)]
2093pub enum WindowBackgroundAppearance {
2094 #[default]
2102 Opaque,
2103 Transparent,
2105 Blurred,
2109 MicaBackdrop,
2111 MicaAltBackdrop,
2113}
2114
2115#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2117pub enum TextRenderingMode {
2118 #[default]
2120 PlatformDefault,
2121 Subpixel,
2123 Grayscale,
2125}
2126
2127#[derive(Clone, Debug)]
2129pub struct PathPromptOptions {
2130 pub files: bool,
2132 pub directories: bool,
2134 pub multiple: bool,
2136 pub prompt: Option<SharedString>,
2138}
2139
2140#[derive(Copy, Clone, Debug, PartialEq)]
2142pub enum PromptLevel {
2143 Info,
2145
2146 Warning,
2148
2149 Critical,
2151}
2152
2153#[derive(Clone, Debug, PartialEq)]
2155pub enum PromptButton {
2156 Ok(SharedString),
2158 Cancel(SharedString),
2160 Other(SharedString),
2162}
2163
2164impl PromptButton {
2165 pub fn new(label: impl Into<SharedString>) -> Self {
2167 PromptButton::Other(label.into())
2168 }
2169
2170 pub fn ok(label: impl Into<SharedString>) -> Self {
2172 PromptButton::Ok(label.into())
2173 }
2174
2175 pub fn cancel(label: impl Into<SharedString>) -> Self {
2177 PromptButton::Cancel(label.into())
2178 }
2179
2180 #[allow(dead_code)]
2182 pub fn is_cancel(&self) -> bool {
2183 matches!(self, PromptButton::Cancel(_))
2184 }
2185
2186 pub fn label(&self) -> &SharedString {
2188 match self {
2189 PromptButton::Ok(label) => label,
2190 PromptButton::Cancel(label) => label,
2191 PromptButton::Other(label) => label,
2192 }
2193 }
2194}
2195
2196impl From<&str> for PromptButton {
2197 fn from(value: &str) -> Self {
2198 match value.to_lowercase().as_str() {
2199 "ok" => PromptButton::Ok("OK".into()),
2200 "cancel" => PromptButton::Cancel("Cancel".into()),
2201 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2202 }
2203 }
2204}
2205
2206#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2208pub enum CursorStyle {
2209 #[default]
2211 Arrow,
2212
2213 IBeam,
2216
2217 Crosshair,
2220
2221 ClosedHand,
2224
2225 OpenHand,
2228
2229 PointingHand,
2232
2233 ResizeLeft,
2236
2237 ResizeRight,
2240
2241 ResizeLeftRight,
2244
2245 ResizeUp,
2248
2249 ResizeDown,
2252
2253 ResizeUpDown,
2256
2257 ResizeUpLeftDownRight,
2260
2261 ResizeUpRightDownLeft,
2264
2265 ResizeColumn,
2268
2269 ResizeRow,
2272
2273 IBeamCursorForVerticalLayout,
2276
2277 OperationNotAllowed,
2280
2281 DragLink,
2284
2285 DragCopy,
2288
2289 ContextualMenu,
2292}
2293
2294#[derive(Clone, Debug, Eq, PartialEq)]
2296pub struct ClipboardItem {
2297 pub entries: Vec<ClipboardEntry>,
2299}
2300
2301#[derive(Clone, Debug, Eq, PartialEq)]
2303pub enum ClipboardEntry {
2304 String(ClipboardString),
2306 Image(Image),
2308 ExternalPaths(crate::ExternalPaths),
2310}
2311
2312impl ClipboardItem {
2313 pub fn new_string(text: String) -> Self {
2315 Self {
2316 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2317 }
2318 }
2319
2320 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2322 Self {
2323 entries: vec![ClipboardEntry::String(ClipboardString {
2324 text,
2325 metadata: Some(metadata),
2326 })],
2327 }
2328 }
2329
2330 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2332 Self {
2333 entries: vec![ClipboardEntry::String(
2334 ClipboardString::new(text).with_json_metadata(metadata),
2335 )],
2336 }
2337 }
2338
2339 pub fn new_image(image: &Image) -> Self {
2341 Self {
2342 entries: vec![ClipboardEntry::Image(image.clone())],
2343 }
2344 }
2345
2346 pub fn text(&self) -> Option<String> {
2349 let mut answer = String::new();
2350
2351 for entry in self.entries.iter() {
2352 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2353 answer.push_str(text);
2354 }
2355 }
2356
2357 if answer.is_empty() {
2358 for entry in self.entries.iter() {
2359 if let ClipboardEntry::ExternalPaths(paths) = entry {
2360 for path in &paths.0 {
2361 use std::fmt::Write as _;
2362 _ = write!(answer, "{}", path.display());
2363 }
2364 }
2365 }
2366 }
2367
2368 if !answer.is_empty() {
2369 Some(answer)
2370 } else {
2371 None
2372 }
2373 }
2374
2375 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2377 pub fn metadata(&self) -> Option<&String> {
2378 match self.entries().first() {
2379 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2380 clipboard_string.metadata.as_ref()
2381 }
2382 _ => None,
2383 }
2384 }
2385
2386 pub fn entries(&self) -> &[ClipboardEntry] {
2388 &self.entries
2389 }
2390
2391 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2393 self.entries.into_iter()
2394 }
2395}
2396
2397impl From<ClipboardString> for ClipboardEntry {
2398 fn from(value: ClipboardString) -> Self {
2399 Self::String(value)
2400 }
2401}
2402
2403impl From<String> for ClipboardEntry {
2404 fn from(value: String) -> Self {
2405 Self::from(ClipboardString::from(value))
2406 }
2407}
2408
2409impl From<Image> for ClipboardEntry {
2410 fn from(value: Image) -> Self {
2411 Self::Image(value)
2412 }
2413}
2414
2415impl From<ClipboardEntry> for ClipboardItem {
2416 fn from(value: ClipboardEntry) -> Self {
2417 Self {
2418 entries: vec![value],
2419 }
2420 }
2421}
2422
2423impl From<String> for ClipboardItem {
2424 fn from(value: String) -> Self {
2425 Self::from(ClipboardEntry::from(value))
2426 }
2427}
2428
2429impl From<Image> for ClipboardItem {
2430 fn from(value: Image) -> Self {
2431 Self::from(ClipboardEntry::from(value))
2432 }
2433}
2434
2435#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2437pub enum ImageFormat {
2438 Png,
2443 Jpeg,
2445 Webp,
2447 Gif,
2449 Svg,
2451 Bmp,
2453 Tiff,
2455 Ico,
2457 Pnm,
2459}
2460
2461impl ImageFormat {
2462 pub const fn mime_type(self) -> &'static str {
2464 match self {
2465 ImageFormat::Png => "image/png",
2466 ImageFormat::Jpeg => "image/jpeg",
2467 ImageFormat::Webp => "image/webp",
2468 ImageFormat::Gif => "image/gif",
2469 ImageFormat::Svg => "image/svg+xml",
2470 ImageFormat::Bmp => "image/bmp",
2471 ImageFormat::Tiff => "image/tiff",
2472 ImageFormat::Ico => "image/ico",
2473 ImageFormat::Pnm => "image/x-portable-anymap",
2474 }
2475 }
2476
2477 pub const fn extension(self) -> &'static str {
2479 match self {
2480 ImageFormat::Png => "png",
2481 ImageFormat::Jpeg => "jpg",
2482 ImageFormat::Webp => "webp",
2483 ImageFormat::Gif => "gif",
2484 ImageFormat::Svg => "svg",
2485 ImageFormat::Bmp => "bmp",
2486 ImageFormat::Tiff => "tiff",
2487 ImageFormat::Ico => "ico",
2488 ImageFormat::Pnm => "pnm",
2489 }
2490 }
2491
2492 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2494 use strum::IntoEnumIterator;
2495 Self::iter()
2496 .find(|format| format.mime_type() == mime_type)
2497 .or_else(|| Self::from_mime_type_alias(mime_type))
2498 }
2499
2500 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2504 match mime_type {
2505 "image/jpg" => Some(Self::Jpeg),
2506 "image/tif" => Some(Self::Tiff),
2507 _ => None,
2508 }
2509 }
2510}
2511
2512#[derive(Clone, Debug, PartialEq, Eq)]
2514pub struct Image {
2515 pub format: ImageFormat,
2517 pub bytes: Vec<u8>,
2519 pub id: u64,
2521}
2522
2523pub(crate) fn decode_static_image(
2524 bytes: &[u8],
2525 format: image::ImageFormat,
2526) -> Result<SmallVec<[Frame; 1]>> {
2527 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
2528 .into_decoder()
2529 .context("creating image decoder")?;
2530 decode_static_image_from_decoder(decoder)
2531}
2532
2533pub(crate) fn decode_static_image_from_decoder(
2534 mut decoder: impl image::ImageDecoder,
2535) -> Result<SmallVec<[Frame; 1]>> {
2536 let orientation = decoder
2537 .orientation()
2538 .context("reading decoder's orientation")?;
2539 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
2540 image.apply_orientation(orientation);
2541
2542 let mut data = image.into_rgba8();
2543 for pixel in data.chunks_exact_mut(4) {
2544 pixel.swap(0, 2);
2545 }
2546
2547 Ok(SmallVec::from_elem(Frame::new(data), 1))
2548}
2549
2550impl Hash for Image {
2551 fn hash<H: Hasher>(&self, state: &mut H) {
2552 state.write_u64(self.id);
2553 }
2554}
2555
2556impl Image {
2557 pub fn empty() -> Self {
2559 Self::from_bytes(ImageFormat::Png, Vec::new())
2560 }
2561
2562 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2564 Self {
2565 id: hash(&bytes),
2566 format,
2567 bytes,
2568 }
2569 }
2570
2571 pub fn id(&self) -> u64 {
2573 self.id
2574 }
2575
2576 pub fn use_render_image(
2578 self: Arc<Self>,
2579 window: &mut Window,
2580 cx: &mut App,
2581 ) -> Option<Arc<RenderImage>> {
2582 ImageSource::Image(self)
2583 .use_data(None, window, cx)
2584 .and_then(|result| result.ok())
2585 }
2586
2587 pub fn get_render_image(
2589 self: Arc<Self>,
2590 window: &mut Window,
2591 cx: &mut App,
2592 ) -> Option<Arc<RenderImage>> {
2593 ImageSource::Image(self)
2594 .get_data(None, window, cx)
2595 .and_then(|result| result.ok())
2596 }
2597
2598 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2600 ImageSource::Image(self).remove_asset(cx);
2601 }
2602
2603 #[cfg(any(test, feature = "test-support"))]
2606 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
2607 ImageSource::Image(self.clone()).is_asset_cached(cx)
2608 }
2609
2610 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2612 let frames = match self.format {
2613 ImageFormat::Gif => {
2614 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2615 let mut frames = SmallVec::new();
2616
2617 for frame in decoder.into_frames() {
2618 match frame {
2619 Ok(mut frame) => {
2620 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2622 pixel.swap(0, 2);
2623 }
2624 frames.push(frame);
2625 }
2626 Err(err) => {
2627 log::debug!("Skipping GIF frame due to decode error: {err}");
2628 }
2629 }
2630 }
2631
2632 if frames.is_empty() {
2633 anyhow::bail!("GIF could not be decoded: all frames failed");
2634 }
2635
2636 frames
2637 }
2638 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
2639 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
2640 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
2641 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
2642 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
2643 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
2644 ImageFormat::Svg => {
2645 return svg_renderer
2646 .render_single_frame(&self.bytes, 1.0)
2647 .map_err(Into::into);
2648 }
2649 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
2650 };
2651
2652 Ok(Arc::new(RenderImage::new(frames)))
2653 }
2654
2655 pub fn format(&self) -> ImageFormat {
2657 self.format
2658 }
2659
2660 pub fn bytes(&self) -> &[u8] {
2662 self.bytes.as_slice()
2663 }
2664}
2665
2666#[derive(Clone, Debug, Eq, PartialEq)]
2668pub struct ClipboardString {
2669 pub text: String,
2671 pub metadata: Option<String>,
2673}
2674
2675impl ClipboardString {
2676 pub fn new(text: String) -> Self {
2678 Self {
2679 text,
2680 metadata: None,
2681 }
2682 }
2683
2684 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2687 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2688 self
2689 }
2690
2691 pub fn text(&self) -> &String {
2693 &self.text
2694 }
2695
2696 pub fn into_text(self) -> String {
2698 self.text
2699 }
2700
2701 pub fn metadata_json<T>(&self) -> Option<T>
2703 where
2704 T: for<'a> Deserialize<'a>,
2705 {
2706 self.metadata
2707 .as_ref()
2708 .and_then(|m| serde_json::from_str(m).ok())
2709 }
2710
2711 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2712 pub fn text_hash(text: &str) -> u64 {
2714 let mut hasher = SeaHasher::new();
2715 text.hash(&mut hasher);
2716 hasher.finish()
2717 }
2718}
2719
2720impl From<String> for ClipboardString {
2721 fn from(value: String) -> Self {
2722 Self {
2723 text: value,
2724 metadata: None,
2725 }
2726 }
2727}
2728
2729#[cfg(test)]
2730mod image_tests {
2731 use super::*;
2732 use std::sync::Arc;
2733
2734 #[test]
2735 fn test_image_to_image_data_applies_exif_orientation() {
2736 let image = Image::from_bytes(
2737 ImageFormat::Jpeg,
2738 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
2739 );
2740
2741 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2742
2743 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
2744
2745 let bytes = render_image.as_bytes(0).unwrap();
2746 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
2747 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
2748 }
2749
2750 #[test]
2751 fn test_svg_image_to_image_data_converts_to_bgra() {
2752 let image = Image::from_bytes(
2753 ImageFormat::Svg,
2754 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2755<rect width="1" height="1" fill="#38BDF8"/>
2756</svg>"##
2757 .to_vec(),
2758 );
2759
2760 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2761 let bytes = render_image.as_bytes(0).unwrap();
2762
2763 for pixel in bytes.chunks_exact(4) {
2764 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2765 }
2766 }
2767}
2768
2769#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2770mod tests {
2771 use super::*;
2772 use std::collections::HashSet;
2773
2774 #[test]
2775 fn test_window_button_layout_parse_standard() {
2776 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2777 assert_eq!(
2778 layout.left,
2779 [
2780 Some(WindowButton::Close),
2781 Some(WindowButton::Minimize),
2782 None
2783 ]
2784 );
2785 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2786 }
2787
2788 #[test]
2789 fn test_window_button_layout_parse_right_only() {
2790 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2791 assert_eq!(layout.left, [None, None, None]);
2792 assert_eq!(
2793 layout.right,
2794 [
2795 Some(WindowButton::Minimize),
2796 Some(WindowButton::Maximize),
2797 Some(WindowButton::Close)
2798 ]
2799 );
2800 }
2801
2802 #[test]
2803 fn test_window_button_layout_parse_left_only() {
2804 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2805 assert_eq!(
2806 layout.left,
2807 [
2808 Some(WindowButton::Close),
2809 Some(WindowButton::Minimize),
2810 Some(WindowButton::Maximize)
2811 ]
2812 );
2813 assert_eq!(layout.right, [None, None, None]);
2814 }
2815
2816 #[test]
2817 fn test_window_button_layout_parse_with_whitespace() {
2818 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2819 assert_eq!(
2820 layout.left,
2821 [
2822 Some(WindowButton::Close),
2823 Some(WindowButton::Minimize),
2824 None
2825 ]
2826 );
2827 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2828 }
2829
2830 #[test]
2831 fn test_window_button_layout_parse_empty() {
2832 let layout = WindowButtonLayout::parse("").unwrap();
2833 assert_eq!(layout.left, [None, None, None]);
2834 assert_eq!(layout.right, [None, None, None]);
2835 }
2836
2837 #[test]
2838 fn test_window_button_layout_parse_intentionally_empty() {
2839 let layout = WindowButtonLayout::parse(":").unwrap();
2840 assert_eq!(layout.left, [None, None, None]);
2841 assert_eq!(layout.right, [None, None, None]);
2842 }
2843
2844 #[test]
2845 fn test_window_button_layout_parse_invalid_buttons() {
2846 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2847 assert_eq!(
2848 layout.left,
2849 [
2850 Some(WindowButton::Close),
2851 Some(WindowButton::Minimize),
2852 None
2853 ]
2854 );
2855 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2856 }
2857
2858 #[test]
2859 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2860 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2861 assert_eq!(
2862 layout.right,
2863 [
2864 Some(WindowButton::Close),
2865 Some(WindowButton::Minimize),
2866 None
2867 ]
2868 );
2869 assert_eq!(layout.format(), ":close,minimize");
2870 }
2871
2872 #[test]
2873 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2874 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2875 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2876 assert_eq!(
2877 layout.right,
2878 [
2879 Some(WindowButton::Maximize),
2880 Some(WindowButton::Minimize),
2881 None
2882 ]
2883 );
2884
2885 let button_ids: Vec<_> = layout
2886 .left
2887 .iter()
2888 .chain(layout.right.iter())
2889 .flatten()
2890 .map(WindowButton::id)
2891 .collect();
2892 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2893 assert_eq!(unique_button_ids.len(), button_ids.len());
2894 assert_eq!(layout.format(), "close:maximize,minimize");
2895 }
2896
2897 #[test]
2898 fn test_window_button_layout_parse_gnome_style() {
2899 let layout = WindowButtonLayout::parse("close").unwrap();
2900 assert_eq!(layout.left, [None, None, None]);
2901 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2902 }
2903
2904 #[test]
2905 fn test_window_button_layout_parse_elementary_style() {
2906 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2907 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2908 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2909 }
2910
2911 #[test]
2912 fn test_window_button_layout_round_trip() {
2913 let cases = [
2914 "close:minimize,maximize",
2915 "minimize,maximize,close:",
2916 ":close",
2917 "close:",
2918 "close:maximize",
2919 ":",
2920 ];
2921
2922 for case in cases {
2923 let layout = WindowButtonLayout::parse(case).unwrap();
2924 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2925 }
2926 }
2927
2928 #[test]
2929 fn test_window_button_layout_linux_default() {
2930 let layout = WindowButtonLayout::linux_default();
2931 assert_eq!(layout.left, [None, None, None]);
2932 assert_eq!(
2933 layout.right,
2934 [
2935 Some(WindowButton::Minimize),
2936 Some(WindowButton::Maximize),
2937 Some(WindowButton::Close)
2938 ]
2939 );
2940
2941 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2942 assert_eq!(round_tripped, layout);
2943 }
2944
2945 #[test]
2946 fn test_window_button_layout_parse_all_invalid() {
2947 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2948 }
2949}