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 ffi::OsString,
68 fmt::{self, Debug},
69 ops::Range,
70 path::{Path, PathBuf},
71 rc::Rc,
72 sync::Arc,
73};
74use strum::EnumIter;
75use uuid::Uuid;
76
77pub use app_menu::*;
78pub use keyboard::*;
79pub use keystroke::*;
80
81#[cfg(any(test, feature = "test-support"))]
82pub(crate) use test::*;
83
84#[cfg(any(test, feature = "test-support"))]
85pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
86
87#[cfg(any(test, feature = "test-support"))]
88pub use threaded_dispatcher::ThreadedDispatcher;
89
90#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
91pub use visual_test::VisualTestPlatform;
92
93#[cfg(any(target_os = "linux", target_os = "freebsd"))]
97#[inline]
98pub fn guess_compositor() -> &'static str {
99 if std::env::var_os("ZED_HEADLESS").is_some() {
100 return "Headless";
101 }
102
103 #[cfg(feature = "wayland")]
104 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
105 #[cfg(not(feature = "wayland"))]
106 let wayland_display: Option<std::ffi::OsString> = None;
107
108 #[cfg(feature = "x11")]
109 let x11_display = std::env::var_os("DISPLAY");
110 #[cfg(not(feature = "x11"))]
111 let x11_display: Option<std::ffi::OsString> = None;
112
113 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
114 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
115
116 if use_wayland {
117 "Wayland"
118 } else if use_x11 {
119 "X11"
120 } else {
121 "Headless"
122 }
123}
124
125#[expect(missing_docs)]
126pub trait Platform: 'static {
127 fn background_executor(&self) -> BackgroundExecutor;
128 fn foreground_executor(&self) -> ForegroundExecutor;
129 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
130
131 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
132 fn quit(&self);
133 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
134 fn activate(&self, ignoring_other_apps: bool);
135 fn hide(&self);
136 fn hide_other_apps(&self);
137 fn unhide_other_apps(&self);
138
139 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
140 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
141 fn active_window(&self) -> Option<AnyWindowHandle>;
142 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
143 None
144 }
145
146 fn is_screen_capture_supported(&self) -> bool {
147 false
148 }
149
150 fn screen_capture_sources(
151 &self,
152 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
153 let (sources_tx, sources_rx) = oneshot::channel();
154 sources_tx
155 .send(Err(anyhow::anyhow!(
156 "gpui was compiled without the screen-capture feature"
157 )))
158 .ok();
159 sources_rx
160 }
161
162 fn open_window(
163 &self,
164 handle: AnyWindowHandle,
165 options: WindowParams,
166 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
167
168 fn window_appearance(&self) -> WindowAppearance;
170
171 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
180
181 fn button_layout(&self) -> Option<WindowButtonLayout> {
183 None
184 }
185
186 fn open_url(&self, url: &str);
187 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
188 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
189
190 fn prompt_for_paths(
191 &self,
192 options: PathPromptOptions,
193 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
194 fn prompt_for_new_path(
195 &self,
196 directory: &Path,
197 suggested_name: Option<&str>,
198 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
199 fn can_select_mixed_files_and_dirs(&self) -> bool;
200 fn reveal_path(&self, path: &Path);
201 fn open_with_system(&self, path: &Path);
202
203 fn on_quit(&self, callback: Box<dyn FnMut()>);
204 fn on_reopen(&self, callback: Box<dyn FnMut()>);
205 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
206
207 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
217
218 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
223
224 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
228 None
229 }
230
231 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
232 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
233 None
234 }
235
236 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
237 fn perform_dock_menu_action(&self, _action: usize) {}
238 fn add_recent_document(&self, _path: &Path) {}
239 fn update_jump_list(
240 &self,
241 _menus: Vec<MenuItem>,
242 _entries: Vec<SmallVec<[PathBuf; 2]>>,
243 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
244 Task::ready(Vec::new())
245 }
246 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
247 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
248 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
249
250 fn thermal_state(&self) -> ThermalState;
251 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
252
253 fn set_app_identity(&self, identifier: &str, name: &str) {
260 _ = (identifier, name);
261 }
262
263 fn show_system_notification(&self, notification: SystemNotification) {
270 _ = notification;
271 }
272
273 fn dismiss_system_notification(&self, tag: &str) {
278 _ = tag;
279 }
280
281 fn on_system_notification_response(
287 &self,
288 callback: Box<dyn FnMut(SystemNotificationResponse)>,
289 ) {
290 _ = callback;
291 }
292
293 fn compositor_name(&self) -> &'static str {
294 ""
295 }
296 fn app_path(&self) -> Result<PathBuf>;
297 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
298
299 fn set_cursor_style(&self, style: CursorStyle);
300
301 fn hide_cursor_until_mouse_moves(&self);
304
305 fn is_cursor_visible(&self) -> bool;
307
308 fn should_auto_hide_scrollbars(&self) -> bool;
309
310 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
311 fn write_to_clipboard(&self, item: ClipboardItem);
312
313 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
321 Task::ready(Ok(self.read_from_clipboard()))
322 }
323
324 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
325 fn read_from_primary(&self) -> Option<ClipboardItem>;
326 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
327 fn write_to_primary(&self, item: ClipboardItem);
328
329 #[cfg(target_os = "macos")]
330 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
331 #[cfg(target_os = "macos")]
332 fn write_to_find_pasteboard(&self, item: ClipboardItem);
333
334 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
335 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
336 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
337
338 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
339 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
340 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
341}
342
343pub trait PlatformDisplay: Debug {
345 fn id(&self) -> DisplayId;
347
348 fn uuid(&self) -> Result<Uuid>;
351
352 fn bounds(&self) -> Bounds<Pixels>;
354
355 fn visible_bounds(&self) -> Bounds<Pixels> {
359 self.bounds()
360 }
361
362 fn default_bounds(&self) -> Bounds<Pixels> {
364 let bounds = self.bounds();
365 let center = bounds.center();
366 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
367
368 let offset = clipped_window_size / 2.0;
369 let origin = point(center.x - offset.width, center.y - offset.height);
370 Bounds::new(origin, clipped_window_size)
371 }
372}
373
374#[derive(Clone, Debug, PartialEq, Eq)]
377pub struct SystemNotification {
378 pub tag: SharedString,
382 pub title: SharedString,
384 pub body: SharedString,
386 pub actions: Vec<SystemNotificationAction>,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq, Hash)]
393pub struct SystemNotificationAction {
394 pub id: SharedString,
397 pub label: SharedString,
399}
400
401#[derive(Clone, Debug, PartialEq, Eq)]
403pub struct SystemNotificationResponse {
404 pub tag: SharedString,
406 pub action_id: Option<SharedString>,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413pub enum ThermalState {
414 Nominal,
416 Fair,
418 Serious,
420 Critical,
422}
423
424#[derive(Clone)]
426pub struct SourceMetadata {
427 pub id: u64,
429 pub label: Option<SharedString>,
431 pub is_main: Option<bool>,
433 pub resolution: Size<DevicePixels>,
435}
436
437pub trait ScreenCaptureSource {
439 fn metadata(&self) -> Result<SourceMetadata>;
441
442 fn stream(
445 &self,
446 foreground_executor: &ForegroundExecutor,
447 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
448 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
449}
450
451pub trait ScreenCaptureStream {
453 fn metadata(&self) -> Result<SourceMetadata>;
455}
456
457pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
459
460#[derive(PartialEq, Eq, Hash, Copy, Clone)]
462pub struct DisplayId(pub(crate) u64);
463
464impl DisplayId {
465 pub fn new(id: u64) -> Self {
467 Self(id)
468 }
469}
470
471impl From<u64> for DisplayId {
472 fn from(id: u64) -> Self {
473 Self(id)
474 }
475}
476
477impl From<DisplayId> for u64 {
478 fn from(id: DisplayId) -> Self {
479 id.0
480 }
481}
482
483impl Debug for DisplayId {
484 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485 write!(f, "DisplayId({})", self.0)
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub enum ResizeEdge {
492 Top,
494 TopRight,
496 Right,
498 BottomRight,
500 Bottom,
502 BottomLeft,
504 Left,
506 TopLeft,
508}
509
510#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
512pub enum WindowDecorations {
513 #[default]
514 Server,
516 Client,
518}
519
520#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
522pub enum Decorations {
523 #[default]
525 Server,
526 Client {
528 tiling: Tiling,
530 },
531}
532
533#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
535pub struct WindowControls {
536 pub fullscreen: bool,
538 pub maximize: bool,
540 pub minimize: bool,
542 pub window_menu: bool,
544}
545
546impl Default for WindowControls {
547 fn default() -> Self {
548 Self {
550 fullscreen: true,
551 maximize: true,
552 minimize: true,
553 window_menu: true,
554 }
555 }
556}
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
560pub enum WindowButton {
561 Minimize,
563 Maximize,
565 Close,
567}
568
569impl WindowButton {
570 pub fn id(&self) -> &'static str {
572 match self {
573 WindowButton::Minimize => "minimize",
574 WindowButton::Maximize => "maximize",
575 WindowButton::Close => "close",
576 }
577 }
578
579 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
580 fn index(&self) -> usize {
581 match self {
582 WindowButton::Minimize => 0,
583 WindowButton::Maximize => 1,
584 WindowButton::Close => 2,
585 }
586 }
587}
588
589pub const MAX_BUTTONS_PER_SIDE: usize = 3;
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
597pub struct WindowButtonLayout {
598 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
600 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
602}
603
604#[cfg(any(target_os = "linux", target_os = "freebsd"))]
605impl WindowButtonLayout {
606 pub fn linux_default() -> Self {
608 Self {
609 left: [None; MAX_BUTTONS_PER_SIDE],
610 right: [
611 Some(WindowButton::Minimize),
612 Some(WindowButton::Maximize),
613 Some(WindowButton::Close),
614 ],
615 }
616 }
617
618 pub fn parse(layout_string: &str) -> Result<Self> {
620 fn parse_side(
621 s: &str,
622 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
623 unrecognized: &mut Vec<String>,
624 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
625 let mut result = [None; MAX_BUTTONS_PER_SIDE];
626 let mut i = 0;
627 for name in s.split(',') {
628 let trimmed = name.trim();
629 if trimmed.is_empty() {
630 continue;
631 }
632 let button = match trimmed {
633 "minimize" => Some(WindowButton::Minimize),
634 "maximize" => Some(WindowButton::Maximize),
635 "close" => Some(WindowButton::Close),
636 other => {
637 unrecognized.push(other.to_string());
638 None
639 }
640 };
641 if let Some(button) = button {
642 if seen_buttons[button.index()] {
643 continue;
644 }
645 if let Some(slot) = result.get_mut(i) {
646 *slot = Some(button);
647 seen_buttons[button.index()] = true;
648 i += 1;
649 }
650 }
651 }
652 result
653 }
654
655 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
656 let mut unrecognized = Vec::new();
657 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
658 let layout = Self {
659 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
660 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
661 };
662
663 if !unrecognized.is_empty()
664 && layout.left.iter().all(Option::is_none)
665 && layout.right.iter().all(Option::is_none)
666 {
667 bail!(
668 "button layout string {:?} contains no valid buttons (unrecognized: {})",
669 layout_string,
670 unrecognized.join(", ")
671 );
672 }
673
674 Ok(layout)
675 }
676
677 #[cfg(test)]
679 pub fn format(&self) -> String {
680 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
681 buttons
682 .iter()
683 .flatten()
684 .map(|button| match button {
685 WindowButton::Minimize => "minimize",
686 WindowButton::Maximize => "maximize",
687 WindowButton::Close => "close",
688 })
689 .collect::<Vec<_>>()
690 .join(",")
691 }
692
693 format!("{}:{}", format_side(&self.left), format_side(&self.right))
694 }
695}
696
697#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
699pub struct Tiling {
700 pub top: bool,
702 pub left: bool,
704 pub right: bool,
706 pub bottom: bool,
708}
709
710impl Tiling {
711 pub fn tiled() -> Self {
713 Self {
714 top: true,
715 left: true,
716 right: true,
717 bottom: true,
718 }
719 }
720
721 pub fn is_tiled(&self) -> bool {
723 self.top || self.left || self.right || self.bottom
724 }
725}
726
727pub struct A11yCallbacks {
729 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
731 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
733 pub deactivation: Box<dyn Fn() + Send + 'static>,
735}
736
737#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
738#[expect(missing_docs)]
739pub struct RequestFrameOptions {
740 pub require_presentation: bool,
742 pub force_render: bool,
744}
745
746#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
759pub enum AppLifecyclePhase {
760 Active,
762 Inactive,
764 Background,
767 Foreground,
769}
770
771#[derive(Debug, Clone, Default, PartialEq)]
778pub struct WindowInsets {
779 pub safe_area: Edges<Pixels>,
784 pub ime: Edges<Pixels>,
788}
789
790impl WindowInsets {
791 pub fn effective(&self) -> Edges<Pixels> {
793 Edges {
794 top: self.safe_area.top.max(self.ime.top),
795 right: self.safe_area.right.max(self.ime.right),
796 bottom: self.safe_area.bottom.max(self.ime.bottom),
797 left: self.safe_area.left.max(self.ime.left),
798 }
799 }
800}
801
802#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
804pub enum TextInputStateChange {
805 FocusGained,
807 FocusLost,
809 SelectionChanged,
811 ContentChanged,
813}
814
815#[expect(missing_docs)]
816pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
817 fn bounds(&self) -> Bounds<Pixels>;
818 fn is_maximized(&self) -> bool;
819 fn window_bounds(&self) -> WindowBounds;
820 fn content_size(&self) -> Size<Pixels>;
821 fn resize(&mut self, size: Size<Pixels>);
822 fn scale_factor(&self) -> f32;
823 fn appearance(&self) -> WindowAppearance;
824 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
825 fn mouse_position(&self) -> Point<Pixels>;
826 fn modifiers(&self) -> Modifiers;
827 fn capslock(&self) -> Capslock;
828 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
829 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
830 fn prompt(
831 &self,
832 level: PromptLevel,
833 msg: &str,
834 detail: Option<&str>,
835 answers: &[PromptButton],
836 ) -> Option<oneshot::Receiver<usize>>;
837 fn activate(&self);
838 fn request_attention(&self) {}
840 fn is_active(&self) -> bool;
841 fn is_hovered(&self) -> bool;
842 fn background_appearance(&self) -> WindowBackgroundAppearance;
843 fn set_title(&mut self, title: &str);
844 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
845 fn minimize(&self);
846 fn zoom(&self);
847 fn toggle_fullscreen(&self);
848 fn is_fullscreen(&self) -> bool;
849 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
850 None
851 }
852 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
853 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
854 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
855 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
856 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
857 fn on_moved(&self, callback: Box<dyn FnMut()>);
858 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
859 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
860 fn on_close(&self, callback: Box<dyn FnOnce()>);
861 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
862 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
863 fn draw(&self, scene: &Scene);
864 fn completed_frame(&self) {}
865 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
866 fn is_subpixel_rendering_supported(&self) -> bool;
867
868 fn get_title(&self) -> String {
870 String::new()
871 }
872 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
873 None
874 }
875 fn tab_bar_visible(&self) -> bool {
876 false
877 }
878 fn set_edited(&mut self, _edited: bool) {}
879 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
880 fn toggle_simple_fullscreen(&self) {}
881 fn is_simple_fullscreen(&self) -> bool {
882 false
883 }
884 #[cfg(target_os = "macos")]
885 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
886 fn show_character_palette(&self) {}
887 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
888 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
889 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
890 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
891 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
892 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
893 fn merge_all_windows(&self) {}
894 fn move_tab_to_new_window(&self) {}
895 fn toggle_window_tab_overview(&self) {}
896 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
897
898 #[cfg(target_os = "windows")]
899 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
900
901 fn inner_window_bounds(&self) -> WindowBounds {
903 self.window_bounds()
904 }
905 fn request_decorations(&self, _decorations: WindowDecorations) {}
906 fn show_window_menu(&self, _position: Point<Pixels>) {}
907 fn start_window_move(&self) {}
908 fn can_start_external_drag(&self) -> bool {
909 false
910 }
911 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
912 false
913 }
914 fn start_window_resize(&self, _edge: ResizeEdge) {}
915 fn set_exclusive_zone(&self, _zone: Pixels) {}
916 #[cfg(all(target_os = "linux", feature = "wayland"))]
917 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
918 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
919 fn window_decorations(&self) -> Decorations {
920 Decorations::Server
921 }
922 fn set_app_id(&mut self, _app_id: &str) {}
923 fn map_window(&mut self) -> anyhow::Result<()> {
924 Ok(())
925 }
926 fn window_controls(&self) -> WindowControls {
927 WindowControls::default()
928 }
929 fn set_client_inset(&self, _inset: Pixels) {}
930 fn gpu_specs(&self) -> Option<GpuSpecs>;
931
932 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
933
934 fn insets(&self) -> WindowInsets {
939 WindowInsets::default()
940 }
941
942 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
948
949 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
952
953 fn set_back_enabled(&self, _enabled: bool) {}
956
957 fn show_soft_keyboard(&self) {}
959
960 fn hide_soft_keyboard(&self) {}
962
963 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
965
966 fn play_system_bell(&self) {}
967
968 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
970
971 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
973
974 fn a11y_update_window_bounds(&self) {}
976
977 #[cfg(any(test, feature = "test-support"))]
978 fn as_test(&mut self) -> Option<&mut TestWindow> {
979 None
980 }
981
982 #[cfg(any(test, feature = "test-support"))]
986 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
987 anyhow::bail!("render_to_image not implemented for this platform")
988 }
989}
990
991#[cfg(any(test, feature = "test-support"))]
993pub trait PlatformHeadlessRenderer {
994 fn render_scene_to_image(
996 &mut self,
997 scene: &Scene,
998 size: Size<DevicePixels>,
999 ) -> Result<RgbaImage>;
1000
1001 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1007
1008 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1010}
1011
1012#[doc(hidden)]
1015pub type RunnableVariant = Runnable<RunnableMeta>;
1016
1017#[doc(hidden)]
1018pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1019
1020#[doc(hidden)]
1021pub enum TasksIncluded {
1022 OnlyCompleted,
1023 CompletedAndRunning,
1024}
1025
1026#[doc(hidden)]
1029pub trait PlatformDispatcher: Send + Sync {
1030 fn is_main_thread(&self) -> bool;
1031 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1032 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1033 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1034
1035 fn dispatch_on_main_thread_when_idle(
1036 &self,
1037 runnable: RunnableVariant,
1038 timeout: Option<Duration>,
1039 ) {
1040 let _ = timeout;
1041 self.dispatch_on_main_thread(runnable, Priority::Low);
1042 }
1043
1044 fn idle_time_remaining(&self) -> Option<Duration> {
1045 None
1046 }
1047
1048 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1049
1050 fn now(&self) -> Instant {
1051 Instant::now()
1052 }
1053
1054 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1055 gpui_util::defer(Box::new(|| {}))
1056 }
1057
1058 #[cfg(any(test, feature = "test-support"))]
1059 fn as_test(&self) -> Option<&TestDispatcher> {
1060 None
1061 }
1062
1063 #[cfg(any(test, feature = "test-support"))]
1066 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1067 None
1068 }
1069}
1070
1071#[expect(missing_docs)]
1072pub trait PlatformTextSystem: Send + Sync {
1073 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1074 fn all_font_names(&self) -> Vec<String>;
1076 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1078 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1080 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1082 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1084 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1086 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1088 fn rasterize_glyph(
1090 &self,
1091 params: &RenderGlyphParams,
1092 raster_bounds: Bounds<DevicePixels>,
1093 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1094 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1096 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1098 -> TextRenderingMode;
1099 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1101 0
1102 }
1103}
1104
1105#[expect(missing_docs)]
1106pub struct NoopTextSystem;
1107
1108#[expect(missing_docs)]
1109impl NoopTextSystem {
1110 #[allow(dead_code)]
1111 pub fn new() -> Self {
1112 Self
1113 }
1114}
1115
1116impl PlatformTextSystem for NoopTextSystem {
1117 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1118 Ok(())
1119 }
1120
1121 fn all_font_names(&self) -> Vec<String> {
1122 Vec::new()
1123 }
1124
1125 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1126 Ok(FontId(1))
1127 }
1128
1129 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1130 FontMetrics {
1131 units_per_em: 1000,
1132 ascent: 1025.0,
1133 descent: -275.0,
1134 line_gap: 0.0,
1135 underline_position: -95.0,
1136 underline_thickness: 60.0,
1137 cap_height: 698.0,
1138 x_height: 516.0,
1139 bounding_box: Bounds {
1140 origin: Point {
1141 x: -260.0,
1142 y: -245.0,
1143 },
1144 size: Size {
1145 width: 1501.0,
1146 height: 1364.0,
1147 },
1148 },
1149 }
1150 }
1151
1152 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1153 Ok(Bounds {
1154 origin: Point { x: 54.0, y: 0.0 },
1155 size: size(392.0, 528.0),
1156 })
1157 }
1158
1159 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1160 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1161 }
1162
1163 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1164 Some(GlyphId(ch.len_utf16() as u32))
1165 }
1166
1167 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1168 Ok(Default::default())
1169 }
1170
1171 fn rasterize_glyph(
1172 &self,
1173 _params: &RenderGlyphParams,
1174 raster_bounds: Bounds<DevicePixels>,
1175 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1176 Ok((raster_bounds.size, Vec::new()))
1177 }
1178
1179 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1180 let mut position = px(0.);
1181 let metrics = self.font_metrics(FontId(0));
1182 let em_width = font_size
1183 * self
1184 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1185 .unwrap()
1186 .width
1187 / metrics.units_per_em as f32;
1188 let mut glyphs = Vec::new();
1189 for (ix, c) in text.char_indices() {
1190 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1191 glyphs.push(ShapedGlyph {
1192 id: glyph,
1193 position: point(position, px(0.)),
1194 index: ix,
1195 is_emoji: glyph.0 == 2,
1196 });
1197 if glyph.0 == 2 {
1198 position += em_width * 2.0;
1199 } else {
1200 position += em_width;
1201 }
1202 } else {
1203 position += em_width
1204 }
1205 }
1206 let mut runs = Vec::default();
1207 if !glyphs.is_empty() {
1208 runs.push(ShapedRun {
1209 font_id: FontId(0),
1210 glyphs,
1211 });
1212 } else {
1213 position = px(0.);
1214 }
1215
1216 LineLayout {
1217 font_size,
1218 width: position,
1219 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1220 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1221 runs,
1222 len: text.len(),
1223 }
1224 }
1225
1226 fn recommended_rendering_mode(
1227 &self,
1228 _font_id: FontId,
1229 _font_size: Pixels,
1230 ) -> TextRenderingMode {
1231 TextRenderingMode::Grayscale
1232 }
1233}
1234
1235#[allow(dead_code)]
1240pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1241 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1242 [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], ];
1256
1257 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1258 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1259
1260 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1261 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1262
1263 [
1264 ratios[0] * NORM13,
1265 ratios[1] * NORM24,
1266 ratios[2] * NORM13,
1267 ratios[3] * NORM24,
1268 ]
1269}
1270
1271#[derive(PartialEq, Eq, Hash, Clone)]
1272#[expect(missing_docs)]
1273pub enum AtlasKey {
1274 Glyph(RenderGlyphParams),
1275 Svg(RenderSvgParams),
1276 Image(RenderImageParams),
1277}
1278
1279impl AtlasKey {
1280 #[cfg_attr(
1281 all(
1282 any(target_os = "linux", target_os = "freebsd"),
1283 not(any(feature = "x11", feature = "wayland"))
1284 ),
1285 allow(dead_code)
1286 )]
1287 pub fn texture_kind(&self) -> AtlasTextureKind {
1289 match self {
1290 AtlasKey::Glyph(params) => {
1291 if params.is_emoji {
1292 AtlasTextureKind::Polychrome
1293 } else if params.subpixel_rendering {
1294 AtlasTextureKind::Subpixel
1295 } else {
1296 AtlasTextureKind::Monochrome
1297 }
1298 }
1299 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1300 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1301 }
1302 }
1303}
1304
1305impl From<RenderGlyphParams> for AtlasKey {
1306 fn from(params: RenderGlyphParams) -> Self {
1307 Self::Glyph(params)
1308 }
1309}
1310
1311impl From<RenderSvgParams> for AtlasKey {
1312 fn from(params: RenderSvgParams) -> Self {
1313 Self::Svg(params)
1314 }
1315}
1316
1317impl From<RenderImageParams> for AtlasKey {
1318 fn from(params: RenderImageParams) -> Self {
1319 Self::Image(params)
1320 }
1321}
1322
1323#[expect(missing_docs)]
1324pub trait PlatformAtlas {
1325 fn get_or_insert_with<'a>(
1326 &self,
1327 key: &AtlasKey,
1328 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1329 ) -> Result<Option<AtlasTile>>;
1330 fn remove(&self, key: &AtlasKey);
1331
1332 #[cfg(any(test, feature = "test-support"))]
1333 fn contains(&self, _key: &AtlasKey) -> bool {
1334 false
1335 }
1336}
1337
1338#[doc(hidden)]
1339pub struct AtlasTextureList<T> {
1340 pub textures: Vec<Option<T>>,
1341 pub free_list: Vec<usize>,
1342}
1343
1344impl<T> Default for AtlasTextureList<T> {
1345 fn default() -> Self {
1346 Self {
1347 textures: Vec::default(),
1348 free_list: Vec::default(),
1349 }
1350 }
1351}
1352
1353impl<T> ops::Index<usize> for AtlasTextureList<T> {
1354 type Output = Option<T>;
1355
1356 fn index(&self, index: usize) -> &Self::Output {
1357 &self.textures[index]
1358 }
1359}
1360
1361impl<T> AtlasTextureList<T> {
1362 #[allow(unused)]
1363 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1364 self.free_list.clear();
1365 self.textures.drain(..)
1366 }
1367
1368 #[allow(dead_code)]
1369 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1370 self.textures.iter_mut().flatten()
1371 }
1372}
1373
1374#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1375#[repr(C)]
1376#[expect(missing_docs)]
1377pub struct AtlasTile {
1378 pub texture_id: AtlasTextureId,
1380 pub tile_id: TileId,
1382 pub padding: u32,
1384 pub bounds: Bounds<DevicePixels>,
1386}
1387
1388#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1389#[repr(C)]
1390#[expect(missing_docs)]
1391pub struct AtlasTextureId {
1392 pub index: u32,
1395 pub kind: AtlasTextureKind,
1397}
1398
1399#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1400#[repr(C)]
1401#[cfg_attr(
1402 all(
1403 any(target_os = "linux", target_os = "freebsd"),
1404 not(any(feature = "x11", feature = "wayland"))
1405 ),
1406 allow(dead_code)
1407)]
1408#[expect(missing_docs)]
1409pub enum AtlasTextureKind {
1410 Monochrome = 0,
1411 Polychrome = 1,
1412 Subpixel = 2,
1413}
1414
1415#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1416#[repr(C)]
1417#[expect(missing_docs)]
1418pub struct TileId(pub u32);
1419
1420impl From<etagere::AllocId> for TileId {
1421 fn from(id: etagere::AllocId) -> Self {
1422 Self(id.serialize())
1423 }
1424}
1425
1426impl From<TileId> for etagere::AllocId {
1427 fn from(id: TileId) -> Self {
1428 Self::deserialize(id.0)
1429 }
1430}
1431
1432#[expect(missing_docs)]
1433pub struct PlatformInputHandler {
1434 cx: AsyncWindowContext,
1435 handler: Box<dyn InputHandler>,
1436}
1437
1438#[expect(missing_docs)]
1439#[cfg_attr(
1440 all(
1441 any(target_os = "linux", target_os = "freebsd"),
1442 not(any(feature = "x11", feature = "wayland"))
1443 ),
1444 allow(dead_code)
1445)]
1446impl PlatformInputHandler {
1447 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1448 Self { cx, handler }
1449 }
1450
1451 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1452 self.cx
1453 .update(|window, cx| {
1454 self.handler
1455 .selected_text_range(ignore_disabled_input, window, cx)
1456 })
1457 .ok()
1458 .flatten()
1459 }
1460
1461 #[cfg_attr(target_os = "windows", allow(dead_code))]
1462 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1463 self.cx
1464 .update(|window, cx| self.handler.marked_text_range(window, cx))
1465 .ok()
1466 .flatten()
1467 }
1468
1469 #[cfg_attr(
1470 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1471 allow(dead_code)
1472 )]
1473 pub fn text_for_range(
1474 &mut self,
1475 range_utf16: Range<usize>,
1476 adjusted: &mut Option<Range<usize>>,
1477 ) -> Option<String> {
1478 self.cx
1479 .update(|window, cx| {
1480 self.handler
1481 .text_for_range(range_utf16, adjusted, window, cx)
1482 })
1483 .ok()
1484 .flatten()
1485 }
1486
1487 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1488 self.cx
1489 .update(|window, cx| {
1490 self.handler
1491 .replace_text_in_range(replacement_range, text, window, cx);
1492 })
1493 .ok();
1494 }
1495
1496 pub fn replace_and_mark_text_in_range(
1497 &mut self,
1498 range_utf16: Option<Range<usize>>,
1499 new_text: &str,
1500 new_selected_range: Option<Range<usize>>,
1501 ) {
1502 self.cx
1503 .update(|window, cx| {
1504 self.handler.replace_and_mark_text_in_range(
1505 range_utf16,
1506 new_text,
1507 new_selected_range,
1508 window,
1509 cx,
1510 )
1511 })
1512 .ok();
1513 }
1514
1515 #[cfg_attr(target_os = "windows", allow(dead_code))]
1516 pub fn unmark_text(&mut self) {
1517 self.cx
1518 .update(|window, cx| self.handler.unmark_text(window, cx))
1519 .ok();
1520 }
1521
1522 pub fn paste(&mut self, item: ClipboardItem) {
1523 self.cx
1524 .update(|window, cx| self.handler.paste(item, window, cx))
1525 .ok();
1526 }
1527
1528 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1529 self.cx
1530 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1531 .ok()
1532 .flatten()
1533 }
1534
1535 #[allow(dead_code)]
1536 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1537 self.handler.apple_press_and_hold_enabled()
1538 }
1539
1540 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1541 self.handler.replace_text_in_range(None, input, window, cx);
1542 }
1543
1544 pub fn compute_ime_candidate_bounds(
1545 marked_range: Option<Range<usize>>,
1546 selection: &UTF16Selection,
1547 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1548 ) -> Option<Bounds<Pixels>> {
1549 if let Some(marked_range) = marked_range {
1550 let mut line_start = marked_range.start;
1552
1553 let caret = selection.range.end;
1557 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1558 for i in (marked_range.start..caret).rev() {
1559 if let Some(b) = bounds_for_range(i..i) {
1560 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1561 line_start = i + 1;
1562 break;
1563 }
1564 }
1565 }
1566 }
1567 bounds_for_range(line_start..line_start)
1568 } else {
1569 let offset = if selection.reversed {
1571 selection.range.start
1572 } else {
1573 selection.range.end
1574 };
1575 bounds_for_range(offset..offset)
1576 }
1577 }
1578
1579 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1580 let marked_range = self.handler.marked_text_range(window, cx);
1581 let selection = self.handler.selected_text_range(true, window, cx)?;
1582 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1583 self.handler.bounds_for_range(range, window, cx)
1584 })
1585 }
1586
1587 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1588 let marked_range = self.marked_text_range();
1589 let selection = self.selected_text_range(true)?;
1590 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1591 self.bounds_for_range(range)
1592 })
1593 }
1594
1595 #[allow(unused)]
1596 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1597 self.cx
1598 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1599 .ok()
1600 .flatten()
1601 }
1602
1603 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
1605 self.cx
1606 .update(|window, cx| {
1607 self.handler
1608 .set_selected_text_range(range_utf16, window, cx)
1609 })
1610 .ok();
1611 }
1612
1613 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
1615 self.cx
1616 .update(|window, cx| self.handler.element_bounds(window, cx))
1617 .ok()
1618 .flatten()
1619 }
1620
1621 pub fn text_length_utf16(&mut self) -> Option<usize> {
1623 self.cx
1624 .update(|window, cx| self.handler.text_length_utf16(window, cx))
1625 .ok()
1626 .flatten()
1627 }
1628
1629 #[allow(dead_code)]
1630 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1631 self.handler.accepts_text_input(window, cx)
1632 }
1633
1634 #[allow(dead_code)]
1635 pub fn query_accepts_text_input(&mut self) -> bool {
1636 self.cx
1637 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1638 .unwrap_or(true)
1639 }
1640
1641 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1647 self.cx
1648 .update(|window, cx| {
1649 !window.has_pending_keystrokes()
1651 && self.handler.prefers_ime_for_printable_keys(window, cx)
1652 })
1653 .unwrap_or(false)
1654 }
1655}
1656
1657#[derive(Debug)]
1660pub struct UTF16Selection {
1661 pub range: Range<usize>,
1664 pub reversed: bool,
1667}
1668
1669pub trait InputHandler: 'static {
1674 fn selected_text_range(
1679 &mut self,
1680 ignore_disabled_input: bool,
1681 window: &mut Window,
1682 cx: &mut App,
1683 ) -> Option<UTF16Selection>;
1684
1685 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1690
1691 fn text_for_range(
1696 &mut self,
1697 range_utf16: Range<usize>,
1698 adjusted_range: &mut Option<Range<usize>>,
1699 window: &mut Window,
1700 cx: &mut App,
1701 ) -> Option<String>;
1702
1703 fn replace_text_in_range(
1708 &mut self,
1709 replacement_range: Option<Range<usize>>,
1710 text: &str,
1711 window: &mut Window,
1712 cx: &mut App,
1713 );
1714
1715 fn replace_and_mark_text_in_range(
1722 &mut self,
1723 range_utf16: Option<Range<usize>>,
1724 new_text: &str,
1725 new_selected_range: Option<Range<usize>>,
1726 window: &mut Window,
1727 cx: &mut App,
1728 );
1729
1730 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1733
1734 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
1741 if let Some(text) = item.text() {
1742 self.replace_text_in_range(None, &text, window, cx);
1743 }
1744 }
1745
1746 fn bounds_for_range(
1751 &mut self,
1752 range_utf16: Range<usize>,
1753 window: &mut Window,
1754 cx: &mut App,
1755 ) -> Option<Bounds<Pixels>>;
1756
1757 fn character_index_for_point(
1761 &mut self,
1762 point: Point<Pixels>,
1763 window: &mut Window,
1764 cx: &mut App,
1765 ) -> Option<usize>;
1766
1767 fn set_selected_text_range(
1777 &mut self,
1778 _range_utf16: Range<usize>,
1779 _window: &mut Window,
1780 _cx: &mut App,
1781 ) {
1782 }
1783
1784 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
1791 None
1792 }
1793
1794 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
1796 None
1797 }
1798
1799 #[allow(dead_code)]
1804 fn apple_press_and_hold_enabled(&mut self) -> bool {
1805 true
1806 }
1807
1808 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1810 true
1811 }
1812
1813 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1822 false
1823 }
1824}
1825
1826#[derive(Debug)]
1828pub struct WindowOptions {
1829 pub window_bounds: Option<WindowBounds>,
1833
1834 pub titlebar: Option<TitlebarOptions>,
1836
1837 pub focus: bool,
1839
1840 pub show: bool,
1842
1843 pub kind: WindowKind,
1845
1846 pub is_movable: bool,
1850
1851 pub app_owns_titlebar_drag: bool,
1863
1864 pub inactive_frame_interval: Option<Duration>,
1868
1869 pub is_resizable: bool,
1871
1872 pub is_minimizable: bool,
1874
1875 pub display_id: Option<DisplayId>,
1878
1879 pub window_background: WindowBackgroundAppearance,
1881
1882 pub app_id: Option<String>,
1884
1885 pub window_min_size: Option<Size<Pixels>>,
1887
1888 pub window_decorations: Option<WindowDecorations>,
1891
1892 pub icon: Option<Arc<image::RgbaImage>>,
1894
1895 pub tabbing_identifier: Option<String>,
1897}
1898
1899#[derive(Debug)]
1901#[cfg_attr(
1902 all(
1903 any(target_os = "linux", target_os = "freebsd"),
1904 not(any(feature = "x11", feature = "wayland"))
1905 ),
1906 allow(dead_code)
1907)]
1908#[allow(missing_docs)]
1909pub struct WindowParams {
1910 pub bounds: Bounds<Pixels>,
1911
1912 #[cfg_attr(feature = "wayland", allow(dead_code))]
1914 pub titlebar: Option<TitlebarOptions>,
1915
1916 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1918 pub kind: WindowKind,
1919
1920 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1922 pub is_movable: bool,
1923
1924 #[cfg_attr(
1926 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1927 allow(dead_code)
1928 )]
1929 pub app_owns_titlebar_drag: bool,
1930
1931 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1933 pub is_resizable: bool,
1934
1935 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1937 pub is_minimizable: bool,
1938
1939 #[cfg_attr(
1940 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1941 allow(dead_code)
1942 )]
1943 pub focus: bool,
1944
1945 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1946 pub show: bool,
1947
1948 #[cfg_attr(feature = "wayland", allow(dead_code))]
1950 pub icon: Option<Arc<image::RgbaImage>>,
1951
1952 #[cfg_attr(feature = "wayland", allow(dead_code))]
1953 pub display_id: Option<DisplayId>,
1954
1955 #[cfg_attr(feature = "wayland", allow(dead_code))]
1956 pub app_id: Option<String>,
1957
1958 pub window_min_size: Option<Size<Pixels>>,
1959
1960 #[cfg(target_os = "macos")]
1961 pub tabbing_identifier: Option<String>,
1962}
1963
1964#[derive(Debug, Copy, Clone, PartialEq)]
1966pub enum WindowBounds {
1967 Windowed(Bounds<Pixels>),
1969 Maximized(Bounds<Pixels>),
1972 Fullscreen(Bounds<Pixels>),
1975}
1976
1977impl Default for WindowBounds {
1978 fn default() -> Self {
1979 WindowBounds::Windowed(Bounds::default())
1980 }
1981}
1982
1983impl WindowBounds {
1984 pub fn get_bounds(&self) -> Bounds<Pixels> {
1986 match self {
1987 WindowBounds::Windowed(bounds) => *bounds,
1988 WindowBounds::Maximized(bounds) => *bounds,
1989 WindowBounds::Fullscreen(bounds) => *bounds,
1990 }
1991 }
1992
1993 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1995 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1996 }
1997}
1998
1999impl Default for WindowOptions {
2000 fn default() -> Self {
2001 Self {
2002 window_bounds: None,
2003 titlebar: Some(TitlebarOptions {
2004 title: Default::default(),
2005 appears_transparent: Default::default(),
2006 traffic_light_position: Default::default(),
2007 }),
2008 focus: true,
2009 show: true,
2010 kind: WindowKind::Normal,
2011 is_movable: true,
2012 app_owns_titlebar_drag: false,
2013 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2014 is_resizable: true,
2015 is_minimizable: true,
2016 display_id: None,
2017 window_background: WindowBackgroundAppearance::default(),
2018 icon: None,
2019 app_id: None,
2020 window_min_size: None,
2021 window_decorations: None,
2022 tabbing_identifier: None,
2023 }
2024 }
2025}
2026
2027#[derive(Debug, Default)]
2029pub struct TitlebarOptions {
2030 pub title: Option<SharedString>,
2032
2033 pub appears_transparent: bool,
2036
2037 pub traffic_light_position: Option<Point<Pixels>>,
2039}
2040
2041#[derive(Clone, Debug, PartialEq, Eq)]
2043pub enum WindowKind {
2044 Normal,
2046
2047 PopUp,
2050
2051 AnchoredPopup(popup::PopupOptions),
2058
2059 Floating,
2061
2062 #[cfg(all(target_os = "linux", feature = "wayland"))]
2065 LayerShell(layer_shell::LayerShellOptions),
2066
2067 Dialog,
2070}
2071
2072#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2077pub enum WindowAppearance {
2078 #[default]
2082 Light,
2083
2084 VibrantLight,
2088
2089 Dark,
2093
2094 VibrantDark,
2098}
2099
2100#[derive(Copy, Clone, Debug, Default, PartialEq)]
2103pub enum WindowBackgroundAppearance {
2104 #[default]
2112 Opaque,
2113 Transparent,
2115 Blurred,
2119 MicaBackdrop,
2121 MicaAltBackdrop,
2123}
2124
2125#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2127pub enum TextRenderingMode {
2128 #[default]
2130 PlatformDefault,
2131 Subpixel,
2133 Grayscale,
2135}
2136
2137#[derive(Clone, Debug)]
2139pub struct PathPromptOptions {
2140 pub files: bool,
2142 pub directories: bool,
2144 pub multiple: bool,
2146 pub prompt: Option<SharedString>,
2148}
2149
2150#[derive(Copy, Clone, Debug, PartialEq)]
2152pub enum PromptLevel {
2153 Info,
2155
2156 Warning,
2158
2159 Critical,
2161}
2162
2163#[derive(Clone, Debug, PartialEq)]
2165pub enum PromptButton {
2166 Ok(SharedString),
2168 Cancel(SharedString),
2170 Other(SharedString),
2172}
2173
2174impl PromptButton {
2175 pub fn new(label: impl Into<SharedString>) -> Self {
2177 PromptButton::Other(label.into())
2178 }
2179
2180 pub fn ok(label: impl Into<SharedString>) -> Self {
2182 PromptButton::Ok(label.into())
2183 }
2184
2185 pub fn cancel(label: impl Into<SharedString>) -> Self {
2187 PromptButton::Cancel(label.into())
2188 }
2189
2190 #[allow(dead_code)]
2192 pub fn is_cancel(&self) -> bool {
2193 matches!(self, PromptButton::Cancel(_))
2194 }
2195
2196 pub fn label(&self) -> &SharedString {
2198 match self {
2199 PromptButton::Ok(label) => label,
2200 PromptButton::Cancel(label) => label,
2201 PromptButton::Other(label) => label,
2202 }
2203 }
2204}
2205
2206impl From<&str> for PromptButton {
2207 fn from(value: &str) -> Self {
2208 match value.to_lowercase().as_str() {
2209 "ok" => PromptButton::Ok("OK".into()),
2210 "cancel" => PromptButton::Cancel("Cancel".into()),
2211 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2212 }
2213 }
2214}
2215
2216#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2218pub enum CursorStyle {
2219 #[default]
2221 Arrow,
2222
2223 IBeam,
2226
2227 Crosshair,
2230
2231 ClosedHand,
2234
2235 OpenHand,
2238
2239 PointingHand,
2242
2243 ResizeLeft,
2246
2247 ResizeRight,
2250
2251 ResizeLeftRight,
2254
2255 ResizeUp,
2258
2259 ResizeDown,
2262
2263 ResizeUpDown,
2266
2267 ResizeUpLeftDownRight,
2270
2271 ResizeUpRightDownLeft,
2274
2275 ResizeColumn,
2278
2279 ResizeRow,
2282
2283 IBeamCursorForVerticalLayout,
2286
2287 OperationNotAllowed,
2290
2291 DragLink,
2294
2295 DragCopy,
2298
2299 ContextualMenu,
2302}
2303
2304#[derive(Clone, Debug, Eq, PartialEq)]
2306pub struct ClipboardItem {
2307 pub entries: Vec<ClipboardEntry>,
2309}
2310
2311#[derive(Clone, Debug, PartialEq, Eq)]
2316pub enum ClipboardReadError {
2317 Unavailable,
2321 Denied(String),
2324 UnsupportedContent,
2327}
2328
2329impl std::fmt::Display for ClipboardReadError {
2330 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2331 match self {
2332 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
2333 Self::Denied(message) => {
2334 write!(formatter, "clipboard access was denied: {message}")
2335 }
2336 Self::UnsupportedContent => {
2337 formatter.write_str("the clipboard contents are unsupported")
2338 }
2339 }
2340 }
2341}
2342
2343impl std::error::Error for ClipboardReadError {}
2344
2345#[derive(Clone, Debug, Eq, PartialEq)]
2347pub enum ClipboardEntry {
2348 String(ClipboardString),
2350 Image(Image),
2352 ExternalPaths(crate::ExternalPaths),
2354}
2355
2356impl ClipboardItem {
2357 pub fn new_string(text: String) -> Self {
2359 Self {
2360 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2361 }
2362 }
2363
2364 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2366 Self {
2367 entries: vec![ClipboardEntry::String(ClipboardString {
2368 text,
2369 metadata: Some(metadata),
2370 })],
2371 }
2372 }
2373
2374 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2376 Self {
2377 entries: vec![ClipboardEntry::String(
2378 ClipboardString::new(text).with_json_metadata(metadata),
2379 )],
2380 }
2381 }
2382
2383 pub fn new_image(image: &Image) -> Self {
2385 Self {
2386 entries: vec![ClipboardEntry::Image(image.clone())],
2387 }
2388 }
2389
2390 pub fn text(&self) -> Option<String> {
2393 let mut answer = String::new();
2394
2395 for entry in self.entries.iter() {
2396 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2397 answer.push_str(text);
2398 }
2399 }
2400
2401 if answer.is_empty() {
2402 for entry in self.entries.iter() {
2403 if let ClipboardEntry::ExternalPaths(paths) = entry {
2404 for path in &paths.0 {
2405 use std::fmt::Write as _;
2406 _ = write!(answer, "{}", path.display());
2407 }
2408 }
2409 }
2410 }
2411
2412 if !answer.is_empty() {
2413 Some(answer)
2414 } else {
2415 None
2416 }
2417 }
2418
2419 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2421 pub fn metadata(&self) -> Option<&String> {
2422 match self.entries().first() {
2423 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2424 clipboard_string.metadata.as_ref()
2425 }
2426 _ => None,
2427 }
2428 }
2429
2430 pub fn entries(&self) -> &[ClipboardEntry] {
2432 &self.entries
2433 }
2434
2435 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2437 self.entries.into_iter()
2438 }
2439}
2440
2441impl From<ClipboardString> for ClipboardEntry {
2442 fn from(value: ClipboardString) -> Self {
2443 Self::String(value)
2444 }
2445}
2446
2447impl From<String> for ClipboardEntry {
2448 fn from(value: String) -> Self {
2449 Self::from(ClipboardString::from(value))
2450 }
2451}
2452
2453impl From<Image> for ClipboardEntry {
2454 fn from(value: Image) -> Self {
2455 Self::Image(value)
2456 }
2457}
2458
2459impl From<ClipboardEntry> for ClipboardItem {
2460 fn from(value: ClipboardEntry) -> Self {
2461 Self {
2462 entries: vec![value],
2463 }
2464 }
2465}
2466
2467impl From<String> for ClipboardItem {
2468 fn from(value: String) -> Self {
2469 Self::from(ClipboardEntry::from(value))
2470 }
2471}
2472
2473impl From<Image> for ClipboardItem {
2474 fn from(value: Image) -> Self {
2475 Self::from(ClipboardEntry::from(value))
2476 }
2477}
2478
2479#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2481pub enum ImageFormat {
2482 Png,
2487 Jpeg,
2489 Webp,
2491 Gif,
2493 Svg,
2495 Bmp,
2497 Tiff,
2499 Ico,
2501 Pnm,
2503}
2504
2505impl ImageFormat {
2506 pub const fn mime_type(self) -> &'static str {
2508 match self {
2509 ImageFormat::Png => "image/png",
2510 ImageFormat::Jpeg => "image/jpeg",
2511 ImageFormat::Webp => "image/webp",
2512 ImageFormat::Gif => "image/gif",
2513 ImageFormat::Svg => "image/svg+xml",
2514 ImageFormat::Bmp => "image/bmp",
2515 ImageFormat::Tiff => "image/tiff",
2516 ImageFormat::Ico => "image/ico",
2517 ImageFormat::Pnm => "image/x-portable-anymap",
2518 }
2519 }
2520
2521 pub const fn extension(self) -> &'static str {
2523 match self {
2524 ImageFormat::Png => "png",
2525 ImageFormat::Jpeg => "jpg",
2526 ImageFormat::Webp => "webp",
2527 ImageFormat::Gif => "gif",
2528 ImageFormat::Svg => "svg",
2529 ImageFormat::Bmp => "bmp",
2530 ImageFormat::Tiff => "tiff",
2531 ImageFormat::Ico => "ico",
2532 ImageFormat::Pnm => "pnm",
2533 }
2534 }
2535
2536 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2538 use strum::IntoEnumIterator;
2539 Self::iter()
2540 .find(|format| format.mime_type() == mime_type)
2541 .or_else(|| Self::from_mime_type_alias(mime_type))
2542 }
2543
2544 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2548 match mime_type {
2549 "image/jpg" => Some(Self::Jpeg),
2550 "image/tif" => Some(Self::Tiff),
2551 _ => None,
2552 }
2553 }
2554}
2555
2556#[derive(Clone, Debug, PartialEq, Eq)]
2558pub struct Image {
2559 pub format: ImageFormat,
2561 pub bytes: Vec<u8>,
2563 pub id: u64,
2565}
2566
2567pub(crate) fn decode_static_image(
2568 bytes: &[u8],
2569 format: image::ImageFormat,
2570) -> Result<SmallVec<[Frame; 1]>> {
2571 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
2572 .into_decoder()
2573 .context("creating image decoder")?;
2574 decode_static_image_from_decoder(decoder)
2575}
2576
2577pub(crate) fn decode_static_image_from_decoder(
2578 mut decoder: impl image::ImageDecoder,
2579) -> Result<SmallVec<[Frame; 1]>> {
2580 let orientation = decoder
2581 .orientation()
2582 .context("reading decoder's orientation")?;
2583 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
2584 image.apply_orientation(orientation);
2585
2586 let mut data = image.into_rgba8();
2587 for pixel in data.chunks_exact_mut(4) {
2588 pixel.swap(0, 2);
2589 }
2590
2591 Ok(SmallVec::from_elem(Frame::new(data), 1))
2592}
2593
2594impl Hash for Image {
2595 fn hash<H: Hasher>(&self, state: &mut H) {
2596 state.write_u64(self.id);
2597 }
2598}
2599
2600impl Image {
2601 pub fn empty() -> Self {
2603 Self::from_bytes(ImageFormat::Png, Vec::new())
2604 }
2605
2606 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2608 Self {
2609 id: hash(&bytes),
2610 format,
2611 bytes,
2612 }
2613 }
2614
2615 pub fn id(&self) -> u64 {
2617 self.id
2618 }
2619
2620 pub fn use_render_image(
2622 self: Arc<Self>,
2623 window: &mut Window,
2624 cx: &mut App,
2625 ) -> Option<Arc<RenderImage>> {
2626 ImageSource::Image(self)
2627 .use_data(None, window, cx)
2628 .and_then(|result| result.ok())
2629 }
2630
2631 pub fn get_render_image(
2633 self: Arc<Self>,
2634 window: &mut Window,
2635 cx: &mut App,
2636 ) -> Option<Arc<RenderImage>> {
2637 ImageSource::Image(self)
2638 .get_data(None, window, cx)
2639 .and_then(|result| result.ok())
2640 }
2641
2642 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2644 ImageSource::Image(self).remove_asset(cx);
2645 }
2646
2647 #[cfg(any(test, feature = "test-support"))]
2650 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
2651 ImageSource::Image(self.clone()).is_asset_cached(cx)
2652 }
2653
2654 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2656 let frames = match self.format {
2657 ImageFormat::Gif => {
2658 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2659 let mut frames = SmallVec::new();
2660
2661 for frame in decoder.into_frames() {
2662 match frame {
2663 Ok(mut frame) => {
2664 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2666 pixel.swap(0, 2);
2667 }
2668 frames.push(frame);
2669 }
2670 Err(err) => {
2671 log::debug!("Skipping GIF frame due to decode error: {err}");
2672 }
2673 }
2674 }
2675
2676 if frames.is_empty() {
2677 anyhow::bail!("GIF could not be decoded: all frames failed");
2678 }
2679
2680 frames
2681 }
2682 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
2683 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
2684 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
2685 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
2686 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
2687 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
2688 ImageFormat::Svg => {
2689 return svg_renderer
2690 .render_single_frame(&self.bytes, 1.0)
2691 .map_err(Into::into);
2692 }
2693 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
2694 };
2695
2696 Ok(Arc::new(RenderImage::new(frames)))
2697 }
2698
2699 pub fn format(&self) -> ImageFormat {
2701 self.format
2702 }
2703
2704 pub fn bytes(&self) -> &[u8] {
2706 self.bytes.as_slice()
2707 }
2708}
2709
2710#[derive(Clone, Debug, Eq, PartialEq)]
2712pub struct ClipboardString {
2713 pub text: String,
2715 pub metadata: Option<String>,
2717}
2718
2719impl ClipboardString {
2720 pub fn new(text: String) -> Self {
2722 Self {
2723 text,
2724 metadata: None,
2725 }
2726 }
2727
2728 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2731 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2732 self
2733 }
2734
2735 pub fn text(&self) -> &String {
2737 &self.text
2738 }
2739
2740 pub fn into_text(self) -> String {
2742 self.text
2743 }
2744
2745 pub fn metadata_json<T>(&self) -> Option<T>
2747 where
2748 T: for<'a> Deserialize<'a>,
2749 {
2750 self.metadata
2751 .as_ref()
2752 .and_then(|m| serde_json::from_str(m).ok())
2753 }
2754
2755 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2756 pub fn text_hash(text: &str) -> u64 {
2758 let mut hasher = SeaHasher::new();
2759 text.hash(&mut hasher);
2760 hasher.finish()
2761 }
2762}
2763
2764impl From<String> for ClipboardString {
2765 fn from(value: String) -> Self {
2766 Self {
2767 text: value,
2768 metadata: None,
2769 }
2770 }
2771}
2772
2773#[cfg(test)]
2774mod image_tests {
2775 use super::*;
2776 use std::sync::Arc;
2777
2778 #[test]
2779 fn test_image_to_image_data_applies_exif_orientation() {
2780 let image = Image::from_bytes(
2781 ImageFormat::Jpeg,
2782 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
2783 );
2784
2785 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2786
2787 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
2788
2789 let bytes = render_image.as_bytes(0).unwrap();
2790 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
2791 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
2792 }
2793
2794 #[test]
2795 fn test_svg_image_to_image_data_converts_to_bgra() {
2796 let image = Image::from_bytes(
2797 ImageFormat::Svg,
2798 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2799<rect width="1" height="1" fill="#38BDF8"/>
2800</svg>"##
2801 .to_vec(),
2802 );
2803
2804 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2805 let bytes = render_image.as_bytes(0).unwrap();
2806
2807 for pixel in bytes.chunks_exact(4) {
2808 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2809 }
2810 }
2811}
2812
2813#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2814mod tests {
2815 use super::*;
2816 use std::collections::HashSet;
2817
2818 #[test]
2819 fn test_window_button_layout_parse_standard() {
2820 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2821 assert_eq!(
2822 layout.left,
2823 [
2824 Some(WindowButton::Close),
2825 Some(WindowButton::Minimize),
2826 None
2827 ]
2828 );
2829 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2830 }
2831
2832 #[test]
2833 fn test_window_button_layout_parse_right_only() {
2834 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2835 assert_eq!(layout.left, [None, None, None]);
2836 assert_eq!(
2837 layout.right,
2838 [
2839 Some(WindowButton::Minimize),
2840 Some(WindowButton::Maximize),
2841 Some(WindowButton::Close)
2842 ]
2843 );
2844 }
2845
2846 #[test]
2847 fn test_window_button_layout_parse_left_only() {
2848 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2849 assert_eq!(
2850 layout.left,
2851 [
2852 Some(WindowButton::Close),
2853 Some(WindowButton::Minimize),
2854 Some(WindowButton::Maximize)
2855 ]
2856 );
2857 assert_eq!(layout.right, [None, None, None]);
2858 }
2859
2860 #[test]
2861 fn test_window_button_layout_parse_with_whitespace() {
2862 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2863 assert_eq!(
2864 layout.left,
2865 [
2866 Some(WindowButton::Close),
2867 Some(WindowButton::Minimize),
2868 None
2869 ]
2870 );
2871 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2872 }
2873
2874 #[test]
2875 fn test_window_button_layout_parse_empty() {
2876 let layout = WindowButtonLayout::parse("").unwrap();
2877 assert_eq!(layout.left, [None, None, None]);
2878 assert_eq!(layout.right, [None, None, None]);
2879 }
2880
2881 #[test]
2882 fn test_window_button_layout_parse_intentionally_empty() {
2883 let layout = WindowButtonLayout::parse(":").unwrap();
2884 assert_eq!(layout.left, [None, None, None]);
2885 assert_eq!(layout.right, [None, None, None]);
2886 }
2887
2888 #[test]
2889 fn test_window_button_layout_parse_invalid_buttons() {
2890 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2891 assert_eq!(
2892 layout.left,
2893 [
2894 Some(WindowButton::Close),
2895 Some(WindowButton::Minimize),
2896 None
2897 ]
2898 );
2899 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2900 }
2901
2902 #[test]
2903 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2904 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2905 assert_eq!(
2906 layout.right,
2907 [
2908 Some(WindowButton::Close),
2909 Some(WindowButton::Minimize),
2910 None
2911 ]
2912 );
2913 assert_eq!(layout.format(), ":close,minimize");
2914 }
2915
2916 #[test]
2917 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2918 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2919 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2920 assert_eq!(
2921 layout.right,
2922 [
2923 Some(WindowButton::Maximize),
2924 Some(WindowButton::Minimize),
2925 None
2926 ]
2927 );
2928
2929 let button_ids: Vec<_> = layout
2930 .left
2931 .iter()
2932 .chain(layout.right.iter())
2933 .flatten()
2934 .map(WindowButton::id)
2935 .collect();
2936 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2937 assert_eq!(unique_button_ids.len(), button_ids.len());
2938 assert_eq!(layout.format(), "close:maximize,minimize");
2939 }
2940
2941 #[test]
2942 fn test_window_button_layout_parse_gnome_style() {
2943 let layout = WindowButtonLayout::parse("close").unwrap();
2944 assert_eq!(layout.left, [None, None, None]);
2945 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2946 }
2947
2948 #[test]
2949 fn test_window_button_layout_parse_elementary_style() {
2950 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2951 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2952 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2953 }
2954
2955 #[test]
2956 fn test_window_button_layout_round_trip() {
2957 let cases = [
2958 "close:minimize,maximize",
2959 "minimize,maximize,close:",
2960 ":close",
2961 "close:",
2962 "close:maximize",
2963 ":",
2964 ];
2965
2966 for case in cases {
2967 let layout = WindowButtonLayout::parse(case).unwrap();
2968 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2969 }
2970 }
2971
2972 #[test]
2973 fn test_window_button_layout_linux_default() {
2974 let layout = WindowButtonLayout::linux_default();
2975 assert_eq!(layout.left, [None, None, None]);
2976 assert_eq!(
2977 layout.right,
2978 [
2979 Some(WindowButton::Minimize),
2980 Some(WindowButton::Maximize),
2981 Some(WindowButton::Close)
2982 ]
2983 );
2984
2985 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2986 assert_eq!(round_tripped, layout);
2987 }
2988
2989 #[test]
2990 fn test_window_button_layout_parse_all_invalid() {
2991 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2992 }
2993}