1mod color_presentation;
2pub use color_presentation::{WindowColorFallback, WindowColorMode, WindowColorStatus};
3
4mod desktop_preferences;
5pub use desktop_preferences::{DesktopPreferences, TitlebarAction};
6
7mod app_menu;
8mod keyboard;
9mod keystroke;
10mod platform_info;
11
12#[cfg(all(target_os = "linux", feature = "wayland"))]
13#[expect(missing_docs)]
14pub mod layer_shell;
15
16pub mod popup;
18
19#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
20mod threaded_dispatcher;
21
22#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
23mod test;
24
25#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
26mod visual_test;
27
28#[cfg(all(
29 feature = "screen-capture",
30 any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
31))]
32pub mod scap_screen_capture;
33
34#[cfg(all(
35 any(target_os = "windows", target_os = "linux"),
36 feature = "screen-capture"
37))]
38pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
39#[cfg(not(feature = "screen-capture"))]
40pub(crate) type PlatformScreenCaptureFrame = ();
41#[cfg(all(target_os = "macos", feature = "screen-capture"))]
42pub(crate) type PlatformScreenCaptureFrame =
43 objc2_core_foundation::CFRetained<objc2_core_video::CVImageBuffer>;
44
45use crate::{
46 Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, Corners,
47 DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Edges, ExternalDragPayload, Font,
48 FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap,
49 LineLayout, MissingGlyphSink, Pixels, PlatformGestures, PlatformInput, Point, Priority,
50 RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
51 ShapedRun, SharedString, Size, SvgRenderer, SystemWindowTab, Task, Window, WindowControlArea,
52 hash, point, px, size,
53};
54#[cfg(any(
55 test,
56 feature = "test-support",
57 target_os = "linux",
58 target_os = "freebsd"
59))]
60use anyhow::bail;
61use anyhow::{Context as _, Result};
62use async_task::Runnable;
63use collections::FxHashMap;
64use futures::channel::oneshot;
65#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
66use image::RgbaImage;
67use image::codecs::gif::GifDecoder;
68use image::{AnimationDecoder as _, DynamicImage, Frame};
69use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
70use scheduler::Instant;
71pub use scheduler::RunnableMeta;
72use schemars::JsonSchema;
73use seahash::SeaHasher;
74use serde::{Deserialize, Serialize};
75use smallvec::SmallVec;
76use std::borrow::Cow;
77use std::collections::hash_map::Entry;
78use std::hash::{Hash, Hasher};
79use std::io::Cursor;
80use std::ops;
81use std::time::Duration;
82use std::{
83 ffi::OsString,
84 fmt::{self, Debug},
85 ops::Range,
86 path::{Path, PathBuf},
87 rc::Rc,
88 sync::Arc,
89};
90use strum::EnumIter;
91use uuid::Uuid;
92
93pub use app_menu::*;
94pub use keyboard::*;
95pub use keystroke::*;
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum WindowVisibility {
119 Visible,
122 Hidden,
125}
126
127impl WindowVisibility {
128 pub fn is_visible(self) -> bool {
130 self == Self::Visible
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137#[non_exhaustive]
138pub enum DisplayEvent {
139 Added(DisplayId),
141 Removed(DisplayId),
144 Changed {
146 id: DisplayId,
148 changes: DisplayChanges,
150 },
151}
152
153bitflags::bitflags! {
154 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156 pub struct DisplayChanges: u8 {
157 const BOUNDS = 1 << 0;
159 const REFRESH_INTERVAL = 1 << 1;
161 }
162}
163
164pub fn refresh_interval_from_hz(hertz: f64) -> Option<Duration> {
167 (hertz.is_finite() && hertz > 0.0).then(|| Duration::from_secs_f64(1.0 / hertz))
168}
169
170#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
177pub enum ActivationPolicy {
178 #[default]
180 Regular,
181 Accessory,
183}
184
185#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
186pub(crate) use test::*;
187
188#[cfg(any(test, feature = "test-support"))]
189pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
190
191#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
192pub use threaded_dispatcher::ThreadedDispatcher;
193
194#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
195pub use visual_test::VisualTestPlatform;
196
197pub struct ActivityGuard {
199 _release: gpui_util::Deferred<Box<dyn FnOnce() + Send>>,
200}
201
202impl ActivityGuard {
203 pub fn new(release: impl FnOnce() + Send + 'static) -> Self {
205 Self {
206 _release: gpui_util::defer(Box::new(release)),
207 }
208 }
209
210 pub fn noop() -> Self {
212 Self::new(|| {})
213 }
214}
215
216#[cfg(any(target_os = "linux", target_os = "freebsd"))]
220#[inline]
221pub fn guess_compositor() -> &'static str {
222 if std::env::var_os("ZED_HEADLESS").is_some() {
223 return "Headless";
224 }
225 GraphicalEnvironment::detect().guess_compositor()
226}
227
228#[cfg(any(target_os = "linux", target_os = "freebsd"))]
237#[derive(Clone, Debug, Default)]
238pub struct GraphicalEnvironment {
239 pub wayland_display: Option<OsString>,
241 pub x11_display: Option<OsString>,
243 pub xdg_runtime_dir: Option<OsString>,
245 pub activation_token: Option<String>,
251}
252
253#[cfg(target_os = "windows")]
259#[derive(Clone, Debug, Default)]
260pub struct GraphicalEnvironment {
261 pub session_id: Option<u32>,
264}
265
266#[cfg(target_os = "windows")]
267impl GraphicalEnvironment {
268 pub fn detect() -> Self {
270 let mut session_id = 0;
271 let result = unsafe {
273 windows::Win32::System::RemoteDesktop::ProcessIdToSessionId(
274 windows::Win32::System::Threading::GetCurrentProcessId(),
275 &mut session_id,
276 )
277 };
278 Self {
279 session_id: result.is_ok().then_some(session_id),
280 }
281 }
282
283 pub fn apply_to(&self, _command: &mut std::process::Command) {}
286}
287
288#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
290#[derive(Clone, Debug, Default)]
291pub struct GraphicalEnvironment;
292
293#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
294impl GraphicalEnvironment {
295 pub fn detect() -> Self {
297 Self
298 }
299
300 pub fn apply_to(&self, _command: &mut std::process::Command) {}
302}
303
304#[derive(Clone, Debug)]
306pub enum WindowingRequest {
307 Headless,
309 Windowed(GraphicalEnvironment),
311}
312
313#[cfg(any(target_os = "linux", target_os = "freebsd"))]
314impl GraphicalEnvironment {
315 pub fn detect() -> Self {
320 Self {
321 wayland_display: std::env::var_os("WAYLAND_DISPLAY"),
322 x11_display: std::env::var_os("DISPLAY"),
323 xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR"),
324 activation_token: None,
325 }
326 }
327
328 pub fn apply_to(&self, command: &mut std::process::Command) {
332 for (name, value) in [
333 ("WAYLAND_DISPLAY", &self.wayland_display),
334 ("DISPLAY", &self.x11_display),
335 ("XDG_RUNTIME_DIR", &self.xdg_runtime_dir),
336 ] {
337 match value {
338 Some(value) => command.env(name, value),
339 None => command.env_remove(name),
340 };
341 }
342 }
343
344 pub fn guess_compositor(&self) -> &'static str {
348 let is_set =
349 |value: &Option<OsString>| value.as_ref().is_some_and(|value| !value.is_empty());
350 if cfg!(feature = "wayland") && is_set(&self.wayland_display) {
351 "Wayland"
352 } else if cfg!(feature = "x11") && is_set(&self.x11_display) {
353 "X11"
354 } else {
355 "Headless"
356 }
357 }
358}
359
360#[cfg(any(target_os = "linux", target_os = "freebsd"))]
361bitflags::bitflags! {
362 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
364 pub struct WindowingModes: u8 {
365 const WAYLAND = 1 << 0;
367 const X11 = 1 << 1;
369 const HEADLESS = 1 << 2;
371 }
372}
373
374pub use platform_info::{OperatingSystem, PlatformInfo, PlatformTarget};
375
376#[expect(missing_docs)]
377pub trait Platform: 'static {
378 fn platform_info(&self) -> PlatformInfo;
380
381 fn background_executor(&self) -> BackgroundExecutor;
382 fn foreground_executor(&self) -> ForegroundExecutor;
383 fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
384
385 fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
386 fn quit(&self);
387 fn set_initial_windowing(&self, _request: WindowingRequest) {}
392 fn request_windowing(&self, _request: WindowingRequest) -> Task<anyhow::Result<()>> {
393 Task::ready(Err(anyhow::anyhow!(
394 "this platform cannot switch between headless and windowed modes"
395 )))
396 }
397 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<OsString>);
398 fn activate(&self, ignoring_other_apps: bool);
399 fn set_activation_policy(&self, _policy: ActivationPolicy) {}
401 fn hide(&self);
402 fn hide_other_apps(&self);
403 fn unhide_other_apps(&self);
404
405 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
406 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
407 fn on_displays_changed(&self, callback: Box<dyn FnMut()>);
412 fn active_window(&self) -> Option<AnyWindowHandle>;
413 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
414 None
415 }
416
417 fn is_screen_capture_supported(&self) -> bool {
418 false
419 }
420
421 fn screen_capture_sources(
422 &self,
423 ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
424 let (sources_tx, sources_rx) = oneshot::channel();
425 sources_tx
426 .send(Err(anyhow::anyhow!(
427 "gpui was compiled without the screen-capture feature"
428 )))
429 .ok();
430 sources_rx
431 }
432
433 fn open_window(
434 &self,
435 handle: AnyWindowHandle,
436 options: WindowParams,
437 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
438
439 fn window_appearance(&self) -> WindowAppearance;
441
442 fn set_window_appearance(&self, _appearance: Option<WindowAppearance>) {}
451
452 fn button_layout(&self) -> Option<WindowButtonLayout> {
454 None
455 }
456
457 fn desktop_preferences(&self) -> DesktopPreferences {
459 DesktopPreferences::default()
460 }
461
462 fn on_desktop_preferences_changed(&self, _callback: Box<dyn FnMut()>) {}
465
466 fn open_url(&self, url: &str);
467 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
468 fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
469
470 fn prompt_for_paths(
471 &self,
472 options: PathPromptOptions,
473 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
474 fn prompt_for_new_path(
475 &self,
476 directory: &Path,
477 suggested_name: Option<&str>,
478 ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
479 fn can_select_mixed_files_and_dirs(&self) -> bool;
480 fn reveal_path(&self, path: &Path);
481 fn open_with_system(&self, path: &Path);
482
483 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>);
484 fn on_reopen(&self, callback: Box<dyn FnMut()>);
485 fn on_system_sleep(&self, callback: Box<dyn FnMut()>);
487 fn on_system_wake(&self, callback: Box<dyn FnMut()>);
489
490 fn on_app_lifecycle(&self, _callback: Box<dyn FnMut(AppLifecyclePhase)>) {}
500
501 fn on_memory_warning(&self, _callback: Box<dyn FnMut()>) {}
506
507 fn gestures(&self) -> Option<Rc<dyn PlatformGestures>> {
511 None
512 }
513
514 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
515 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
516 None
517 }
518
519 fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
520 fn perform_dock_menu_action(&self, _action: usize) {}
521 fn add_recent_document(&self, _path: &Path) {}
522 fn update_jump_list(
523 &self,
524 _menus: Vec<MenuItem>,
525 _entries: Vec<SmallVec<[PathBuf; 2]>>,
526 ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
527 Task::ready(Vec::new())
528 }
529 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
530 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
531 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
532
533 fn thermal_state(&self) -> ThermalState;
534 fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
535 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>>;
536
537 fn power_mode(&self) -> PowerMode {
540 PowerMode::Unknown
541 }
542 fn on_power_mode_change(&self, _callback: Box<dyn FnMut()>) {}
544
545 fn set_app_identity(&self, identifier: &str, name: &str) {
552 _ = (identifier, name);
553 }
554
555 fn show_system_notification(&self, notification: SystemNotification) {
562 _ = notification;
563 }
564
565 fn dismiss_system_notification(&self, tag: &str) {
570 _ = tag;
571 }
572
573 fn on_system_notification_response(
579 &self,
580 callback: Box<dyn FnMut(SystemNotificationResponse)>,
581 ) {
582 _ = callback;
583 }
584
585 fn compositor_name(&self) -> &'static str {
586 ""
587 }
588 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
591 None
592 }
593 fn app_path(&self) -> Result<PathBuf>;
594 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
595
596 fn set_cursor_style(&self, style: CursorStyle);
597
598 fn hide_cursor_until_mouse_moves(&self);
601
602 fn is_cursor_visible(&self) -> bool;
604
605 fn should_auto_hide_scrollbars(&self) -> bool;
606
607 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
608 fn write_to_clipboard(&self, item: ClipboardItem);
609
610 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
618 Task::ready(Ok(self.read_from_clipboard()))
619 }
620
621 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
622 fn read_from_primary(&self) -> Option<ClipboardItem>;
623 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
624 fn write_to_primary(&self, item: ClipboardItem);
625
626 #[cfg(target_os = "macos")]
627 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
628 #[cfg(target_os = "macos")]
629 fn write_to_find_pasteboard(&self, item: ClipboardItem);
630
631 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
632
633 #[cfg(target_os = "macos")]
637 fn write_credentials_if_not_exists(
638 &self,
639 _url: &str,
640 _username: &str,
641 _password: &[u8],
642 ) -> Task<Result<bool>> {
643 Task::ready(Err(anyhow::anyhow!(
644 "this platform does not support atomic credential writes"
645 )))
646 }
647
648 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
649 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
650
651 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
652 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
653 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
654}
655
656pub trait PlatformDisplay: Debug {
658 fn id(&self) -> DisplayId;
660
661 fn uuid(&self) -> Result<Uuid>;
664
665 fn bounds(&self) -> Bounds<Pixels>;
667
668 fn refresh_interval(&self) -> Option<Duration>;
675
676 fn visible_bounds(&self) -> Bounds<Pixels> {
680 self.bounds()
681 }
682
683 fn default_bounds(&self) -> Bounds<Pixels> {
685 let bounds = self.bounds();
686 let center = bounds.center();
687 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
688
689 let offset = clipped_window_size / 2.0;
690 let origin = point(center.x - offset.width, center.y - offset.height);
691 Bounds::new(origin, clipped_window_size)
692 }
693}
694
695#[derive(Clone, Debug, PartialEq, Eq)]
698pub struct SystemNotification {
699 pub tag: SharedString,
703 pub title: SharedString,
705 pub body: SharedString,
707 pub actions: Vec<SystemNotificationAction>,
710}
711
712#[derive(Clone, Debug, PartialEq, Eq, Hash)]
714pub struct SystemNotificationAction {
715 pub id: SharedString,
718 pub label: SharedString,
720}
721
722#[derive(Clone, Debug, PartialEq, Eq)]
724pub struct SystemNotificationResponse {
725 pub tag: SharedString,
727 pub action_id: Option<SharedString>,
730}
731
732#[derive(Debug, Clone, Copy, PartialEq, Eq)]
734pub enum PowerMode {
735 Unknown,
738 LowPower,
740 Balanced,
742 HighPower,
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
748pub enum ThermalState {
749 Nominal,
751 Fair,
753 Serious,
755 Critical,
757}
758
759#[derive(Clone)]
761pub struct SourceMetadata {
762 pub id: u64,
764 pub label: Option<SharedString>,
766 pub is_main: Option<bool>,
768 pub resolution: Size<DevicePixels>,
770}
771
772pub trait ScreenCaptureSource {
774 fn metadata(&self) -> Result<SourceMetadata>;
776
777 fn stream(
780 &self,
781 foreground_executor: &ForegroundExecutor,
782 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
783 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
784}
785
786pub trait ScreenCaptureStream {
788 fn metadata(&self) -> Result<SourceMetadata>;
790}
791
792pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
794
795#[derive(PartialEq, Eq, Hash, Copy, Clone)]
797pub struct DisplayId(pub(crate) u64);
798
799impl DisplayId {
800 pub fn new(id: u64) -> Self {
802 Self(id)
803 }
804}
805
806impl From<u64> for DisplayId {
807 fn from(id: u64) -> Self {
808 Self(id)
809 }
810}
811
812impl From<DisplayId> for u64 {
813 fn from(id: DisplayId) -> Self {
814 id.0
815 }
816}
817
818impl Debug for DisplayId {
819 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
820 write!(f, "DisplayId({})", self.0)
821 }
822}
823
824#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub enum ResizeEdge {
827 Top,
829 TopRight,
831 Right,
833 BottomRight,
835 Bottom,
837 BottomLeft,
839 Left,
841 TopLeft,
843}
844
845#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
847pub enum WindowDecorations {
848 #[default]
849 Server,
851 Client,
853}
854
855#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
857pub enum Decorations {
858 #[default]
860 Server,
861 Client {
863 tiling: Tiling,
865 },
866}
867
868#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
870pub struct WindowControls {
871 pub fullscreen: bool,
873 pub maximize: bool,
875 pub minimize: bool,
877 pub window_menu: bool,
879}
880
881impl Default for WindowControls {
882 fn default() -> Self {
883 Self {
885 fullscreen: true,
886 maximize: true,
887 minimize: true,
888 window_menu: true,
889 }
890 }
891}
892
893#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
895pub enum WindowButton {
896 Minimize,
898 Maximize,
900 Close,
902}
903
904impl WindowButton {
905 pub fn id(&self) -> &'static str {
907 match self {
908 WindowButton::Minimize => "minimize",
909 WindowButton::Maximize => "maximize",
910 WindowButton::Close => "close",
911 }
912 }
913
914 #[cfg(any(
915 test,
916 feature = "test-support",
917 target_os = "linux",
918 target_os = "freebsd"
919 ))]
920 fn index(&self) -> usize {
921 match self {
922 WindowButton::Minimize => 0,
923 WindowButton::Maximize => 1,
924 WindowButton::Close => 2,
925 }
926 }
927}
928
929pub const MAX_BUTTONS_PER_SIDE: usize = 3;
931
932#[derive(Debug, Clone, Copy, PartialEq, Eq)]
937pub struct WindowButtonLayout {
938 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
940 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
942}
943
944#[cfg(any(
945 test,
946 feature = "test-support",
947 target_os = "linux",
948 target_os = "freebsd"
949))]
950impl WindowButtonLayout {
951 pub fn linux_default() -> Self {
953 Self {
954 left: [None; MAX_BUTTONS_PER_SIDE],
955 right: [
956 Some(WindowButton::Minimize),
957 Some(WindowButton::Maximize),
958 Some(WindowButton::Close),
959 ],
960 }
961 }
962
963 pub fn parse(layout_string: &str) -> Result<Self> {
965 fn parse_side(
966 s: &str,
967 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
968 unrecognized: &mut Vec<String>,
969 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
970 let mut result = [None; MAX_BUTTONS_PER_SIDE];
971 let mut i = 0;
972 for name in s.split(',') {
973 let trimmed = name.trim();
974 if trimmed.is_empty() {
975 continue;
976 }
977 let button = match trimmed {
978 "minimize" => Some(WindowButton::Minimize),
979 "maximize" => Some(WindowButton::Maximize),
980 "close" => Some(WindowButton::Close),
981 other => {
982 unrecognized.push(other.to_string());
983 None
984 }
985 };
986 if let Some(button) = button {
987 if seen_buttons[button.index()] {
988 continue;
989 }
990 if let Some(slot) = result.get_mut(i) {
991 *slot = Some(button);
992 seen_buttons[button.index()] = true;
993 i += 1;
994 }
995 }
996 }
997 result
998 }
999
1000 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
1001 let mut unrecognized = Vec::new();
1002 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
1003 let layout = Self {
1004 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
1005 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
1006 };
1007
1008 if !unrecognized.is_empty()
1009 && layout.left.iter().all(Option::is_none)
1010 && layout.right.iter().all(Option::is_none)
1011 {
1012 bail!(
1013 "button layout string {:?} contains no valid buttons (unrecognized: {})",
1014 layout_string,
1015 unrecognized.join(", ")
1016 );
1017 }
1018
1019 Ok(layout)
1020 }
1021
1022 #[cfg(test)]
1024 pub fn format(&self) -> String {
1025 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
1026 buttons
1027 .iter()
1028 .flatten()
1029 .map(|button| match button {
1030 WindowButton::Minimize => "minimize",
1031 WindowButton::Maximize => "maximize",
1032 WindowButton::Close => "close",
1033 })
1034 .collect::<Vec<_>>()
1035 .join(",")
1036 }
1037
1038 format!("{}:{}", format_side(&self.left), format_side(&self.right))
1039 }
1040}
1041
1042#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1044pub struct Tiling {
1045 pub top: bool,
1047 pub left: bool,
1049 pub right: bool,
1051 pub bottom: bool,
1053}
1054
1055impl Tiling {
1056 pub fn tiled() -> Self {
1058 Self {
1059 top: true,
1060 left: true,
1061 right: true,
1062 bottom: true,
1063 }
1064 }
1065
1066 pub fn is_tiled(&self) -> bool {
1068 self.top || self.left || self.right || self.bottom
1069 }
1070}
1071
1072pub struct A11yCallbacks {
1074 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1076 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1078 pub deactivation: Box<dyn Fn() + Send + 'static>,
1080}
1081
1082#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1084pub enum FrameRequestSource {
1085 #[default]
1087 NativeCallback,
1088 LocalSchedule,
1090}
1091
1092#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1093#[expect(missing_docs)]
1094pub struct RequestFrameOptions {
1095 pub require_presentation: bool,
1097 pub force_render: bool,
1099 pub signal_at: Option<Instant>,
1105 pub signal_source: FrameRequestSource,
1107}
1108
1109pub struct PlatformFrameSignal {
1118 #[cfg(feature = "profiler")]
1119 origin: Instant,
1120 #[cfg(feature = "profiler")]
1121 first_signal: std::sync::atomic::AtomicU64,
1122}
1123
1124impl Default for PlatformFrameSignal {
1125 fn default() -> Self {
1126 Self::new()
1127 }
1128}
1129
1130impl PlatformFrameSignal {
1131 pub fn new() -> Self {
1133 Self {
1134 #[cfg(feature = "profiler")]
1135 origin: Instant::now(),
1136 #[cfg(feature = "profiler")]
1137 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1138 }
1139 }
1140
1141 #[inline]
1145 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1146 if cfg!(feature = "profiler") {
1147 Some(capture())
1148 } else {
1149 None
1150 }
1151 }
1152
1153 #[inline]
1155 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1156 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1157 #[cfg(feature = "profiler")]
1158 {
1159 let nanoseconds = at
1160 .saturating_duration_since(self.origin)
1161 .as_nanos()
1162 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1163 let source_bit = match source {
1164 FrameRequestSource::NativeCallback => 0,
1165 FrameRequestSource::LocalSchedule => 1,
1166 };
1167 self.first_signal.fetch_min(
1168 (nanoseconds << 1) | source_bit,
1169 std::sync::atomic::Ordering::Relaxed,
1170 );
1171 }
1172 }
1173
1174 #[inline]
1176 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1177 #[cfg(feature = "profiler")]
1178 {
1179 let signal = self
1180 .first_signal
1181 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1182 (signal != u64::MAX).then(|| {
1183 let source = match signal & 1 {
1184 0 => FrameRequestSource::NativeCallback,
1185 _ => FrameRequestSource::LocalSchedule,
1186 };
1187 (self.origin + Duration::from_nanos(signal >> 1), source)
1188 })
1189 }
1190 #[cfg(not(feature = "profiler"))]
1191 {
1192 None
1193 }
1194 }
1195}
1196
1197#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1210pub enum AppLifecyclePhase {
1211 Active,
1213 Inactive,
1215 Background,
1218 Foreground,
1220 Disconnected,
1224}
1225
1226#[derive(Debug, Clone, Default, PartialEq)]
1233pub struct WindowInsets {
1234 pub safe_area: Edges<Pixels>,
1239 pub ime: Edges<Pixels>,
1243 pub corner_radii: Corners<Pixels>,
1248}
1249
1250impl WindowInsets {
1251 pub fn effective(&self) -> Edges<Pixels> {
1253 Edges {
1254 top: self.safe_area.top.max(self.ime.top),
1255 right: self.safe_area.right.max(self.ime.right),
1256 bottom: self.safe_area.bottom.max(self.ime.bottom),
1257 left: self.safe_area.left.max(self.ime.left),
1258 }
1259 }
1260}
1261
1262#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1264pub enum TextInputStateChange {
1265 FocusGained,
1267 FocusLost,
1269 SelectionChanged,
1271 ContentChanged,
1273}
1274
1275#[expect(missing_docs)]
1276pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1277 fn set_color_mode(&mut self, mode: WindowColorMode) -> anyhow::Result<WindowColorStatus> {
1279 match mode {
1280 WindowColorMode::Default => Ok(self.color_status()),
1281 _ => anyhow::bail!("wide-gamut SDR presentation is unsupported"),
1282 }
1283 }
1284 fn color_status(&self) -> WindowColorStatus {
1286 WindowColorStatus::default()
1287 }
1288
1289 fn bounds(&self) -> Bounds<Pixels>;
1290 fn is_maximized(&self) -> bool;
1291 fn window_bounds(&self) -> WindowBounds;
1292 fn content_size(&self) -> Size<Pixels>;
1293 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1299 Bounds::new(Point::default(), self.content_size())
1300 }
1301 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1306 fn prepare_frame(&self) -> bool {
1311 false
1312 }
1313 fn resize(&mut self, size: Size<Pixels>);
1314 fn scale_factor(&self) -> f32;
1315 fn appearance(&self) -> WindowAppearance;
1316 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1317 fn mouse_position(&self) -> Point<Pixels>;
1318 fn modifiers(&self) -> Modifiers;
1319 fn capslock(&self) -> Capslock;
1320 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1321 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1322 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1327 fn prompt(
1328 &self,
1329 level: PromptLevel,
1330 msg: &str,
1331 detail: Option<&str>,
1332 answers: &[PromptButton],
1333 ) -> Option<oneshot::Receiver<usize>>;
1334 fn activate(&self);
1335 fn request_attention(&self) {}
1337 fn is_active(&self) -> bool;
1338 fn visibility(&self) -> WindowVisibility;
1341 fn is_hovered(&self) -> bool;
1342 fn background_appearance(&self) -> WindowBackgroundAppearance;
1343 fn set_title(&mut self, title: &str);
1344 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1345 fn minimize(&self);
1346 fn zoom(&self);
1347 fn toggle_fullscreen(&self);
1348 fn is_fullscreen(&self) -> bool;
1349 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1350 None
1351 }
1352 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1353 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1354 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1355 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1359 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1360 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1361 fn on_moved(&self, callback: Box<dyn FnMut()>);
1362 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1366 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1367 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1368 fn on_close(&self, callback: Box<dyn FnOnce()>);
1369 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1370 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1371 fn draw(&self, scene: &Scene);
1372 #[cfg(any(test, feature = "test-support"))]
1374 fn submit_scene_for_validation(&self, _scene: &Scene) -> anyhow::Result<()> {
1375 anyhow::bail!("native window ownership validation is unsupported on this backend")
1376 }
1377 fn schedule_frame(&self) {}
1378 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1379 fn is_subpixel_rendering_supported(&self) -> bool;
1380
1381 fn get_title(&self) -> String {
1383 String::new()
1384 }
1385 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1386 None
1387 }
1388 fn tab_bar_visible(&self) -> bool {
1389 false
1390 }
1391 fn set_edited(&mut self, _edited: bool) {}
1392 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1393 fn toggle_simple_fullscreen(&self) {}
1394 fn is_simple_fullscreen(&self) -> bool {
1395 false
1396 }
1397 #[cfg(target_os = "macos")]
1398 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1399 fn show_character_palette(&self) {}
1400 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1401 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1402 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1403 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1404 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1405 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1406 fn merge_all_windows(&self) {}
1407 fn move_tab_to_new_window(&self) {}
1408 fn toggle_window_tab_overview(&self) {}
1409 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1410
1411 fn native_window_state(&self) -> Option<Vec<u8>> {
1412 None
1413 }
1414 fn restore_native_window_state(&self, _state: &[u8]) {}
1415
1416 #[cfg(target_os = "windows")]
1417 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1418
1419 fn inner_window_bounds(&self) -> WindowBounds {
1421 self.window_bounds()
1422 }
1423 fn request_decorations(&self, _decorations: WindowDecorations) {}
1424 fn show_window_menu(&self, _position: Point<Pixels>) {}
1425 fn start_window_move(&self) {}
1426 fn can_start_external_drag(&self) -> bool {
1427 false
1428 }
1429 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1430 false
1431 }
1432 fn start_window_resize(&self, _edge: ResizeEdge) {}
1433 fn set_exclusive_zone(&self, _zone: Pixels) {}
1434 #[cfg(all(target_os = "linux", feature = "wayland"))]
1435 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1436 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1437 fn window_decorations(&self) -> Decorations {
1438 Decorations::Server
1439 }
1440 fn set_app_id(&mut self, _app_id: &str) {}
1441 fn map_window(&mut self) -> anyhow::Result<()> {
1442 Ok(())
1443 }
1444 fn window_controls(&self) -> WindowControls {
1445 WindowControls::default()
1446 }
1447 fn set_client_inset(&self, _inset: Pixels) {}
1448 fn gpu_specs(&self) -> Option<GpuSpecs>;
1449
1450 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1451
1452 fn insets(&self) -> WindowInsets {
1457 WindowInsets::default()
1458 }
1459
1460 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1466
1467 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1470
1471 fn set_back_enabled(&self, _enabled: bool) {}
1474
1475 fn show_soft_keyboard(&self) {}
1477
1478 fn hide_soft_keyboard(&self) {}
1480
1481 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1483
1484 fn play_system_bell(&self) {}
1485
1486 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1488
1489 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1491
1492 fn a11y_update_window_bounds(&self) {}
1494
1495 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1496 fn as_test(&mut self) -> Option<&mut TestWindow> {
1497 None
1498 }
1499
1500 #[cfg(any(test, feature = "test-support"))]
1503 fn render_to_linear_p3(&self, _scene: &Scene) -> Result<LinearP3Image> {
1504 anyhow::bail!("linear P3 readback is unsupported on this backend")
1505 }
1506
1507 #[cfg(any(test, feature = "test-support"))]
1511 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1512 anyhow::bail!("render_to_image not implemented for this platform")
1513 }
1514}
1515
1516#[cfg(any(test, feature = "test-support"))]
1520#[derive(Debug)]
1521pub struct LinearP3Image {
1522 pub size: Size<DevicePixels>,
1524 pub pixels: Vec<[u16; 4]>,
1526}
1527
1528#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1530pub trait PlatformHeadlessRenderer {
1531 fn render_scene_to_image(
1533 &mut self,
1534 scene: &Scene,
1535 size: Size<DevicePixels>,
1536 ) -> Result<RgbaImage>;
1537
1538 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1544
1545 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1547}
1548
1549#[doc(hidden)]
1552pub type RunnableVariant = Runnable<RunnableMeta>;
1553
1554#[doc(hidden)]
1555pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1556
1557#[doc(hidden)]
1558pub enum TasksIncluded {
1559 OnlyCompleted,
1560 CompletedAndRunning,
1561}
1562
1563#[doc(hidden)]
1566pub trait PlatformDispatcher: Send + Sync {
1567 fn is_main_thread(&self) -> bool;
1568 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1569 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1570 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1571
1572 fn dispatch_on_main_thread_when_idle(
1573 &self,
1574 runnable: RunnableVariant,
1575 timeout: Option<Duration>,
1576 ) {
1577 let _ = timeout;
1578 self.dispatch_on_main_thread(runnable, Priority::Low);
1579 }
1580
1581 fn idle_time_remaining(&self) -> Option<Duration> {
1582 None
1583 }
1584
1585 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1586
1587 fn now(&self) -> Instant {
1588 Instant::now()
1589 }
1590
1591 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1592 gpui_util::defer(Box::new(|| {}))
1593 }
1594
1595 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1596 ActivityGuard::noop()
1597 }
1598
1599 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1600 fn as_test(&self) -> Option<&TestDispatcher> {
1601 None
1602 }
1603
1604 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1607 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1608 None
1609 }
1610}
1611
1612#[expect(missing_docs)]
1613pub trait PlatformTextSystem: Send + Sync {
1614 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1615 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1617 fn all_font_names(&self) -> Vec<String>;
1619 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1621 fn font_capabilities(&self) -> crate::FontCapabilities {
1623 crate::FontCapabilities::default()
1624 }
1625 fn font_table(
1634 &self,
1635 _font_id: FontId,
1636 _tag: crate::FontTag,
1637 ) -> Result<crate::FontQuery<Option<Vec<u8>>>> {
1638 Ok(crate::FontQuery::Unsupported)
1639 }
1640 fn font_axes(
1643 &self,
1644 font_id: FontId,
1645 ) -> Result<crate::FontQuery<Vec<crate::VariationAxisInfo>>> {
1646 crate::text_system::font_inspection::query_axes(|tag| self.font_table(font_id, tag))
1647 }
1648 fn opentype_features(
1653 &self,
1654 font_id: FontId,
1655 ) -> Result<crate::FontQuery<Vec<crate::OpenTypeFeatureInfo>>> {
1656 crate::text_system::font_inspection::query_features(|tag| self.font_table(font_id, tag))
1657 }
1658 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1660 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1662 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1664 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1666 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1668 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1670 fn rasterize_glyph(
1672 &self,
1673 params: &RenderGlyphParams,
1674 raster_bounds: Bounds<DevicePixels>,
1675 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1676 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1678 fn shape_paragraph(
1680 &self,
1681 _request: crate::ParagraphRequest<'_>,
1682 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1683 anyhow::bail!("paragraph layout is unsupported by this text backend")
1684 }
1685 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1687 -> TextRenderingMode;
1688 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1690 0
1691 }
1692}
1693
1694#[expect(missing_docs)]
1695pub struct NoopTextSystem;
1696
1697#[expect(missing_docs)]
1698impl NoopTextSystem {
1699 #[allow(dead_code)]
1700 pub fn new() -> Self {
1701 Self
1702 }
1703}
1704
1705impl PlatformTextSystem for NoopTextSystem {
1706 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1707 Ok(())
1708 }
1709
1710 fn all_font_names(&self) -> Vec<String> {
1711 Vec::new()
1712 }
1713
1714 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1715 Ok(FontId(1))
1716 }
1717
1718 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1719 FontMetrics {
1720 units_per_em: 1000,
1721 ascent: 1025.0,
1722 descent: -275.0,
1723 line_gap: 0.0,
1724 underline_position: -95.0,
1725 underline_thickness: 60.0,
1726 cap_height: 698.0,
1727 x_height: 516.0,
1728 bounding_box: Bounds {
1729 origin: Point {
1730 x: -260.0,
1731 y: -245.0,
1732 },
1733 size: Size {
1734 width: 1501.0,
1735 height: 1364.0,
1736 },
1737 },
1738 }
1739 }
1740
1741 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1742 Ok(Bounds {
1743 origin: Point { x: 54.0, y: 0.0 },
1744 size: size(392.0, 528.0),
1745 })
1746 }
1747
1748 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1749 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1750 }
1751
1752 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1753 Some(GlyphId(ch.len_utf16() as u32))
1754 }
1755
1756 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1757 Ok(Default::default())
1758 }
1759
1760 fn rasterize_glyph(
1761 &self,
1762 _params: &RenderGlyphParams,
1763 raster_bounds: Bounds<DevicePixels>,
1764 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1765 Ok((raster_bounds.size, Vec::new()))
1766 }
1767
1768 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1769 let mut position = px(0.);
1770 let metrics = self.font_metrics(FontId(0));
1771 let em_width = font_size
1772 * self
1773 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1774 .unwrap()
1775 .width
1776 / metrics.units_per_em as f32;
1777 let mut glyphs = Vec::new();
1778 for (ix, c) in text.char_indices() {
1779 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1780 glyphs.push(ShapedGlyph {
1781 id: glyph,
1782 position: point(position, px(0.)),
1783 index: ix,
1784 is_emoji: glyph.0 == 2,
1785 });
1786 if glyph.0 == 2 {
1787 position += em_width * 2.0;
1788 } else {
1789 position += em_width;
1790 }
1791 } else {
1792 position += em_width
1793 }
1794 }
1795 let mut runs = Vec::default();
1796 if !glyphs.is_empty() {
1797 runs.push(ShapedRun {
1798 font_id: FontId(0),
1799 glyphs,
1800 });
1801 } else {
1802 position = px(0.);
1803 }
1804
1805 LineLayout {
1806 font_size,
1807 width: position,
1808 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1809 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1810 runs,
1811 len: text.len(),
1812 }
1813 }
1814
1815 fn recommended_rendering_mode(
1816 &self,
1817 _font_id: FontId,
1818 _font_size: Pixels,
1819 ) -> TextRenderingMode {
1820 TextRenderingMode::Grayscale
1821 }
1822}
1823
1824#[allow(dead_code)]
1829pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1830 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1831 [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], ];
1845
1846 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1847 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1848
1849 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1850 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1851
1852 [
1853 ratios[0] * NORM13,
1854 ratios[1] * NORM24,
1855 ratios[2] * NORM13,
1856 ratios[3] * NORM24,
1857 ]
1858}
1859
1860#[derive(PartialEq, Eq, Hash, Clone)]
1861#[expect(missing_docs)]
1862pub enum AtlasKey {
1863 Glyph(RenderGlyphParams),
1864 Svg(RenderSvgParams),
1865 Image(RenderImageParams),
1866}
1867
1868impl AtlasKey {
1869 pub fn texture_kind(&self) -> AtlasTextureKind {
1871 match self {
1872 AtlasKey::Glyph(params) => {
1873 if params.is_emoji {
1874 AtlasTextureKind::Polychrome
1875 } else if params.subpixel_rendering {
1876 AtlasTextureKind::Subpixel
1877 } else {
1878 AtlasTextureKind::Monochrome
1879 }
1880 }
1881 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1882 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1883 }
1884 }
1885}
1886
1887impl From<RenderGlyphParams> for AtlasKey {
1888 fn from(params: RenderGlyphParams) -> Self {
1889 Self::Glyph(params)
1890 }
1891}
1892
1893impl From<RenderSvgParams> for AtlasKey {
1894 fn from(params: RenderSvgParams) -> Self {
1895 Self::Svg(params)
1896 }
1897}
1898
1899impl From<RenderImageParams> for AtlasKey {
1900 fn from(params: RenderImageParams) -> Self {
1901 Self::Image(params)
1902 }
1903}
1904
1905#[expect(missing_docs)]
1906pub trait PlatformAtlas {
1907 fn get_or_insert_with<'a>(
1909 &self,
1910 key: AtlasKey,
1911 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1912 ) -> Result<Option<AtlasTile>>;
1913 fn remove(&self, key: &AtlasKey);
1914
1915 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1916 fn contains(&self, _key: &AtlasKey) -> bool {
1917 false
1918 }
1919}
1920
1921#[doc(hidden)]
1922pub trait AtlasBackend {
1923 fn insert(
1924 &mut self,
1925 kind: AtlasTextureKind,
1926 size: Size<DevicePixels>,
1927 bytes: &[u8],
1928 ) -> Result<AtlasTile>;
1929
1930 fn remove(&mut self, tile: AtlasTile);
1931}
1932
1933#[doc(hidden)]
1934pub struct AtlasState<Backend> {
1935 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1936 pub backend: Backend,
1937}
1938
1939impl<Backend> AtlasState<Backend> {
1940 pub fn new(backend: Backend) -> Self {
1941 Self {
1942 tiles_by_key: FxHashMap::default(),
1943 backend,
1944 }
1945 }
1946
1947 pub fn contains(&self, key: &AtlasKey) -> bool {
1948 self.tiles_by_key.contains_key(key)
1949 }
1950
1951 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1952 self.tiles_by_key.clear();
1953 reset_backend(&mut self.backend);
1954 }
1955}
1956
1957impl<Backend: Default> Default for AtlasState<Backend> {
1958 fn default() -> Self {
1959 Self::new(Backend::default())
1960 }
1961}
1962
1963impl<Backend: AtlasBackend> AtlasState<Backend> {
1964 pub fn get_or_insert_with<'a>(
1965 &mut self,
1966 key: AtlasKey,
1967 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1968 ) -> Result<Option<AtlasTile>> {
1969 match self.tiles_by_key.entry(key) {
1970 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1971 Entry::Vacant(entry) => {
1972 profiling::scope!("new tile");
1973 let Some((size, bytes)) = build()? else {
1974 return Ok(None);
1975 };
1976 let tile = self
1977 .backend
1978 .insert(entry.key().texture_kind(), size, &bytes)?;
1979 entry.insert(tile);
1980 Ok(Some(tile))
1981 }
1982 }
1983 }
1984
1985 pub fn remove(&mut self, key: &AtlasKey) {
1986 if let Some(tile) = self.tiles_by_key.remove(key) {
1987 self.backend.remove(tile);
1988 }
1989 }
1990}
1991
1992#[derive(Default)]
1996pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
1997
1998#[doc(hidden)]
1999#[derive(Default)]
2000pub struct HeadlessAtlasBackend {
2001 next_id: u32,
2002}
2003
2004impl AtlasBackend for HeadlessAtlasBackend {
2005 fn insert(
2006 &mut self,
2007 kind: AtlasTextureKind,
2008 size: Size<DevicePixels>,
2009 _bytes: &[u8],
2010 ) -> Result<AtlasTile> {
2011 self.next_id += 1;
2012 let texture_id = self.next_id;
2013 self.next_id += 1;
2014 let tile_id = self.next_id;
2015 Ok(AtlasTile {
2016 texture_id: AtlasTextureId {
2017 index: texture_id,
2018 kind,
2019 },
2020 tile_id: TileId(tile_id),
2021 padding: 0,
2022 bounds: Bounds {
2023 origin: Point::default(),
2024 size,
2025 },
2026 })
2027 }
2028
2029 fn remove(&mut self, _tile: AtlasTile) {}
2030}
2031
2032impl PlatformAtlas for HeadlessAtlas {
2033 fn get_or_insert_with<'a>(
2034 &self,
2035 key: AtlasKey,
2036 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2037 ) -> Result<Option<AtlasTile>> {
2038 self.0.lock().get_or_insert_with(key, build)
2039 }
2040
2041 fn remove(&self, key: &AtlasKey) {
2042 self.0.lock().remove(key);
2043 }
2044
2045 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2046 fn contains(&self, key: &AtlasKey) -> bool {
2047 self.0.lock().contains(key)
2048 }
2049}
2050
2051#[doc(hidden)]
2052pub struct AtlasTextureList<T> {
2053 pub textures: Vec<Option<T>>,
2054 pub free_list: Vec<usize>,
2055}
2056
2057impl<T> Default for AtlasTextureList<T> {
2058 fn default() -> Self {
2059 Self {
2060 textures: Vec::default(),
2061 free_list: Vec::default(),
2062 }
2063 }
2064}
2065
2066impl<T> ops::Index<usize> for AtlasTextureList<T> {
2067 type Output = Option<T>;
2068
2069 fn index(&self, index: usize) -> &Self::Output {
2070 &self.textures[index]
2071 }
2072}
2073
2074impl<T> AtlasTextureList<T> {
2075 #[allow(unused)]
2076 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
2077 self.free_list.clear();
2078 self.textures.drain(..)
2079 }
2080
2081 #[allow(dead_code)]
2082 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
2083 self.textures.iter_mut().flatten()
2084 }
2085}
2086
2087#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2088#[repr(C)]
2089#[expect(missing_docs)]
2090pub struct AtlasTile {
2091 pub texture_id: AtlasTextureId,
2093 pub tile_id: TileId,
2095 pub padding: u32,
2097 pub bounds: Bounds<DevicePixels>,
2099}
2100
2101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2102#[repr(C)]
2103#[expect(missing_docs)]
2104pub struct AtlasTextureId {
2105 pub index: u32,
2108 pub kind: AtlasTextureKind,
2110}
2111
2112#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2113#[repr(C)]
2114#[cfg_attr(
2115 all(
2116 any(target_os = "linux", target_os = "freebsd"),
2117 not(any(feature = "x11", feature = "wayland"))
2118 ),
2119 allow(dead_code)
2120)]
2121#[expect(missing_docs)]
2122pub enum AtlasTextureKind {
2123 Monochrome = 0,
2124 Polychrome = 1,
2125 Subpixel = 2,
2126}
2127
2128#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2129#[repr(C)]
2130#[expect(missing_docs)]
2131pub struct TileId(pub u32);
2132
2133impl From<etagere::AllocId> for TileId {
2134 fn from(id: etagere::AllocId) -> Self {
2135 Self(id.serialize())
2136 }
2137}
2138
2139impl From<TileId> for etagere::AllocId {
2140 fn from(id: TileId) -> Self {
2141 Self::deserialize(id.0)
2142 }
2143}
2144
2145#[expect(missing_docs)]
2146pub struct PlatformInputHandler {
2147 cx: AsyncWindowContext,
2148 handler: Box<dyn InputHandler>,
2149 focus_id: Option<crate::FocusId>,
2150}
2151
2152#[expect(missing_docs)]
2153#[cfg_attr(
2154 all(
2155 any(target_os = "linux", target_os = "freebsd"),
2156 not(any(feature = "x11", feature = "wayland"))
2157 ),
2158 allow(dead_code)
2159)]
2160impl PlatformInputHandler {
2161 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2162 Self {
2163 cx,
2164 handler,
2165 focus_id: None,
2166 }
2167 }
2168
2169 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2170 self.focus_id = Some(focus_id);
2171 self
2172 }
2173
2174 pub fn focus_id(&self) -> Option<crate::FocusId> {
2179 self.focus_id
2180 }
2181
2182 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2183 self.cx
2184 .update(|window, cx| {
2185 self.handler
2186 .selected_text_range(ignore_disabled_input, window, cx)
2187 })
2188 .ok()
2189 .flatten()
2190 }
2191
2192 #[cfg_attr(target_os = "windows", allow(dead_code))]
2193 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2194 self.cx
2195 .update(|window, cx| self.handler.marked_text_range(window, cx))
2196 .ok()
2197 .flatten()
2198 }
2199
2200 #[cfg_attr(
2201 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2202 allow(dead_code)
2203 )]
2204 pub fn text_for_range(
2205 &mut self,
2206 range_utf16: Range<usize>,
2207 adjusted: &mut Option<Range<usize>>,
2208 ) -> Option<String> {
2209 self.cx
2210 .update(|window, cx| {
2211 self.handler
2212 .text_for_range(range_utf16, adjusted, window, cx)
2213 })
2214 .ok()
2215 .flatten()
2216 }
2217
2218 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2219 self.cx
2220 .update(|window, cx| {
2221 self.handler
2222 .replace_text_in_range(replacement_range, text, window, cx);
2223 })
2224 .ok();
2225 }
2226
2227 pub fn replace_and_mark_text_in_range(
2228 &mut self,
2229 range_utf16: Option<Range<usize>>,
2230 new_text: &str,
2231 new_selected_range: Option<Range<usize>>,
2232 ) {
2233 self.cx
2234 .update(|window, cx| {
2235 self.handler.replace_and_mark_text_in_range(
2236 range_utf16,
2237 new_text,
2238 new_selected_range,
2239 window,
2240 cx,
2241 )
2242 })
2243 .ok();
2244 }
2245
2246 #[cfg_attr(target_os = "windows", allow(dead_code))]
2247 pub fn unmark_text(&mut self) {
2248 self.cx
2249 .update(|window, cx| self.handler.unmark_text(window, cx))
2250 .ok();
2251 }
2252
2253 pub fn paste(&mut self, item: ClipboardItem) {
2254 self.cx
2255 .update(|window, cx| self.handler.paste(item, window, cx))
2256 .ok();
2257 }
2258
2259 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2260 self.cx
2261 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2262 .ok()
2263 .flatten()
2264 }
2265
2266 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2267 self.cx
2268 .update(|window, cx| {
2269 self.handler
2270 .selection_bounds_for_range(range_utf16, window, cx)
2271 })
2272 .unwrap_or_default()
2273 }
2274
2275 #[allow(dead_code)]
2276 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2277 self.handler.apple_press_and_hold_enabled()
2278 }
2279
2280 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2281 self.handler.replace_text_in_range(None, input, window, cx);
2282 }
2283
2284 pub fn compute_ime_candidate_bounds(
2285 marked_range: Option<Range<usize>>,
2286 selection: &UTF16Selection,
2287 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2288 ) -> Option<Bounds<Pixels>> {
2289 if let Some(marked_range) = marked_range {
2290 let mut line_start = marked_range.start;
2292
2293 let caret = selection.range.end;
2297 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2298 for i in (marked_range.start..caret).rev() {
2299 if let Some(b) = bounds_for_range(i..i) {
2300 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2301 line_start = i + 1;
2302 break;
2303 }
2304 }
2305 }
2306 }
2307 bounds_for_range(line_start..line_start)
2308 } else {
2309 let offset = if selection.reversed {
2311 selection.range.start
2312 } else {
2313 selection.range.end
2314 };
2315 bounds_for_range(offset..offset)
2316 }
2317 }
2318
2319 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2320 let marked_range = self.handler.marked_text_range(window, cx);
2321 let selection = self.handler.selected_text_range(true, window, cx)?;
2322 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2323 self.handler.bounds_for_range(range, window, cx)
2324 })
2325 }
2326
2327 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2328 let marked_range = self.marked_text_range();
2329 let selection = self.selected_text_range(true)?;
2330 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2331 self.bounds_for_range(range)
2332 })
2333 }
2334
2335 #[allow(unused)]
2336 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2337 self.cx
2338 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2339 .ok()
2340 .flatten()
2341 }
2342
2343 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2345 self.cx
2346 .update(|window, cx| {
2347 self.handler
2348 .set_selected_text_range(range_utf16, window, cx)
2349 })
2350 .ok();
2351 }
2352
2353 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2355 self.cx
2356 .update(|window, cx| self.handler.element_bounds(window, cx))
2357 .ok()
2358 .flatten()
2359 }
2360
2361 pub fn text_length_utf16(&mut self) -> Option<usize> {
2363 self.cx
2364 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2365 .ok()
2366 .flatten()
2367 }
2368
2369 #[allow(dead_code)]
2370 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2371 self.handler.accepts_text_input(window, cx)
2372 }
2373
2374 #[allow(dead_code)]
2375 pub fn query_accepts_text_input(&mut self) -> bool {
2376 self.cx
2377 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2378 .unwrap_or(true)
2379 }
2380
2381 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2386 let focus_id = self.focus_id;
2387 self.cx
2388 .update(|window, cx| {
2389 focus_id.is_some()
2390 && window.focus == focus_id
2391 && self.handler.accepts_text_input(window, cx)
2392 })
2393 .unwrap_or(false)
2394 }
2395
2396 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2402 self.cx
2403 .update(|window, cx| {
2404 !window.has_pending_keystrokes()
2406 && self.handler.prefers_ime_for_printable_keys(window, cx)
2407 })
2408 .unwrap_or(false)
2409 }
2410
2411 pub fn text_input_configuration(
2413 &mut self,
2414 window: &mut Window,
2415 cx: &mut App,
2416 ) -> TextInputConfiguration {
2417 self.handler.text_input_configuration(window, cx)
2418 }
2419
2420 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2422 self.cx
2423 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2424 .ok()
2425 .flatten()
2426 }
2427}
2428
2429#[derive(Debug)]
2432pub struct UTF16Selection {
2433 pub range: Range<usize>,
2436 pub reversed: bool,
2439}
2440
2441pub trait InputHandler: 'static {
2446 fn selected_text_range(
2451 &mut self,
2452 ignore_disabled_input: bool,
2453 window: &mut Window,
2454 cx: &mut App,
2455 ) -> Option<UTF16Selection>;
2456
2457 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2462
2463 fn text_for_range(
2468 &mut self,
2469 range_utf16: Range<usize>,
2470 adjusted_range: &mut Option<Range<usize>>,
2471 window: &mut Window,
2472 cx: &mut App,
2473 ) -> Option<String>;
2474
2475 fn replace_text_in_range(
2480 &mut self,
2481 replacement_range: Option<Range<usize>>,
2482 text: &str,
2483 window: &mut Window,
2484 cx: &mut App,
2485 );
2486
2487 fn replace_and_mark_text_in_range(
2494 &mut self,
2495 range_utf16: Option<Range<usize>>,
2496 new_text: &str,
2497 new_selected_range: Option<Range<usize>>,
2498 window: &mut Window,
2499 cx: &mut App,
2500 );
2501
2502 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2505
2506 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2513 if let Some(text) = item.text() {
2514 self.replace_text_in_range(None, &text, window, cx);
2515 }
2516 }
2517
2518 fn bounds_for_range(
2523 &mut self,
2524 range_utf16: Range<usize>,
2525 window: &mut Window,
2526 cx: &mut App,
2527 ) -> Option<Bounds<Pixels>>;
2528
2529 fn selection_bounds_for_range(
2536 &mut self,
2537 _range_utf16: Range<usize>,
2538 _window: &mut Window,
2539 _cx: &mut App,
2540 ) -> Vec<Bounds<Pixels>> {
2541 Vec::new()
2542 }
2543
2544 fn character_index_for_point(
2548 &mut self,
2549 point: Point<Pixels>,
2550 window: &mut Window,
2551 cx: &mut App,
2552 ) -> Option<usize>;
2553
2554 fn set_selected_text_range(
2564 &mut self,
2565 _range_utf16: Range<usize>,
2566 _window: &mut Window,
2567 _cx: &mut App,
2568 ) {
2569 }
2570
2571 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2578 None
2579 }
2580
2581 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2583 None
2584 }
2585
2586 #[allow(dead_code)]
2591 fn apple_press_and_hold_enabled(&mut self) -> bool {
2592 true
2593 }
2594
2595 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2597 true
2598 }
2599
2600 fn text_input_editable_range(
2611 &mut self,
2612 _window: &mut Window,
2613 _cx: &mut App,
2614 ) -> Option<Range<usize>> {
2615 None
2616 }
2617
2618 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2627 false
2628 }
2629
2630 fn text_input_configuration(
2636 &mut self,
2637 _window: &mut Window,
2638 _cx: &mut App,
2639 ) -> TextInputConfiguration {
2640 TextInputConfiguration::default()
2641 }
2642}
2643
2644#[derive(Clone, Debug, Default, PartialEq, Eq)]
2654pub struct TextInputConfiguration {
2655 pub autocorrect: bool,
2657 pub autocapitalize: Autocapitalize,
2659 pub suggestions: bool,
2661 pub input_action: TextInputAction,
2663}
2664
2665#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2667pub enum Autocapitalize {
2668 #[default]
2670 None,
2671 Words,
2673 Sentences,
2675 Characters,
2677}
2678
2679#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2689pub enum TextInputAction {
2690 #[default]
2692 Unspecified,
2693 Enter,
2695 Done,
2697 Go,
2699 Next,
2701 Previous,
2703 Search,
2705 Send,
2707}
2708
2709#[derive(Debug)]
2711pub struct WindowOptions {
2712 pub window_bounds: Option<WindowBounds>,
2716
2717 pub titlebar: Option<TitlebarOptions>,
2719
2720 pub focus: bool,
2722
2723 pub show: bool,
2725
2726 pub kind: WindowKind,
2728
2729 pub is_movable: bool,
2733
2734 pub app_owns_titlebar_drag: bool,
2746
2747 pub inactive_frame_interval: Option<Duration>,
2751
2752 pub is_resizable: bool,
2754
2755 pub is_minimizable: bool,
2757
2758 pub display_id: Option<DisplayId>,
2761
2762 pub window_background: WindowBackgroundAppearance,
2764
2765 pub app_id: Option<String>,
2767
2768 pub window_min_size: Option<Size<Pixels>>,
2770
2771 pub window_decorations: Option<WindowDecorations>,
2774
2775 pub icon: Option<Arc<image::RgbaImage>>,
2777
2778 pub tabbing_identifier: Option<String>,
2780
2781 pub use_display_p3: bool,
2786}
2787
2788#[derive(Debug)]
2790#[cfg_attr(
2791 all(
2792 any(target_os = "linux", target_os = "freebsd"),
2793 not(any(feature = "x11", feature = "wayland"))
2794 ),
2795 allow(dead_code)
2796)]
2797#[allow(missing_docs)]
2798pub struct WindowParams {
2799 pub bounds: Bounds<Pixels>,
2800
2801 #[cfg_attr(feature = "wayland", allow(dead_code))]
2803 pub titlebar: Option<TitlebarOptions>,
2804
2805 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2807 pub kind: WindowKind,
2808
2809 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2811 pub is_movable: bool,
2812
2813 #[cfg_attr(
2815 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2816 allow(dead_code)
2817 )]
2818 pub app_owns_titlebar_drag: bool,
2819
2820 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2822 pub is_resizable: bool,
2823
2824 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2826 pub is_minimizable: bool,
2827
2828 #[cfg_attr(
2829 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2830 allow(dead_code)
2831 )]
2832 pub focus: bool,
2833
2834 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2835 pub show: bool,
2836
2837 #[cfg_attr(feature = "wayland", allow(dead_code))]
2839 pub icon: Option<Arc<image::RgbaImage>>,
2840
2841 #[cfg_attr(feature = "wayland", allow(dead_code))]
2842 pub display_id: Option<DisplayId>,
2843
2844 #[cfg_attr(feature = "wayland", allow(dead_code))]
2845 pub app_id: Option<String>,
2846
2847 pub window_min_size: Option<Size<Pixels>>,
2848
2849 #[cfg(target_os = "macos")]
2850 pub tabbing_identifier: Option<String>,
2851
2852 #[cfg(target_os = "macos")]
2853 pub use_display_p3: bool,
2854}
2855
2856#[derive(Debug, Copy, Clone, PartialEq)]
2858pub enum WindowBounds {
2859 Windowed(Bounds<Pixels>),
2861 Maximized(Bounds<Pixels>),
2864 Fullscreen(Bounds<Pixels>),
2867}
2868
2869impl Default for WindowBounds {
2870 fn default() -> Self {
2871 WindowBounds::Windowed(Bounds::default())
2872 }
2873}
2874
2875impl WindowBounds {
2876 pub fn get_bounds(&self) -> Bounds<Pixels> {
2878 match self {
2879 WindowBounds::Windowed(bounds) => *bounds,
2880 WindowBounds::Maximized(bounds) => *bounds,
2881 WindowBounds::Fullscreen(bounds) => *bounds,
2882 }
2883 }
2884
2885 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2887 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2888 }
2889}
2890
2891impl Default for WindowOptions {
2892 fn default() -> Self {
2893 Self {
2894 window_bounds: None,
2895 titlebar: Some(TitlebarOptions {
2896 title: Default::default(),
2897 appears_transparent: Default::default(),
2898 traffic_light_position: Default::default(),
2899 }),
2900 focus: true,
2901 show: true,
2902 kind: WindowKind::Normal,
2903 is_movable: true,
2904 app_owns_titlebar_drag: false,
2905 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2906 is_resizable: true,
2907 is_minimizable: true,
2908 display_id: None,
2909 window_background: WindowBackgroundAppearance::default(),
2910 icon: None,
2911 app_id: None,
2912 window_min_size: None,
2913 window_decorations: None,
2914 tabbing_identifier: None,
2915 use_display_p3: false,
2916 }
2917 }
2918}
2919
2920#[derive(Debug, Default)]
2922pub struct TitlebarOptions {
2923 pub title: Option<SharedString>,
2925
2926 pub appears_transparent: bool,
2929
2930 pub traffic_light_position: Option<Point<Pixels>>,
2932}
2933
2934#[derive(Clone, Debug, PartialEq, Eq)]
2936pub enum WindowKind {
2937 Normal,
2939
2940 PopUp,
2943
2944 AnchoredPopup(popup::PopupOptions),
2951
2952 Floating,
2954
2955 #[cfg(all(target_os = "linux", feature = "wayland"))]
2958 LayerShell(layer_shell::LayerShellOptions),
2959
2960 Dialog,
2963}
2964
2965#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2970pub enum WindowAppearance {
2971 #[default]
2975 Light,
2976
2977 VibrantLight,
2981
2982 Dark,
2986
2987 VibrantDark,
2991}
2992
2993#[derive(Copy, Clone, Debug, Default, PartialEq)]
2996pub enum WindowBackgroundAppearance {
2997 #[default]
3005 Opaque,
3006 Transparent,
3008 Blurred,
3012 MicaBackdrop,
3014 MicaAltBackdrop,
3016}
3017
3018#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
3020pub enum TextRenderingMode {
3021 #[default]
3023 PlatformDefault,
3024 Subpixel,
3026 Grayscale,
3028}
3029
3030#[derive(Clone, Debug)]
3032pub struct PathPromptOptions {
3033 pub files: bool,
3035 pub directories: bool,
3037 pub multiple: bool,
3039 pub prompt: Option<SharedString>,
3041}
3042
3043#[derive(Copy, Clone, Debug, PartialEq)]
3045pub enum PromptLevel {
3046 Info,
3048
3049 Warning,
3051
3052 Critical,
3054}
3055
3056#[derive(Clone, Debug, PartialEq)]
3058pub enum PromptButton {
3059 Ok(SharedString),
3061 Cancel(SharedString),
3063 Other(SharedString),
3065}
3066
3067impl PromptButton {
3068 pub fn new(label: impl Into<SharedString>) -> Self {
3070 PromptButton::Other(label.into())
3071 }
3072
3073 pub fn ok(label: impl Into<SharedString>) -> Self {
3075 PromptButton::Ok(label.into())
3076 }
3077
3078 pub fn cancel(label: impl Into<SharedString>) -> Self {
3080 PromptButton::Cancel(label.into())
3081 }
3082
3083 #[allow(dead_code)]
3085 pub fn is_cancel(&self) -> bool {
3086 matches!(self, PromptButton::Cancel(_))
3087 }
3088
3089 pub fn label(&self) -> &SharedString {
3091 match self {
3092 PromptButton::Ok(label) => label,
3093 PromptButton::Cancel(label) => label,
3094 PromptButton::Other(label) => label,
3095 }
3096 }
3097}
3098
3099impl From<&str> for PromptButton {
3100 fn from(value: &str) -> Self {
3101 match value.to_lowercase().as_str() {
3102 "ok" => PromptButton::Ok("OK".into()),
3103 "cancel" => PromptButton::Cancel("Cancel".into()),
3104 _ => PromptButton::Other(SharedString::from(value.to_owned())),
3105 }
3106 }
3107}
3108
3109#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3111pub enum CursorStyle {
3112 #[default]
3114 Arrow,
3115
3116 IBeam,
3119
3120 Crosshair,
3123
3124 ClosedHand,
3127
3128 OpenHand,
3131
3132 PointingHand,
3135
3136 ResizeLeft,
3139
3140 ResizeRight,
3143
3144 ResizeLeftRight,
3147
3148 ResizeUp,
3151
3152 ResizeDown,
3155
3156 ResizeUpDown,
3159
3160 ResizeUpLeftDownRight,
3163
3164 ResizeUpRightDownLeft,
3167
3168 ResizeColumn,
3171
3172 ResizeRow,
3175
3176 IBeamCursorForVerticalLayout,
3179
3180 OperationNotAllowed,
3183
3184 DragLink,
3187
3188 DragCopy,
3191
3192 ContextualMenu,
3195}
3196
3197#[derive(Clone, Debug, Eq, PartialEq)]
3199pub struct ClipboardItem {
3200 pub entries: Vec<ClipboardEntry>,
3202}
3203
3204#[derive(Clone, Debug, PartialEq, Eq)]
3209pub enum ClipboardReadError {
3210 Unavailable,
3214 Denied(String),
3217 UnsupportedContent,
3220}
3221
3222impl std::fmt::Display for ClipboardReadError {
3223 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3224 match self {
3225 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3226 Self::Denied(message) => {
3227 write!(formatter, "clipboard access was denied: {message}")
3228 }
3229 Self::UnsupportedContent => {
3230 formatter.write_str("the clipboard contents are unsupported")
3231 }
3232 }
3233 }
3234}
3235
3236impl std::error::Error for ClipboardReadError {}
3237
3238#[derive(Clone, Debug, Eq, PartialEq)]
3240pub enum ClipboardEntry {
3241 String(ClipboardString),
3243 Image(Image),
3245 ExternalPaths(crate::ExternalPaths),
3247}
3248
3249impl ClipboardItem {
3250 pub fn new_string(text: String) -> Self {
3252 Self {
3253 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3254 }
3255 }
3256
3257 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3259 Self {
3260 entries: vec![ClipboardEntry::String(ClipboardString {
3261 text,
3262 metadata: Some(metadata),
3263 })],
3264 }
3265 }
3266
3267 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3269 Self {
3270 entries: vec![ClipboardEntry::String(
3271 ClipboardString::new(text).with_json_metadata(metadata),
3272 )],
3273 }
3274 }
3275
3276 pub fn new_image(image: &Image) -> Self {
3278 Self {
3279 entries: vec![ClipboardEntry::Image(image.clone())],
3280 }
3281 }
3282
3283 pub fn text(&self) -> Option<String> {
3286 let mut answer = String::new();
3287
3288 for entry in self.entries.iter() {
3289 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3290 answer.push_str(text);
3291 }
3292 }
3293
3294 if answer.is_empty() {
3295 for entry in self.entries.iter() {
3296 if let ClipboardEntry::ExternalPaths(paths) = entry {
3297 for path in &paths.0 {
3298 use std::fmt::Write as _;
3299 _ = write!(answer, "{}", path.display());
3300 }
3301 }
3302 }
3303 }
3304
3305 if !answer.is_empty() {
3306 Some(answer)
3307 } else {
3308 None
3309 }
3310 }
3311
3312 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3314 pub fn metadata(&self) -> Option<&String> {
3315 match self.entries().first() {
3316 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3317 clipboard_string.metadata.as_ref()
3318 }
3319 _ => None,
3320 }
3321 }
3322
3323 pub fn entries(&self) -> &[ClipboardEntry] {
3325 &self.entries
3326 }
3327
3328 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3330 self.entries.into_iter()
3331 }
3332}
3333
3334impl From<ClipboardString> for ClipboardEntry {
3335 fn from(value: ClipboardString) -> Self {
3336 Self::String(value)
3337 }
3338}
3339
3340impl From<String> for ClipboardEntry {
3341 fn from(value: String) -> Self {
3342 Self::from(ClipboardString::from(value))
3343 }
3344}
3345
3346impl From<Image> for ClipboardEntry {
3347 fn from(value: Image) -> Self {
3348 Self::Image(value)
3349 }
3350}
3351
3352impl From<ClipboardEntry> for ClipboardItem {
3353 fn from(value: ClipboardEntry) -> Self {
3354 Self {
3355 entries: vec![value],
3356 }
3357 }
3358}
3359
3360impl From<String> for ClipboardItem {
3361 fn from(value: String) -> Self {
3362 Self::from(ClipboardEntry::from(value))
3363 }
3364}
3365
3366impl From<Image> for ClipboardItem {
3367 fn from(value: Image) -> Self {
3368 Self::from(ClipboardEntry::from(value))
3369 }
3370}
3371
3372#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3374pub enum ImageFormat {
3375 Png,
3380 Jpeg,
3382 Webp,
3384 Gif,
3386 Svg,
3388 Bmp,
3390 Tiff,
3392 Ico,
3394 Pnm,
3396}
3397
3398impl ImageFormat {
3399 pub const fn mime_type(self) -> &'static str {
3401 match self {
3402 ImageFormat::Png => "image/png",
3403 ImageFormat::Jpeg => "image/jpeg",
3404 ImageFormat::Webp => "image/webp",
3405 ImageFormat::Gif => "image/gif",
3406 ImageFormat::Svg => "image/svg+xml",
3407 ImageFormat::Bmp => "image/bmp",
3408 ImageFormat::Tiff => "image/tiff",
3409 ImageFormat::Ico => "image/ico",
3410 ImageFormat::Pnm => "image/x-portable-anymap",
3411 }
3412 }
3413
3414 pub const fn extension(self) -> &'static str {
3416 match self {
3417 ImageFormat::Png => "png",
3418 ImageFormat::Jpeg => "jpg",
3419 ImageFormat::Webp => "webp",
3420 ImageFormat::Gif => "gif",
3421 ImageFormat::Svg => "svg",
3422 ImageFormat::Bmp => "bmp",
3423 ImageFormat::Tiff => "tiff",
3424 ImageFormat::Ico => "ico",
3425 ImageFormat::Pnm => "pnm",
3426 }
3427 }
3428
3429 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3431 use strum::IntoEnumIterator;
3432 Self::iter()
3433 .find(|format| format.mime_type() == mime_type)
3434 .or_else(|| Self::from_mime_type_alias(mime_type))
3435 }
3436
3437 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3441 match mime_type {
3442 "image/jpg" => Some(Self::Jpeg),
3443 "image/tif" => Some(Self::Tiff),
3444 _ => None,
3445 }
3446 }
3447}
3448
3449#[derive(Clone, Debug, PartialEq, Eq)]
3451pub struct Image {
3452 pub format: ImageFormat,
3454 pub bytes: Vec<u8>,
3456 pub id: u64,
3458}
3459
3460pub(crate) fn decode_static_image(
3461 bytes: &[u8],
3462 format: image::ImageFormat,
3463) -> Result<SmallVec<[Frame; 1]>> {
3464 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3465 .into_decoder()
3466 .context("creating image decoder")?;
3467 decode_static_image_from_decoder(decoder)
3468}
3469
3470pub(crate) fn decode_static_image_from_decoder(
3471 mut decoder: impl image::ImageDecoder,
3472) -> Result<SmallVec<[Frame; 1]>> {
3473 let orientation = decoder
3474 .orientation()
3475 .context("reading decoder's orientation")?;
3476 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3477 image.apply_orientation(orientation);
3478
3479 let mut data = image.into_rgba8();
3480 for pixel in data.chunks_exact_mut(4) {
3481 pixel.swap(0, 2);
3482 }
3483
3484 Ok(SmallVec::from_elem(Frame::new(data), 1))
3485}
3486
3487impl Hash for Image {
3488 fn hash<H: Hasher>(&self, state: &mut H) {
3489 state.write_u64(self.id);
3490 }
3491}
3492
3493impl Image {
3494 pub fn empty() -> Self {
3496 Self::from_bytes(ImageFormat::Png, Vec::new())
3497 }
3498
3499 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3501 Self {
3502 id: hash(&bytes),
3503 format,
3504 bytes,
3505 }
3506 }
3507
3508 pub fn id(&self) -> u64 {
3510 self.id
3511 }
3512
3513 pub fn use_render_image(
3515 self: Arc<Self>,
3516 window: &mut Window,
3517 cx: &mut App,
3518 ) -> Option<Arc<RenderImage>> {
3519 ImageSource::Image(self)
3520 .use_data(None, window, cx)
3521 .and_then(|result| result.ok())
3522 }
3523
3524 pub fn get_render_image(
3526 self: Arc<Self>,
3527 window: &mut Window,
3528 cx: &mut App,
3529 ) -> Option<Arc<RenderImage>> {
3530 ImageSource::Image(self)
3531 .get_data(None, window, cx)
3532 .and_then(|result| result.ok())
3533 }
3534
3535 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3537 ImageSource::Image(self).remove_asset(cx);
3538 }
3539
3540 #[cfg(any(test, feature = "test-support"))]
3543 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3544 ImageSource::Image(self.clone()).is_asset_cached(cx)
3545 }
3546
3547 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3549 let frames = match self.format {
3550 ImageFormat::Gif => {
3551 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3552 let mut frames = SmallVec::new();
3553
3554 for frame in decoder.into_frames() {
3555 match frame {
3556 Ok(mut frame) => {
3557 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3559 pixel.swap(0, 2);
3560 }
3561 frames.push(frame);
3562 }
3563 Err(err) => {
3564 log::debug!("Skipping GIF frame due to decode error: {err}");
3565 }
3566 }
3567 }
3568
3569 if frames.is_empty() {
3570 anyhow::bail!("GIF could not be decoded: all frames failed");
3571 }
3572
3573 frames
3574 }
3575 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3576 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3577 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3578 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3579 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3580 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3581 ImageFormat::Svg => {
3582 return svg_renderer
3583 .render_single_frame(&self.bytes, 1.0)
3584 .map_err(Into::into);
3585 }
3586 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3587 };
3588
3589 Ok(Arc::new(RenderImage::new(frames)))
3590 }
3591
3592 pub fn format(&self) -> ImageFormat {
3594 self.format
3595 }
3596
3597 pub fn bytes(&self) -> &[u8] {
3599 self.bytes.as_slice()
3600 }
3601}
3602
3603#[derive(Clone, Debug, Eq, PartialEq)]
3605pub struct ClipboardString {
3606 pub text: String,
3608 pub metadata: Option<String>,
3610}
3611
3612impl ClipboardString {
3613 pub fn new(text: String) -> Self {
3615 Self {
3616 text,
3617 metadata: None,
3618 }
3619 }
3620
3621 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3624 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3625 self
3626 }
3627
3628 pub fn text(&self) -> &String {
3630 &self.text
3631 }
3632
3633 pub fn into_text(self) -> String {
3635 self.text
3636 }
3637
3638 pub fn metadata_json<T>(&self) -> Option<T>
3640 where
3641 T: for<'a> Deserialize<'a>,
3642 {
3643 self.metadata
3644 .as_ref()
3645 .and_then(|m| serde_json::from_str(m).ok())
3646 }
3647
3648 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3649 pub fn text_hash(text: &str) -> u64 {
3651 let mut hasher = SeaHasher::new();
3652 text.hash(&mut hasher);
3653 hasher.finish()
3654 }
3655}
3656
3657impl From<String> for ClipboardString {
3658 fn from(value: String) -> Self {
3659 Self {
3660 text: value,
3661 metadata: None,
3662 }
3663 }
3664}
3665
3666#[cfg(test)]
3667mod image_tests {
3668 use super::*;
3669 use std::sync::Arc;
3670
3671 #[test]
3672 fn test_image_to_image_data_applies_exif_orientation() {
3673 let image = Image::from_bytes(
3674 ImageFormat::Jpeg,
3675 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3676 );
3677
3678 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3679
3680 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3681
3682 let bytes = render_image.as_bytes(0).unwrap();
3683 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3684 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3685 }
3686
3687 #[test]
3688 fn test_svg_image_to_image_data_converts_to_bgra() {
3689 let image = Image::from_bytes(
3690 ImageFormat::Svg,
3691 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3692<rect width="1" height="1" fill="#38BDF8"/>
3693</svg>"##
3694 .to_vec(),
3695 );
3696
3697 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3698 let bytes = render_image.as_bytes(0).unwrap();
3699
3700 for pixel in bytes.chunks_exact(4) {
3701 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3702 }
3703 }
3704}
3705
3706#[cfg(test)]
3707mod atlas_tests {
3708 use super::*;
3709
3710 const TILE_SIZE: Size<DevicePixels> = Size {
3711 width: DevicePixels(1),
3712 height: DevicePixels(1),
3713 };
3714
3715 #[derive(Default)]
3716 struct RecordingAtlasBackend {
3717 insert_calls: u32,
3718 fail_next_insert: bool,
3719 removed_tiles: Vec<AtlasTile>,
3720 }
3721
3722 impl AtlasBackend for RecordingAtlasBackend {
3723 fn insert(
3724 &mut self,
3725 kind: AtlasTextureKind,
3726 size: Size<DevicePixels>,
3727 _bytes: &[u8],
3728 ) -> Result<AtlasTile> {
3729 self.insert_calls += 1;
3730 if std::mem::take(&mut self.fail_next_insert) {
3731 anyhow::bail!("backend failed");
3732 }
3733 Ok(AtlasTile {
3734 texture_id: AtlasTextureId { index: 0, kind },
3735 tile_id: TileId(self.insert_calls),
3736 padding: 0,
3737 bounds: Bounds {
3738 origin: Point::default(),
3739 size,
3740 },
3741 })
3742 }
3743
3744 fn remove(&mut self, tile: AtlasTile) {
3745 self.removed_tiles.push(tile);
3746 }
3747 }
3748
3749 fn image_key(image_id: usize) -> AtlasKey {
3750 AtlasKey::Image(RenderImageParams {
3751 image_id: crate::ImageId(image_id),
3752 frame_index: 0,
3753 })
3754 }
3755
3756 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3757 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3758 }
3759
3760 #[test]
3761 fn only_successful_inserts_are_cached() -> Result<()> {
3762 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3763 let key = image_key(1);
3764
3765 assert_eq!(
3766 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3767 None
3768 );
3769 state
3770 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3771 .expect_err("builder error should propagate");
3772 assert!(!state.contains(&key));
3773 assert_eq!(state.backend.insert_calls, 0);
3774
3775 state.backend.fail_next_insert = true;
3776 state
3777 .get_or_insert_with(key.clone(), &mut build_tile)
3778 .expect_err("backend error should propagate");
3779 assert!(!state.contains(&key));
3780 assert_eq!(state.backend.insert_calls, 1);
3781
3782 let tile = state
3783 .get_or_insert_with(key.clone(), &mut build_tile)?
3784 .context("builder should produce a tile")?;
3785 assert_eq!(tile.texture_id.kind, key.texture_kind());
3786 assert_eq!(
3787 state.get_or_insert_with(key.clone(), &mut || {
3788 anyhow::bail!("cache hit must not call the builder")
3789 })?,
3790 Some(tile)
3791 );
3792 assert!(state.contains(&key));
3793 assert_eq!(state.backend.insert_calls, 2);
3794 Ok(())
3795 }
3796
3797 #[test]
3798 fn remove_and_clear_invalidate_keys() -> Result<()> {
3799 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3800 let key = image_key(1);
3801 let other_key = image_key(2);
3802 let tile = state
3803 .get_or_insert_with(key.clone(), &mut build_tile)?
3804 .context("builder should produce a tile")?;
3805 state
3806 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3807 .context("builder should produce another tile")?;
3808
3809 state.remove(&key);
3810 state.remove(&key);
3811 assert!(!state.contains(&key));
3812 assert!(state.contains(&other_key));
3813 assert_eq!(state.backend.removed_tiles, vec![tile]);
3814
3815 let mut reset_calls = 0;
3816 state.clear(|_| reset_calls += 1);
3817 assert_eq!(reset_calls, 1);
3818 assert!(!state.contains(&other_key));
3819 assert_eq!(state.backend.removed_tiles, vec![tile]);
3820 Ok(())
3821 }
3822}
3823
3824#[cfg(test)]
3825mod frame_signal_tests {
3826 use super::*;
3827
3828 #[cfg(feature = "profiler")]
3829 #[test]
3830 fn coalesced_signals_retain_the_earliest_until_drained() {
3831 for (first_source, later_source) in [
3832 (
3833 FrameRequestSource::NativeCallback,
3834 FrameRequestSource::LocalSchedule,
3835 ),
3836 (
3837 FrameRequestSource::LocalSchedule,
3838 FrameRequestSource::NativeCallback,
3839 ),
3840 ] {
3841 let signal = PlatformFrameSignal::new();
3842 let first =
3843 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3844 assert_eq!(signal.take(), None);
3845 signal.record(first + Duration::from_millis(16), later_source);
3846 signal.record(first, first_source);
3847 signal.record(first + Duration::from_millis(32), later_source);
3848 assert_eq!(signal.take(), Some((first, first_source)));
3849 assert_eq!(signal.take(), None);
3850 let next = first + Duration::from_millis(48);
3851 signal.record(next, later_source);
3852 assert_eq!(signal.take(), Some((next, later_source)));
3853 }
3854 }
3855
3856 #[cfg(not(feature = "profiler"))]
3857 #[test]
3858 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3859 let captured = PlatformFrameSignal::capture(|| {
3860 panic!("profiler-disabled builds must not evaluate the capture closure");
3861 });
3862 assert_eq!(captured, None);
3863 let signal = PlatformFrameSignal::new();
3864 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3865 assert_eq!(signal.take(), None);
3866 }
3867}
3868
3869#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3870mod tests {
3871 use super::*;
3872 use std::collections::HashSet;
3873
3874 #[test]
3875 fn test_refresh_interval_from_hz() {
3876 assert_eq!(
3877 refresh_interval_from_hz(50.0),
3878 Some(Duration::from_millis(20))
3879 );
3880 assert_eq!(
3881 refresh_interval_from_hz(120.0),
3882 Some(Duration::from_secs(1) / 120)
3883 );
3884 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3885 assert_eq!(refresh_interval_from_hz(unknown), None);
3886 }
3887 }
3888
3889 #[test]
3890 fn test_window_button_layout_parse_standard() {
3891 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3892 assert_eq!(
3893 layout.left,
3894 [
3895 Some(WindowButton::Close),
3896 Some(WindowButton::Minimize),
3897 None
3898 ]
3899 );
3900 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3901 }
3902
3903 #[test]
3904 fn test_window_button_layout_parse_right_only() {
3905 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3906 assert_eq!(layout.left, [None, None, None]);
3907 assert_eq!(
3908 layout.right,
3909 [
3910 Some(WindowButton::Minimize),
3911 Some(WindowButton::Maximize),
3912 Some(WindowButton::Close)
3913 ]
3914 );
3915 }
3916
3917 #[test]
3918 fn test_window_button_layout_parse_left_only() {
3919 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3920 assert_eq!(
3921 layout.left,
3922 [
3923 Some(WindowButton::Close),
3924 Some(WindowButton::Minimize),
3925 Some(WindowButton::Maximize)
3926 ]
3927 );
3928 assert_eq!(layout.right, [None, None, None]);
3929 }
3930
3931 #[test]
3932 fn test_window_button_layout_parse_with_whitespace() {
3933 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3934 assert_eq!(
3935 layout.left,
3936 [
3937 Some(WindowButton::Close),
3938 Some(WindowButton::Minimize),
3939 None
3940 ]
3941 );
3942 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3943 }
3944
3945 #[test]
3946 fn test_window_button_layout_parse_empty() {
3947 let layout = WindowButtonLayout::parse("").unwrap();
3948 assert_eq!(layout.left, [None, None, None]);
3949 assert_eq!(layout.right, [None, None, None]);
3950 }
3951
3952 #[test]
3953 fn test_window_button_layout_parse_intentionally_empty() {
3954 let layout = WindowButtonLayout::parse(":").unwrap();
3955 assert_eq!(layout.left, [None, None, None]);
3956 assert_eq!(layout.right, [None, None, None]);
3957 }
3958
3959 #[test]
3960 fn test_window_button_layout_parse_invalid_buttons() {
3961 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
3962 assert_eq!(
3963 layout.left,
3964 [
3965 Some(WindowButton::Close),
3966 Some(WindowButton::Minimize),
3967 None
3968 ]
3969 );
3970 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3971 }
3972
3973 #[test]
3974 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
3975 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
3976 assert_eq!(
3977 layout.right,
3978 [
3979 Some(WindowButton::Close),
3980 Some(WindowButton::Minimize),
3981 None
3982 ]
3983 );
3984 assert_eq!(layout.format(), ":close,minimize");
3985 }
3986
3987 #[test]
3988 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
3989 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
3990 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
3991 assert_eq!(
3992 layout.right,
3993 [
3994 Some(WindowButton::Maximize),
3995 Some(WindowButton::Minimize),
3996 None
3997 ]
3998 );
3999
4000 let button_ids: Vec<_> = layout
4001 .left
4002 .iter()
4003 .chain(layout.right.iter())
4004 .flatten()
4005 .map(WindowButton::id)
4006 .collect();
4007 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
4008 assert_eq!(unique_button_ids.len(), button_ids.len());
4009 assert_eq!(layout.format(), "close:maximize,minimize");
4010 }
4011
4012 #[test]
4013 fn test_window_button_layout_parse_gnome_style() {
4014 let layout = WindowButtonLayout::parse("close").unwrap();
4015 assert_eq!(layout.left, [None, None, None]);
4016 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
4017 }
4018
4019 #[test]
4020 fn test_window_button_layout_parse_elementary_style() {
4021 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
4022 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
4023 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4024 }
4025
4026 #[test]
4027 fn test_window_button_layout_round_trip() {
4028 let cases = [
4029 "close:minimize,maximize",
4030 "minimize,maximize,close:",
4031 ":close",
4032 "close:",
4033 "close:maximize",
4034 ":",
4035 ];
4036
4037 for case in cases {
4038 let layout = WindowButtonLayout::parse(case).unwrap();
4039 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
4040 }
4041 }
4042
4043 #[test]
4044 fn test_window_button_layout_linux_default() {
4045 let layout = WindowButtonLayout::linux_default();
4046 assert_eq!(layout.left, [None, None, None]);
4047 assert_eq!(
4048 layout.right,
4049 [
4050 Some(WindowButton::Minimize),
4051 Some(WindowButton::Maximize),
4052 Some(WindowButton::Close)
4053 ]
4054 );
4055
4056 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
4057 assert_eq!(round_tripped, layout);
4058 }
4059
4060 #[test]
4061 fn test_window_button_layout_parse_all_invalid() {
4062 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
4063 }
4064}