1mod app_menu;
2mod keyboard;
3mod keystroke;
4mod platform_view;
5
6#[cfg(all(target_os = "linux", feature = "wayland"))]
7#[expect(missing_docs)]
8pub mod layer_shell;
9
10pub mod popup;
12
13#[cfg(any(test, feature = "test-support"))]
14mod threaded_dispatcher;
15
16#[cfg(any(test, feature = "test-support"))]
17mod test;
18
19#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
20mod visual_test;
21
22#[cfg(all(
23 feature = "screen-capture",
24 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
25))]
26pub mod scap_screen_capture;
27
28#[cfg(all(
29 any(target_os = "windows", target_os = "linux"),
30 feature = "screen-capture"
31))]
32pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
33#[cfg(not(feature = "screen-capture"))]
34pub(crate) type PlatformScreenCaptureFrame = ();
35#[cfg(all(target_os = "macos", feature = "screen-capture"))]
36pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
37
38use crate::{
39 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
40 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
41 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
42 LineLayout, Pixels, PlatformGestures, PlatformInput, Point, Priority, RenderGlyphParams,
43 RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString,
44 Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size,
45};
46#[cfg(any(target_os = "linux", target_os = "freebsd"))]
47use anyhow::bail;
48use anyhow::{Context as _, Result};
49use async_task::Runnable;
50use futures::channel::oneshot;
51#[cfg(any(test, feature = "test-support"))]
52use image::RgbaImage;
53use image::codecs::gif::GifDecoder;
54use image::{AnimationDecoder as _, DynamicImage, Frame};
55use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
56use scheduler::Instant;
57pub use scheduler::RunnableMeta;
58use schemars::JsonSchema;
59use seahash::SeaHasher;
60use serde::{Deserialize, Serialize};
61use smallvec::SmallVec;
62use std::borrow::Cow;
63use std::hash::{Hash, Hasher};
64use std::io::Cursor;
65use std::ops;
66use std::time::Duration;
67use std::{
68 any::Any,
69 fmt::{self, Debug},
70 ops::Range,
71 path::{Path, PathBuf},
72 rc::Rc,
73 sync::Arc,
74};
75
76pub trait PlatformNativeSurface {
79 fn set_bounds(&self, bounds: Bounds<DevicePixels>) -> Result<()>;
81 fn set_visible(&self, visible: bool) -> Result<()>;
83 fn platform_handle(&self) -> Box<dyn Any>;
86}
87use strum::EnumIter;
88use uuid::Uuid;
89
90pub use app_menu::*;
91pub use keyboard::*;
92pub use keystroke::*;
93pub(crate) use platform_view::PlatformViewRegistry;
94pub use platform_view::{
95 PlatformViewHandle, PlatformViewHosting, PlatformViewId, PlatformViewPlacement,
96 PlatformViewUpdate, flip_bounds_origin_y, platform_view_physical_bounds,
97 snap_platform_view_bounds,
98};
99
100#[cfg(any(test, feature = "test-support"))]
101pub(crate) use test::*;
102
103#[cfg(any(test, feature = "test-support"))]
104pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
105
106#[cfg(any(test, feature = "test-support"))]
107pub use threaded_dispatcher::ThreadedDispatcher;
108
109#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
110pub use visual_test::VisualTestPlatform;
111
112#[cfg(any(target_os = "linux", target_os = "freebsd"))]
116#[inline]
117pub fn guess_compositor() -> &'static str {
118 if std::env::var_os("ZED_HEADLESS").is_some() {
119 return "Headless";
120 }
121
122 #[cfg(feature = "wayland")]
123 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
124 #[cfg(not(feature = "wayland"))]
125 let wayland_display: Option<std::ffi::OsString> = None;
126
127 #[cfg(feature = "x11")]
128 let x11_display = std::env::var_os("DISPLAY");
129 #[cfg(not(feature = "x11"))]
130 let x11_display: Option<std::ffi::OsString> = None;
131
132 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
133 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
134
135 if use_wayland {
136 "Wayland"
137 } else if use_x11 {
138 "X11"
139 } else {
140 "Headless"
141 }
142}
143
144#[expect(missing_docs)]
145pub trait Platform: 'static {
146 fn background_executor(&self) -> BackgroundExecutor;
147 fn foreground_executor(&self) -> ForegroundExecutor;
148 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
149
150 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
151 fn quit(&self);
152 fn restart(&self, binary_path: Option<PathBuf>);
153 fn activate(&self, ignoring_other_apps: bool);
154 fn hide(&self);
155 fn hide_other_apps(&self);
156 fn unhide_other_apps(&self);
157
158 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
159 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
160 fn active_window(&self) -> Option<AnyWindowHandle>;
161 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
162 None
163 }
164
165 fn is_screen_capture_supported(&self) -> bool {
166 false
167 }
168
169 fn screen_capture_sources(
170 &self,
171 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
172 let (sources_tx, sources_rx) = oneshot::channel();
173 sources_tx
174 .send(Err(anyhow::anyhow!(
175 "gpui was compiled without the screen-capture feature"
176 )))
177 .ok();
178 sources_rx
179 }
180
181 fn open_window(
182 &self,
183 handle: AnyWindowHandle,
184 options: WindowParams,
185 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
186
187 fn window_appearance(&self) -> WindowAppearance;
189
190 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
199
200 fn button_layout(&self) -> Option<WindowButtonLayout> {
202 None
203 }
204
205 fn open_url(&self, url: &str);
206 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
207 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
208
209 fn prompt_for_paths(
210 &self,
211 options: PathPromptOptions,
212 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
213 fn prompt_for_new_path(
214 &self,
215 directory: &Path,
216 suggested_name: Option<&str>,
217 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
218 fn can_select_mixed_files_and_dirs(&self) -> bool;
219 fn reveal_path(&self, path: &Path);
220 fn open_with_system(&self, path: &Path);
221
222 fn on_quit(&self, callback: Box<dyn FnMut()>);
223 fn on_reopen(&self, callback: Box<dyn FnMut()>);
224 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
225
226 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
236
237 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
242
243 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
247 None
248 }
249
250 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
251 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
252 None
253 }
254
255 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
256 fn perform_dock_menu_action(&self, _action: usize) {}
257 fn add_recent_document(&self, _path: &Path) {}
258 fn update_jump_list(
259 &self,
260 _menus: Vec<MenuItem>,
261 _entries: Vec<SmallVec<[PathBuf; 2]>>,
262 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
263 Task::ready(Vec::new())
264 }
265 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
266 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
267 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
268
269 fn thermal_state(&self) -> ThermalState;
270 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
271
272 fn set_app_identity(&self, identifier: &str, name: &str) {
279 _ = (identifier, name);
280 }
281
282 fn show_system_notification(&self, notification: SystemNotification) {
289 _ = notification;
290 }
291
292 fn dismiss_system_notification(&self, tag: &str) {
297 _ = tag;
298 }
299
300 fn on_system_notification_response(
306 &self,
307 callback: Box<dyn FnMut(SystemNotificationResponse)>,
308 ) {
309 _ = callback;
310 }
311
312 fn compositor_name(&self) -> &'static str {
313 ""
314 }
315 fn app_path(&self) -> Result<PathBuf>;
316 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
317
318 fn set_cursor_style(&self, style: CursorStyle);
319
320 fn hide_cursor_until_mouse_moves(&self);
323
324 fn is_cursor_visible(&self) -> bool;
326
327 fn should_auto_hide_scrollbars(&self) -> bool;
328
329 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
330 fn write_to_clipboard(&self, item: ClipboardItem);
331
332 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
333 fn read_from_primary(&self) -> Option<ClipboardItem>;
334 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
335 fn write_to_primary(&self, item: ClipboardItem);
336
337 #[cfg(target_os = "macos")]
338 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
339 #[cfg(target_os = "macos")]
340 fn write_to_find_pasteboard(&self, item: ClipboardItem);
341
342 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
343 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
344 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
345
346 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
347 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
348 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
349}
350
351pub trait PlatformDisplay: Debug {
353 fn id(&self) -> DisplayId;
355
356 fn uuid(&self) -> Result<Uuid>;
359
360 fn bounds(&self) -> Bounds<Pixels>;
362
363 fn visible_bounds(&self) -> Bounds<Pixels> {
367 self.bounds()
368 }
369
370 fn default_bounds(&self) -> Bounds<Pixels> {
372 let bounds = self.bounds();
373 let center = bounds.center();
374 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
375
376 let offset = clipped_window_size / 2.0;
377 let origin = point(center.x - offset.width, center.y - offset.height);
378 Bounds::new(origin, clipped_window_size)
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Eq)]
385pub struct SystemNotification {
386 pub tag: SharedString,
390 pub title: SharedString,
392 pub body: SharedString,
394 pub actions: Vec<SystemNotificationAction>,
397}
398
399#[derive(Clone, Debug, PartialEq, Eq, Hash)]
401pub struct SystemNotificationAction {
402 pub id: SharedString,
405 pub label: SharedString,
407}
408
409#[derive(Clone, Debug, PartialEq, Eq)]
411pub struct SystemNotificationResponse {
412 pub tag: SharedString,
414 pub action_id: Option<SharedString>,
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum ThermalState {
422 Nominal,
424 Fair,
426 Serious,
428 Critical,
430}
431
432#[derive(Clone)]
434pub struct SourceMetadata {
435 pub id: u64,
437 pub label: Option<SharedString>,
439 pub is_main: Option<bool>,
441 pub resolution: Size<DevicePixels>,
443}
444
445pub trait ScreenCaptureSource {
447 fn metadata(&self) -> Result<SourceMetadata>;
449
450 fn stream(
453 &self,
454 foreground_executor: &ForegroundExecutor,
455 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
456 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
457}
458
459pub trait ScreenCaptureStream {
461 fn metadata(&self) -> Result<SourceMetadata>;
463}
464
465pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
467
468#[derive(PartialEq, Eq, Hash, Copy, Clone)]
470pub struct DisplayId(pub(crate) u64);
471
472impl DisplayId {
473 pub fn new(id: u64) -> Self {
475 Self(id)
476 }
477}
478
479impl From<u64> for DisplayId {
480 fn from(id: u64) -> Self {
481 Self(id)
482 }
483}
484
485impl From<DisplayId> for u64 {
486 fn from(id: DisplayId) -> Self {
487 id.0
488 }
489}
490
491impl Debug for DisplayId {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 write!(f, "DisplayId({})", self.0)
494 }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum ResizeEdge {
500 Top,
502 TopRight,
504 Right,
506 BottomRight,
508 Bottom,
510 BottomLeft,
512 Left,
514 TopLeft,
516}
517
518#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
520pub enum WindowDecorations {
521 #[default]
522 Server,
524 Client,
526}
527
528#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
530pub enum Decorations {
531 #[default]
533 Server,
534 Client {
536 tiling: Tiling,
538 },
539}
540
541#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
543pub struct WindowControls {
544 pub fullscreen: bool,
546 pub maximize: bool,
548 pub minimize: bool,
550 pub window_menu: bool,
552}
553
554impl Default for WindowControls {
555 fn default() -> Self {
556 Self {
558 fullscreen: true,
559 maximize: true,
560 minimize: true,
561 window_menu: true,
562 }
563 }
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
568pub enum WindowButton {
569 Minimize,
571 Maximize,
573 Close,
575}
576
577impl WindowButton {
578 pub fn id(&self) -> &'static str {
580 match self {
581 WindowButton::Minimize => "minimize",
582 WindowButton::Maximize => "maximize",
583 WindowButton::Close => "close",
584 }
585 }
586
587 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
588 fn index(&self) -> usize {
589 match self {
590 WindowButton::Minimize => 0,
591 WindowButton::Maximize => 1,
592 WindowButton::Close => 2,
593 }
594 }
595}
596
597pub const MAX_BUTTONS_PER_SIDE: usize = 3;
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
606pub struct WindowButtonLayout {
607 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
609 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
611}
612
613#[cfg(any(target_os = "linux", target_os = "freebsd"))]
614impl WindowButtonLayout {
615 pub fn linux_default() -> Self {
617 Self {
618 left: [None; MAX_BUTTONS_PER_SIDE],
619 right: [
620 Some(WindowButton::Minimize),
621 Some(WindowButton::Maximize),
622 Some(WindowButton::Close),
623 ],
624 }
625 }
626
627 pub fn parse(layout_string: &str) -> Result<Self> {
629 fn parse_side(
630 s: &str,
631 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
632 unrecognized: &mut Vec<String>,
633 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
634 let mut result = [None; MAX_BUTTONS_PER_SIDE];
635 let mut i = 0;
636 for name in s.split(',') {
637 let trimmed = name.trim();
638 if trimmed.is_empty() {
639 continue;
640 }
641 let button = match trimmed {
642 "minimize" => Some(WindowButton::Minimize),
643 "maximize" => Some(WindowButton::Maximize),
644 "close" => Some(WindowButton::Close),
645 other => {
646 unrecognized.push(other.to_string());
647 None
648 }
649 };
650 if let Some(button) = button {
651 if seen_buttons[button.index()] {
652 continue;
653 }
654 if let Some(slot) = result.get_mut(i) {
655 *slot = Some(button);
656 seen_buttons[button.index()] = true;
657 i += 1;
658 }
659 }
660 }
661 result
662 }
663
664 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
665 let mut unrecognized = Vec::new();
666 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
667 let layout = Self {
668 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
669 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
670 };
671
672 if !unrecognized.is_empty()
673 && layout.left.iter().all(Option::is_none)
674 && layout.right.iter().all(Option::is_none)
675 {
676 bail!(
677 "button layout string {:?} contains no valid buttons (unrecognized: {})",
678 layout_string,
679 unrecognized.join(", ")
680 );
681 }
682
683 Ok(layout)
684 }
685
686 #[cfg(test)]
688 pub fn format(&self) -> String {
689 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
690 buttons
691 .iter()
692 .flatten()
693 .map(|button| match button {
694 WindowButton::Minimize => "minimize",
695 WindowButton::Maximize => "maximize",
696 WindowButton::Close => "close",
697 })
698 .collect::<Vec<_>>()
699 .join(",")
700 }
701
702 format!("{}:{}", format_side(&self.left), format_side(&self.right))
703 }
704}
705
706#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
708pub struct Tiling {
709 pub top: bool,
711 pub left: bool,
713 pub right: bool,
715 pub bottom: bool,
717}
718
719impl Tiling {
720 pub fn tiled() -> Self {
722 Self {
723 top: true,
724 left: true,
725 right: true,
726 bottom: true,
727 }
728 }
729
730 pub fn is_tiled(&self) -> bool {
732 self.top || self.left || self.right || self.bottom
733 }
734}
735
736pub struct A11yCallbacks {
738 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
740 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
742 pub deactivation: Box<dyn Fn() + Send + 'static>,
744}
745
746#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
747#[expect(missing_docs)]
748pub struct RequestFrameOptions {
749 pub require_presentation: bool,
751 pub force_render: bool,
753}
754
755#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
768pub enum AppLifecyclePhase {
769 Active,
771 Inactive,
773 Background,
776 Foreground,
778}
779
780#[derive(Debug, Clone, Default, PartialEq)]
787pub struct WindowInsets {
788 pub safe_area: Edges<Pixels>,
793 pub ime: Edges<Pixels>,
797}
798
799impl WindowInsets {
800 pub fn effective(&self) -> Edges<Pixels> {
802 Edges {
803 top: self.safe_area.top.max(self.ime.top),
804 right: self.safe_area.right.max(self.ime.right),
805 bottom: self.safe_area.bottom.max(self.ime.bottom),
806 left: self.safe_area.left.max(self.ime.left),
807 }
808 }
809}
810
811#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
813pub enum TextInputStateChange {
814 FocusGained,
816 FocusLost,
818 SelectionChanged,
820 ContentChanged,
822}
823
824#[expect(missing_docs)]
825pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
826 fn bounds(&self) -> Bounds<Pixels>;
827 fn is_maximized(&self) -> bool;
828 fn window_bounds(&self) -> WindowBounds;
829 fn content_size(&self) -> Size<Pixels>;
830 fn resize(&mut self, size: Size<Pixels>);
831 fn scale_factor(&self) -> f32;
832 fn appearance(&self) -> WindowAppearance;
833 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
834 fn mouse_position(&self) -> Point<Pixels>;
835 fn modifiers(&self) -> Modifiers;
836 fn capslock(&self) -> Capslock;
837 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
838 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
839 fn prompt(
840 &self,
841 level: PromptLevel,
842 msg: &str,
843 detail: Option<&str>,
844 answers: &[PromptButton],
845 ) -> Option<oneshot::Receiver<usize>>;
846 fn activate(&self);
847 fn request_attention(&self) {}
849 fn is_active(&self) -> bool;
850 fn is_hovered(&self) -> bool;
851 fn background_appearance(&self) -> WindowBackgroundAppearance;
852 fn set_title(&mut self, title: &str);
853 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
854 fn minimize(&self);
855 fn zoom(&self);
856 fn toggle_fullscreen(&self);
857 fn is_fullscreen(&self) -> bool;
858 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
859 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
860 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
861 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
862 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
863 fn on_moved(&self, callback: Box<dyn FnMut()>);
864 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
865 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
866 fn on_close(&self, callback: Box<dyn FnOnce()>);
867 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
868 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
869 fn draw(&self, scene: &Scene);
870 fn draw_layered(&self, scene: &Scene, _overlay_start: usize) {
876 self.draw(scene);
877 }
878 fn enable_scene_overlay(&self) -> anyhow::Result<()> {
883 anyhow::bail!("layered GPUI scenes are not supported on this platform")
884 }
885 fn create_native_surface(&self) -> Result<Rc<dyn PlatformNativeSurface>> {
887 anyhow::bail!("native surface portals are not supported on this platform")
888 }
889 fn update_platform_views(&self, _update: &PlatformViewUpdate) {}
896 fn completed_frame(&self) {}
897 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
898 fn is_subpixel_rendering_supported(&self) -> bool;
899
900 fn get_title(&self) -> String {
902 String::new()
903 }
904 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
905 None
906 }
907 fn tab_bar_visible(&self) -> bool {
908 false
909 }
910 fn set_edited(&mut self, _edited: bool) {}
911 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
912 #[cfg(target_os = "macos")]
913 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
914 fn show_character_palette(&self) {}
915 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
916 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
917 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
918 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
919 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
920 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
921 fn merge_all_windows(&self) {}
922 fn move_tab_to_new_window(&self) {}
923 fn toggle_window_tab_overview(&self) {}
924 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
925
926 #[cfg(target_os = "windows")]
927 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
928
929 fn inner_window_bounds(&self) -> WindowBounds {
931 self.window_bounds()
932 }
933 fn request_decorations(&self, _decorations: WindowDecorations) {}
934 fn show_window_menu(&self, _position: Point<Pixels>) {}
935 fn start_window_move(&self) {}
936 fn can_start_external_drag(&self) -> bool {
937 false
938 }
939 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
940 false
941 }
942 fn start_window_resize(&self, _edge: ResizeEdge) {}
943 fn set_exclusive_zone(&self, _zone: Pixels) {}
944 #[cfg(all(target_os = "linux", feature = "wayland"))]
945 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
946 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
947 fn window_decorations(&self) -> Decorations {
948 Decorations::Server
949 }
950 fn set_app_id(&mut self, _app_id: &str) {}
951 fn map_window(&mut self) -> anyhow::Result<()> {
952 Ok(())
953 }
954 fn window_controls(&self) -> WindowControls {
955 WindowControls::default()
956 }
957 fn set_client_inset(&self, _inset: Pixels) {}
958 fn gpu_specs(&self) -> Option<GpuSpecs>;
959
960 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
961
962 fn insets(&self) -> WindowInsets {
967 WindowInsets::default()
968 }
969
970 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
976
977 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
980
981 fn set_back_enabled(&self, _enabled: bool) {}
984
985 fn show_soft_keyboard(&self) {}
987
988 fn hide_soft_keyboard(&self) {}
990
991 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
993
994 fn play_system_bell(&self) {}
995
996 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
998
999 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1001
1002 fn a11y_update_window_bounds(&self) {}
1004
1005 #[cfg(any(test, feature = "test-support"))]
1006 fn as_test(&mut self) -> Option<&mut TestWindow> {
1007 None
1008 }
1009
1010 #[cfg(any(test, feature = "test-support"))]
1014 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1015 anyhow::bail!("render_to_image not implemented for this platform")
1016 }
1017}
1018
1019#[cfg(any(test, feature = "test-support"))]
1021pub trait PlatformHeadlessRenderer {
1022 fn render_scene_to_image(
1024 &mut self,
1025 scene: &Scene,
1026 size: Size<DevicePixels>,
1027 ) -> Result<RgbaImage>;
1028
1029 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1035
1036 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1038}
1039
1040#[doc(hidden)]
1043pub type RunnableVariant = Runnable<RunnableMeta>;
1044
1045#[doc(hidden)]
1046pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1047
1048#[doc(hidden)]
1049pub enum TasksIncluded {
1050 OnlyCompleted,
1051 CompletedAndRunning,
1052}
1053
1054#[doc(hidden)]
1057pub trait PlatformDispatcher: Send + Sync {
1058 fn is_main_thread(&self) -> bool;
1059 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1060 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1061 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1062
1063 fn dispatch_on_main_thread_when_idle(
1064 &self,
1065 runnable: RunnableVariant,
1066 timeout: Option<Duration>,
1067 ) {
1068 let _ = timeout;
1069 self.dispatch_on_main_thread(runnable, Priority::Low);
1070 }
1071
1072 fn idle_time_remaining(&self) -> Option<Duration> {
1073 None
1074 }
1075
1076 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1077
1078 fn now(&self) -> Instant {
1079 Instant::now()
1080 }
1081
1082 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1083 gpui_util::defer(Box::new(|| {}))
1084 }
1085
1086 #[cfg(any(test, feature = "test-support"))]
1087 fn as_test(&self) -> Option<&TestDispatcher> {
1088 None
1089 }
1090
1091 #[cfg(any(test, feature = "test-support"))]
1094 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1095 None
1096 }
1097}
1098
1099#[expect(missing_docs)]
1100pub trait PlatformTextSystem: Send + Sync {
1101 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1102 fn all_font_names(&self) -> Vec<String>;
1104 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1106 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1108 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1110 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1112 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1114 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1116 fn rasterize_glyph(
1118 &self,
1119 params: &RenderGlyphParams,
1120 raster_bounds: Bounds<DevicePixels>,
1121 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1122 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1124 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1126 -> TextRenderingMode;
1127 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1129 0
1130 }
1131}
1132
1133#[expect(missing_docs)]
1134pub struct NoopTextSystem;
1135
1136#[expect(missing_docs)]
1137impl NoopTextSystem {
1138 #[allow(dead_code)]
1139 pub fn new() -> Self {
1140 Self
1141 }
1142}
1143
1144impl PlatformTextSystem for NoopTextSystem {
1145 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1146 Ok(())
1147 }
1148
1149 fn all_font_names(&self) -> Vec<String> {
1150 Vec::new()
1151 }
1152
1153 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1154 Ok(FontId(1))
1155 }
1156
1157 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1158 FontMetrics {
1159 units_per_em: 1000,
1160 ascent: 1025.0,
1161 descent: -275.0,
1162 line_gap: 0.0,
1163 underline_position: -95.0,
1164 underline_thickness: 60.0,
1165 cap_height: 698.0,
1166 x_height: 516.0,
1167 bounding_box: Bounds {
1168 origin: Point {
1169 x: -260.0,
1170 y: -245.0,
1171 },
1172 size: Size {
1173 width: 1501.0,
1174 height: 1364.0,
1175 },
1176 },
1177 }
1178 }
1179
1180 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1181 Ok(Bounds {
1182 origin: Point { x: 54.0, y: 0.0 },
1183 size: size(392.0, 528.0),
1184 })
1185 }
1186
1187 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1188 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1189 }
1190
1191 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1192 Some(GlyphId(ch.len_utf16() as u32))
1193 }
1194
1195 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1196 Ok(Default::default())
1197 }
1198
1199 fn rasterize_glyph(
1200 &self,
1201 _params: &RenderGlyphParams,
1202 raster_bounds: Bounds<DevicePixels>,
1203 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1204 Ok((raster_bounds.size, Vec::new()))
1205 }
1206
1207 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1208 let mut position = px(0.);
1209 let metrics = self.font_metrics(FontId(0));
1210 let em_width = font_size
1211 * self
1212 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1213 .unwrap()
1214 .width
1215 / metrics.units_per_em as f32;
1216 let mut glyphs = Vec::new();
1217 for (ix, c) in text.char_indices() {
1218 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1219 glyphs.push(ShapedGlyph {
1220 id: glyph,
1221 position: point(position, px(0.)),
1222 index: ix,
1223 is_emoji: glyph.0 == 2,
1224 });
1225 if glyph.0 == 2 {
1226 position += em_width * 2.0;
1227 } else {
1228 position += em_width;
1229 }
1230 } else {
1231 position += em_width
1232 }
1233 }
1234 let mut runs = Vec::default();
1235 if !glyphs.is_empty() {
1236 runs.push(ShapedRun {
1237 font_id: FontId(0),
1238 glyphs,
1239 });
1240 } else {
1241 position = px(0.);
1242 }
1243
1244 LineLayout {
1245 font_size,
1246 width: position,
1247 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1248 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1249 runs,
1250 len: text.len(),
1251 }
1252 }
1253
1254 fn recommended_rendering_mode(
1255 &self,
1256 _font_id: FontId,
1257 _font_size: Pixels,
1258 ) -> TextRenderingMode {
1259 TextRenderingMode::Grayscale
1260 }
1261}
1262
1263#[allow(dead_code)]
1268pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1269 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1270 [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], ];
1284
1285 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1286 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1287
1288 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1289 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1290
1291 [
1292 ratios[0] * NORM13,
1293 ratios[1] * NORM24,
1294 ratios[2] * NORM13,
1295 ratios[3] * NORM24,
1296 ]
1297}
1298
1299#[derive(PartialEq, Eq, Hash, Clone)]
1300#[expect(missing_docs)]
1301pub enum AtlasKey {
1302 Glyph(RenderGlyphParams),
1303 Svg(RenderSvgParams),
1304 Image(RenderImageParams),
1305}
1306
1307impl AtlasKey {
1308 #[cfg_attr(
1309 all(
1310 any(target_os = "linux", target_os = "freebsd"),
1311 not(any(feature = "x11", feature = "wayland"))
1312 ),
1313 allow(dead_code)
1314 )]
1315 pub fn texture_kind(&self) -> AtlasTextureKind {
1317 match self {
1318 AtlasKey::Glyph(params) => {
1319 if params.is_emoji {
1320 AtlasTextureKind::Polychrome
1321 } else if params.subpixel_rendering {
1322 AtlasTextureKind::Subpixel
1323 } else {
1324 AtlasTextureKind::Monochrome
1325 }
1326 }
1327 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1328 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1329 }
1330 }
1331}
1332
1333impl From<RenderGlyphParams> for AtlasKey {
1334 fn from(params: RenderGlyphParams) -> Self {
1335 Self::Glyph(params)
1336 }
1337}
1338
1339impl From<RenderSvgParams> for AtlasKey {
1340 fn from(params: RenderSvgParams) -> Self {
1341 Self::Svg(params)
1342 }
1343}
1344
1345impl From<RenderImageParams> for AtlasKey {
1346 fn from(params: RenderImageParams) -> Self {
1347 Self::Image(params)
1348 }
1349}
1350
1351#[expect(missing_docs)]
1352pub trait PlatformAtlas {
1353 fn get_or_insert_with<'a>(
1354 &self,
1355 key: &AtlasKey,
1356 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1357 ) -> Result<Option<AtlasTile>>;
1358 fn remove(&self, key: &AtlasKey);
1359
1360 #[cfg(any(test, feature = "test-support"))]
1361 fn contains(&self, _key: &AtlasKey) -> bool {
1362 false
1363 }
1364}
1365
1366#[doc(hidden)]
1367pub struct AtlasTextureList<T> {
1368 pub textures: Vec<Option<T>>,
1369 pub free_list: Vec<usize>,
1370}
1371
1372impl<T> Default for AtlasTextureList<T> {
1373 fn default() -> Self {
1374 Self {
1375 textures: Vec::default(),
1376 free_list: Vec::default(),
1377 }
1378 }
1379}
1380
1381impl<T> ops::Index<usize> for AtlasTextureList<T> {
1382 type Output = Option<T>;
1383
1384 fn index(&self, index: usize) -> &Self::Output {
1385 &self.textures[index]
1386 }
1387}
1388
1389impl<T> AtlasTextureList<T> {
1390 #[allow(unused)]
1391 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1392 self.free_list.clear();
1393 self.textures.drain(..)
1394 }
1395
1396 #[allow(dead_code)]
1397 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1398 self.textures.iter_mut().flatten()
1399 }
1400}
1401
1402#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1403#[repr(C)]
1404#[expect(missing_docs)]
1405pub struct AtlasTile {
1406 pub texture_id: AtlasTextureId,
1408 pub tile_id: TileId,
1410 pub padding: u32,
1412 pub bounds: Bounds<DevicePixels>,
1414}
1415
1416#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1417#[repr(C)]
1418#[expect(missing_docs)]
1419pub struct AtlasTextureId {
1420 pub index: u32,
1423 pub kind: AtlasTextureKind,
1425}
1426
1427#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1428#[repr(C)]
1429#[cfg_attr(
1430 all(
1431 any(target_os = "linux", target_os = "freebsd"),
1432 not(any(feature = "x11", feature = "wayland"))
1433 ),
1434 allow(dead_code)
1435)]
1436#[expect(missing_docs)]
1437pub enum AtlasTextureKind {
1438 Monochrome = 0,
1439 Polychrome = 1,
1440 Subpixel = 2,
1441}
1442
1443#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1444#[repr(C)]
1445#[expect(missing_docs)]
1446pub struct TileId(pub u32);
1447
1448impl From<etagere::AllocId> for TileId {
1449 fn from(id: etagere::AllocId) -> Self {
1450 Self(id.serialize())
1451 }
1452}
1453
1454impl From<TileId> for etagere::AllocId {
1455 fn from(id: TileId) -> Self {
1456 Self::deserialize(id.0)
1457 }
1458}
1459
1460#[expect(missing_docs)]
1461pub struct PlatformInputHandler {
1462 cx: AsyncWindowContext,
1463 handler: Box<dyn InputHandler>,
1464}
1465
1466#[expect(missing_docs)]
1467#[cfg_attr(
1468 all(
1469 any(target_os = "linux", target_os = "freebsd"),
1470 not(any(feature = "x11", feature = "wayland"))
1471 ),
1472 allow(dead_code)
1473)]
1474impl PlatformInputHandler {
1475 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1476 Self { cx, handler }
1477 }
1478
1479 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1480 self.cx
1481 .update(|window, cx| {
1482 self.handler
1483 .selected_text_range(ignore_disabled_input, window, cx)
1484 })
1485 .ok()
1486 .flatten()
1487 }
1488
1489 #[cfg_attr(target_os = "windows", allow(dead_code))]
1490 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1491 self.cx
1492 .update(|window, cx| self.handler.marked_text_range(window, cx))
1493 .ok()
1494 .flatten()
1495 }
1496
1497 #[cfg_attr(
1498 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1499 allow(dead_code)
1500 )]
1501 pub fn text_for_range(
1502 &mut self,
1503 range_utf16: Range<usize>,
1504 adjusted: &mut Option<Range<usize>>,
1505 ) -> Option<String> {
1506 self.cx
1507 .update(|window, cx| {
1508 self.handler
1509 .text_for_range(range_utf16, adjusted, window, cx)
1510 })
1511 .ok()
1512 .flatten()
1513 }
1514
1515 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1516 self.cx
1517 .update(|window, cx| {
1518 self.handler
1519 .replace_text_in_range(replacement_range, text, window, cx);
1520 })
1521 .ok();
1522 }
1523
1524 pub fn replace_and_mark_text_in_range(
1525 &mut self,
1526 range_utf16: Option<Range<usize>>,
1527 new_text: &str,
1528 new_selected_range: Option<Range<usize>>,
1529 ) {
1530 self.cx
1531 .update(|window, cx| {
1532 self.handler.replace_and_mark_text_in_range(
1533 range_utf16,
1534 new_text,
1535 new_selected_range,
1536 window,
1537 cx,
1538 )
1539 })
1540 .ok();
1541 }
1542
1543 #[cfg_attr(target_os = "windows", allow(dead_code))]
1544 pub fn unmark_text(&mut self) {
1545 self.cx
1546 .update(|window, cx| self.handler.unmark_text(window, cx))
1547 .ok();
1548 }
1549
1550 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1551 self.cx
1552 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1553 .ok()
1554 .flatten()
1555 }
1556
1557 #[allow(dead_code)]
1558 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1559 self.handler.apple_press_and_hold_enabled()
1560 }
1561
1562 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1563 self.handler.replace_text_in_range(None, input, window, cx);
1564 }
1565
1566 pub fn compute_ime_candidate_bounds(
1567 marked_range: Option<Range<usize>>,
1568 selection: &UTF16Selection,
1569 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1570 ) -> Option<Bounds<Pixels>> {
1571 if let Some(marked_range) = marked_range {
1572 let mut line_start = marked_range.start;
1574
1575 let caret = selection.range.end;
1579 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1580 for i in (marked_range.start..caret).rev() {
1581 if let Some(b) = bounds_for_range(i..i) {
1582 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1583 line_start = i + 1;
1584 break;
1585 }
1586 }
1587 }
1588 }
1589 bounds_for_range(line_start..line_start)
1590 } else {
1591 let offset = if selection.reversed {
1593 selection.range.start
1594 } else {
1595 selection.range.end
1596 };
1597 bounds_for_range(offset..offset)
1598 }
1599 }
1600
1601 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1602 let marked_range = self.handler.marked_text_range(window, cx);
1603 let selection = self.handler.selected_text_range(true, window, cx)?;
1604 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1605 self.handler.bounds_for_range(range, window, cx)
1606 })
1607 }
1608
1609 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1610 let marked_range = self.marked_text_range();
1611 let selection = self.selected_text_range(true)?;
1612 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1613 self.bounds_for_range(range)
1614 })
1615 }
1616
1617 #[allow(unused)]
1618 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1619 self.cx
1620 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1621 .ok()
1622 .flatten()
1623 }
1624
1625 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
1627 self.cx
1628 .update(|window, cx| {
1629 self.handler
1630 .set_selected_text_range(range_utf16, window, cx)
1631 })
1632 .ok();
1633 }
1634
1635 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
1637 self.cx
1638 .update(|window, cx| self.handler.element_bounds(window, cx))
1639 .ok()
1640 .flatten()
1641 }
1642
1643 pub fn text_length_utf16(&mut self) -> Option<usize> {
1645 self.cx
1646 .update(|window, cx| self.handler.text_length_utf16(window, cx))
1647 .ok()
1648 .flatten()
1649 }
1650
1651 #[allow(dead_code)]
1652 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1653 self.handler.accepts_text_input(window, cx)
1654 }
1655
1656 #[allow(dead_code)]
1657 pub fn query_accepts_text_input(&mut self) -> bool {
1658 self.cx
1659 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1660 .unwrap_or(true)
1661 }
1662
1663 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1669 self.cx
1670 .update(|window, cx| {
1671 !window.has_pending_keystrokes()
1673 && self.handler.prefers_ime_for_printable_keys(window, cx)
1674 })
1675 .unwrap_or(false)
1676 }
1677}
1678
1679#[derive(Debug)]
1682pub struct UTF16Selection {
1683 pub range: Range<usize>,
1686 pub reversed: bool,
1689}
1690
1691pub trait InputHandler: 'static {
1696 fn selected_text_range(
1701 &mut self,
1702 ignore_disabled_input: bool,
1703 window: &mut Window,
1704 cx: &mut App,
1705 ) -> Option<UTF16Selection>;
1706
1707 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1712
1713 fn text_for_range(
1718 &mut self,
1719 range_utf16: Range<usize>,
1720 adjusted_range: &mut Option<Range<usize>>,
1721 window: &mut Window,
1722 cx: &mut App,
1723 ) -> Option<String>;
1724
1725 fn replace_text_in_range(
1730 &mut self,
1731 replacement_range: Option<Range<usize>>,
1732 text: &str,
1733 window: &mut Window,
1734 cx: &mut App,
1735 );
1736
1737 fn replace_and_mark_text_in_range(
1744 &mut self,
1745 range_utf16: Option<Range<usize>>,
1746 new_text: &str,
1747 new_selected_range: Option<Range<usize>>,
1748 window: &mut Window,
1749 cx: &mut App,
1750 );
1751
1752 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1755
1756 fn bounds_for_range(
1761 &mut self,
1762 range_utf16: Range<usize>,
1763 window: &mut Window,
1764 cx: &mut App,
1765 ) -> Option<Bounds<Pixels>>;
1766
1767 fn character_index_for_point(
1771 &mut self,
1772 point: Point<Pixels>,
1773 window: &mut Window,
1774 cx: &mut App,
1775 ) -> Option<usize>;
1776
1777 fn set_selected_text_range(
1787 &mut self,
1788 _range_utf16: Range<usize>,
1789 _window: &mut Window,
1790 _cx: &mut App,
1791 ) {
1792 }
1793
1794 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
1801 None
1802 }
1803
1804 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
1806 None
1807 }
1808
1809 #[allow(dead_code)]
1814 fn apple_press_and_hold_enabled(&mut self) -> bool {
1815 true
1816 }
1817
1818 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1820 true
1821 }
1822
1823 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1832 false
1833 }
1834}
1835
1836#[derive(Debug)]
1838pub struct WindowOptions {
1839 pub window_bounds: Option<WindowBounds>,
1843
1844 pub titlebar: Option<TitlebarOptions>,
1846
1847 pub focus: bool,
1849
1850 pub show: bool,
1852
1853 pub kind: WindowKind,
1855
1856 pub is_movable: bool,
1860
1861 pub app_owns_titlebar_drag: bool,
1873
1874 pub is_resizable: bool,
1876
1877 pub is_minimizable: bool,
1879
1880 pub display_id: Option<DisplayId>,
1883
1884 pub window_background: WindowBackgroundAppearance,
1886
1887 pub app_id: Option<String>,
1889
1890 pub window_min_size: Option<Size<Pixels>>,
1892
1893 pub window_decorations: Option<WindowDecorations>,
1896
1897 pub icon: Option<Arc<image::RgbaImage>>,
1899
1900 pub tabbing_identifier: Option<String>,
1902}
1903
1904#[derive(Debug)]
1906#[cfg_attr(
1907 all(
1908 any(target_os = "linux", target_os = "freebsd"),
1909 not(any(feature = "x11", feature = "wayland"))
1910 ),
1911 allow(dead_code)
1912)]
1913#[allow(missing_docs)]
1914pub struct WindowParams {
1915 pub bounds: Bounds<Pixels>,
1916
1917 #[cfg_attr(feature = "wayland", allow(dead_code))]
1919 pub titlebar: Option<TitlebarOptions>,
1920
1921 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1923 pub kind: WindowKind,
1924
1925 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1927 pub is_movable: bool,
1928
1929 #[cfg_attr(
1931 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1932 allow(dead_code)
1933 )]
1934 pub app_owns_titlebar_drag: bool,
1935
1936 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1938 pub is_resizable: bool,
1939
1940 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1942 pub is_minimizable: bool,
1943
1944 #[cfg_attr(
1945 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1946 allow(dead_code)
1947 )]
1948 pub focus: bool,
1949
1950 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1951 pub show: bool,
1952
1953 #[cfg_attr(feature = "wayland", allow(dead_code))]
1955 pub icon: Option<Arc<image::RgbaImage>>,
1956
1957 #[cfg_attr(feature = "wayland", allow(dead_code))]
1958 pub display_id: Option<DisplayId>,
1959
1960 #[cfg_attr(feature = "wayland", allow(dead_code))]
1961 pub app_id: Option<String>,
1962
1963 pub window_min_size: Option<Size<Pixels>>,
1964
1965 #[cfg(target_os = "macos")]
1966 pub tabbing_identifier: Option<String>,
1967}
1968
1969#[derive(Debug, Copy, Clone, PartialEq)]
1971pub enum WindowBounds {
1972 Windowed(Bounds<Pixels>),
1974 Maximized(Bounds<Pixels>),
1977 Fullscreen(Bounds<Pixels>),
1980}
1981
1982impl Default for WindowBounds {
1983 fn default() -> Self {
1984 WindowBounds::Windowed(Bounds::default())
1985 }
1986}
1987
1988impl WindowBounds {
1989 pub fn get_bounds(&self) -> Bounds<Pixels> {
1991 match self {
1992 WindowBounds::Windowed(bounds) => *bounds,
1993 WindowBounds::Maximized(bounds) => *bounds,
1994 WindowBounds::Fullscreen(bounds) => *bounds,
1995 }
1996 }
1997
1998 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2000 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2001 }
2002}
2003
2004impl Default for WindowOptions {
2005 fn default() -> Self {
2006 Self {
2007 window_bounds: None,
2008 titlebar: Some(TitlebarOptions {
2009 title: Default::default(),
2010 appears_transparent: Default::default(),
2011 traffic_light_position: Default::default(),
2012 }),
2013 focus: true,
2014 show: true,
2015 kind: WindowKind::Normal,
2016 is_movable: true,
2017 app_owns_titlebar_drag: false,
2018 is_resizable: true,
2019 is_minimizable: true,
2020 display_id: None,
2021 window_background: WindowBackgroundAppearance::default(),
2022 icon: None,
2023 app_id: None,
2024 window_min_size: None,
2025 window_decorations: None,
2026 tabbing_identifier: None,
2027 }
2028 }
2029}
2030
2031#[derive(Debug, Default)]
2033pub struct TitlebarOptions {
2034 pub title: Option<SharedString>,
2036
2037 pub appears_transparent: bool,
2040
2041 pub traffic_light_position: Option<Point<Pixels>>,
2043}
2044
2045#[derive(Clone, Debug, PartialEq, Eq)]
2047pub enum WindowKind {
2048 Normal,
2050
2051 PopUp,
2054
2055 AnchoredPopup(popup::PopupOptions),
2062
2063 Floating,
2065
2066 #[cfg(all(target_os = "linux", feature = "wayland"))]
2069 LayerShell(layer_shell::LayerShellOptions),
2070
2071 Dialog,
2074}
2075
2076#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2081pub enum WindowAppearance {
2082 #[default]
2086 Light,
2087
2088 VibrantLight,
2092
2093 Dark,
2097
2098 VibrantDark,
2102}
2103
2104#[derive(Copy, Clone, Debug, Default, PartialEq)]
2107pub enum WindowBackgroundAppearance {
2108 #[default]
2116 Opaque,
2117 Transparent,
2119 Blurred,
2123 MicaBackdrop,
2125 MicaAltBackdrop,
2127}
2128
2129#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2131pub enum TextRenderingMode {
2132 #[default]
2134 PlatformDefault,
2135 Subpixel,
2137 Grayscale,
2139}
2140
2141#[derive(Clone, Debug)]
2143pub struct PathPromptOptions {
2144 pub files: bool,
2146 pub directories: bool,
2148 pub multiple: bool,
2150 pub prompt: Option<SharedString>,
2152}
2153
2154#[derive(Copy, Clone, Debug, PartialEq)]
2156pub enum PromptLevel {
2157 Info,
2159
2160 Warning,
2162
2163 Critical,
2165}
2166
2167#[derive(Clone, Debug, PartialEq)]
2169pub enum PromptButton {
2170 Ok(SharedString),
2172 Cancel(SharedString),
2174 Other(SharedString),
2176}
2177
2178impl PromptButton {
2179 pub fn new(label: impl Into<SharedString>) -> Self {
2181 PromptButton::Other(label.into())
2182 }
2183
2184 pub fn ok(label: impl Into<SharedString>) -> Self {
2186 PromptButton::Ok(label.into())
2187 }
2188
2189 pub fn cancel(label: impl Into<SharedString>) -> Self {
2191 PromptButton::Cancel(label.into())
2192 }
2193
2194 #[allow(dead_code)]
2196 pub fn is_cancel(&self) -> bool {
2197 matches!(self, PromptButton::Cancel(_))
2198 }
2199
2200 pub fn label(&self) -> &SharedString {
2202 match self {
2203 PromptButton::Ok(label) => label,
2204 PromptButton::Cancel(label) => label,
2205 PromptButton::Other(label) => label,
2206 }
2207 }
2208}
2209
2210impl From<&str> for PromptButton {
2211 fn from(value: &str) -> Self {
2212 match value.to_lowercase().as_str() {
2213 "ok" => PromptButton::Ok("OK".into()),
2214 "cancel" => PromptButton::Cancel("Cancel".into()),
2215 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2216 }
2217 }
2218}
2219
2220#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2222pub enum CursorStyle {
2223 #[default]
2225 Arrow,
2226
2227 IBeam,
2230
2231 Crosshair,
2234
2235 ClosedHand,
2238
2239 OpenHand,
2242
2243 PointingHand,
2246
2247 ResizeLeft,
2250
2251 ResizeRight,
2254
2255 ResizeLeftRight,
2258
2259 ResizeUp,
2262
2263 ResizeDown,
2266
2267 ResizeUpDown,
2270
2271 ResizeUpLeftDownRight,
2274
2275 ResizeUpRightDownLeft,
2278
2279 ResizeColumn,
2282
2283 ResizeRow,
2286
2287 IBeamCursorForVerticalLayout,
2290
2291 OperationNotAllowed,
2294
2295 DragLink,
2298
2299 DragCopy,
2302
2303 ContextualMenu,
2306}
2307
2308#[derive(Clone, Debug, Eq, PartialEq)]
2310pub struct ClipboardItem {
2311 pub entries: Vec<ClipboardEntry>,
2313}
2314
2315#[derive(Clone, Debug, Eq, PartialEq)]
2317pub enum ClipboardEntry {
2318 String(ClipboardString),
2320 Image(Image),
2322 ExternalPaths(crate::ExternalPaths),
2324}
2325
2326impl ClipboardItem {
2327 pub fn new_string(text: String) -> Self {
2329 Self {
2330 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2331 }
2332 }
2333
2334 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2336 Self {
2337 entries: vec![ClipboardEntry::String(ClipboardString {
2338 text,
2339 metadata: Some(metadata),
2340 })],
2341 }
2342 }
2343
2344 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2346 Self {
2347 entries: vec![ClipboardEntry::String(
2348 ClipboardString::new(text).with_json_metadata(metadata),
2349 )],
2350 }
2351 }
2352
2353 pub fn new_image(image: &Image) -> Self {
2355 Self {
2356 entries: vec![ClipboardEntry::Image(image.clone())],
2357 }
2358 }
2359
2360 pub fn text(&self) -> Option<String> {
2363 let mut answer = String::new();
2364
2365 for entry in self.entries.iter() {
2366 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2367 answer.push_str(text);
2368 }
2369 }
2370
2371 if answer.is_empty() {
2372 for entry in self.entries.iter() {
2373 if let ClipboardEntry::ExternalPaths(paths) = entry {
2374 for path in &paths.0 {
2375 use std::fmt::Write as _;
2376 _ = write!(answer, "{}", path.display());
2377 }
2378 }
2379 }
2380 }
2381
2382 if !answer.is_empty() {
2383 Some(answer)
2384 } else {
2385 None
2386 }
2387 }
2388
2389 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2391 pub fn metadata(&self) -> Option<&String> {
2392 match self.entries().first() {
2393 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2394 clipboard_string.metadata.as_ref()
2395 }
2396 _ => None,
2397 }
2398 }
2399
2400 pub fn entries(&self) -> &[ClipboardEntry] {
2402 &self.entries
2403 }
2404
2405 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2407 self.entries.into_iter()
2408 }
2409}
2410
2411impl From<ClipboardString> for ClipboardEntry {
2412 fn from(value: ClipboardString) -> Self {
2413 Self::String(value)
2414 }
2415}
2416
2417impl From<String> for ClipboardEntry {
2418 fn from(value: String) -> Self {
2419 Self::from(ClipboardString::from(value))
2420 }
2421}
2422
2423impl From<Image> for ClipboardEntry {
2424 fn from(value: Image) -> Self {
2425 Self::Image(value)
2426 }
2427}
2428
2429impl From<ClipboardEntry> for ClipboardItem {
2430 fn from(value: ClipboardEntry) -> Self {
2431 Self {
2432 entries: vec![value],
2433 }
2434 }
2435}
2436
2437impl From<String> for ClipboardItem {
2438 fn from(value: String) -> Self {
2439 Self::from(ClipboardEntry::from(value))
2440 }
2441}
2442
2443impl From<Image> for ClipboardItem {
2444 fn from(value: Image) -> Self {
2445 Self::from(ClipboardEntry::from(value))
2446 }
2447}
2448
2449#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2451pub enum ImageFormat {
2452 Png,
2457 Jpeg,
2459 Webp,
2461 Gif,
2463 Svg,
2465 Bmp,
2467 Tiff,
2469 Ico,
2471 Pnm,
2473}
2474
2475impl ImageFormat {
2476 pub const fn mime_type(self) -> &'static str {
2478 match self {
2479 ImageFormat::Png => "image/png",
2480 ImageFormat::Jpeg => "image/jpeg",
2481 ImageFormat::Webp => "image/webp",
2482 ImageFormat::Gif => "image/gif",
2483 ImageFormat::Svg => "image/svg+xml",
2484 ImageFormat::Bmp => "image/bmp",
2485 ImageFormat::Tiff => "image/tiff",
2486 ImageFormat::Ico => "image/ico",
2487 ImageFormat::Pnm => "image/x-portable-anymap",
2488 }
2489 }
2490
2491 pub const fn extension(self) -> &'static str {
2493 match self {
2494 ImageFormat::Png => "png",
2495 ImageFormat::Jpeg => "jpg",
2496 ImageFormat::Webp => "webp",
2497 ImageFormat::Gif => "gif",
2498 ImageFormat::Svg => "svg",
2499 ImageFormat::Bmp => "bmp",
2500 ImageFormat::Tiff => "tiff",
2501 ImageFormat::Ico => "ico",
2502 ImageFormat::Pnm => "pnm",
2503 }
2504 }
2505
2506 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2508 use strum::IntoEnumIterator;
2509 Self::iter()
2510 .find(|format| format.mime_type() == mime_type)
2511 .or_else(|| Self::from_mime_type_alias(mime_type))
2512 }
2513
2514 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2518 match mime_type {
2519 "image/jpg" => Some(Self::Jpeg),
2520 "image/tif" => Some(Self::Tiff),
2521 _ => None,
2522 }
2523 }
2524}
2525
2526#[derive(Clone, Debug, PartialEq, Eq)]
2528pub struct Image {
2529 pub format: ImageFormat,
2531 pub bytes: Vec<u8>,
2533 pub id: u64,
2535}
2536
2537pub(crate) fn decode_static_image(
2538 bytes: &[u8],
2539 format: image::ImageFormat,
2540) -> Result<SmallVec<[Frame; 1]>> {
2541 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
2542 .into_decoder()
2543 .context("creating image decoder")?;
2544 decode_static_image_from_decoder(decoder)
2545}
2546
2547pub(crate) fn decode_static_image_from_decoder(
2548 mut decoder: impl image::ImageDecoder,
2549) -> Result<SmallVec<[Frame; 1]>> {
2550 let orientation = decoder
2551 .orientation()
2552 .context("reading decoder's orientation")?;
2553 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
2554 image.apply_orientation(orientation);
2555
2556 let mut data = image.into_rgba8();
2557 for pixel in data.chunks_exact_mut(4) {
2558 pixel.swap(0, 2);
2559 }
2560
2561 Ok(SmallVec::from_elem(Frame::new(data), 1))
2562}
2563
2564impl Hash for Image {
2565 fn hash<H: Hasher>(&self, state: &mut H) {
2566 state.write_u64(self.id);
2567 }
2568}
2569
2570impl Image {
2571 pub fn empty() -> Self {
2573 Self::from_bytes(ImageFormat::Png, Vec::new())
2574 }
2575
2576 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2578 Self {
2579 id: hash(&bytes),
2580 format,
2581 bytes,
2582 }
2583 }
2584
2585 pub fn id(&self) -> u64 {
2587 self.id
2588 }
2589
2590 pub fn use_render_image(
2592 self: Arc<Self>,
2593 window: &mut Window,
2594 cx: &mut App,
2595 ) -> Option<Arc<RenderImage>> {
2596 ImageSource::Image(self)
2597 .use_data(None, window, cx)
2598 .and_then(|result| result.ok())
2599 }
2600
2601 pub fn get_render_image(
2603 self: Arc<Self>,
2604 window: &mut Window,
2605 cx: &mut App,
2606 ) -> Option<Arc<RenderImage>> {
2607 ImageSource::Image(self)
2608 .get_data(None, window, cx)
2609 .and_then(|result| result.ok())
2610 }
2611
2612 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2614 ImageSource::Image(self).remove_asset(cx);
2615 }
2616
2617 #[cfg(any(test, feature = "test-support"))]
2620 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
2621 ImageSource::Image(self.clone()).is_asset_cached(cx)
2622 }
2623
2624 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2626 let frames = match self.format {
2627 ImageFormat::Gif => {
2628 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2629 let mut frames = SmallVec::new();
2630
2631 for frame in decoder.into_frames() {
2632 match frame {
2633 Ok(mut frame) => {
2634 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2636 pixel.swap(0, 2);
2637 }
2638 frames.push(frame);
2639 }
2640 Err(err) => {
2641 log::debug!("Skipping GIF frame due to decode error: {err}");
2642 }
2643 }
2644 }
2645
2646 if frames.is_empty() {
2647 anyhow::bail!("GIF could not be decoded: all frames failed");
2648 }
2649
2650 frames
2651 }
2652 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
2653 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
2654 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
2655 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
2656 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
2657 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
2658 ImageFormat::Svg => {
2659 return svg_renderer
2660 .render_single_frame(&self.bytes, 1.0)
2661 .map_err(Into::into);
2662 }
2663 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
2664 };
2665
2666 Ok(Arc::new(RenderImage::new(frames)))
2667 }
2668
2669 pub fn format(&self) -> ImageFormat {
2671 self.format
2672 }
2673
2674 pub fn bytes(&self) -> &[u8] {
2676 self.bytes.as_slice()
2677 }
2678}
2679
2680#[derive(Clone, Debug, Eq, PartialEq)]
2682pub struct ClipboardString {
2683 pub text: String,
2685 pub metadata: Option<String>,
2687}
2688
2689impl ClipboardString {
2690 pub fn new(text: String) -> Self {
2692 Self {
2693 text,
2694 metadata: None,
2695 }
2696 }
2697
2698 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2701 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2702 self
2703 }
2704
2705 pub fn text(&self) -> &String {
2707 &self.text
2708 }
2709
2710 pub fn into_text(self) -> String {
2712 self.text
2713 }
2714
2715 pub fn metadata_json<T>(&self) -> Option<T>
2717 where
2718 T: for<'a> Deserialize<'a>,
2719 {
2720 self.metadata
2721 .as_ref()
2722 .and_then(|m| serde_json::from_str(m).ok())
2723 }
2724
2725 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2726 pub fn text_hash(text: &str) -> u64 {
2728 let mut hasher = SeaHasher::new();
2729 text.hash(&mut hasher);
2730 hasher.finish()
2731 }
2732}
2733
2734impl From<String> for ClipboardString {
2735 fn from(value: String) -> Self {
2736 Self {
2737 text: value,
2738 metadata: None,
2739 }
2740 }
2741}
2742
2743#[cfg(test)]
2744mod image_tests {
2745 use super::*;
2746 use std::sync::Arc;
2747
2748 #[test]
2749 fn test_image_to_image_data_applies_exif_orientation() {
2750 let image = Image::from_bytes(
2751 ImageFormat::Jpeg,
2752 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
2753 );
2754
2755 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2756
2757 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
2758
2759 let bytes = render_image.as_bytes(0).unwrap();
2760 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
2761 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
2762 }
2763
2764 #[test]
2765 fn test_svg_image_to_image_data_converts_to_bgra() {
2766 let image = Image::from_bytes(
2767 ImageFormat::Svg,
2768 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2769<rect width="1" height="1" fill="#38BDF8"/>
2770</svg>"##
2771 .to_vec(),
2772 );
2773
2774 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2775 let bytes = render_image.as_bytes(0).unwrap();
2776
2777 for pixel in bytes.chunks_exact(4) {
2778 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2779 }
2780 }
2781}
2782
2783#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2784mod tests {
2785 use super::*;
2786 use std::collections::HashSet;
2787
2788 #[test]
2789 fn test_window_button_layout_parse_standard() {
2790 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2791 assert_eq!(
2792 layout.left,
2793 [
2794 Some(WindowButton::Close),
2795 Some(WindowButton::Minimize),
2796 None
2797 ]
2798 );
2799 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2800 }
2801
2802 #[test]
2803 fn test_window_button_layout_parse_right_only() {
2804 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2805 assert_eq!(layout.left, [None, None, None]);
2806 assert_eq!(
2807 layout.right,
2808 [
2809 Some(WindowButton::Minimize),
2810 Some(WindowButton::Maximize),
2811 Some(WindowButton::Close)
2812 ]
2813 );
2814 }
2815
2816 #[test]
2817 fn test_window_button_layout_parse_left_only() {
2818 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2819 assert_eq!(
2820 layout.left,
2821 [
2822 Some(WindowButton::Close),
2823 Some(WindowButton::Minimize),
2824 Some(WindowButton::Maximize)
2825 ]
2826 );
2827 assert_eq!(layout.right, [None, None, None]);
2828 }
2829
2830 #[test]
2831 fn test_window_button_layout_parse_with_whitespace() {
2832 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2833 assert_eq!(
2834 layout.left,
2835 [
2836 Some(WindowButton::Close),
2837 Some(WindowButton::Minimize),
2838 None
2839 ]
2840 );
2841 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2842 }
2843
2844 #[test]
2845 fn test_window_button_layout_parse_empty() {
2846 let layout = WindowButtonLayout::parse("").unwrap();
2847 assert_eq!(layout.left, [None, None, None]);
2848 assert_eq!(layout.right, [None, None, None]);
2849 }
2850
2851 #[test]
2852 fn test_window_button_layout_parse_intentionally_empty() {
2853 let layout = WindowButtonLayout::parse(":").unwrap();
2854 assert_eq!(layout.left, [None, None, None]);
2855 assert_eq!(layout.right, [None, None, None]);
2856 }
2857
2858 #[test]
2859 fn test_window_button_layout_parse_invalid_buttons() {
2860 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2861 assert_eq!(
2862 layout.left,
2863 [
2864 Some(WindowButton::Close),
2865 Some(WindowButton::Minimize),
2866 None
2867 ]
2868 );
2869 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2870 }
2871
2872 #[test]
2873 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2874 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2875 assert_eq!(
2876 layout.right,
2877 [
2878 Some(WindowButton::Close),
2879 Some(WindowButton::Minimize),
2880 None
2881 ]
2882 );
2883 assert_eq!(layout.format(), ":close,minimize");
2884 }
2885
2886 #[test]
2887 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2888 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2889 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2890 assert_eq!(
2891 layout.right,
2892 [
2893 Some(WindowButton::Maximize),
2894 Some(WindowButton::Minimize),
2895 None
2896 ]
2897 );
2898
2899 let button_ids: Vec<_> = layout
2900 .left
2901 .iter()
2902 .chain(layout.right.iter())
2903 .flatten()
2904 .map(WindowButton::id)
2905 .collect();
2906 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2907 assert_eq!(unique_button_ids.len(), button_ids.len());
2908 assert_eq!(layout.format(), "close:maximize,minimize");
2909 }
2910
2911 #[test]
2912 fn test_window_button_layout_parse_gnome_style() {
2913 let layout = WindowButtonLayout::parse("close").unwrap();
2914 assert_eq!(layout.left, [None, None, None]);
2915 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2916 }
2917
2918 #[test]
2919 fn test_window_button_layout_parse_elementary_style() {
2920 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2921 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2922 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2923 }
2924
2925 #[test]
2926 fn test_window_button_layout_round_trip() {
2927 let cases = [
2928 "close:minimize,maximize",
2929 "minimize,maximize,close:",
2930 ":close",
2931 "close:",
2932 "close:maximize",
2933 ":",
2934 ];
2935
2936 for case in cases {
2937 let layout = WindowButtonLayout::parse(case).unwrap();
2938 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2939 }
2940 }
2941
2942 #[test]
2943 fn test_window_button_layout_linux_default() {
2944 let layout = WindowButtonLayout::linux_default();
2945 assert_eq!(layout.left, [None, None, None]);
2946 assert_eq!(
2947 layout.right,
2948 [
2949 Some(WindowButton::Minimize),
2950 Some(WindowButton::Maximize),
2951 Some(WindowButton::Close)
2952 ]
2953 );
2954
2955 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2956 assert_eq!(round_tripped, layout);
2957 }
2958
2959 #[test]
2960 fn test_window_button_layout_parse_all_invalid() {
2961 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2962 }
2963}