1mod app_menu;
2mod keyboard;
3mod keystroke;
4
5#[cfg(any(test, feature = "bench"))]
6mod bench_dispatcher;
7
8#[cfg(all(target_os = "linux", feature = "wayland"))]
9#[expect(missing_docs)]
10pub mod layer_shell;
11
12#[cfg(any(test, feature = "test-support"))]
13mod test;
14
15#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
16mod visual_test;
17
18#[cfg(all(
19 feature = "screen-capture",
20 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
21))]
22pub mod scap_screen_capture;
23
24#[cfg(all(
25 any(target_os = "windows", target_os = "linux"),
26 feature = "screen-capture"
27))]
28pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
29#[cfg(not(feature = "screen-capture"))]
30pub(crate) type PlatformScreenCaptureFrame = ();
31#[cfg(all(target_os = "macos", feature = "screen-capture"))]
32pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
33
34use crate::rgpui_util;
35use crate::scheduler::Instant;
36pub use crate::scheduler::RunnableMeta;
37use crate::{
38 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
39 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
40 ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels,
41 PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
42 RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
43 SystemWindowTab, Task, Window, WindowControlArea, hash, point, px, size,
44};
45use crate::{Tray, TrayIconEvent, TrayMenuItem};
46use anyhow::Result;
47#[cfg(any(target_os = "linux", target_os = "freebsd"))]
48use anyhow::bail;
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 _, Frame};
55use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
56use schemars::JsonSchema;
57use seahash::SeaHasher;
58use serde::{Deserialize, Serialize};
59use smallvec::SmallVec;
60use std::borrow::Cow;
61use std::hash::{Hash, Hasher};
62use std::io::Cursor;
63use std::ops;
64use std::time::Duration;
65use std::{
66 fmt::{self, Debug},
67 ops::Range,
68 path::{Path, PathBuf},
69 rc::Rc,
70 sync::Arc,
71};
72use strum::EnumIter;
73use uuid::Uuid;
74
75pub use app_menu::*;
76pub use keyboard::*;
77pub use keystroke::*;
78
79#[cfg(any(test, feature = "test-support"))]
80pub(crate) use test::*;
81
82#[cfg(any(test, feature = "test-support"))]
83pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
84
85#[cfg(any(test, feature = "bench"))]
86pub use bench_dispatcher::BenchDispatcher;
87
88#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
89pub use visual_test::VisualTestPlatform;
90
91#[cfg(any(target_os = "linux", target_os = "freebsd"))]
95#[inline]
96pub fn guess_compositor() -> &'static str {
97 if std::env::var_os("ZED_HEADLESS").is_some() {
98 return "Headless";
99 }
100
101 #[cfg(feature = "wayland")]
102 let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
103 #[cfg(not(feature = "wayland"))]
104 let wayland_display: Option<std::ffi::OsString> = None;
105
106 #[cfg(feature = "x11")]
107 let x11_display = std::env::var_os("DISPLAY");
108 #[cfg(not(feature = "x11"))]
109 let x11_display: Option<std::ffi::OsString> = None;
110
111 let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
112 let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
113
114 if use_wayland {
115 "Wayland"
116 } else if use_x11 {
117 "X11"
118 } else {
119 "Headless"
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum SystemPowerEvent {
130 Sleep,
132 WakeUp,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum PowerSaveBlockerKind {
139 PreventSleep,
141 PreventDisplaySleep,
143}
144
145#[derive(Debug, Clone)]
147pub struct OsInfo {
148 pub name: String,
150 pub version: String,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PermissionStatus {
157 NotDetermined,
159 Granted,
161 Denied,
163 Unavailable,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum PermissionType {
172 Accessibility,
176
177 ScreenCapture,
181
182 InputMonitoring,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum NetworkStatus {
191 Disconnected,
193 ConnectedBelowRequired,
195 Connected,
197}
198
199#[derive(Debug, Clone)]
201pub struct MediaKeyEvent {
202 pub key_code: u16,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum BiometricStatus {
209 Unavailable,
211 Unlocked,
213 Locked,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum AttentionType {
220 Informational,
222 Critical,
224}
225
226#[derive(Debug, Clone)]
228pub struct DialogOptions {
229 pub dialog_type: DialogType,
231 pub title: String,
233 pub message: String,
235 pub confirm_label: Option<String>,
237 pub cancel_label: Option<String>,
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
243pub enum DialogType {
244 Info,
246 Warning,
248 Error,
250}
251
252#[derive(Debug, Clone)]
254pub struct FocusedWindowInfo {
255 pub app_name: String,
257 pub window_title: String,
259 pub bundle_id: Option<String>,
261 pub pid: Option<u32>,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq)]
267pub enum WindowPosition {
268 Center,
270 CenterOnDisplay(DisplayId),
272 TrayCenter(Bounds<Pixels>),
274 TopRight {
276 margin: Pixels,
278 },
279 BottomRight {
281 margin: Pixels,
283 },
284 TopLeft {
286 margin: Pixels,
288 },
289 BottomLeft {
291 margin: Pixels,
293 },
294}
295
296#[expect(missing_docs)]
297pub trait Platform: 'static {
298 fn background_executor(&self) -> BackgroundExecutor;
299 fn foreground_executor(&self) -> ForegroundExecutor;
300 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
301
302 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
303 fn quit(&self);
304 fn restart(&self, binary_path: Option<PathBuf>);
305 fn activate(&self, ignoring_other_apps: bool);
306 fn hide(&self);
307 fn hide_other_apps(&self);
308 fn unhide_other_apps(&self);
309
310 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
311 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
312 fn active_window(&self) -> Option<AnyWindowHandle>;
313 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
314 None
315 }
316
317 fn is_screen_capture_supported(&self) -> bool {
318 false
319 }
320
321 fn screen_capture_sources(
322 &self,
323 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
324 let (sources_tx, sources_rx) = oneshot::channel();
325 sources_tx
326 .send(Err(anyhow::anyhow!(
327 "rgpui was compiled without the screen-capture feature"
328 )))
329 .ok();
330 sources_rx
331 }
332
333 fn open_window(
334 &self,
335 handle: AnyWindowHandle,
336 options: WindowParams,
337 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
338
339 fn window_appearance(&self) -> WindowAppearance;
341
342 fn button_layout(&self) -> Option<WindowButtonLayout> {
344 None
345 }
346
347 fn open_url(&self, url: &str);
348 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
349 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
350
351 fn prompt_for_paths(
352 &self,
353 options: PathPromptOptions,
354 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
355 fn prompt_for_new_path(
356 &self,
357 directory: &Path,
358 suggested_name: Option<&str>,
359 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
360 fn can_select_mixed_files_and_dirs(&self) -> bool;
361 fn reveal_path(&self, path: &Path);
362 fn open_with_system(&self, path: &Path);
363
364 fn on_quit(&self, callback: Box<dyn FnMut()>);
365 fn on_reopen(&self, callback: Box<dyn FnMut()>);
366
367 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
368 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
369 None
370 }
371
372 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
373 fn perform_dock_menu_action(&self, _action: usize) {}
374 fn add_recent_document(&self, _path: &Path) {}
375 fn update_jump_list(
376 &self,
377 _menus: Vec<MenuItem>,
378 _entries: Vec<SmallVec<[PathBuf; 2]>>,
379 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
380 Task::ready(Vec::new())
381 }
382 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
383 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
384 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
385
386 fn thermal_state(&self) -> ThermalState;
387 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
388
389 fn compositor_name(&self) -> &'static str {
390 ""
391 }
392 fn app_path(&self) -> Result<PathBuf>;
393 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
394
395 fn set_cursor_style(&self, style: CursorStyle);
396
397 fn hide_cursor_until_mouse_moves(&self);
400
401 fn is_cursor_visible(&self) -> bool;
403
404 fn should_auto_hide_scrollbars(&self) -> bool;
405
406 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
407 fn write_to_clipboard(&self, item: ClipboardItem);
408
409 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
410 fn read_from_primary(&self) -> Option<ClipboardItem>;
411 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
412 fn write_to_primary(&self, item: ClipboardItem);
413
414 #[cfg(target_os = "macos")]
415 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
416 #[cfg(target_os = "macos")]
417 fn write_to_find_pasteboard(&self, item: ClipboardItem);
418
419 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
420 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
421 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
422
423 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
424 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
425 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
426
427 fn set_tray(&self, _tray: Tray, _menus: Option<Vec<MenuItem>>, _keymap: &Keymap) {}
429 fn set_tray_icon(&self, _icon: Option<&[u8]>) {}
430 fn set_tray_menu(&self, _menu: Vec<TrayMenuItem>) {}
431 fn set_tray_tooltip(&self, _tooltip: &str) {}
432 fn set_tray_panel_mode(&self, _enabled: bool) {}
433 fn get_tray_icon_bounds(&self) -> Option<Bounds<Pixels>> {
434 None
435 }
436 fn on_tray_icon_event(&self, _callback: Box<dyn FnMut(TrayIconEvent)>) {}
437 fn on_tray_menu_action(&self, _callback: Box<dyn FnMut(SharedString)>) {}
438
439 fn set_keep_alive_without_windows(&self, _keep_alive: bool) {}
441
442 fn register_global_hotkey(&self, _id: u32, _keystroke: &Keystroke) -> Result<()> {
444 Ok(())
445 }
446 fn unregister_global_hotkey(&self, _id: u32) {}
447 fn on_global_hotkey(&self, _callback: Box<dyn FnMut(u32)>) {}
448
449 fn show_notification(&self, _title: &str, _body: &str) -> Result<()> {
451 Ok(())
452 }
453
454 fn set_auto_launch(&self, _app_id: &str, _enabled: bool) -> Result<()> {
456 Ok(())
457 }
458 fn is_auto_launch_enabled(&self, _app_id: &str) -> bool {
459 false
460 }
461
462 fn focused_window_info(&self) -> Option<FocusedWindowInfo> {
464 None
465 }
466
467 fn accessibility_status(&self) -> PermissionStatus {
469 PermissionStatus::Unavailable
470 }
471 fn request_accessibility_permission(&self) {}
472
473 fn microphone_status(&self) -> PermissionStatus {
475 PermissionStatus::Unavailable
476 }
477 fn request_microphone_permission(&self, _callback: Box<dyn FnOnce(bool)>) {}
478
479 fn on_system_power_event(&self, _callback: Box<dyn FnMut(SystemPowerEvent)>) {}
481
482 fn start_power_save_blocker(&self, _kind: PowerSaveBlockerKind) -> Option<u32> {
484 None
485 }
486 fn stop_power_save_blocker(&self, _id: u32) {}
487
488 fn system_idle_time(&self) -> Option<Duration> {
490 None
491 }
492
493 fn network_status(&self) -> NetworkStatus {
495 NetworkStatus::Connected
496 }
497 fn on_network_status_change(&self, _callback: Box<dyn FnMut(NetworkStatus)>) {}
498
499 fn on_media_key_event(&self, _callback: Box<dyn FnMut(MediaKeyEvent)>) {}
501
502 fn request_user_attention(&self, _attention_type: AttentionType) {}
504 fn cancel_user_attention(&self) {}
505
506 fn set_dock_badge(&self, _label: Option<&str>) {}
508
509 fn show_context_menu(
511 &self,
512 _position: Point<Pixels>,
513 _items: Vec<TrayMenuItem>,
514 _callback: Box<dyn FnMut(SharedString)>,
515 ) {
516 }
517
518 fn show_dialog(&self, _options: DialogOptions) -> oneshot::Receiver<usize> {
520 let (tx, rx) = oneshot::channel();
521 let _ = tx.send(0);
522 rx
523 }
524
525 fn os_info(&self) -> OsInfo {
527 OsInfo {
528 name: String::new(),
529 version: String::new(),
530 }
531 }
532
533 fn biometric_status(&self) -> BiometricStatus {
535 BiometricStatus::Unavailable
536 }
537 fn authenticate_biometric(&self, _reason: &str, _callback: Box<dyn FnOnce(bool)>) {}
538}
539
540pub trait PlatformDisplay: Debug {
542 fn id(&self) -> DisplayId;
544
545 fn uuid(&self) -> Result<Uuid>;
548
549 fn bounds(&self) -> Bounds<Pixels>;
551
552 fn visible_bounds(&self) -> Bounds<Pixels> {
556 self.bounds()
557 }
558
559 fn default_bounds(&self) -> Bounds<Pixels> {
561 let bounds = self.bounds();
562 let center = bounds.center();
563 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
564
565 let offset = clipped_window_size / 2.0;
566 let origin = point(center.x - offset.width, center.y - offset.height);
567 Bounds::new(origin, clipped_window_size)
568 }
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573pub enum ThermalState {
574 Nominal,
576 Fair,
578 Serious,
580 Critical,
582}
583
584#[derive(Clone)]
586pub struct SourceMetadata {
587 pub id: u64,
589 pub label: Option<SharedString>,
591 pub is_main: Option<bool>,
593 pub resolution: Size<DevicePixels>,
595}
596
597pub trait ScreenCaptureSource {
599 fn metadata(&self) -> Result<SourceMetadata>;
601
602 fn stream(
605 &self,
606 foreground_executor: &ForegroundExecutor,
607 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
608 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
609}
610
611pub trait ScreenCaptureStream {
613 fn metadata(&self) -> Result<SourceMetadata>;
615}
616
617pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
619
620#[derive(PartialEq, Eq, Hash, Copy, Clone)]
622pub struct DisplayId(pub(crate) u64);
623
624impl DisplayId {
625 pub fn new(id: u64) -> Self {
627 Self(id)
628 }
629}
630
631impl From<u64> for DisplayId {
632 fn from(id: u64) -> Self {
633 Self(id)
634 }
635}
636
637impl From<DisplayId> for u64 {
638 fn from(id: DisplayId) -> Self {
639 id.0
640 }
641}
642
643impl Debug for DisplayId {
644 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645 write!(f, "DisplayId({})", self.0)
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq)]
651pub enum ResizeEdge {
652 Top,
654 TopRight,
656 Right,
658 BottomRight,
660 Bottom,
662 BottomLeft,
664 Left,
666 TopLeft,
668}
669
670#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
672pub enum WindowDecorations {
673 #[default]
674 Server,
676 Client,
678}
679
680#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
682pub enum Decorations {
683 #[default]
685 Server,
686 Client {
688 tiling: Tiling,
690 },
691}
692
693#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
695pub struct WindowControls {
696 pub fullscreen: bool,
698 pub maximize: bool,
700 pub minimize: bool,
702 pub window_menu: bool,
704}
705
706impl Default for WindowControls {
707 fn default() -> Self {
708 Self {
710 fullscreen: true,
711 maximize: true,
712 minimize: true,
713 window_menu: true,
714 }
715 }
716}
717
718#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
720pub enum WindowButton {
721 Minimize,
723 Maximize,
725 Close,
727}
728
729impl WindowButton {
730 pub fn id(&self) -> &'static str {
732 match self {
733 WindowButton::Minimize => "minimize",
734 WindowButton::Maximize => "maximize",
735 WindowButton::Close => "close",
736 }
737 }
738
739 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
740 fn index(&self) -> usize {
741 match self {
742 WindowButton::Minimize => 0,
743 WindowButton::Maximize => 1,
744 WindowButton::Close => 2,
745 }
746 }
747}
748
749pub const MAX_BUTTONS_PER_SIDE: usize = 3;
751
752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757pub struct WindowButtonLayout {
758 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
760 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
762}
763
764#[cfg(any(target_os = "linux", target_os = "freebsd"))]
765impl WindowButtonLayout {
766 pub fn linux_default() -> Self {
768 Self {
769 left: [None; MAX_BUTTONS_PER_SIDE],
770 right: [
771 Some(WindowButton::Minimize),
772 Some(WindowButton::Maximize),
773 Some(WindowButton::Close),
774 ],
775 }
776 }
777
778 pub fn parse(layout_string: &str) -> Result<Self> {
780 fn parse_side(
781 s: &str,
782 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
783 unrecognized: &mut Vec<String>,
784 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
785 let mut result = [None; MAX_BUTTONS_PER_SIDE];
786 let mut i = 0;
787 for name in s.split(',') {
788 let trimmed = name.trim();
789 if trimmed.is_empty() {
790 continue;
791 }
792 let button = match trimmed {
793 "minimize" => Some(WindowButton::Minimize),
794 "maximize" => Some(WindowButton::Maximize),
795 "close" => Some(WindowButton::Close),
796 other => {
797 unrecognized.push(other.to_string());
798 None
799 }
800 };
801 if let Some(button) = button {
802 if seen_buttons[button.index()] {
803 continue;
804 }
805 if let Some(slot) = result.get_mut(i) {
806 *slot = Some(button);
807 seen_buttons[button.index()] = true;
808 i += 1;
809 }
810 }
811 }
812 result
813 }
814
815 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
816 let mut unrecognized = Vec::new();
817 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
818 let layout = Self {
819 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
820 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
821 };
822
823 if !unrecognized.is_empty()
824 && layout.left.iter().all(Option::is_none)
825 && layout.right.iter().all(Option::is_none)
826 {
827 bail!(
828 "button layout string {:?} contains no valid buttons (unrecognized: {})",
829 layout_string,
830 unrecognized.join(", ")
831 );
832 }
833
834 Ok(layout)
835 }
836
837 #[cfg(test)]
839 pub fn format(&self) -> String {
840 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
841 buttons
842 .iter()
843 .flatten()
844 .map(|button| match button {
845 WindowButton::Minimize => "minimize",
846 WindowButton::Maximize => "maximize",
847 WindowButton::Close => "close",
848 })
849 .collect::<Vec<_>>()
850 .join(",")
851 }
852
853 format!("{}:{}", format_side(&self.left), format_side(&self.right))
854 }
855}
856
857#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
859pub struct Tiling {
860 pub top: bool,
862 pub left: bool,
864 pub right: bool,
866 pub bottom: bool,
868}
869
870impl Tiling {
871 pub fn tiled() -> Self {
873 Self {
874 top: true,
875 left: true,
876 right: true,
877 bottom: true,
878 }
879 }
880
881 pub fn is_tiled(&self) -> bool {
883 self.top || self.left || self.right || self.bottom
884 }
885}
886
887pub struct A11yCallbacks {
889 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
891 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
893 pub deactivation: Box<dyn Fn() + Send + 'static>,
895}
896
897#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
898#[expect(missing_docs)]
899pub struct RequestFrameOptions {
900 pub require_presentation: bool,
902 pub force_render: bool,
904}
905
906#[expect(missing_docs)]
907pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
908 fn bounds(&self) -> Bounds<Pixels>;
909 fn is_maximized(&self) -> bool;
910 fn window_bounds(&self) -> WindowBounds;
911 fn content_size(&self) -> Size<Pixels>;
912 fn resize(&mut self, size: Size<Pixels>);
913 fn scale_factor(&self) -> f32;
914 fn appearance(&self) -> WindowAppearance;
915 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
916 fn mouse_position(&self) -> Point<Pixels>;
917 fn modifiers(&self) -> Modifiers;
918 fn capslock(&self) -> Capslock;
919 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
920 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
921 fn prompt(
922 &self,
923 level: PromptLevel,
924 msg: &str,
925 detail: Option<&str>,
926 answers: &[PromptButton],
927 ) -> Option<oneshot::Receiver<usize>>;
928 fn activate(&self);
929 fn is_active(&self) -> bool;
930 fn is_hovered(&self) -> bool;
931 fn background_appearance(&self) -> WindowBackgroundAppearance;
932 fn set_title(&mut self, title: &str);
933 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
934 fn minimize(&self);
935 fn zoom(&self);
936 fn toggle_fullscreen(&self);
937 fn is_fullscreen(&self) -> bool;
938 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
939 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
940 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
941 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
942 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
943 fn on_moved(&self, callback: Box<dyn FnMut()>);
944 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
945 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
946 fn on_close(&self, callback: Box<dyn FnOnce()>);
947 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
948 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
949 fn draw(&self, scene: &Scene);
950 fn completed_frame(&self) {}
951 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
952 fn is_subpixel_rendering_supported(&self) -> bool;
953
954 fn get_title(&self) -> String {
956 String::new()
957 }
958 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
959 None
960 }
961 fn tab_bar_visible(&self) -> bool {
962 false
963 }
964 fn set_edited(&mut self, _edited: bool) {}
965 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
966 #[cfg(target_os = "macos")]
967 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
968 fn show_character_palette(&self) {}
969 fn titlebar_double_click(&self) {}
970 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
971 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
972 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
973 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
974 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
975 fn merge_all_windows(&self) {}
976 fn move_tab_to_new_window(&self) {}
977 fn toggle_window_tab_overview(&self) {}
978 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
979
980 #[cfg(target_os = "windows")]
981 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
982
983 fn inner_window_bounds(&self) -> WindowBounds {
985 self.window_bounds()
986 }
987 fn request_decorations(&self, _decorations: WindowDecorations) {}
988 fn show_window_menu(&self, _position: Point<Pixels>) {}
989 fn start_window_move(&self) {}
990 fn start_window_resize(&self, _edge: ResizeEdge) {}
991 fn window_decorations(&self) -> Decorations {
992 Decorations::Server
993 }
994 fn set_app_id(&mut self, _app_id: &str) {}
995 fn map_window(&mut self) -> anyhow::Result<()> {
996 Ok(())
997 }
998 fn window_controls(&self) -> WindowControls {
999 WindowControls::default()
1000 }
1001 fn set_client_inset(&self, _inset: Pixels) {}
1002 fn gpu_specs(&self) -> Option<GpuSpecs>;
1003
1004 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1005
1006 fn play_system_bell(&self) {}
1007
1008 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1010
1011 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1013
1014 fn a11y_update_window_bounds(&self) {}
1016
1017 #[cfg(any(test, feature = "test-support"))]
1018 fn as_test(&mut self) -> Option<&mut TestWindow> {
1019 None
1020 }
1021
1022 #[cfg(any(test, feature = "test-support"))]
1026 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1027 anyhow::bail!("render_to_image not implemented for this platform")
1028 }
1029
1030 fn set_position(&mut self, _position: Point<Pixels>) {}
1032
1033 fn hide(&self) {}
1035
1036 fn set_mouse_passthrough(&self, _passthrough: bool) {}
1038
1039 fn window_extended_style(&self) -> u32 {
1041 0
1042 }
1043 fn set_window_extended_style(&self, _style: u32) {}
1044
1045 fn set_titlebar_visible(&self, _visible: bool) {}
1047}
1048
1049#[cfg(any(test, feature = "test-support"))]
1051pub trait PlatformHeadlessRenderer {
1052 fn render_scene_to_image(
1054 &mut self,
1055 scene: &Scene,
1056 size: Size<DevicePixels>,
1057 ) -> Result<RgbaImage>;
1058
1059 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1063
1064 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1066}
1067
1068#[doc(hidden)]
1071pub type RunnableVariant = Runnable<RunnableMeta>;
1072
1073#[doc(hidden)]
1074pub type TimerResolutionGuard = rgpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1075
1076#[doc(hidden)]
1077pub enum TasksIncluded {
1078 OnlyCompleted,
1079 CompletedAndRunning,
1080}
1081
1082#[doc(hidden)]
1085pub trait PlatformDispatcher: Send + Sync {
1086 fn is_main_thread(&self) -> bool;
1087 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1088 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1089 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1090
1091 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1092
1093 fn now(&self) -> Instant {
1094 Instant::now()
1095 }
1096
1097 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1098 rgpui_util::defer(Box::new(|| {}))
1099 }
1100
1101 #[cfg(any(test, feature = "test-support"))]
1102 fn as_test(&self) -> Option<&TestDispatcher> {
1103 None
1104 }
1105
1106 #[cfg(any(test, feature = "bench"))]
1108 fn as_bench(&self) -> Option<&BenchDispatcher> {
1109 None
1110 }
1111}
1112
1113#[expect(missing_docs)]
1114pub trait PlatformTextSystem: Send + Sync {
1115 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1116 fn all_font_names(&self) -> Vec<String>;
1118 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1120 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1122 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1124 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1126 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1128 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1130 fn rasterize_glyph(
1132 &self,
1133 params: &RenderGlyphParams,
1134 raster_bounds: Bounds<DevicePixels>,
1135 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1136 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1138 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1140 -> TextRenderingMode;
1141 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1143 0
1144 }
1145}
1146
1147#[expect(missing_docs)]
1148pub struct NoopTextSystem;
1149
1150#[expect(missing_docs)]
1151impl NoopTextSystem {
1152 #[allow(dead_code)]
1153 pub fn new() -> Self {
1154 Self
1155 }
1156}
1157
1158impl PlatformTextSystem for NoopTextSystem {
1159 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1160 Ok(())
1161 }
1162
1163 fn all_font_names(&self) -> Vec<String> {
1164 Vec::new()
1165 }
1166
1167 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1168 Ok(FontId(1))
1169 }
1170
1171 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1172 FontMetrics {
1173 units_per_em: 1000,
1174 ascent: 1025.0,
1175 descent: -275.0,
1176 line_gap: 0.0,
1177 underline_position: -95.0,
1178 underline_thickness: 60.0,
1179 cap_height: 698.0,
1180 x_height: 516.0,
1181 bounding_box: Bounds {
1182 origin: Point {
1183 x: -260.0,
1184 y: -245.0,
1185 },
1186 size: Size {
1187 width: 1501.0,
1188 height: 1364.0,
1189 },
1190 },
1191 }
1192 }
1193
1194 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1195 Ok(Bounds {
1196 origin: Point { x: 54.0, y: 0.0 },
1197 size: size(392.0, 528.0),
1198 })
1199 }
1200
1201 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1202 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1203 }
1204
1205 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1206 Some(GlyphId(ch.len_utf16() as u32))
1207 }
1208
1209 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1210 Ok(Default::default())
1211 }
1212
1213 fn rasterize_glyph(
1214 &self,
1215 _params: &RenderGlyphParams,
1216 raster_bounds: Bounds<DevicePixels>,
1217 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1218 Ok((raster_bounds.size, Vec::new()))
1219 }
1220
1221 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1222 let mut position = px(0.);
1223 let metrics = self.font_metrics(FontId(0));
1224 let em_width = font_size
1225 * self
1226 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1227 .unwrap()
1228 .width
1229 / metrics.units_per_em as f32;
1230 let mut glyphs = Vec::new();
1231 for (ix, c) in text.char_indices() {
1232 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1233 glyphs.push(ShapedGlyph {
1234 id: glyph,
1235 position: point(position, px(0.)),
1236 index: ix,
1237 is_emoji: glyph.0 == 2,
1238 });
1239 if glyph.0 == 2 {
1240 position += em_width * 2.0;
1241 } else {
1242 position += em_width;
1243 }
1244 } else {
1245 position += em_width
1246 }
1247 }
1248 let mut runs = Vec::default();
1249 if !glyphs.is_empty() {
1250 runs.push(ShapedRun {
1251 font_id: FontId(0),
1252 glyphs,
1253 });
1254 } else {
1255 position = px(0.);
1256 }
1257
1258 LineLayout {
1259 font_size,
1260 width: position,
1261 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1262 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1263 runs,
1264 len: text.len(),
1265 }
1266 }
1267
1268 fn recommended_rendering_mode(
1269 &self,
1270 _font_id: FontId,
1271 _font_size: Pixels,
1272 ) -> TextRenderingMode {
1273 TextRenderingMode::Grayscale
1274 }
1275}
1276
1277#[allow(dead_code)]
1282pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1283 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1284 [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], ];
1298
1299 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1300 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1301
1302 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1303 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1304
1305 [
1306 ratios[0] * NORM13,
1307 ratios[1] * NORM24,
1308 ratios[2] * NORM13,
1309 ratios[3] * NORM24,
1310 ]
1311}
1312
1313#[derive(PartialEq, Eq, Hash, Clone)]
1314#[expect(missing_docs)]
1315pub enum AtlasKey {
1316 Glyph(RenderGlyphParams),
1317 Svg(RenderSvgParams),
1318 Image(RenderImageParams),
1319}
1320
1321impl AtlasKey {
1322 #[cfg_attr(
1323 all(
1324 any(target_os = "linux", target_os = "freebsd"),
1325 not(any(feature = "x11", feature = "wayland"))
1326 ),
1327 allow(dead_code)
1328 )]
1329 pub fn texture_kind(&self) -> AtlasTextureKind {
1331 match self {
1332 AtlasKey::Glyph(params) => {
1333 if params.is_emoji {
1334 AtlasTextureKind::Polychrome
1335 } else if params.subpixel_rendering {
1336 AtlasTextureKind::Subpixel
1337 } else {
1338 AtlasTextureKind::Monochrome
1339 }
1340 }
1341 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1342 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1343 }
1344 }
1345}
1346
1347impl From<RenderGlyphParams> for AtlasKey {
1348 fn from(params: RenderGlyphParams) -> Self {
1349 Self::Glyph(params)
1350 }
1351}
1352
1353impl From<RenderSvgParams> for AtlasKey {
1354 fn from(params: RenderSvgParams) -> Self {
1355 Self::Svg(params)
1356 }
1357}
1358
1359impl From<RenderImageParams> for AtlasKey {
1360 fn from(params: RenderImageParams) -> Self {
1361 Self::Image(params)
1362 }
1363}
1364
1365#[expect(missing_docs)]
1366pub trait PlatformAtlas {
1367 fn get_or_insert_with<'a>(
1368 &self,
1369 key: &AtlasKey,
1370 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1371 ) -> Result<Option<AtlasTile>>;
1372 fn remove(&self, key: &AtlasKey);
1373}
1374
1375#[doc(hidden)]
1376pub struct AtlasTextureList<T> {
1377 pub textures: Vec<Option<T>>,
1378 pub free_list: Vec<usize>,
1379}
1380
1381impl<T> Default for AtlasTextureList<T> {
1382 fn default() -> Self {
1383 Self {
1384 textures: Vec::default(),
1385 free_list: Vec::default(),
1386 }
1387 }
1388}
1389
1390impl<T> ops::Index<usize> for AtlasTextureList<T> {
1391 type Output = Option<T>;
1392
1393 fn index(&self, index: usize) -> &Self::Output {
1394 &self.textures[index]
1395 }
1396}
1397
1398impl<T> AtlasTextureList<T> {
1399 #[allow(unused)]
1400 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1401 self.free_list.clear();
1402 self.textures.drain(..)
1403 }
1404
1405 #[allow(dead_code)]
1406 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1407 self.textures.iter_mut().flatten()
1408 }
1409}
1410
1411#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1412#[repr(C)]
1413#[expect(missing_docs)]
1414pub struct AtlasTile {
1415 pub texture_id: AtlasTextureId,
1417 pub tile_id: TileId,
1419 pub padding: u32,
1421 pub bounds: Bounds<DevicePixels>,
1423}
1424
1425#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1426#[repr(C)]
1427#[expect(missing_docs)]
1428pub struct AtlasTextureId {
1429 pub index: u32,
1432 pub kind: AtlasTextureKind,
1434}
1435
1436#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1437#[repr(C)]
1438#[cfg_attr(
1439 all(
1440 any(target_os = "linux", target_os = "freebsd"),
1441 not(any(feature = "x11", feature = "wayland"))
1442 ),
1443 allow(dead_code)
1444)]
1445#[expect(missing_docs)]
1446pub enum AtlasTextureKind {
1447 Monochrome = 0,
1448 Polychrome = 1,
1449 Subpixel = 2,
1450}
1451
1452#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1453#[repr(C)]
1454#[expect(missing_docs)]
1455pub struct TileId(pub u32);
1456
1457impl From<etagere::AllocId> for TileId {
1458 fn from(id: etagere::AllocId) -> Self {
1459 Self(id.serialize())
1460 }
1461}
1462
1463impl From<TileId> for etagere::AllocId {
1464 fn from(id: TileId) -> Self {
1465 Self::deserialize(id.0)
1466 }
1467}
1468
1469#[expect(missing_docs)]
1470pub struct PlatformInputHandler {
1471 cx: AsyncWindowContext,
1472 handler: Box<dyn InputHandler>,
1473}
1474
1475#[expect(missing_docs)]
1476#[cfg_attr(
1477 all(
1478 any(target_os = "linux", target_os = "freebsd"),
1479 not(any(feature = "x11", feature = "wayland"))
1480 ),
1481 allow(dead_code)
1482)]
1483impl PlatformInputHandler {
1484 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1485 Self { cx, handler }
1486 }
1487
1488 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1489 self.cx
1490 .update(|window, cx| {
1491 self.handler
1492 .selected_text_range(ignore_disabled_input, window, cx)
1493 })
1494 .ok()
1495 .flatten()
1496 }
1497
1498 #[cfg_attr(target_os = "windows", allow(dead_code))]
1499 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1500 self.cx
1501 .update(|window, cx| self.handler.marked_text_range(window, cx))
1502 .ok()
1503 .flatten()
1504 }
1505
1506 #[cfg_attr(
1507 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1508 allow(dead_code)
1509 )]
1510 pub fn text_for_range(
1511 &mut self,
1512 range_utf16: Range<usize>,
1513 adjusted: &mut Option<Range<usize>>,
1514 ) -> Option<String> {
1515 self.cx
1516 .update(|window, cx| {
1517 self.handler
1518 .text_for_range(range_utf16, adjusted, window, cx)
1519 })
1520 .ok()
1521 .flatten()
1522 }
1523
1524 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1525 self.cx
1526 .update(|window, cx| {
1527 self.handler
1528 .replace_text_in_range(replacement_range, text, window, cx);
1529 })
1530 .ok();
1531 }
1532
1533 pub fn replace_and_mark_text_in_range(
1534 &mut self,
1535 range_utf16: Option<Range<usize>>,
1536 new_text: &str,
1537 new_selected_range: Option<Range<usize>>,
1538 ) {
1539 self.cx
1540 .update(|window, cx| {
1541 self.handler.replace_and_mark_text_in_range(
1542 range_utf16,
1543 new_text,
1544 new_selected_range,
1545 window,
1546 cx,
1547 )
1548 })
1549 .ok();
1550 }
1551
1552 #[cfg_attr(target_os = "windows", allow(dead_code))]
1553 pub fn unmark_text(&mut self) {
1554 self.cx
1555 .update(|window, cx| self.handler.unmark_text(window, cx))
1556 .ok();
1557 }
1558
1559 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1560 self.cx
1561 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1562 .ok()
1563 .flatten()
1564 }
1565
1566 #[allow(dead_code)]
1567 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1568 self.handler.apple_press_and_hold_enabled()
1569 }
1570
1571 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1572 self.handler.replace_text_in_range(None, input, window, cx);
1573 }
1574
1575 pub fn compute_ime_candidate_bounds(
1576 marked_range: Option<Range<usize>>,
1577 selection: &UTF16Selection,
1578 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
1579 ) -> Option<Bounds<Pixels>> {
1580 if let Some(marked_range) = marked_range {
1581 let mut line_start = marked_range.start;
1583
1584 let caret = selection.range.end;
1588 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
1589 for i in (marked_range.start..caret).rev() {
1590 if let Some(b) = bounds_for_range(i..i) {
1591 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
1592 line_start = i + 1;
1593 break;
1594 }
1595 }
1596 }
1597 }
1598 bounds_for_range(line_start..line_start)
1599 } else {
1600 let offset = if selection.reversed {
1602 selection.range.start
1603 } else {
1604 selection.range.end
1605 };
1606 bounds_for_range(offset..offset)
1607 }
1608 }
1609
1610 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1611 let marked_range = self.handler.marked_text_range(window, cx);
1612 let selection = self.handler.selected_text_range(true, window, cx)?;
1613 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1614 self.handler.bounds_for_range(range, window, cx)
1615 })
1616 }
1617
1618 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
1619 let marked_range = self.marked_text_range();
1620 let selection = self.selected_text_range(true)?;
1621 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
1622 self.bounds_for_range(range)
1623 })
1624 }
1625
1626 #[allow(unused)]
1627 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1628 self.cx
1629 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1630 .ok()
1631 .flatten()
1632 }
1633
1634 #[allow(dead_code)]
1635 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1636 self.handler.accepts_text_input(window, cx)
1637 }
1638
1639 #[allow(dead_code)]
1640 pub fn query_accepts_text_input(&mut self) -> bool {
1641 self.cx
1642 .update(|window, cx| self.handler.accepts_text_input(window, cx))
1643 .unwrap_or(true)
1644 }
1645
1646 #[allow(dead_code)]
1647 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1648 self.cx
1649 .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1650 .unwrap_or(false)
1651 }
1652}
1653
1654#[derive(Debug)]
1657pub struct UTF16Selection {
1658 pub range: Range<usize>,
1661 pub reversed: bool,
1664}
1665
1666pub trait InputHandler: 'static {
1671 fn selected_text_range(
1676 &mut self,
1677 ignore_disabled_input: bool,
1678 window: &mut Window,
1679 cx: &mut App,
1680 ) -> Option<UTF16Selection>;
1681
1682 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1687
1688 fn text_for_range(
1693 &mut self,
1694 range_utf16: Range<usize>,
1695 adjusted_range: &mut Option<Range<usize>>,
1696 window: &mut Window,
1697 cx: &mut App,
1698 ) -> Option<String>;
1699
1700 fn replace_text_in_range(
1705 &mut self,
1706 replacement_range: Option<Range<usize>>,
1707 text: &str,
1708 window: &mut Window,
1709 cx: &mut App,
1710 );
1711
1712 fn replace_and_mark_text_in_range(
1719 &mut self,
1720 range_utf16: Option<Range<usize>>,
1721 new_text: &str,
1722 new_selected_range: Option<Range<usize>>,
1723 window: &mut Window,
1724 cx: &mut App,
1725 );
1726
1727 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1730
1731 fn bounds_for_range(
1736 &mut self,
1737 range_utf16: Range<usize>,
1738 window: &mut Window,
1739 cx: &mut App,
1740 ) -> Option<Bounds<Pixels>>;
1741
1742 fn character_index_for_point(
1746 &mut self,
1747 point: Point<Pixels>,
1748 window: &mut Window,
1749 cx: &mut App,
1750 ) -> Option<usize>;
1751
1752 #[allow(dead_code)]
1757 fn apple_press_and_hold_enabled(&mut self) -> bool {
1758 true
1759 }
1760
1761 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1763 true
1764 }
1765
1766 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1775 false
1776 }
1777}
1778
1779#[derive(Debug)]
1781pub struct WindowOptions {
1782 pub window_bounds: Option<WindowBounds>,
1786
1787 pub titlebar: Option<TitlebarOptions>,
1789
1790 pub focus: bool,
1792
1793 pub show: bool,
1795
1796 pub kind: WindowKind,
1798
1799 pub is_movable: bool,
1801
1802 pub is_resizable: bool,
1804
1805 pub is_minimizable: bool,
1807
1808 pub display_id: Option<DisplayId>,
1811
1812 pub window_background: WindowBackgroundAppearance,
1814
1815 pub app_id: Option<String>,
1817
1818 pub window_min_size: Option<Size<Pixels>>,
1820
1821 pub window_decorations: Option<WindowDecorations>,
1824
1825 pub icon: Option<Arc<image::RgbaImage>>,
1827
1828 pub tabbing_identifier: Option<String>,
1830
1831 pub mouse_passthrough: bool,
1833}
1834
1835#[derive(Debug)]
1837#[cfg_attr(
1838 all(
1839 any(target_os = "linux", target_os = "freebsd"),
1840 not(any(feature = "x11", feature = "wayland"))
1841 ),
1842 allow(dead_code)
1843)]
1844#[allow(missing_docs)]
1845pub struct WindowParams {
1846 pub bounds: Bounds<Pixels>,
1847
1848 #[cfg_attr(feature = "wayland", allow(dead_code))]
1850 pub titlebar: Option<TitlebarOptions>,
1851
1852 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1854 pub kind: WindowKind,
1855
1856 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1858 pub is_movable: bool,
1859
1860 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1862 pub is_resizable: bool,
1863
1864 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1866 pub is_minimizable: bool,
1867
1868 #[cfg_attr(
1869 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1870 allow(dead_code)
1871 )]
1872 pub focus: bool,
1873
1874 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1875 pub show: bool,
1876
1877 #[cfg_attr(feature = "wayland", allow(dead_code))]
1879 pub icon: Option<Arc<image::RgbaImage>>,
1880
1881 #[cfg_attr(feature = "wayland", allow(dead_code))]
1882 pub display_id: Option<DisplayId>,
1883
1884 pub window_min_size: Option<Size<Pixels>>,
1885 #[cfg(target_os = "macos")]
1886 pub tabbing_identifier: Option<String>,
1887
1888 pub window_decorations: WindowDecorations,
1890
1891 pub mouse_passthrough: bool,
1893}
1894
1895#[derive(Debug, Copy, Clone, PartialEq)]
1897pub enum WindowBounds {
1898 Windowed(Bounds<Pixels>),
1900 Maximized(Bounds<Pixels>),
1903 Fullscreen(Bounds<Pixels>),
1906}
1907
1908impl Default for WindowBounds {
1909 fn default() -> Self {
1910 WindowBounds::Windowed(Bounds::default())
1911 }
1912}
1913
1914impl WindowBounds {
1915 pub fn get_bounds(&self) -> Bounds<Pixels> {
1917 match self {
1918 WindowBounds::Windowed(bounds) => *bounds,
1919 WindowBounds::Maximized(bounds) => *bounds,
1920 WindowBounds::Fullscreen(bounds) => *bounds,
1921 }
1922 }
1923
1924 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1926 WindowBounds::Windowed(Bounds::centered(None, size, cx))
1927 }
1928}
1929
1930impl Default for WindowOptions {
1931 fn default() -> Self {
1932 Self {
1933 window_bounds: None,
1934 titlebar: Some(TitlebarOptions {
1935 title: Default::default(),
1936 appears_transparent: Default::default(),
1937 traffic_light_position: Default::default(),
1938 }),
1939 focus: true,
1940 show: true,
1941 kind: WindowKind::Normal,
1942 is_movable: true,
1943 is_resizable: true,
1944 is_minimizable: true,
1945 display_id: None,
1946 window_background: WindowBackgroundAppearance::default(),
1947 icon: None,
1948 app_id: None,
1949 window_min_size: None,
1950 window_decorations: None,
1951 tabbing_identifier: None,
1952 mouse_passthrough: false,
1953 }
1954 }
1955}
1956
1957#[derive(Debug, Default)]
1959pub struct TitlebarOptions {
1960 pub title: Option<SharedString>,
1962
1963 pub appears_transparent: bool,
1966
1967 pub traffic_light_position: Option<Point<Pixels>>,
1969}
1970
1971#[derive(Clone, Debug, PartialEq, Eq)]
1973pub enum WindowKind {
1974 Normal,
1976
1977 PopUp,
1980
1981 Floating,
1983
1984 #[cfg(all(target_os = "linux", feature = "wayland"))]
1987 LayerShell(layer_shell::LayerShellOptions),
1988
1989 Dialog,
1992
1993 Overlay,
1995}
1996
1997#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2002pub enum WindowAppearance {
2003 #[default]
2007 Light,
2008
2009 VibrantLight,
2013
2014 Dark,
2018
2019 VibrantDark,
2023}
2024
2025#[derive(Copy, Clone, Debug, Default, PartialEq)]
2028pub enum WindowBackgroundAppearance {
2029 #[default]
2037 Opaque,
2038 Transparent,
2040 Blurred,
2044 MicaBackdrop,
2046 MicaAltBackdrop,
2048}
2049
2050#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2052pub enum TextRenderingMode {
2053 #[default]
2055 PlatformDefault,
2056 Subpixel,
2058 Grayscale,
2060}
2061
2062#[derive(Clone, Debug)]
2064pub struct PathPromptOptions {
2065 pub files: bool,
2067 pub directories: bool,
2069 pub multiple: bool,
2071 pub prompt: Option<SharedString>,
2073}
2074
2075#[derive(Copy, Clone, Debug, PartialEq)]
2077pub enum PromptLevel {
2078 Info,
2080
2081 Warning,
2083
2084 Critical,
2086}
2087
2088#[derive(Clone, Debug, PartialEq)]
2090pub enum PromptButton {
2091 Ok(SharedString),
2093 Cancel(SharedString),
2095 Other(SharedString),
2097}
2098
2099impl PromptButton {
2100 pub fn new(label: impl Into<SharedString>) -> Self {
2102 PromptButton::Other(label.into())
2103 }
2104
2105 pub fn ok(label: impl Into<SharedString>) -> Self {
2107 PromptButton::Ok(label.into())
2108 }
2109
2110 pub fn cancel(label: impl Into<SharedString>) -> Self {
2112 PromptButton::Cancel(label.into())
2113 }
2114
2115 #[allow(dead_code)]
2117 pub fn is_cancel(&self) -> bool {
2118 matches!(self, PromptButton::Cancel(_))
2119 }
2120
2121 pub fn label(&self) -> &SharedString {
2123 match self {
2124 PromptButton::Ok(label) => label,
2125 PromptButton::Cancel(label) => label,
2126 PromptButton::Other(label) => label,
2127 }
2128 }
2129}
2130
2131impl From<&str> for PromptButton {
2132 fn from(value: &str) -> Self {
2133 match value.to_lowercase().as_str() {
2134 "ok" => PromptButton::Ok("OK".into()),
2135 "cancel" => PromptButton::Cancel("Cancel".into()),
2136 _ => PromptButton::Other(SharedString::from(value.to_owned())),
2137 }
2138 }
2139}
2140
2141#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
2143pub enum CursorStyle {
2144 #[default]
2146 Arrow,
2147
2148 IBeam,
2151
2152 Crosshair,
2155
2156 ClosedHand,
2159
2160 OpenHand,
2163
2164 PointingHand,
2167
2168 ResizeLeft,
2171
2172 ResizeRight,
2175
2176 ResizeLeftRight,
2179
2180 ResizeUp,
2183
2184 ResizeDown,
2187
2188 ResizeUpDown,
2191
2192 ResizeUpLeftDownRight,
2195
2196 ResizeUpRightDownLeft,
2199
2200 ResizeColumn,
2203
2204 ResizeRow,
2207
2208 IBeamCursorForVerticalLayout,
2211
2212 OperationNotAllowed,
2215
2216 DragLink,
2219
2220 DragCopy,
2223
2224 ContextualMenu,
2227}
2228
2229#[derive(Clone, Debug, Eq, PartialEq)]
2231pub struct ClipboardItem {
2232 pub entries: Vec<ClipboardEntry>,
2234}
2235
2236#[derive(Clone, Debug, Eq, PartialEq)]
2238pub enum ClipboardEntry {
2239 String(ClipboardString),
2241 Image(Image),
2243 ExternalPaths(crate::ExternalPaths),
2245}
2246
2247impl ClipboardItem {
2248 pub fn new_string(text: String) -> Self {
2250 Self {
2251 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2252 }
2253 }
2254
2255 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2257 Self {
2258 entries: vec![ClipboardEntry::String(ClipboardString {
2259 text,
2260 metadata: Some(metadata),
2261 })],
2262 }
2263 }
2264
2265 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2267 Self {
2268 entries: vec![ClipboardEntry::String(
2269 ClipboardString::new(text).with_json_metadata(metadata),
2270 )],
2271 }
2272 }
2273
2274 pub fn new_image(image: &Image) -> Self {
2276 Self {
2277 entries: vec![ClipboardEntry::Image(image.clone())],
2278 }
2279 }
2280
2281 pub fn text(&self) -> Option<String> {
2284 let mut answer = String::new();
2285
2286 for entry in self.entries.iter() {
2287 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2288 answer.push_str(text);
2289 }
2290 }
2291
2292 if answer.is_empty() {
2293 for entry in self.entries.iter() {
2294 if let ClipboardEntry::ExternalPaths(paths) = entry {
2295 for path in &paths.0 {
2296 use std::fmt::Write as _;
2297 _ = write!(answer, "{}", path.display());
2298 }
2299 }
2300 }
2301 }
2302
2303 if !answer.is_empty() {
2304 Some(answer)
2305 } else {
2306 None
2307 }
2308 }
2309
2310 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2312 pub fn metadata(&self) -> Option<&String> {
2313 match self.entries().first() {
2314 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2315 clipboard_string.metadata.as_ref()
2316 }
2317 _ => None,
2318 }
2319 }
2320
2321 pub fn entries(&self) -> &[ClipboardEntry] {
2323 &self.entries
2324 }
2325
2326 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2328 self.entries.into_iter()
2329 }
2330}
2331
2332impl From<ClipboardString> for ClipboardEntry {
2333 fn from(value: ClipboardString) -> Self {
2334 Self::String(value)
2335 }
2336}
2337
2338impl From<String> for ClipboardEntry {
2339 fn from(value: String) -> Self {
2340 Self::from(ClipboardString::from(value))
2341 }
2342}
2343
2344impl From<Image> for ClipboardEntry {
2345 fn from(value: Image) -> Self {
2346 Self::Image(value)
2347 }
2348}
2349
2350impl From<ClipboardEntry> for ClipboardItem {
2351 fn from(value: ClipboardEntry) -> Self {
2352 Self {
2353 entries: vec![value],
2354 }
2355 }
2356}
2357
2358impl From<String> for ClipboardItem {
2359 fn from(value: String) -> Self {
2360 Self::from(ClipboardEntry::from(value))
2361 }
2362}
2363
2364impl From<Image> for ClipboardItem {
2365 fn from(value: Image) -> Self {
2366 Self::from(ClipboardEntry::from(value))
2367 }
2368}
2369
2370#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2372pub enum ImageFormat {
2373 Png,
2378 Jpeg,
2380 Webp,
2382 Gif,
2384 Svg,
2386 Bmp,
2388 Tiff,
2390 Ico,
2392 Pnm,
2394}
2395
2396impl ImageFormat {
2397 pub const fn mime_type(self) -> &'static str {
2399 match self {
2400 ImageFormat::Png => "image/png",
2401 ImageFormat::Jpeg => "image/jpeg",
2402 ImageFormat::Webp => "image/webp",
2403 ImageFormat::Gif => "image/gif",
2404 ImageFormat::Svg => "image/svg+xml",
2405 ImageFormat::Bmp => "image/bmp",
2406 ImageFormat::Tiff => "image/tiff",
2407 ImageFormat::Ico => "image/ico",
2408 ImageFormat::Pnm => "image/x-portable-anymap",
2409 }
2410 }
2411
2412 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2414 use strum::IntoEnumIterator;
2415 Self::iter()
2416 .find(|format| format.mime_type() == mime_type)
2417 .or_else(|| Self::from_mime_type_alias(mime_type))
2418 }
2419
2420 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2424 match mime_type {
2425 "image/jpg" => Some(Self::Jpeg),
2426 "image/tif" => Some(Self::Tiff),
2427 _ => None,
2428 }
2429 }
2430}
2431
2432#[derive(Clone, Debug, PartialEq, Eq)]
2434pub struct Image {
2435 pub format: ImageFormat,
2437 pub bytes: Vec<u8>,
2439 pub id: u64,
2441}
2442
2443impl Hash for Image {
2444 fn hash<H: Hasher>(&self, state: &mut H) {
2445 state.write_u64(self.id);
2446 }
2447}
2448
2449impl Image {
2450 pub fn empty() -> Self {
2452 Self::from_bytes(ImageFormat::Png, Vec::new())
2453 }
2454
2455 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2457 Self {
2458 id: hash(&bytes),
2459 format,
2460 bytes,
2461 }
2462 }
2463
2464 pub fn id(&self) -> u64 {
2466 self.id
2467 }
2468
2469 pub fn use_render_image(
2471 self: Arc<Self>,
2472 window: &mut Window,
2473 cx: &mut App,
2474 ) -> Option<Arc<RenderImage>> {
2475 ImageSource::Image(self)
2476 .use_data(None, window, cx)
2477 .and_then(|result| result.ok())
2478 }
2479
2480 pub fn get_render_image(
2482 self: Arc<Self>,
2483 window: &mut Window,
2484 cx: &mut App,
2485 ) -> Option<Arc<RenderImage>> {
2486 ImageSource::Image(self)
2487 .get_data(None, window, cx)
2488 .and_then(|result| result.ok())
2489 }
2490
2491 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2493 ImageSource::Image(self).remove_asset(cx);
2494 }
2495
2496 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2498 fn frames_for_image(
2499 bytes: &[u8],
2500 format: image::ImageFormat,
2501 ) -> Result<SmallVec<[Frame; 1]>> {
2502 let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2503
2504 for pixel in data.chunks_exact_mut(4) {
2506 pixel.swap(0, 2);
2507 }
2508
2509 Ok(SmallVec::from_elem(Frame::new(data), 1))
2510 }
2511
2512 let frames = match self.format {
2513 ImageFormat::Gif => {
2514 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2515 let mut frames = SmallVec::new();
2516
2517 for frame in decoder.into_frames() {
2518 match frame {
2519 Ok(mut frame) => {
2520 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2522 pixel.swap(0, 2);
2523 }
2524 frames.push(frame);
2525 }
2526 Err(err) => {
2527 log::debug!("Skipping GIF frame due to decode error: {err}");
2528 }
2529 }
2530 }
2531
2532 if frames.is_empty() {
2533 anyhow::bail!("GIF could not be decoded: all frames failed");
2534 }
2535
2536 frames
2537 }
2538 ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2539 ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2540 ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2541 ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2542 ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2543 ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2544 ImageFormat::Svg => {
2545 return svg_renderer
2546 .render_single_frame(&self.bytes, 1.0)
2547 .map_err(Into::into);
2548 }
2549 ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2550 };
2551
2552 Ok(Arc::new(RenderImage::new(frames)))
2553 }
2554
2555 pub fn format(&self) -> ImageFormat {
2557 self.format
2558 }
2559
2560 pub fn bytes(&self) -> &[u8] {
2562 self.bytes.as_slice()
2563 }
2564}
2565
2566#[derive(Clone, Debug, Eq, PartialEq)]
2568pub struct ClipboardString {
2569 pub text: String,
2571 pub metadata: Option<String>,
2573}
2574
2575impl ClipboardString {
2576 pub fn new(text: String) -> Self {
2578 Self {
2579 text,
2580 metadata: None,
2581 }
2582 }
2583
2584 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2587 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2588 self
2589 }
2590
2591 pub fn text(&self) -> &String {
2593 &self.text
2594 }
2595
2596 pub fn into_text(self) -> String {
2598 self.text
2599 }
2600
2601 pub fn metadata_json<T>(&self) -> Option<T>
2603 where
2604 T: for<'a> Deserialize<'a>,
2605 {
2606 self.metadata
2607 .as_ref()
2608 .and_then(|m| serde_json::from_str(m).ok())
2609 }
2610
2611 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2612 pub fn text_hash(text: &str) -> u64 {
2614 let mut hasher = SeaHasher::new();
2615 text.hash(&mut hasher);
2616 hasher.finish()
2617 }
2618}
2619
2620impl From<String> for ClipboardString {
2621 fn from(value: String) -> Self {
2622 Self {
2623 text: value,
2624 metadata: None,
2625 }
2626 }
2627}
2628
2629#[cfg(test)]
2630mod image_tests {
2631 use super::*;
2632 use std::sync::Arc;
2633
2634 #[test]
2635 fn test_svg_image_to_image_data_converts_to_bgra() {
2636 let image = Image::from_bytes(
2637 ImageFormat::Svg,
2638 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2639<rect width="1" height="1" fill="#38BDF8"/>
2640</svg>"##
2641 .to_vec(),
2642 );
2643
2644 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2645 let bytes = render_image.as_bytes(0).unwrap();
2646
2647 for pixel in bytes.chunks_exact(4) {
2648 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2649 }
2650 }
2651}
2652
2653#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2654mod tests {
2655 use super::*;
2656 use rgpui::collections::HashSet;
2657
2658 #[test]
2659 fn test_window_button_layout_parse_standard() {
2660 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2661 assert_eq!(
2662 layout.left,
2663 [
2664 Some(WindowButton::Close),
2665 Some(WindowButton::Minimize),
2666 None
2667 ]
2668 );
2669 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2670 }
2671
2672 #[test]
2673 fn test_window_button_layout_parse_right_only() {
2674 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2675 assert_eq!(layout.left, [None, None, None]);
2676 assert_eq!(
2677 layout.right,
2678 [
2679 Some(WindowButton::Minimize),
2680 Some(WindowButton::Maximize),
2681 Some(WindowButton::Close)
2682 ]
2683 );
2684 }
2685
2686 #[test]
2687 fn test_window_button_layout_parse_left_only() {
2688 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2689 assert_eq!(
2690 layout.left,
2691 [
2692 Some(WindowButton::Close),
2693 Some(WindowButton::Minimize),
2694 Some(WindowButton::Maximize)
2695 ]
2696 );
2697 assert_eq!(layout.right, [None, None, None]);
2698 }
2699
2700 #[test]
2701 fn test_window_button_layout_parse_with_whitespace() {
2702 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2703 assert_eq!(
2704 layout.left,
2705 [
2706 Some(WindowButton::Close),
2707 Some(WindowButton::Minimize),
2708 None
2709 ]
2710 );
2711 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2712 }
2713
2714 #[test]
2715 fn test_window_button_layout_parse_empty() {
2716 let layout = WindowButtonLayout::parse("").unwrap();
2717 assert_eq!(layout.left, [None, None, None]);
2718 assert_eq!(layout.right, [None, None, None]);
2719 }
2720
2721 #[test]
2722 fn test_window_button_layout_parse_intentionally_empty() {
2723 let layout = WindowButtonLayout::parse(":").unwrap();
2724 assert_eq!(layout.left, [None, None, None]);
2725 assert_eq!(layout.right, [None, None, None]);
2726 }
2727
2728 #[test]
2729 fn test_window_button_layout_parse_invalid_buttons() {
2730 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2731 assert_eq!(
2732 layout.left,
2733 [
2734 Some(WindowButton::Close),
2735 Some(WindowButton::Minimize),
2736 None
2737 ]
2738 );
2739 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2740 }
2741
2742 #[test]
2743 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2744 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2745 assert_eq!(
2746 layout.right,
2747 [
2748 Some(WindowButton::Close),
2749 Some(WindowButton::Minimize),
2750 None
2751 ]
2752 );
2753 assert_eq!(layout.format(), ":close,minimize");
2754 }
2755
2756 #[test]
2757 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2758 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2759 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2760 assert_eq!(
2761 layout.right,
2762 [
2763 Some(WindowButton::Maximize),
2764 Some(WindowButton::Minimize),
2765 None
2766 ]
2767 );
2768
2769 let button_ids: Vec<_> = layout
2770 .left
2771 .iter()
2772 .chain(layout.right.iter())
2773 .flatten()
2774 .map(WindowButton::id)
2775 .collect();
2776 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2777 assert_eq!(unique_button_ids.len(), button_ids.len());
2778 assert_eq!(layout.format(), "close:maximize,minimize");
2779 }
2780
2781 #[test]
2782 fn test_window_button_layout_parse_gnome_style() {
2783 let layout = WindowButtonLayout::parse("close").unwrap();
2784 assert_eq!(layout.left, [None, None, None]);
2785 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2786 }
2787
2788 #[test]
2789 fn test_window_button_layout_parse_elementary_style() {
2790 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2791 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2792 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2793 }
2794
2795 #[test]
2796 fn test_window_button_layout_round_trip() {
2797 let cases = [
2798 "close:minimize,maximize",
2799 "minimize,maximize,close:",
2800 ":close",
2801 "close:",
2802 "close:maximize",
2803 ":",
2804 ];
2805
2806 for case in cases {
2807 let layout = WindowButtonLayout::parse(case).unwrap();
2808 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2809 }
2810 }
2811
2812 #[test]
2813 fn test_window_button_layout_linux_default() {
2814 let layout = WindowButtonLayout::linux_default();
2815 assert_eq!(layout.left, [None, None, None]);
2816 assert_eq!(
2817 layout.right,
2818 [
2819 Some(WindowButton::Minimize),
2820 Some(WindowButton::Maximize),
2821 Some(WindowButton::Close)
2822 ]
2823 );
2824
2825 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2826 assert_eq!(round_tripped, layout);
2827 }
2828
2829 #[test]
2830 fn test_window_button_layout_parse_all_invalid() {
2831 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2832 }
2833}