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 gpu_time(&self) -> Option<Duration> {
868 None
869 }
870 fn schedule_frame(&self) {}
871 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
872 fn is_subpixel_rendering_supported(&self) -> bool;
873
874 fn get_title(&self) -> String {
876 String::new()
877 }
878 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
879 None
880 }
881 fn tab_bar_visible(&self) -> bool {
882 false
883 }
884 fn set_edited(&mut self, _edited: bool) {}
885 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
886 fn toggle_simple_fullscreen(&self) {}
887 fn is_simple_fullscreen(&self) -> bool {
888 false
889 }
890 #[cfg(target_os = "macos")]
891 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
892 fn show_character_palette(&self) {}
893 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
894 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
895 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
896 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
897 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
898 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
899 fn merge_all_windows(&self) {}
900 fn move_tab_to_new_window(&self) {}
901 fn toggle_window_tab_overview(&self) {}
902 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
903
904 #[cfg(target_os = "windows")]
905 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
906
907 fn inner_window_bounds(&self) -> WindowBounds {
909 self.window_bounds()
910 }
911 fn request_decorations(&self, _decorations: WindowDecorations) {}
912 fn show_window_menu(&self, _position: Point<Pixels>) {}
913 fn start_window_move(&self) {}
914 fn can_start_external_drag(&self) -> bool {
915 false
916 }
917 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
918 false
919 }
920 fn start_window_resize(&self, _edge: ResizeEdge) {}
921 fn set_exclusive_zone(&self, _zone: Pixels) {}
922 #[cfg(all(target_os = "linux", feature = "wayland"))]
923 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
924 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
925 fn window_decorations(&self) -> Decorations {
926 Decorations::Server
927 }
928 fn set_app_id(&mut self, _app_id: &str) {}
929 fn map_window(&mut self) -> anyhow::Result<()> {
930 Ok(())
931 }
932 fn window_controls(&self) -> WindowControls {
933 WindowControls::default()
934 }
935 fn set_client_inset(&self, _inset: Pixels) {}
936 fn gpu_specs(&self) -> Option<GpuSpecs>;
937
938 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
939
940 fn insets(&self) -> WindowInsets {
945 WindowInsets::default()
946 }
947
948 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
954
955 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
958
959 fn set_back_enabled(&self, _enabled: bool) {}
962
963 fn show_soft_keyboard(&self) {}
965
966 fn hide_soft_keyboard(&self) {}
968
969 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
971
972 fn play_system_bell(&self) {}
973
974 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
976
977 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
979
980 fn a11y_update_window_bounds(&self) {}
982
983 #[cfg(any(test, feature = "test-support"))]
984 fn as_test(&mut self) -> Option<&mut TestWindow> {
985 None
986 }
987
988 #[cfg(any(test, feature = "test-support"))]
992 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
993 anyhow::bail!("render_to_image not implemented for this platform")
994 }
995}
996
997#[cfg(any(test, feature = "test-support"))]
999pub trait PlatformHeadlessRenderer {
1000 fn render_scene_to_image(
1002 &mut self,
1003 scene: &Scene,
1004 size: Size<DevicePixels>,
1005 ) -> Result<RgbaImage>;
1006
1007 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1013
1014 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1016}
1017
1018#[doc(hidden)]
1021pub type RunnableVariant = Runnable<RunnableMeta>;
1022
1023#[doc(hidden)]
1024pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1025
1026#[doc(hidden)]
1027pub enum TasksIncluded {
1028 OnlyCompleted,
1029 CompletedAndRunning,
1030}
1031
1032#[doc(hidden)]
1035pub trait PlatformDispatcher: Send + Sync {
1036 fn is_main_thread(&self) -> bool;
1037 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1038 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1039 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1040
1041 fn dispatch_on_main_thread_when_idle(
1042 &self,
1043 runnable: RunnableVariant,
1044 timeout: Option<Duration>,
1045 ) {
1046 let _ = timeout;
1047 self.dispatch_on_main_thread(runnable, Priority::Low);
1048 }
1049
1050 fn idle_time_remaining(&self) -> Option<Duration> {
1051 None
1052 }
1053
1054 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1055
1056 fn now(&self) -> Instant {
1057 Instant::now()
1058 }
1059
1060 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1061 gpui_util::defer(Box::new(|| {}))
1062 }
1063
1064 #[cfg(any(test, feature = "test-support"))]
1065 fn as_test(&self) -> Option<&TestDispatcher> {
1066 None
1067 }
1068
1069 #[cfg(any(test, feature = "test-support"))]
1072 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1073 None
1074 }
1075}
1076
1077#[expect(missing_docs)]
1078pub trait PlatformTextSystem: Send + Sync {
1079 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1080 fn all_font_names(&self) -> Vec<String>;
1082 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1084 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1086 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1088 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1090 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1092 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1094 fn rasterize_glyph(
1096 &self,
1097 params: &RenderGlyphParams,
1098 raster_bounds: Bounds<DevicePixels>,
1099 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1100 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1102 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1104 -> TextRenderingMode;
1105 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1107 0
1108 }
1109}
1110
1111#[expect(missing_docs)]
1112pub struct NoopTextSystem;
1113
1114#[expect(missing_docs)]
1115impl NoopTextSystem {
1116 #[allow(dead_code)]
1117 pub fn new() -> Self {
1118 Self
1119 }
1120}
1121
1122impl PlatformTextSystem for NoopTextSystem {
1123 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1124 Ok(())
1125 }
1126
1127 fn all_font_names(&self) -> Vec<String> {
1128 Vec::new()
1129 }
1130
1131 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1132 Ok(FontId(1))
1133 }
1134
1135 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1136 FontMetrics {
1137 units_per_em: 1000,
1138 ascent: 1025.0,
1139 descent: -275.0,
1140 line_gap: 0.0,
1141 underline_position: -95.0,
1142 underline_thickness: 60.0,
1143 cap_height: 698.0,
1144 x_height: 516.0,
1145 bounding_box: Bounds {
1146 origin: Point {
1147 x: -260.0,
1148 y: -245.0,
1149 },
1150 size: Size {
1151 width: 1501.0,
1152 height: 1364.0,
1153 },
1154 },
1155 }
1156 }
1157
1158 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1159 Ok(Bounds {
1160 origin: Point { x: 54.0, y: 0.0 },
1161 size: size(392.0, 528.0),
1162 })
1163 }
1164
1165 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1166 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1167 }
1168
1169 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1170 Some(GlyphId(ch.len_utf16() as u32))
1171 }
1172
1173 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1174 Ok(Default::default())
1175 }
1176
1177 fn rasterize_glyph(
1178 &self,
1179 _params: &RenderGlyphParams,
1180 raster_bounds: Bounds<DevicePixels>,
1181 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1182 Ok((raster_bounds.size, Vec::new()))
1183 }
1184
1185 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1186 let mut position = px(0.);
1187 let metrics = self.font_metrics(FontId(0));
1188 let em_width = font_size
1189 * self
1190 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1191 .unwrap()
1192 .width
1193 / metrics.units_per_em as f32;
1194 let mut glyphs = Vec::new();
1195 for (ix, c) in text.char_indices() {
1196 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1197 glyphs.push(ShapedGlyph {
1198 id: glyph,
1199 position: point(position, px(0.)),
1200 index: ix,
1201 is_emoji: glyph.0 == 2,
1202 });
1203 if glyph.0 == 2 {
1204 position += em_width * 2.0;
1205 } else {
1206 position += em_width;
1207 }
1208 } else {
1209 position += em_width
1210 }
1211 }
1212 let mut runs = Vec::default();
1213 if !glyphs.is_empty() {
1214 runs.push(ShapedRun {
1215 font_id: FontId(0),
1216 glyphs,
1217 });
1218 } else {
1219 position = px(0.);
1220 }
1221
1222 LineLayout {
1223 font_size,
1224 width: position,
1225 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1226 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1227 runs,
1228 len: text.len(),
1229 }
1230 }
1231
1232 fn recommended_rendering_mode(
1233 &self,
1234 _font_id: FontId,
1235 _font_size: Pixels,
1236 ) -> TextRenderingMode {
1237 TextRenderingMode::Grayscale
1238 }
1239}
1240
1241#[allow(dead_code)]
1246pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1247 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1248 [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], ];
1262
1263 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1264 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1265
1266 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1267 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1268
1269 [
1270 ratios[0] * NORM13,
1271 ratios[1] * NORM24,
1272 ratios[2] * NORM13,
1273 ratios[3] * NORM24,
1274 ]
1275}
1276
1277#[derive(PartialEq, Eq, Hash, Clone)]
1278#[expect(missing_docs)]
1279pub enum AtlasKey {
1280 Glyph(RenderGlyphParams),
1281 Svg(RenderSvgParams),
1282 Image(RenderImageParams),
1283}
1284
1285impl AtlasKey {
1286 #[cfg_attr(
1287 all(
1288 any(target_os = "linux", target_os = "freebsd"),
1289 not(any(feature = "x11", feature = "wayland"))
1290 ),
1291 allow(dead_code)
1292 )]
1293 pub fn texture_kind(&self) -> AtlasTextureKind {
1295 match self {
1296 AtlasKey::Glyph(params) => {
1297 if params.is_emoji {
1298 AtlasTextureKind::Polychrome
1299 } else if params.subpixel_rendering {
1300 AtlasTextureKind::Subpixel
1301 } else {
1302 AtlasTextureKind::Monochrome
1303 }
1304 }
1305 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1306 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1307 }
1308 }
1309}
1310
1311impl From<RenderGlyphParams> for AtlasKey {
1312 fn from(params: RenderGlyphParams) -> Self {
1313 Self::Glyph(params)
1314 }
1315}
1316
1317impl From<RenderSvgParams> for AtlasKey {
1318 fn from(params: RenderSvgParams) -> Self {
1319 Self::Svg(params)
1320 }
1321}
1322
1323impl From<RenderImageParams> for AtlasKey {
1324 fn from(params: RenderImageParams) -> Self {
1325 Self::Image(params)
1326 }
1327}
1328
1329#[expect(missing_docs)]
1330pub trait PlatformAtlas {
1331 fn get_or_insert_with<'a>(
1332 &self,
1333 key: &AtlasKey,
1334 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1335 ) -> Result<Option<AtlasTile>>;
1336 fn remove(&self, key: &AtlasKey);
1337
1338 #[cfg(any(test, feature = "test-support"))]
1339 fn contains(&self, _key: &AtlasKey) -> bool {
1340 false
1341 }
1342}
1343
1344#[doc(hidden)]
1345pub struct AtlasTextureList<T> {
1346 pub textures: Vec<Option<T>>,
1347 pub free_list: Vec<usize>,
1348}
1349
1350impl<T> Default for AtlasTextureList<T> {
1351 fn default() -> Self {
1352 Self {
1353 textures: Vec::default(),
1354 free_list: Vec::default(),
1355 }
1356 }
1357}
1358
1359impl<T> ops::Index<usize> for AtlasTextureList<T> {
1360 type Output = Option<T>;
1361
1362 fn index(&self, index: usize) -> &Self::Output {
1363 &self.textures[index]
1364 }
1365}
1366
1367impl<T> AtlasTextureList<T> {
1368 #[allow(unused)]
1369 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1370 self.free_list.clear();
1371 self.textures.drain(..)
1372 }
1373
1374 #[allow(dead_code)]
1375 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1376 self.textures.iter_mut().flatten()
1377 }
1378}
1379
1380#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1381#[repr(C)]
1382#[expect(missing_docs)]
1383pub struct AtlasTile {
1384 pub texture_id: AtlasTextureId,
1386 pub tile_id: TileId,
1388 pub padding: u32,
1390 pub bounds: Bounds<DevicePixels>,
1392}
1393
1394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1395#[repr(C)]
1396#[expect(missing_docs)]
1397pub struct AtlasTextureId {
1398 pub index: u32,
1401 pub kind: AtlasTextureKind,
1403}
1404
1405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1406#[repr(C)]
1407#[cfg_attr(
1408 all(
1409 any(target_os = "linux", target_os = "freebsd"),
1410 not(any(feature = "x11", feature = "wayland"))
1411 ),
1412 allow(dead_code)
1413)]
1414#[expect(missing_docs)]
1415pub enum AtlasTextureKind {
1416 Monochrome = 0,
1417 Polychrome = 1,
1418 Subpixel = 2,
1419}
1420
1421#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1422#[repr(C)]
1423#[expect(missing_docs)]
1424pub struct TileId(pub u32);
1425
1426impl From<etagere::AllocId> for TileId {
1427 fn from(id: etagere::AllocId) -> Self {
1428 Self(id.serialize())
1429 }
1430}
1431
1432impl From<TileId> for etagere::AllocId {
1433 fn from(id: TileId) -> Self {
1434 Self::deserialize(id.0)
1435 }
1436}
1437
1438#[expect(missing_docs)]
1439pub struct PlatformInputHandler {
1440 cx: AsyncWindowContext,
1441 handler: Box<dyn InputHandler>,
1442}
1443
1444#[expect(missing_docs)]
1445#[cfg_attr(
1446 all(
1447 any(target_os = "linux", target_os = "freebsd"),
1448 not(any(feature = "x11", feature = "wayland"))
1449 ),
1450 allow(dead_code)
1451)]
1452impl PlatformInputHandler {
1453 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1454 Self { cx, handler }
1455 }
1456
1457 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1458 self.cx
1459 .update(|window, cx| {
1460 self.handler
1461 .selected_text_range(ignore_disabled_input, window, cx)
1462 })
1463 .ok()
1464 .flatten()
1465 }
1466
1467 #[cfg_attr(target_os = "windows", allow(dead_code))]
1468 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1469 self.cx
1470 .update(|window, cx| self.handler.marked_text_range(window, cx))
1471 .ok()
1472 .flatten()
1473 }
1474
1475 #[cfg_attr(
1476 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1477 allow(dead_code)
1478 )]
1479 pub fn text_for_range(
1480 &mut self,
1481 range_utf16: Range<usize>,
1482 adjusted: &mut Option<Range<usize>>,
1483 ) -> Option<String> {
1484 self.cx
1485 .update(|window, cx| {
1486 self.handler
1487 .text_for_range(range_utf16, adjusted, window, cx)
1488 })
1489 .ok()
1490 .flatten()
1491 }
1492
1493 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1494 self.cx
1495 .update(|window, cx| {
1496 self.handler
1497 .replace_text_in_range(replacement_range, text, window, cx);
1498 })
1499 .ok();
1500 }
1501
1502 pub fn replace_and_mark_text_in_range(
1503 &mut self,
1504 range_utf16: Option<Range<usize>>,
1505 new_text: &str,
1506 new_selected_range: Option<Range<usize>>,
1507 ) {
1508 self.cx
1509 .update(|window, cx| {
1510 self.handler.replace_and_mark_text_in_range(
1511 range_utf16,
1512 new_text,
1513 new_selected_range,
1514 window,
1515 cx,
1516 )
1517 })
1518 .ok();
1519 }
1520
1521 #[cfg_attr(target_os = "windows", allow(dead_code))]
1522 pub fn unmark_text(&mut self) {
1523 self.cx
1524 .update(|window, cx| self.handler.unmark_text(window, cx))
1525 .ok();
1526 }
1527
1528 pub fn paste(&mut self, item: ClipboardItem) {
1529 self.cx
1530 .update(|window, cx| self.handler.paste(item, window, cx))
1531 .ok();
1532 }
1533
1534 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1535 self.cx
1536 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1537 .ok()
1538 .flatten()
1539 }
1540
1541 #[allow(dead_code)]
1542 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1543 self.handler.apple_press_and_hold_enabled()
1544 }
1545
1546 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1547 self.handler.replace_text_in_range(None, input, window, cx);
1548 }
1549
1550 pub fn compute_ime_candidate_bounds(
1551 marked_range: Option<Range<usize>>,
1552 selection: &UTF16Selection,
1553 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1554 ) -> Option<Bounds<Pixels>> {
1555 if let Some(marked_range) = marked_range {
1556 let mut line_start = marked_range.start;
1558
1559 let caret = selection.range.end;
1563 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1564 for i in (marked_range.start..caret).rev() {
1565 if let Some(b) = bounds_for_range(i..i) {
1566 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1567 line_start = i + 1;
1568 break;
1569 }
1570 }
1571 }
1572 }
1573 bounds_for_range(line_start..line_start)
1574 } else {
1575 let offset = if selection.reversed {
1577 selection.range.start
1578 } else {
1579 selection.range.end
1580 };
1581 bounds_for_range(offset..offset)
1582 }
1583 }
1584
1585 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1586 let marked_range = self.handler.marked_text_range(window, cx);
1587 let selection = self.handler.selected_text_range(true, window, cx)?;
1588 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1589 self.handler.bounds_for_range(range, window, cx)
1590 })
1591 }
1592
1593 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1594 let marked_range = self.marked_text_range();
1595 let selection = self.selected_text_range(true)?;
1596 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1597 self.bounds_for_range(range)
1598 })
1599 }
1600
1601 #[allow(unused)]
1602 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1603 self.cx
1604 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1605 .ok()
1606 .flatten()
1607 }
1608
1609 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
1611 self.cx
1612 .update(|window, cx| {
1613 self.handler
1614 .set_selected_text_range(range_utf16, window, cx)
1615 })
1616 .ok();
1617 }
1618
1619 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
1621 self.cx
1622 .update(|window, cx| self.handler.element_bounds(window, cx))
1623 .ok()
1624 .flatten()
1625 }
1626
1627 pub fn text_length_utf16(&mut self) -> Option<usize> {
1629 self.cx
1630 .update(|window, cx| self.handler.text_length_utf16(window, cx))
1631 .ok()
1632 .flatten()
1633 }
1634
1635 #[allow(dead_code)]
1636 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1637 self.handler.accepts_text_input(window, cx)
1638 }
1639
1640 #[allow(dead_code)]
1641 pub fn query_accepts_text_input(&mut self) -> bool {
1642 self.cx
1643 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1644 .unwrap_or(true)
1645 }
1646
1647 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1653 self.cx
1654 .update(|window, cx| {
1655 !window.has_pending_keystrokes()
1657 && self.handler.prefers_ime_for_printable_keys(window, cx)
1658 })
1659 .unwrap_or(false)
1660 }
1661}
1662
1663#[derive(Debug)]
1666pub struct UTF16Selection {
1667 pub range: Range<usize>,
1670 pub reversed: bool,
1673}
1674
1675pub trait InputHandler: 'static {
1680 fn selected_text_range(
1685 &mut self,
1686 ignore_disabled_input: bool,
1687 window: &mut Window,
1688 cx: &mut App,
1689 ) -> Option<UTF16Selection>;
1690
1691 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1696
1697 fn text_for_range(
1702 &mut self,
1703 range_utf16: Range<usize>,
1704 adjusted_range: &mut Option<Range<usize>>,
1705 window: &mut Window,
1706 cx: &mut App,
1707 ) -> Option<String>;
1708
1709 fn replace_text_in_range(
1714 &mut self,
1715 replacement_range: Option<Range<usize>>,
1716 text: &str,
1717 window: &mut Window,
1718 cx: &mut App,
1719 );
1720
1721 fn replace_and_mark_text_in_range(
1728 &mut self,
1729 range_utf16: Option<Range<usize>>,
1730 new_text: &str,
1731 new_selected_range: Option<Range<usize>>,
1732 window: &mut Window,
1733 cx: &mut App,
1734 );
1735
1736 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1739
1740 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
1747 if let Some(text) = item.text() {
1748 self.replace_text_in_range(None, &text, window, cx);
1749 }
1750 }
1751
1752 fn bounds_for_range(
1757 &mut self,
1758 range_utf16: Range<usize>,
1759 window: &mut Window,
1760 cx: &mut App,
1761 ) -> Option<Bounds<Pixels>>;
1762
1763 fn character_index_for_point(
1767 &mut self,
1768 point: Point<Pixels>,
1769 window: &mut Window,
1770 cx: &mut App,
1771 ) -> Option<usize>;
1772
1773 fn set_selected_text_range(
1783 &mut self,
1784 _range_utf16: Range<usize>,
1785 _window: &mut Window,
1786 _cx: &mut App,
1787 ) {
1788 }
1789
1790 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
1797 None
1798 }
1799
1800 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
1802 None
1803 }
1804
1805 #[allow(dead_code)]
1810 fn apple_press_and_hold_enabled(&mut self) -> bool {
1811 true
1812 }
1813
1814 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1816 true
1817 }
1818
1819 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1828 false
1829 }
1830}
1831
1832#[derive(Debug)]
1834pub struct WindowOptions {
1835 pub window_bounds: Option<WindowBounds>,
1839
1840 pub titlebar: Option<TitlebarOptions>,
1842
1843 pub focus: bool,
1845
1846 pub show: bool,
1848
1849 pub kind: WindowKind,
1851
1852 pub is_movable: bool,
1856
1857 pub app_owns_titlebar_drag: bool,
1869
1870 pub inactive_frame_interval: Option<Duration>,
1874
1875 pub is_resizable: bool,
1877
1878 pub is_minimizable: bool,
1880
1881 pub display_id: Option<DisplayId>,
1884
1885 pub window_background: WindowBackgroundAppearance,
1887
1888 pub app_id: Option<String>,
1890
1891 pub window_min_size: Option<Size<Pixels>>,
1893
1894 pub window_decorations: Option<WindowDecorations>,
1897
1898 pub icon: Option<Arc<image::RgbaImage>>,
1900
1901 pub tabbing_identifier: Option<String>,
1903}
1904
1905#[derive(Debug)]
1907#[cfg_attr(
1908 all(
1909 any(target_os = "linux", target_os = "freebsd"),
1910 not(any(feature = "x11", feature = "wayland"))
1911 ),
1912 allow(dead_code)
1913)]
1914#[allow(missing_docs)]
1915pub struct WindowParams {
1916 pub bounds: Bounds<Pixels>,
1917
1918 #[cfg_attr(feature = "wayland", allow(dead_code))]
1920 pub titlebar: Option<TitlebarOptions>,
1921
1922 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1924 pub kind: WindowKind,
1925
1926 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1928 pub is_movable: bool,
1929
1930 #[cfg_attr(
1932 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1933 allow(dead_code)
1934 )]
1935 pub app_owns_titlebar_drag: bool,
1936
1937 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1939 pub is_resizable: bool,
1940
1941 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1943 pub is_minimizable: bool,
1944
1945 #[cfg_attr(
1946 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1947 allow(dead_code)
1948 )]
1949 pub focus: bool,
1950
1951 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1952 pub show: bool,
1953
1954 #[cfg_attr(feature = "wayland", allow(dead_code))]
1956 pub icon: Option<Arc<image::RgbaImage>>,
1957
1958 #[cfg_attr(feature = "wayland", allow(dead_code))]
1959 pub display_id: Option<DisplayId>,
1960
1961 #[cfg_attr(feature = "wayland", allow(dead_code))]
1962 pub app_id: Option<String>,
1963
1964 pub window_min_size: Option<Size<Pixels>>,
1965
1966 #[cfg(target_os = "macos")]
1967 pub tabbing_identifier: Option<String>,
1968}
1969
1970#[derive(Debug, Copy, Clone, PartialEq)]
1972pub enum WindowBounds {
1973 Windowed(Bounds<Pixels>),
1975 Maximized(Bounds<Pixels>),
1978 Fullscreen(Bounds<Pixels>),
1981}
1982
1983impl Default for WindowBounds {
1984 fn default() -> Self {
1985 WindowBounds::Windowed(Bounds::default())
1986 }
1987}
1988
1989impl WindowBounds {
1990 pub fn get_bounds(&self) -> Bounds<Pixels> {
1992 match self {
1993 WindowBounds::Windowed(bounds) => *bounds,
1994 WindowBounds::Maximized(bounds) => *bounds,
1995 WindowBounds::Fullscreen(bounds) => *bounds,
1996 }
1997 }
1998
1999 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2001 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2002 }
2003}
2004
2005impl Default for WindowOptions {
2006 fn default() -> Self {
2007 Self {
2008 window_bounds: None,
2009 titlebar: Some(TitlebarOptions {
2010 title: Default::default(),
2011 appears_transparent: Default::default(),
2012 traffic_light_position: Default::default(),
2013 }),
2014 focus: true,
2015 show: true,
2016 kind: WindowKind::Normal,
2017 is_movable: true,
2018 app_owns_titlebar_drag: false,
2019 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2020 is_resizable: true,
2021 is_minimizable: true,
2022 display_id: None,
2023 window_background: WindowBackgroundAppearance::default(),
2024 icon: None,
2025 app_id: None,
2026 window_min_size: None,
2027 window_decorations: None,
2028 tabbing_identifier: None,
2029 }
2030 }
2031}
2032
2033#[derive(Debug, Default)]
2035pub struct TitlebarOptions {
2036 pub title: Option<SharedString>,
2038
2039 pub appears_transparent: bool,
2042
2043 pub traffic_light_position: Option<Point<Pixels>>,
2045}
2046
2047#[derive(Clone, Debug, PartialEq, Eq)]
2049pub enum WindowKind {
2050 Normal,
2052
2053 PopUp,
2056
2057 AnchoredPopup(popup::PopupOptions),
2064
2065 Floating,
2067
2068 #[cfg(all(target_os = "linux", feature = "wayland"))]
2071 LayerShell(layer_shell::LayerShellOptions),
2072
2073 Dialog,
2076}
2077
2078#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2083pub enum WindowAppearance {
2084 #[default]
2088 Light,
2089
2090 VibrantLight,
2094
2095 Dark,
2099
2100 VibrantDark,
2104}
2105
2106#[derive(Copy, Clone, Debug, Default, PartialEq)]
2109pub enum WindowBackgroundAppearance {
2110 #[default]
2118 Opaque,
2119 Transparent,
2121 Blurred,
2125 MicaBackdrop,
2127 MicaAltBackdrop,
2129}
2130
2131#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2133pub enum TextRenderingMode {
2134 #[default]
2136 PlatformDefault,
2137 Subpixel,
2139 Grayscale,
2141}
2142
2143#[derive(Clone, Debug)]
2145pub struct PathPromptOptions {
2146 pub files: bool,
2148 pub directories: bool,
2150 pub multiple: bool,
2152 pub prompt: Option<SharedString>,
2154}
2155
2156#[derive(Copy, Clone, Debug, PartialEq)]
2158pub enum PromptLevel {
2159 Info,
2161
2162 Warning,
2164
2165 Critical,
2167}
2168
2169#[derive(Clone, Debug, PartialEq)]
2171pub enum PromptButton {
2172 Ok(SharedString),
2174 Cancel(SharedString),
2176 Other(SharedString),
2178}
2179
2180impl PromptButton {
2181 pub fn new(label: impl Into<SharedString>) -> Self {
2183 PromptButton::Other(label.into())
2184 }
2185
2186 pub fn ok(label: impl Into<SharedString>) -> Self {
2188 PromptButton::Ok(label.into())
2189 }
2190
2191 pub fn cancel(label: impl Into<SharedString>) -> Self {
2193 PromptButton::Cancel(label.into())
2194 }
2195
2196 #[allow(dead_code)]
2198 pub fn is_cancel(&self) -> bool {
2199 matches!(self, PromptButton::Cancel(_))
2200 }
2201
2202 pub fn label(&self) -> &SharedString {
2204 match self {
2205 PromptButton::Ok(label) => label,
2206 PromptButton::Cancel(label) => label,
2207 PromptButton::Other(label) => label,
2208 }
2209 }
2210}
2211
2212impl From<&str> for PromptButton {
2213 fn from(value: &str) -> Self {
2214 match value.to_lowercase().as_str() {
2215 "ok" => PromptButton::Ok("OK".into()),
2216 "cancel" => PromptButton::Cancel("Cancel".into()),
2217 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2218 }
2219 }
2220}
2221
2222#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2224pub enum CursorStyle {
2225 #[default]
2227 Arrow,
2228
2229 IBeam,
2232
2233 Crosshair,
2236
2237 ClosedHand,
2240
2241 OpenHand,
2244
2245 PointingHand,
2248
2249 ResizeLeft,
2252
2253 ResizeRight,
2256
2257 ResizeLeftRight,
2260
2261 ResizeUp,
2264
2265 ResizeDown,
2268
2269 ResizeUpDown,
2272
2273 ResizeUpLeftDownRight,
2276
2277 ResizeUpRightDownLeft,
2280
2281 ResizeColumn,
2284
2285 ResizeRow,
2288
2289 IBeamCursorForVerticalLayout,
2292
2293 OperationNotAllowed,
2296
2297 DragLink,
2300
2301 DragCopy,
2304
2305 ContextualMenu,
2308}
2309
2310#[derive(Clone, Debug, Eq, PartialEq)]
2312pub struct ClipboardItem {
2313 pub entries: Vec<ClipboardEntry>,
2315}
2316
2317#[derive(Clone, Debug, PartialEq, Eq)]
2322pub enum ClipboardReadError {
2323 Unavailable,
2327 Denied(String),
2330 UnsupportedContent,
2333}
2334
2335impl std::fmt::Display for ClipboardReadError {
2336 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2337 match self {
2338 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
2339 Self::Denied(message) => {
2340 write!(formatter, "clipboard access was denied: {message}")
2341 }
2342 Self::UnsupportedContent => {
2343 formatter.write_str("the clipboard contents are unsupported")
2344 }
2345 }
2346 }
2347}
2348
2349impl std::error::Error for ClipboardReadError {}
2350
2351#[derive(Clone, Debug, Eq, PartialEq)]
2353pub enum ClipboardEntry {
2354 String(ClipboardString),
2356 Image(Image),
2358 ExternalPaths(crate::ExternalPaths),
2360}
2361
2362impl ClipboardItem {
2363 pub fn new_string(text: String) -> Self {
2365 Self {
2366 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2367 }
2368 }
2369
2370 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2372 Self {
2373 entries: vec![ClipboardEntry::String(ClipboardString {
2374 text,
2375 metadata: Some(metadata),
2376 })],
2377 }
2378 }
2379
2380 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2382 Self {
2383 entries: vec![ClipboardEntry::String(
2384 ClipboardString::new(text).with_json_metadata(metadata),
2385 )],
2386 }
2387 }
2388
2389 pub fn new_image(image: &Image) -> Self {
2391 Self {
2392 entries: vec![ClipboardEntry::Image(image.clone())],
2393 }
2394 }
2395
2396 pub fn text(&self) -> Option<String> {
2399 let mut answer = String::new();
2400
2401 for entry in self.entries.iter() {
2402 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2403 answer.push_str(text);
2404 }
2405 }
2406
2407 if answer.is_empty() {
2408 for entry in self.entries.iter() {
2409 if let ClipboardEntry::ExternalPaths(paths) = entry {
2410 for path in &paths.0 {
2411 use std::fmt::Write as _;
2412 _ = write!(answer, "{}", path.display());
2413 }
2414 }
2415 }
2416 }
2417
2418 if !answer.is_empty() {
2419 Some(answer)
2420 } else {
2421 None
2422 }
2423 }
2424
2425 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2427 pub fn metadata(&self) -> Option<&String> {
2428 match self.entries().first() {
2429 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2430 clipboard_string.metadata.as_ref()
2431 }
2432 _ => None,
2433 }
2434 }
2435
2436 pub fn entries(&self) -> &[ClipboardEntry] {
2438 &self.entries
2439 }
2440
2441 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2443 self.entries.into_iter()
2444 }
2445}
2446
2447impl From<ClipboardString> for ClipboardEntry {
2448 fn from(value: ClipboardString) -> Self {
2449 Self::String(value)
2450 }
2451}
2452
2453impl From<String> for ClipboardEntry {
2454 fn from(value: String) -> Self {
2455 Self::from(ClipboardString::from(value))
2456 }
2457}
2458
2459impl From<Image> for ClipboardEntry {
2460 fn from(value: Image) -> Self {
2461 Self::Image(value)
2462 }
2463}
2464
2465impl From<ClipboardEntry> for ClipboardItem {
2466 fn from(value: ClipboardEntry) -> Self {
2467 Self {
2468 entries: vec![value],
2469 }
2470 }
2471}
2472
2473impl From<String> for ClipboardItem {
2474 fn from(value: String) -> Self {
2475 Self::from(ClipboardEntry::from(value))
2476 }
2477}
2478
2479impl From<Image> for ClipboardItem {
2480 fn from(value: Image) -> Self {
2481 Self::from(ClipboardEntry::from(value))
2482 }
2483}
2484
2485#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2487pub enum ImageFormat {
2488 Png,
2493 Jpeg,
2495 Webp,
2497 Gif,
2499 Svg,
2501 Bmp,
2503 Tiff,
2505 Ico,
2507 Pnm,
2509}
2510
2511impl ImageFormat {
2512 pub const fn mime_type(self) -> &'static str {
2514 match self {
2515 ImageFormat::Png => "image/png",
2516 ImageFormat::Jpeg => "image/jpeg",
2517 ImageFormat::Webp => "image/webp",
2518 ImageFormat::Gif => "image/gif",
2519 ImageFormat::Svg => "image/svg+xml",
2520 ImageFormat::Bmp => "image/bmp",
2521 ImageFormat::Tiff => "image/tiff",
2522 ImageFormat::Ico => "image/ico",
2523 ImageFormat::Pnm => "image/x-portable-anymap",
2524 }
2525 }
2526
2527 pub const fn extension(self) -> &'static str {
2529 match self {
2530 ImageFormat::Png => "png",
2531 ImageFormat::Jpeg => "jpg",
2532 ImageFormat::Webp => "webp",
2533 ImageFormat::Gif => "gif",
2534 ImageFormat::Svg => "svg",
2535 ImageFormat::Bmp => "bmp",
2536 ImageFormat::Tiff => "tiff",
2537 ImageFormat::Ico => "ico",
2538 ImageFormat::Pnm => "pnm",
2539 }
2540 }
2541
2542 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2544 use strum::IntoEnumIterator;
2545 Self::iter()
2546 .find(|format| format.mime_type() == mime_type)
2547 .or_else(|| Self::from_mime_type_alias(mime_type))
2548 }
2549
2550 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2554 match mime_type {
2555 "image/jpg" => Some(Self::Jpeg),
2556 "image/tif" => Some(Self::Tiff),
2557 _ => None,
2558 }
2559 }
2560}
2561
2562#[derive(Clone, Debug, PartialEq, Eq)]
2564pub struct Image {
2565 pub format: ImageFormat,
2567 pub bytes: Vec<u8>,
2569 pub id: u64,
2571}
2572
2573pub(crate) fn decode_static_image(
2574 bytes: &[u8],
2575 format: image::ImageFormat,
2576) -> Result<SmallVec<[Frame; 1]>> {
2577 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
2578 .into_decoder()
2579 .context("creating image decoder")?;
2580 decode_static_image_from_decoder(decoder)
2581}
2582
2583pub(crate) fn decode_static_image_from_decoder(
2584 mut decoder: impl image::ImageDecoder,
2585) -> Result<SmallVec<[Frame; 1]>> {
2586 let orientation = decoder
2587 .orientation()
2588 .context("reading decoder's orientation")?;
2589 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
2590 image.apply_orientation(orientation);
2591
2592 let mut data = image.into_rgba8();
2593 for pixel in data.chunks_exact_mut(4) {
2594 pixel.swap(0, 2);
2595 }
2596
2597 Ok(SmallVec::from_elem(Frame::new(data), 1))
2598}
2599
2600impl Hash for Image {
2601 fn hash<H: Hasher>(&self, state: &mut H) {
2602 state.write_u64(self.id);
2603 }
2604}
2605
2606impl Image {
2607 pub fn empty() -> Self {
2609 Self::from_bytes(ImageFormat::Png, Vec::new())
2610 }
2611
2612 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2614 Self {
2615 id: hash(&bytes),
2616 format,
2617 bytes,
2618 }
2619 }
2620
2621 pub fn id(&self) -> u64 {
2623 self.id
2624 }
2625
2626 pub fn use_render_image(
2628 self: Arc<Self>,
2629 window: &mut Window,
2630 cx: &mut App,
2631 ) -> Option<Arc<RenderImage>> {
2632 ImageSource::Image(self)
2633 .use_data(None, window, cx)
2634 .and_then(|result| result.ok())
2635 }
2636
2637 pub fn get_render_image(
2639 self: Arc<Self>,
2640 window: &mut Window,
2641 cx: &mut App,
2642 ) -> Option<Arc<RenderImage>> {
2643 ImageSource::Image(self)
2644 .get_data(None, window, cx)
2645 .and_then(|result| result.ok())
2646 }
2647
2648 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2650 ImageSource::Image(self).remove_asset(cx);
2651 }
2652
2653 #[cfg(any(test, feature = "test-support"))]
2656 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
2657 ImageSource::Image(self.clone()).is_asset_cached(cx)
2658 }
2659
2660 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2662 let frames = match self.format {
2663 ImageFormat::Gif => {
2664 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2665 let mut frames = SmallVec::new();
2666
2667 for frame in decoder.into_frames() {
2668 match frame {
2669 Ok(mut frame) => {
2670 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2672 pixel.swap(0, 2);
2673 }
2674 frames.push(frame);
2675 }
2676 Err(err) => {
2677 log::debug!("Skipping GIF frame due to decode error: {err}");
2678 }
2679 }
2680 }
2681
2682 if frames.is_empty() {
2683 anyhow::bail!("GIF could not be decoded: all frames failed");
2684 }
2685
2686 frames
2687 }
2688 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
2689 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
2690 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
2691 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
2692 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
2693 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
2694 ImageFormat::Svg => {
2695 return svg_renderer
2696 .render_single_frame(&self.bytes, 1.0)
2697 .map_err(Into::into);
2698 }
2699 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
2700 };
2701
2702 Ok(Arc::new(RenderImage::new(frames)))
2703 }
2704
2705 pub fn format(&self) -> ImageFormat {
2707 self.format
2708 }
2709
2710 pub fn bytes(&self) -> &[u8] {
2712 self.bytes.as_slice()
2713 }
2714}
2715
2716#[derive(Clone, Debug, Eq, PartialEq)]
2718pub struct ClipboardString {
2719 pub text: String,
2721 pub metadata: Option<String>,
2723}
2724
2725impl ClipboardString {
2726 pub fn new(text: String) -> Self {
2728 Self {
2729 text,
2730 metadata: None,
2731 }
2732 }
2733
2734 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2737 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2738 self
2739 }
2740
2741 pub fn text(&self) -> &String {
2743 &self.text
2744 }
2745
2746 pub fn into_text(self) -> String {
2748 self.text
2749 }
2750
2751 pub fn metadata_json<T>(&self) -> Option<T>
2753 where
2754 T: for<'a> Deserialize<'a>,
2755 {
2756 self.metadata
2757 .as_ref()
2758 .and_then(|m| serde_json::from_str(m).ok())
2759 }
2760
2761 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2762 pub fn text_hash(text: &str) -> u64 {
2764 let mut hasher = SeaHasher::new();
2765 text.hash(&mut hasher);
2766 hasher.finish()
2767 }
2768}
2769
2770impl From<String> for ClipboardString {
2771 fn from(value: String) -> Self {
2772 Self {
2773 text: value,
2774 metadata: None,
2775 }
2776 }
2777}
2778
2779#[cfg(test)]
2780mod image_tests {
2781 use super::*;
2782 use std::sync::Arc;
2783
2784 #[test]
2785 fn test_image_to_image_data_applies_exif_orientation() {
2786 let image = Image::from_bytes(
2787 ImageFormat::Jpeg,
2788 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
2789 );
2790
2791 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2792
2793 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
2794
2795 let bytes = render_image.as_bytes(0).unwrap();
2796 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
2797 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
2798 }
2799
2800 #[test]
2801 fn test_svg_image_to_image_data_converts_to_bgra() {
2802 let image = Image::from_bytes(
2803 ImageFormat::Svg,
2804 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2805<rect width="1" height="1" fill="#38BDF8"/>
2806</svg>"##
2807 .to_vec(),
2808 );
2809
2810 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2811 let bytes = render_image.as_bytes(0).unwrap();
2812
2813 for pixel in bytes.chunks_exact(4) {
2814 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2815 }
2816 }
2817}
2818
2819#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2820mod tests {
2821 use super::*;
2822 use std::collections::HashSet;
2823
2824 #[test]
2825 fn test_window_button_layout_parse_standard() {
2826 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2827 assert_eq!(
2828 layout.left,
2829 [
2830 Some(WindowButton::Close),
2831 Some(WindowButton::Minimize),
2832 None
2833 ]
2834 );
2835 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2836 }
2837
2838 #[test]
2839 fn test_window_button_layout_parse_right_only() {
2840 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2841 assert_eq!(layout.left, [None, None, None]);
2842 assert_eq!(
2843 layout.right,
2844 [
2845 Some(WindowButton::Minimize),
2846 Some(WindowButton::Maximize),
2847 Some(WindowButton::Close)
2848 ]
2849 );
2850 }
2851
2852 #[test]
2853 fn test_window_button_layout_parse_left_only() {
2854 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2855 assert_eq!(
2856 layout.left,
2857 [
2858 Some(WindowButton::Close),
2859 Some(WindowButton::Minimize),
2860 Some(WindowButton::Maximize)
2861 ]
2862 );
2863 assert_eq!(layout.right, [None, None, None]);
2864 }
2865
2866 #[test]
2867 fn test_window_button_layout_parse_with_whitespace() {
2868 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2869 assert_eq!(
2870 layout.left,
2871 [
2872 Some(WindowButton::Close),
2873 Some(WindowButton::Minimize),
2874 None
2875 ]
2876 );
2877 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2878 }
2879
2880 #[test]
2881 fn test_window_button_layout_parse_empty() {
2882 let layout = WindowButtonLayout::parse("").unwrap();
2883 assert_eq!(layout.left, [None, None, None]);
2884 assert_eq!(layout.right, [None, None, None]);
2885 }
2886
2887 #[test]
2888 fn test_window_button_layout_parse_intentionally_empty() {
2889 let layout = WindowButtonLayout::parse(":").unwrap();
2890 assert_eq!(layout.left, [None, None, None]);
2891 assert_eq!(layout.right, [None, None, None]);
2892 }
2893
2894 #[test]
2895 fn test_window_button_layout_parse_invalid_buttons() {
2896 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2897 assert_eq!(
2898 layout.left,
2899 [
2900 Some(WindowButton::Close),
2901 Some(WindowButton::Minimize),
2902 None
2903 ]
2904 );
2905 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2906 }
2907
2908 #[test]
2909 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2910 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2911 assert_eq!(
2912 layout.right,
2913 [
2914 Some(WindowButton::Close),
2915 Some(WindowButton::Minimize),
2916 None
2917 ]
2918 );
2919 assert_eq!(layout.format(), ":close,minimize");
2920 }
2921
2922 #[test]
2923 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2924 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2925 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2926 assert_eq!(
2927 layout.right,
2928 [
2929 Some(WindowButton::Maximize),
2930 Some(WindowButton::Minimize),
2931 None
2932 ]
2933 );
2934
2935 let button_ids: Vec<_> = layout
2936 .left
2937 .iter()
2938 .chain(layout.right.iter())
2939 .flatten()
2940 .map(WindowButton::id)
2941 .collect();
2942 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2943 assert_eq!(unique_button_ids.len(), button_ids.len());
2944 assert_eq!(layout.format(), "close:maximize,minimize");
2945 }
2946
2947 #[test]
2948 fn test_window_button_layout_parse_gnome_style() {
2949 let layout = WindowButtonLayout::parse("close").unwrap();
2950 assert_eq!(layout.left, [None, None, None]);
2951 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2952 }
2953
2954 #[test]
2955 fn test_window_button_layout_parse_elementary_style() {
2956 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2957 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2958 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2959 }
2960
2961 #[test]
2962 fn test_window_button_layout_round_trip() {
2963 let cases = [
2964 "close:minimize,maximize",
2965 "minimize,maximize,close:",
2966 ":close",
2967 "close:",
2968 "close:maximize",
2969 ":",
2970 ];
2971
2972 for case in cases {
2973 let layout = WindowButtonLayout::parse(case).unwrap();
2974 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2975 }
2976 }
2977
2978 #[test]
2979 fn test_window_button_layout_linux_default() {
2980 let layout = WindowButtonLayout::linux_default();
2981 assert_eq!(layout.left, [None, None, None]);
2982 assert_eq!(
2983 layout.right,
2984 [
2985 Some(WindowButton::Minimize),
2986 Some(WindowButton::Maximize),
2987 Some(WindowButton::Close)
2988 ]
2989 );
2990
2991 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2992 assert_eq!(round_tripped, layout);
2993 }
2994
2995 #[test]
2996 fn test_window_button_layout_parse_all_invalid() {
2997 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2998 }
2999}