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 on_gpu_device_lost(&self, _callback: Box<dyn FnMut(GpuDeviceLost)>) {}
551
552 fn set_app_identity(&self, identifier: &str, name: &str) {
559 _ = (identifier, name);
560 }
561
562 fn show_system_notification(&self, notification: SystemNotification) {
569 _ = notification;
570 }
571
572 fn dismiss_system_notification(&self, tag: &str) {
577 _ = tag;
578 }
579
580 fn on_system_notification_response(
586 &self,
587 callback: Box<dyn FnMut(SystemNotificationResponse)>,
588 ) {
589 _ = callback;
590 }
591
592 fn compositor_name(&self) -> &'static str {
593 ""
594 }
595 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
598 None
599 }
600 fn app_path(&self) -> Result<PathBuf>;
601 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
602
603 fn set_cursor_style(&self, style: CursorStyle);
604
605 fn hide_cursor_until_mouse_moves(&self);
608
609 fn is_cursor_visible(&self) -> bool;
611
612 fn should_auto_hide_scrollbars(&self) -> bool;
613
614 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
615 fn write_to_clipboard(&self, item: ClipboardItem);
616
617 fn read_from_clipboard_async(&self) -> Task<Result<Option<ClipboardItem>, ClipboardReadError>> {
625 Task::ready(Ok(self.read_from_clipboard()))
626 }
627
628 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
629 fn read_from_primary(&self) -> Option<ClipboardItem>;
630 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
631 fn write_to_primary(&self, item: ClipboardItem);
632
633 #[cfg(target_os = "macos")]
634 fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
635 #[cfg(target_os = "macos")]
636 fn write_to_find_pasteboard(&self, item: ClipboardItem);
637
638 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
639
640 #[cfg(target_os = "macos")]
644 fn write_credentials_if_not_exists(
645 &self,
646 _url: &str,
647 _username: &str,
648 _password: &[u8],
649 ) -> Task<Result<bool>> {
650 Task::ready(Err(anyhow::anyhow!(
651 "this platform does not support atomic credential writes"
652 )))
653 }
654
655 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
656 fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
657
658 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
659 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
660 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
661}
662
663pub trait PlatformDisplay: Debug {
665 fn id(&self) -> DisplayId;
667
668 fn uuid(&self) -> Result<Uuid>;
671
672 fn bounds(&self) -> Bounds<Pixels>;
674
675 fn refresh_interval(&self) -> Option<Duration>;
682
683 fn visible_bounds(&self) -> Bounds<Pixels> {
687 self.bounds()
688 }
689
690 fn default_bounds(&self) -> Bounds<Pixels> {
692 let bounds = self.bounds();
693 let center = bounds.center();
694 let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
695
696 let offset = clipped_window_size / 2.0;
697 let origin = point(center.x - offset.width, center.y - offset.height);
698 Bounds::new(origin, clipped_window_size)
699 }
700}
701
702#[derive(Clone, Debug, PartialEq, Eq)]
705pub struct SystemNotification {
706 pub tag: SharedString,
710 pub title: SharedString,
712 pub body: SharedString,
714 pub actions: Vec<SystemNotificationAction>,
717}
718
719#[derive(Clone, Debug, PartialEq, Eq, Hash)]
721pub struct SystemNotificationAction {
722 pub id: SharedString,
725 pub label: SharedString,
727}
728
729#[derive(Clone, Debug, PartialEq, Eq)]
731pub struct SystemNotificationResponse {
732 pub tag: SharedString,
734 pub action_id: Option<SharedString>,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq)]
745#[non_exhaustive]
746pub struct GpuDeviceLost {
747 pub reason: SharedString,
749}
750
751impl GpuDeviceLost {
752 pub fn new(reason: impl Into<SharedString>) -> Self {
754 Self {
755 reason: reason.into(),
756 }
757 }
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
762pub enum PowerMode {
763 Unknown,
766 LowPower,
768 Balanced,
770 HighPower,
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq)]
776pub enum ThermalState {
777 Nominal,
779 Fair,
781 Serious,
783 Critical,
785}
786
787#[derive(Clone)]
789pub struct SourceMetadata {
790 pub id: u64,
792 pub label: Option<SharedString>,
794 pub is_main: Option<bool>,
796 pub resolution: Size<DevicePixels>,
798}
799
800pub trait ScreenCaptureSource {
802 fn metadata(&self) -> Result<SourceMetadata>;
804
805 fn stream(
808 &self,
809 foreground_executor: &ForegroundExecutor,
810 frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
811 ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
812}
813
814pub trait ScreenCaptureStream {
816 fn metadata(&self) -> Result<SourceMetadata>;
818}
819
820pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
822
823#[derive(PartialEq, Eq, Hash, Copy, Clone)]
825pub struct DisplayId(pub(crate) u64);
826
827impl DisplayId {
828 pub fn new(id: u64) -> Self {
830 Self(id)
831 }
832}
833
834impl From<u64> for DisplayId {
835 fn from(id: u64) -> Self {
836 Self(id)
837 }
838}
839
840impl From<DisplayId> for u64 {
841 fn from(id: DisplayId) -> Self {
842 id.0
843 }
844}
845
846impl Debug for DisplayId {
847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848 write!(f, "DisplayId({})", self.0)
849 }
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq)]
854pub enum ResizeEdge {
855 Top,
857 TopRight,
859 Right,
861 BottomRight,
863 Bottom,
865 BottomLeft,
867 Left,
869 TopLeft,
871}
872
873#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
875pub enum WindowDecorations {
876 #[default]
877 Server,
879 Client,
881}
882
883#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
885pub enum Decorations {
886 #[default]
888 Server,
889 Client {
891 tiling: Tiling,
893 },
894}
895
896#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
898pub struct WindowControls {
899 pub fullscreen: bool,
901 pub maximize: bool,
903 pub minimize: bool,
905 pub window_menu: bool,
907}
908
909impl Default for WindowControls {
910 fn default() -> Self {
911 Self {
913 fullscreen: true,
914 maximize: true,
915 minimize: true,
916 window_menu: true,
917 }
918 }
919}
920
921#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
923pub enum WindowButton {
924 Minimize,
926 Maximize,
928 Close,
930}
931
932impl WindowButton {
933 pub fn id(&self) -> &'static str {
935 match self {
936 WindowButton::Minimize => "minimize",
937 WindowButton::Maximize => "maximize",
938 WindowButton::Close => "close",
939 }
940 }
941
942 #[cfg(any(
943 test,
944 feature = "test-support",
945 target_os = "linux",
946 target_os = "freebsd"
947 ))]
948 fn index(&self) -> usize {
949 match self {
950 WindowButton::Minimize => 0,
951 WindowButton::Maximize => 1,
952 WindowButton::Close => 2,
953 }
954 }
955}
956
957pub const MAX_BUTTONS_PER_SIDE: usize = 3;
959
960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
965pub struct WindowButtonLayout {
966 pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
968 pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
970}
971
972#[cfg(any(
973 test,
974 feature = "test-support",
975 target_os = "linux",
976 target_os = "freebsd"
977))]
978impl WindowButtonLayout {
979 pub fn linux_default() -> Self {
981 Self {
982 left: [None; MAX_BUTTONS_PER_SIDE],
983 right: [
984 Some(WindowButton::Minimize),
985 Some(WindowButton::Maximize),
986 Some(WindowButton::Close),
987 ],
988 }
989 }
990
991 pub fn parse(layout_string: &str) -> Result<Self> {
993 fn parse_side(
994 s: &str,
995 seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
996 unrecognized: &mut Vec<String>,
997 ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
998 let mut result = [None; MAX_BUTTONS_PER_SIDE];
999 let mut i = 0;
1000 for name in s.split(',') {
1001 let trimmed = name.trim();
1002 if trimmed.is_empty() {
1003 continue;
1004 }
1005 let button = match trimmed {
1006 "minimize" => Some(WindowButton::Minimize),
1007 "maximize" => Some(WindowButton::Maximize),
1008 "close" => Some(WindowButton::Close),
1009 other => {
1010 unrecognized.push(other.to_string());
1011 None
1012 }
1013 };
1014 if let Some(button) = button {
1015 if seen_buttons[button.index()] {
1016 continue;
1017 }
1018 if let Some(slot) = result.get_mut(i) {
1019 *slot = Some(button);
1020 seen_buttons[button.index()] = true;
1021 i += 1;
1022 }
1023 }
1024 }
1025 result
1026 }
1027
1028 let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
1029 let mut unrecognized = Vec::new();
1030 let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
1031 let layout = Self {
1032 left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
1033 right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
1034 };
1035
1036 if !unrecognized.is_empty()
1037 && layout.left.iter().all(Option::is_none)
1038 && layout.right.iter().all(Option::is_none)
1039 {
1040 bail!(
1041 "button layout string {:?} contains no valid buttons (unrecognized: {})",
1042 layout_string,
1043 unrecognized.join(", ")
1044 );
1045 }
1046
1047 Ok(layout)
1048 }
1049
1050 #[cfg(test)]
1052 pub fn format(&self) -> String {
1053 fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
1054 buttons
1055 .iter()
1056 .flatten()
1057 .map(|button| match button {
1058 WindowButton::Minimize => "minimize",
1059 WindowButton::Maximize => "maximize",
1060 WindowButton::Close => "close",
1061 })
1062 .collect::<Vec<_>>()
1063 .join(",")
1064 }
1065
1066 format!("{}:{}", format_side(&self.left), format_side(&self.right))
1067 }
1068}
1069
1070#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
1072pub struct Tiling {
1073 pub top: bool,
1075 pub left: bool,
1077 pub right: bool,
1079 pub bottom: bool,
1081}
1082
1083impl Tiling {
1084 pub fn tiled() -> Self {
1086 Self {
1087 top: true,
1088 left: true,
1089 right: true,
1090 bottom: true,
1091 }
1092 }
1093
1094 pub fn is_tiled(&self) -> bool {
1096 self.top || self.left || self.right || self.bottom
1097 }
1098}
1099
1100pub struct A11yCallbacks {
1102 pub activation: Box<dyn Fn() -> Option<accesskit::TreeUpdate> + Send + 'static>,
1104 pub action: Box<dyn Fn(accesskit::ActionRequest) + Send + 'static>,
1106 pub deactivation: Box<dyn Fn() + Send + 'static>,
1108}
1109
1110#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1112pub enum FrameRequestSource {
1113 #[default]
1115 NativeCallback,
1116 LocalSchedule,
1118}
1119
1120#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
1121#[expect(missing_docs)]
1122pub struct RequestFrameOptions {
1123 pub require_presentation: bool,
1125 pub force_render: bool,
1127 pub signal_at: Option<Instant>,
1133 pub signal_source: FrameRequestSource,
1135}
1136
1137pub struct PlatformFrameSignal {
1146 #[cfg(feature = "profiler")]
1147 origin: Instant,
1148 #[cfg(feature = "profiler")]
1149 first_signal: std::sync::atomic::AtomicU64,
1150}
1151
1152impl Default for PlatformFrameSignal {
1153 fn default() -> Self {
1154 Self::new()
1155 }
1156}
1157
1158impl PlatformFrameSignal {
1159 pub fn new() -> Self {
1161 Self {
1162 #[cfg(feature = "profiler")]
1163 origin: Instant::now(),
1164 #[cfg(feature = "profiler")]
1165 first_signal: std::sync::atomic::AtomicU64::new(u64::MAX),
1166 }
1167 }
1168
1169 #[inline]
1173 pub fn capture(capture: impl FnOnce() -> Instant) -> Option<Instant> {
1174 if cfg!(feature = "profiler") {
1175 Some(capture())
1176 } else {
1177 None
1178 }
1179 }
1180
1181 #[inline]
1183 #[cfg_attr(not(feature = "profiler"), expect(unused_variables))]
1184 pub fn record(&self, at: Instant, source: FrameRequestSource) {
1185 #[cfg(feature = "profiler")]
1186 {
1187 let nanoseconds = at
1188 .saturating_duration_since(self.origin)
1189 .as_nanos()
1190 .min(u128::from((u64::MAX >> 1) - 1)) as u64;
1191 let source_bit = match source {
1192 FrameRequestSource::NativeCallback => 0,
1193 FrameRequestSource::LocalSchedule => 1,
1194 };
1195 self.first_signal.fetch_min(
1196 (nanoseconds << 1) | source_bit,
1197 std::sync::atomic::Ordering::Relaxed,
1198 );
1199 }
1200 }
1201
1202 #[inline]
1204 pub fn take(&self) -> Option<(Instant, FrameRequestSource)> {
1205 #[cfg(feature = "profiler")]
1206 {
1207 let signal = self
1208 .first_signal
1209 .swap(u64::MAX, std::sync::atomic::Ordering::Relaxed);
1210 (signal != u64::MAX).then(|| {
1211 let source = match signal & 1 {
1212 0 => FrameRequestSource::NativeCallback,
1213 _ => FrameRequestSource::LocalSchedule,
1214 };
1215 (self.origin + Duration::from_nanos(signal >> 1), source)
1216 })
1217 }
1218 #[cfg(not(feature = "profiler"))]
1219 {
1220 None
1221 }
1222 }
1223}
1224
1225#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1238pub enum AppLifecyclePhase {
1239 Active,
1241 Inactive,
1243 Background,
1246 Foreground,
1248 Disconnected,
1252}
1253
1254#[derive(Debug, Clone, Default, PartialEq)]
1261pub struct WindowInsets {
1262 pub safe_area: Edges<Pixels>,
1267 pub ime: Edges<Pixels>,
1271 pub corner_radii: Corners<Pixels>,
1276}
1277
1278impl WindowInsets {
1279 pub fn effective(&self) -> Edges<Pixels> {
1281 Edges {
1282 top: self.safe_area.top.max(self.ime.top),
1283 right: self.safe_area.right.max(self.ime.right),
1284 bottom: self.safe_area.bottom.max(self.ime.bottom),
1285 left: self.safe_area.left.max(self.ime.left),
1286 }
1287 }
1288}
1289
1290#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1292pub enum TextInputStateChange {
1293 FocusGained,
1295 FocusLost,
1297 SelectionChanged,
1299 ContentChanged,
1301}
1302
1303#[expect(missing_docs)]
1304pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
1305 fn set_color_mode(&mut self, mode: WindowColorMode) -> anyhow::Result<WindowColorStatus> {
1307 match mode {
1308 WindowColorMode::Default => Ok(self.color_status()),
1309 _ => anyhow::bail!("wide-gamut SDR presentation is unsupported"),
1310 }
1311 }
1312 fn color_status(&self) -> WindowColorStatus {
1314 WindowColorStatus::default()
1315 }
1316
1317 fn bounds(&self) -> Bounds<Pixels>;
1318 fn is_maximized(&self) -> bool;
1319 fn window_bounds(&self) -> WindowBounds;
1320 fn content_size(&self) -> Size<Pixels>;
1321 fn visual_viewport_bounds(&self) -> Bounds<Pixels> {
1327 Bounds::new(Point::default(), self.content_size())
1328 }
1329 fn on_visual_viewport_changed(&self, _callback: Box<dyn FnMut()>) {}
1334 fn prepare_frame(&self) -> bool {
1339 false
1340 }
1341 fn resize(&mut self, size: Size<Pixels>);
1342 fn scale_factor(&self) -> f32;
1343 fn appearance(&self) -> WindowAppearance;
1344 fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
1345 fn mouse_position(&self) -> Point<Pixels>;
1346 fn modifiers(&self) -> Modifiers;
1347 fn capslock(&self) -> Capslock;
1348 fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
1349 fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
1350 fn set_text_input_configuration(&mut self, _configuration: TextInputConfiguration) {}
1355 fn prompt(
1356 &self,
1357 level: PromptLevel,
1358 msg: &str,
1359 detail: Option<&str>,
1360 answers: &[PromptButton],
1361 ) -> Option<oneshot::Receiver<usize>>;
1362 fn activate(&self);
1363 fn request_attention(&self) {}
1365 fn is_active(&self) -> bool;
1366 fn visibility(&self) -> WindowVisibility;
1369 fn is_hovered(&self) -> bool;
1370 fn background_appearance(&self) -> WindowBackgroundAppearance;
1371 fn set_title(&mut self, title: &str);
1372 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
1373 fn minimize(&self);
1374 fn zoom(&self);
1375 fn toggle_fullscreen(&self);
1376 fn is_fullscreen(&self) -> bool;
1377 fn frame_waker(&self) -> Option<Rc<dyn Fn()>> {
1378 None
1379 }
1380 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
1381 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
1382 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
1383 fn on_visibility_change(&self, callback: Box<dyn FnMut(WindowVisibility)>);
1387 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
1388 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
1389 fn on_moved(&self, callback: Box<dyn FnMut()>);
1390 fn on_display_changed(&self, callback: Box<dyn FnMut()>);
1394 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
1395 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
1396 fn on_close(&self, callback: Box<dyn FnOnce()>);
1397 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
1398 fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
1399 fn draw(&self, scene: &Scene);
1400 #[cfg(any(test, feature = "test-support"))]
1402 fn submit_scene_for_validation(&self, _scene: &Scene) -> anyhow::Result<()> {
1403 anyhow::bail!("native window ownership validation is unsupported on this backend")
1404 }
1405 fn schedule_frame(&self) {}
1406 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1407 fn is_subpixel_rendering_supported(&self) -> bool;
1408
1409 fn get_title(&self) -> String {
1411 String::new()
1412 }
1413 fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
1414 None
1415 }
1416 fn tab_bar_visible(&self) -> bool {
1417 false
1418 }
1419 fn set_edited(&mut self, _edited: bool) {}
1420 fn set_document_path(&self, _path: Option<&std::path::Path>) {}
1421 fn toggle_simple_fullscreen(&self) {}
1422 fn is_simple_fullscreen(&self) -> bool {
1423 false
1424 }
1425 #[cfg(target_os = "macos")]
1426 fn set_traffic_light_position(&self, _position: Point<Pixels>) {}
1427 fn show_character_palette(&self) {}
1428 fn titlebar_double_click(&self, _is_resizable: bool, _is_minimizable: bool) {}
1429 fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
1430 fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
1431 fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
1432 fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
1433 fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
1434 fn merge_all_windows(&self) {}
1435 fn move_tab_to_new_window(&self) {}
1436 fn toggle_window_tab_overview(&self) {}
1437 fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
1438
1439 fn native_window_state(&self) -> Option<Vec<u8>> {
1440 None
1441 }
1442 fn restore_native_window_state(&self, _state: &[u8]) {}
1443
1444 #[cfg(target_os = "windows")]
1445 fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
1446
1447 fn inner_window_bounds(&self) -> WindowBounds {
1449 self.window_bounds()
1450 }
1451 fn request_decorations(&self, _decorations: WindowDecorations) {}
1452 fn show_window_menu(&self, _position: Point<Pixels>) {}
1453 fn start_window_move(&self) {}
1454 fn can_start_external_drag(&self) -> bool {
1455 false
1456 }
1457 fn start_external_drag(&self, _payload: &ExternalDragPayload) -> bool {
1458 false
1459 }
1460 fn start_window_resize(&self, _edge: ResizeEdge) {}
1461 fn set_exclusive_zone(&self, _zone: Pixels) {}
1462 #[cfg(all(target_os = "linux", feature = "wayland"))]
1463 fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
1464 fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
1465 fn window_decorations(&self) -> Decorations {
1466 Decorations::Server
1467 }
1468 fn set_app_id(&mut self, _app_id: &str) {}
1469 fn map_window(&mut self) -> anyhow::Result<()> {
1470 Ok(())
1471 }
1472 fn window_controls(&self) -> WindowControls {
1473 WindowControls::default()
1474 }
1475 fn set_client_inset(&self, _inset: Pixels) {}
1476 fn set_client_corner_radii(&self, _radii: Corners<Pixels>) {}
1477 fn gpu_specs(&self) -> Option<GpuSpecs>;
1478
1479 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1480
1481 fn insets(&self) -> WindowInsets {
1486 WindowInsets::default()
1487 }
1488
1489 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1495
1496 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1499
1500 fn set_back_enabled(&self, _enabled: bool) {}
1503
1504 fn show_soft_keyboard(&self) {}
1506
1507 fn hide_soft_keyboard(&self) {}
1509
1510 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1512
1513 fn play_system_bell(&self) {}
1514
1515 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1517
1518 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1520
1521 fn a11y_update_window_bounds(&self) {}
1523
1524 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1525 fn as_test(&mut self) -> Option<&mut TestWindow> {
1526 None
1527 }
1528
1529 #[cfg(any(test, feature = "test-support"))]
1532 fn render_to_linear_p3(&self, _scene: &Scene) -> Result<LinearP3Image> {
1533 anyhow::bail!("linear P3 readback is unsupported on this backend")
1534 }
1535
1536 #[cfg(any(test, feature = "test-support"))]
1540 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1541 anyhow::bail!("render_to_image not implemented for this platform")
1542 }
1543}
1544
1545#[cfg(any(test, feature = "test-support"))]
1549#[derive(Debug)]
1550pub struct LinearP3Image {
1551 pub size: Size<DevicePixels>,
1553 pub pixels: Vec<[u16; 4]>,
1555}
1556
1557#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1559pub trait PlatformHeadlessRenderer {
1560 fn render_scene_to_image(
1562 &mut self,
1563 scene: &Scene,
1564 size: Size<DevicePixels>,
1565 ) -> Result<RgbaImage>;
1566
1567 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1573
1574 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1576}
1577
1578#[doc(hidden)]
1581pub type RunnableVariant = Runnable<RunnableMeta>;
1582
1583#[doc(hidden)]
1584pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1585
1586#[doc(hidden)]
1587pub enum TasksIncluded {
1588 OnlyCompleted,
1589 CompletedAndRunning,
1590}
1591
1592#[doc(hidden)]
1595pub trait PlatformDispatcher: Send + Sync {
1596 fn is_main_thread(&self) -> bool;
1597 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1598 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1599 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1600
1601 fn dispatch_on_main_thread_when_idle(
1602 &self,
1603 runnable: RunnableVariant,
1604 timeout: Option<Duration>,
1605 ) {
1606 let _ = timeout;
1607 self.dispatch_on_main_thread(runnable, Priority::Low);
1608 }
1609
1610 fn idle_time_remaining(&self) -> Option<Duration> {
1611 None
1612 }
1613
1614 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1615
1616 fn now(&self) -> Instant {
1617 Instant::now()
1618 }
1619
1620 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1621 gpui_util::defer(Box::new(|| {}))
1622 }
1623
1624 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1625 ActivityGuard::noop()
1626 }
1627
1628 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1629 fn as_test(&self) -> Option<&TestDispatcher> {
1630 None
1631 }
1632
1633 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1636 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1637 None
1638 }
1639}
1640
1641#[expect(missing_docs)]
1642pub trait PlatformTextSystem: Send + Sync {
1643 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1644 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1646 fn all_font_names(&self) -> Vec<String>;
1648 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1650 fn font_capabilities(&self) -> crate::FontCapabilities {
1652 crate::FontCapabilities::default()
1653 }
1654 fn font_table(
1663 &self,
1664 _font_id: FontId,
1665 _tag: crate::FontTag,
1666 ) -> Result<crate::FontQuery<Option<Vec<u8>>>> {
1667 Ok(crate::FontQuery::Unsupported)
1668 }
1669 fn font_axes(
1672 &self,
1673 font_id: FontId,
1674 ) -> Result<crate::FontQuery<Vec<crate::VariationAxisInfo>>> {
1675 crate::text_system::font_inspection::query_axes(|tag| self.font_table(font_id, tag))
1676 }
1677 fn opentype_features(
1682 &self,
1683 font_id: FontId,
1684 ) -> Result<crate::FontQuery<Vec<crate::OpenTypeFeatureInfo>>> {
1685 crate::text_system::font_inspection::query_features(|tag| self.font_table(font_id, tag))
1686 }
1687 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1689 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1691 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1693 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1695 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1697 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1699 fn rasterize_glyph(
1701 &self,
1702 params: &RenderGlyphParams,
1703 raster_bounds: Bounds<DevicePixels>,
1704 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1705 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1707 fn shape_paragraph(
1709 &self,
1710 _request: crate::ParagraphRequest<'_>,
1711 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1712 anyhow::bail!("paragraph layout is unsupported by this text backend")
1713 }
1714 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1716 -> TextRenderingMode;
1717 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1719 0
1720 }
1721}
1722
1723#[expect(missing_docs)]
1724pub struct NoopTextSystem;
1725
1726#[expect(missing_docs)]
1727impl NoopTextSystem {
1728 #[allow(dead_code)]
1729 pub fn new() -> Self {
1730 Self
1731 }
1732}
1733
1734impl PlatformTextSystem for NoopTextSystem {
1735 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1736 Ok(())
1737 }
1738
1739 fn all_font_names(&self) -> Vec<String> {
1740 Vec::new()
1741 }
1742
1743 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1744 Ok(FontId(1))
1745 }
1746
1747 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1748 FontMetrics {
1749 units_per_em: 1000,
1750 ascent: 1025.0,
1751 descent: -275.0,
1752 line_gap: 0.0,
1753 underline_position: -95.0,
1754 underline_thickness: 60.0,
1755 cap_height: 698.0,
1756 x_height: 516.0,
1757 bounding_box: Bounds {
1758 origin: Point {
1759 x: -260.0,
1760 y: -245.0,
1761 },
1762 size: Size {
1763 width: 1501.0,
1764 height: 1364.0,
1765 },
1766 },
1767 }
1768 }
1769
1770 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1771 Ok(Bounds {
1772 origin: Point { x: 54.0, y: 0.0 },
1773 size: size(392.0, 528.0),
1774 })
1775 }
1776
1777 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1778 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1779 }
1780
1781 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1782 Some(GlyphId(ch.len_utf16() as u32))
1783 }
1784
1785 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1786 Ok(Default::default())
1787 }
1788
1789 fn rasterize_glyph(
1790 &self,
1791 _params: &RenderGlyphParams,
1792 raster_bounds: Bounds<DevicePixels>,
1793 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1794 Ok((raster_bounds.size, Vec::new()))
1795 }
1796
1797 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1798 let mut position = px(0.);
1799 let metrics = self.font_metrics(FontId(0));
1800 let em_width = font_size
1801 * self
1802 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1803 .unwrap()
1804 .width
1805 / metrics.units_per_em as f32;
1806 let mut glyphs = Vec::new();
1807 for (ix, c) in text.char_indices() {
1808 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1809 glyphs.push(ShapedGlyph {
1810 id: glyph,
1811 position: point(position, px(0.)),
1812 index: ix,
1813 is_emoji: glyph.0 == 2,
1814 });
1815 if glyph.0 == 2 {
1816 position += em_width * 2.0;
1817 } else {
1818 position += em_width;
1819 }
1820 } else {
1821 position += em_width
1822 }
1823 }
1824 let mut runs = Vec::default();
1825 if !glyphs.is_empty() {
1826 runs.push(ShapedRun {
1827 font_id: FontId(0),
1828 glyphs,
1829 });
1830 } else {
1831 position = px(0.);
1832 }
1833
1834 LineLayout {
1835 font_size,
1836 width: position,
1837 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1838 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1839 runs,
1840 len: text.len(),
1841 }
1842 }
1843
1844 fn recommended_rendering_mode(
1845 &self,
1846 _font_id: FontId,
1847 _font_size: Pixels,
1848 ) -> TextRenderingMode {
1849 TextRenderingMode::Grayscale
1850 }
1851}
1852
1853#[allow(dead_code)]
1858pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1859 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1860 [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], ];
1874
1875 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1876 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1877
1878 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1879 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1880
1881 [
1882 ratios[0] * NORM13,
1883 ratios[1] * NORM24,
1884 ratios[2] * NORM13,
1885 ratios[3] * NORM24,
1886 ]
1887}
1888
1889#[derive(PartialEq, Eq, Hash, Clone)]
1890#[expect(missing_docs)]
1891pub enum AtlasKey {
1892 Glyph(RenderGlyphParams),
1893 Svg(RenderSvgParams),
1894 Image(RenderImageParams),
1895}
1896
1897impl AtlasKey {
1898 pub fn texture_kind(&self) -> AtlasTextureKind {
1900 match self {
1901 AtlasKey::Glyph(params) => {
1902 if params.is_emoji {
1903 AtlasTextureKind::Polychrome
1904 } else if params.subpixel_rendering {
1905 AtlasTextureKind::Subpixel
1906 } else {
1907 AtlasTextureKind::Monochrome
1908 }
1909 }
1910 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1911 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1912 }
1913 }
1914}
1915
1916impl From<RenderGlyphParams> for AtlasKey {
1917 fn from(params: RenderGlyphParams) -> Self {
1918 Self::Glyph(params)
1919 }
1920}
1921
1922impl From<RenderSvgParams> for AtlasKey {
1923 fn from(params: RenderSvgParams) -> Self {
1924 Self::Svg(params)
1925 }
1926}
1927
1928impl From<RenderImageParams> for AtlasKey {
1929 fn from(params: RenderImageParams) -> Self {
1930 Self::Image(params)
1931 }
1932}
1933
1934#[expect(missing_docs)]
1935pub trait PlatformAtlas {
1936 fn get_or_insert_with<'a>(
1938 &self,
1939 key: AtlasKey,
1940 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1941 ) -> Result<Option<AtlasTile>>;
1942 fn remove(&self, key: &AtlasKey);
1943
1944 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1945 fn contains(&self, _key: &AtlasKey) -> bool {
1946 false
1947 }
1948}
1949
1950#[doc(hidden)]
1951pub trait AtlasBackend {
1952 fn insert(
1953 &mut self,
1954 kind: AtlasTextureKind,
1955 size: Size<DevicePixels>,
1956 bytes: &[u8],
1957 ) -> Result<AtlasTile>;
1958
1959 fn remove(&mut self, tile: AtlasTile);
1960}
1961
1962#[doc(hidden)]
1963pub struct AtlasState<Backend> {
1964 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1965 pub backend: Backend,
1966}
1967
1968impl<Backend> AtlasState<Backend> {
1969 pub fn new(backend: Backend) -> Self {
1970 Self {
1971 tiles_by_key: FxHashMap::default(),
1972 backend,
1973 }
1974 }
1975
1976 pub fn contains(&self, key: &AtlasKey) -> bool {
1977 self.tiles_by_key.contains_key(key)
1978 }
1979
1980 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1981 self.tiles_by_key.clear();
1982 reset_backend(&mut self.backend);
1983 }
1984}
1985
1986impl<Backend: Default> Default for AtlasState<Backend> {
1987 fn default() -> Self {
1988 Self::new(Backend::default())
1989 }
1990}
1991
1992impl<Backend: AtlasBackend> AtlasState<Backend> {
1993 pub fn get_or_insert_with<'a>(
1994 &mut self,
1995 key: AtlasKey,
1996 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1997 ) -> Result<Option<AtlasTile>> {
1998 match self.tiles_by_key.entry(key) {
1999 Entry::Occupied(entry) => Ok(Some(*entry.get())),
2000 Entry::Vacant(entry) => {
2001 profiling::scope!("new tile");
2002 let Some((size, bytes)) = build()? else {
2003 return Ok(None);
2004 };
2005 let tile = self
2006 .backend
2007 .insert(entry.key().texture_kind(), size, &bytes)?;
2008 entry.insert(tile);
2009 Ok(Some(tile))
2010 }
2011 }
2012 }
2013
2014 pub fn remove(&mut self, key: &AtlasKey) {
2015 if let Some(tile) = self.tiles_by_key.remove(key) {
2016 self.backend.remove(tile);
2017 }
2018 }
2019}
2020
2021#[derive(Default)]
2025pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
2026
2027#[doc(hidden)]
2028#[derive(Default)]
2029pub struct HeadlessAtlasBackend {
2030 next_id: u32,
2031}
2032
2033impl AtlasBackend for HeadlessAtlasBackend {
2034 fn insert(
2035 &mut self,
2036 kind: AtlasTextureKind,
2037 size: Size<DevicePixels>,
2038 _bytes: &[u8],
2039 ) -> Result<AtlasTile> {
2040 self.next_id += 1;
2041 let texture_id = self.next_id;
2042 self.next_id += 1;
2043 let tile_id = self.next_id;
2044 Ok(AtlasTile {
2045 texture_id: AtlasTextureId {
2046 index: texture_id,
2047 kind,
2048 },
2049 tile_id: TileId(tile_id),
2050 padding: 0,
2051 bounds: Bounds {
2052 origin: Point::default(),
2053 size,
2054 },
2055 })
2056 }
2057
2058 fn remove(&mut self, _tile: AtlasTile) {}
2059}
2060
2061impl PlatformAtlas for HeadlessAtlas {
2062 fn get_or_insert_with<'a>(
2063 &self,
2064 key: AtlasKey,
2065 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2066 ) -> Result<Option<AtlasTile>> {
2067 self.0.lock().get_or_insert_with(key, build)
2068 }
2069
2070 fn remove(&self, key: &AtlasKey) {
2071 self.0.lock().remove(key);
2072 }
2073
2074 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2075 fn contains(&self, key: &AtlasKey) -> bool {
2076 self.0.lock().contains(key)
2077 }
2078}
2079
2080#[doc(hidden)]
2081pub struct AtlasTextureList<T> {
2082 pub textures: Vec<Option<T>>,
2083 pub free_list: Vec<usize>,
2084}
2085
2086impl<T> Default for AtlasTextureList<T> {
2087 fn default() -> Self {
2088 Self {
2089 textures: Vec::default(),
2090 free_list: Vec::default(),
2091 }
2092 }
2093}
2094
2095impl<T> ops::Index<usize> for AtlasTextureList<T> {
2096 type Output = Option<T>;
2097
2098 fn index(&self, index: usize) -> &Self::Output {
2099 &self.textures[index]
2100 }
2101}
2102
2103impl<T> AtlasTextureList<T> {
2104 #[allow(unused)]
2105 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
2106 self.free_list.clear();
2107 self.textures.drain(..)
2108 }
2109
2110 #[allow(dead_code)]
2111 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
2112 self.textures.iter_mut().flatten()
2113 }
2114}
2115
2116#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2117#[repr(C)]
2118#[expect(missing_docs)]
2119pub struct AtlasTile {
2120 pub texture_id: AtlasTextureId,
2122 pub tile_id: TileId,
2124 pub padding: u32,
2126 pub bounds: Bounds<DevicePixels>,
2128}
2129
2130#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2131#[repr(C)]
2132#[expect(missing_docs)]
2133pub struct AtlasTextureId {
2134 pub index: u32,
2137 pub kind: AtlasTextureKind,
2139}
2140
2141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2142#[repr(C)]
2143#[cfg_attr(
2144 all(
2145 any(target_os = "linux", target_os = "freebsd"),
2146 not(any(feature = "x11", feature = "wayland"))
2147 ),
2148 allow(dead_code)
2149)]
2150#[expect(missing_docs)]
2151pub enum AtlasTextureKind {
2152 Monochrome = 0,
2153 Polychrome = 1,
2154 Subpixel = 2,
2155}
2156
2157#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2158#[repr(C)]
2159#[expect(missing_docs)]
2160pub struct TileId(pub u32);
2161
2162impl From<etagere::AllocId> for TileId {
2163 fn from(id: etagere::AllocId) -> Self {
2164 Self(id.serialize())
2165 }
2166}
2167
2168impl From<TileId> for etagere::AllocId {
2169 fn from(id: TileId) -> Self {
2170 Self::deserialize(id.0)
2171 }
2172}
2173
2174#[expect(missing_docs)]
2175pub struct PlatformInputHandler {
2176 cx: AsyncWindowContext,
2177 handler: Box<dyn InputHandler>,
2178 focus_id: Option<crate::FocusId>,
2179}
2180
2181#[expect(missing_docs)]
2182#[cfg_attr(
2183 all(
2184 any(target_os = "linux", target_os = "freebsd"),
2185 not(any(feature = "x11", feature = "wayland"))
2186 ),
2187 allow(dead_code)
2188)]
2189impl PlatformInputHandler {
2190 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2191 Self {
2192 cx,
2193 handler,
2194 focus_id: None,
2195 }
2196 }
2197
2198 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2199 self.focus_id = Some(focus_id);
2200 self
2201 }
2202
2203 pub fn focus_id(&self) -> Option<crate::FocusId> {
2208 self.focus_id
2209 }
2210
2211 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2212 self.cx
2213 .update(|window, cx| {
2214 self.handler
2215 .selected_text_range(ignore_disabled_input, window, cx)
2216 })
2217 .ok()
2218 .flatten()
2219 }
2220
2221 #[cfg_attr(target_os = "windows", allow(dead_code))]
2222 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2223 self.cx
2224 .update(|window, cx| self.handler.marked_text_range(window, cx))
2225 .ok()
2226 .flatten()
2227 }
2228
2229 #[cfg_attr(
2230 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2231 allow(dead_code)
2232 )]
2233 pub fn text_for_range(
2234 &mut self,
2235 range_utf16: Range<usize>,
2236 adjusted: &mut Option<Range<usize>>,
2237 ) -> Option<String> {
2238 self.cx
2239 .update(|window, cx| {
2240 self.handler
2241 .text_for_range(range_utf16, adjusted, window, cx)
2242 })
2243 .ok()
2244 .flatten()
2245 }
2246
2247 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2248 self.cx
2249 .update(|window, cx| {
2250 self.handler
2251 .replace_text_in_range(replacement_range, text, window, cx);
2252 })
2253 .ok();
2254 }
2255
2256 pub fn replace_and_mark_text_in_range(
2257 &mut self,
2258 range_utf16: Option<Range<usize>>,
2259 new_text: &str,
2260 new_selected_range: Option<Range<usize>>,
2261 ) {
2262 self.cx
2263 .update(|window, cx| {
2264 self.handler.replace_and_mark_text_in_range(
2265 range_utf16,
2266 new_text,
2267 new_selected_range,
2268 window,
2269 cx,
2270 )
2271 })
2272 .ok();
2273 }
2274
2275 #[cfg_attr(target_os = "windows", allow(dead_code))]
2276 pub fn unmark_text(&mut self) {
2277 self.cx
2278 .update(|window, cx| self.handler.unmark_text(window, cx))
2279 .ok();
2280 }
2281
2282 pub fn paste(&mut self, item: ClipboardItem) {
2283 self.cx
2284 .update(|window, cx| self.handler.paste(item, window, cx))
2285 .ok();
2286 }
2287
2288 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2289 self.cx
2290 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2291 .ok()
2292 .flatten()
2293 }
2294
2295 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2296 self.cx
2297 .update(|window, cx| {
2298 self.handler
2299 .selection_bounds_for_range(range_utf16, window, cx)
2300 })
2301 .unwrap_or_default()
2302 }
2303
2304 #[allow(dead_code)]
2305 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2306 self.handler.apple_press_and_hold_enabled()
2307 }
2308
2309 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2310 self.handler.replace_text_in_range(None, input, window, cx);
2311 }
2312
2313 pub fn compute_ime_candidate_bounds(
2314 marked_range: Option<Range<usize>>,
2315 selection: &UTF16Selection,
2316 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2317 ) -> Option<Bounds<Pixels>> {
2318 if let Some(marked_range) = marked_range {
2319 let mut line_start = marked_range.start;
2321
2322 let caret = selection.range.end;
2326 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2327 for i in (marked_range.start..caret).rev() {
2328 if let Some(b) = bounds_for_range(i..i) {
2329 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2330 line_start = i + 1;
2331 break;
2332 }
2333 }
2334 }
2335 }
2336 bounds_for_range(line_start..line_start)
2337 } else {
2338 let offset = if selection.reversed {
2340 selection.range.start
2341 } else {
2342 selection.range.end
2343 };
2344 bounds_for_range(offset..offset)
2345 }
2346 }
2347
2348 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2349 let marked_range = self.handler.marked_text_range(window, cx);
2350 let selection = self.handler.selected_text_range(true, window, cx)?;
2351 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2352 self.handler.bounds_for_range(range, window, cx)
2353 })
2354 }
2355
2356 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2357 let marked_range = self.marked_text_range();
2358 let selection = self.selected_text_range(true)?;
2359 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2360 self.bounds_for_range(range)
2361 })
2362 }
2363
2364 #[allow(unused)]
2365 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2366 self.cx
2367 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2368 .ok()
2369 .flatten()
2370 }
2371
2372 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2374 self.cx
2375 .update(|window, cx| {
2376 self.handler
2377 .set_selected_text_range(range_utf16, window, cx)
2378 })
2379 .ok();
2380 }
2381
2382 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2384 self.cx
2385 .update(|window, cx| self.handler.element_bounds(window, cx))
2386 .ok()
2387 .flatten()
2388 }
2389
2390 pub fn text_length_utf16(&mut self) -> Option<usize> {
2392 self.cx
2393 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2394 .ok()
2395 .flatten()
2396 }
2397
2398 #[allow(dead_code)]
2399 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2400 self.handler.accepts_text_input(window, cx)
2401 }
2402
2403 #[allow(dead_code)]
2404 pub fn query_accepts_text_input(&mut self) -> bool {
2405 self.cx
2406 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2407 .unwrap_or(true)
2408 }
2409
2410 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2415 let focus_id = self.focus_id;
2416 self.cx
2417 .update(|window, cx| {
2418 focus_id.is_some()
2419 && window.focus == focus_id
2420 && self.handler.accepts_text_input(window, cx)
2421 })
2422 .unwrap_or(false)
2423 }
2424
2425 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2431 self.cx
2432 .update(|window, cx| {
2433 !window.has_pending_keystrokes()
2435 && self.handler.prefers_ime_for_printable_keys(window, cx)
2436 })
2437 .unwrap_or(false)
2438 }
2439
2440 pub fn text_input_configuration(
2442 &mut self,
2443 window: &mut Window,
2444 cx: &mut App,
2445 ) -> TextInputConfiguration {
2446 self.handler.text_input_configuration(window, cx)
2447 }
2448
2449 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2451 self.cx
2452 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2453 .ok()
2454 .flatten()
2455 }
2456}
2457
2458#[derive(Debug)]
2461pub struct UTF16Selection {
2462 pub range: Range<usize>,
2465 pub reversed: bool,
2468}
2469
2470pub trait InputHandler: 'static {
2475 fn selected_text_range(
2480 &mut self,
2481 ignore_disabled_input: bool,
2482 window: &mut Window,
2483 cx: &mut App,
2484 ) -> Option<UTF16Selection>;
2485
2486 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2491
2492 fn text_for_range(
2497 &mut self,
2498 range_utf16: Range<usize>,
2499 adjusted_range: &mut Option<Range<usize>>,
2500 window: &mut Window,
2501 cx: &mut App,
2502 ) -> Option<String>;
2503
2504 fn replace_text_in_range(
2509 &mut self,
2510 replacement_range: Option<Range<usize>>,
2511 text: &str,
2512 window: &mut Window,
2513 cx: &mut App,
2514 );
2515
2516 fn replace_and_mark_text_in_range(
2523 &mut self,
2524 range_utf16: Option<Range<usize>>,
2525 new_text: &str,
2526 new_selected_range: Option<Range<usize>>,
2527 window: &mut Window,
2528 cx: &mut App,
2529 );
2530
2531 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2534
2535 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2542 if let Some(text) = item.text() {
2543 self.replace_text_in_range(None, &text, window, cx);
2544 }
2545 }
2546
2547 fn bounds_for_range(
2552 &mut self,
2553 range_utf16: Range<usize>,
2554 window: &mut Window,
2555 cx: &mut App,
2556 ) -> Option<Bounds<Pixels>>;
2557
2558 fn selection_bounds_for_range(
2565 &mut self,
2566 _range_utf16: Range<usize>,
2567 _window: &mut Window,
2568 _cx: &mut App,
2569 ) -> Vec<Bounds<Pixels>> {
2570 Vec::new()
2571 }
2572
2573 fn character_index_for_point(
2577 &mut self,
2578 point: Point<Pixels>,
2579 window: &mut Window,
2580 cx: &mut App,
2581 ) -> Option<usize>;
2582
2583 fn set_selected_text_range(
2593 &mut self,
2594 _range_utf16: Range<usize>,
2595 _window: &mut Window,
2596 _cx: &mut App,
2597 ) {
2598 }
2599
2600 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2607 None
2608 }
2609
2610 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2612 None
2613 }
2614
2615 #[allow(dead_code)]
2620 fn apple_press_and_hold_enabled(&mut self) -> bool {
2621 true
2622 }
2623
2624 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2626 true
2627 }
2628
2629 fn text_input_editable_range(
2640 &mut self,
2641 _window: &mut Window,
2642 _cx: &mut App,
2643 ) -> Option<Range<usize>> {
2644 None
2645 }
2646
2647 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2656 false
2657 }
2658
2659 fn text_input_configuration(
2665 &mut self,
2666 _window: &mut Window,
2667 _cx: &mut App,
2668 ) -> TextInputConfiguration {
2669 TextInputConfiguration::default()
2670 }
2671}
2672
2673#[derive(Clone, Debug, Default, PartialEq, Eq)]
2683pub struct TextInputConfiguration {
2684 pub autocorrect: bool,
2686 pub autocapitalize: Autocapitalize,
2688 pub suggestions: bool,
2690 pub input_action: TextInputAction,
2692}
2693
2694#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2696pub enum Autocapitalize {
2697 #[default]
2699 None,
2700 Words,
2702 Sentences,
2704 Characters,
2706}
2707
2708#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2718pub enum TextInputAction {
2719 #[default]
2721 Unspecified,
2722 Enter,
2724 Done,
2726 Go,
2728 Next,
2730 Previous,
2732 Search,
2734 Send,
2736}
2737
2738#[derive(Debug)]
2740pub struct WindowOptions {
2741 pub window_bounds: Option<WindowBounds>,
2745
2746 pub titlebar: Option<TitlebarOptions>,
2748
2749 pub focus: bool,
2751
2752 pub show: bool,
2754
2755 pub kind: WindowKind,
2757
2758 pub is_movable: bool,
2762
2763 pub app_owns_titlebar_drag: bool,
2775
2776 pub inactive_frame_interval: Option<Duration>,
2795
2796 pub low_power_frame_interval: Option<Duration>,
2804
2805 pub thermal_frame_interval: Option<Duration>,
2813
2814 pub is_resizable: bool,
2816
2817 pub is_minimizable: bool,
2819
2820 pub display_id: Option<DisplayId>,
2823
2824 pub window_background: WindowBackgroundAppearance,
2826
2827 pub app_id: Option<String>,
2829
2830 pub window_min_size: Option<Size<Pixels>>,
2832
2833 pub window_decorations: Option<WindowDecorations>,
2836
2837 pub icon: Option<Arc<image::RgbaImage>>,
2839
2840 pub tabbing_identifier: Option<String>,
2842
2843 pub use_display_p3: bool,
2848}
2849
2850#[derive(Debug)]
2852#[cfg_attr(
2853 all(
2854 any(target_os = "linux", target_os = "freebsd"),
2855 not(any(feature = "x11", feature = "wayland"))
2856 ),
2857 allow(dead_code)
2858)]
2859#[allow(missing_docs)]
2860pub struct WindowParams {
2861 pub bounds: Bounds<Pixels>,
2862
2863 #[cfg_attr(feature = "wayland", allow(dead_code))]
2865 pub titlebar: Option<TitlebarOptions>,
2866
2867 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2869 pub kind: WindowKind,
2870
2871 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2873 pub is_movable: bool,
2874
2875 #[cfg_attr(
2877 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2878 allow(dead_code)
2879 )]
2880 pub app_owns_titlebar_drag: bool,
2881
2882 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2884 pub is_resizable: bool,
2885
2886 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2888 pub is_minimizable: bool,
2889
2890 #[cfg_attr(
2891 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2892 allow(dead_code)
2893 )]
2894 pub focus: bool,
2895
2896 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2897 pub show: bool,
2898
2899 #[cfg_attr(feature = "wayland", allow(dead_code))]
2901 pub icon: Option<Arc<image::RgbaImage>>,
2902
2903 #[cfg_attr(feature = "wayland", allow(dead_code))]
2904 pub display_id: Option<DisplayId>,
2905
2906 #[cfg_attr(feature = "wayland", allow(dead_code))]
2907 pub app_id: Option<String>,
2908
2909 pub window_min_size: Option<Size<Pixels>>,
2910
2911 #[cfg(target_os = "macos")]
2912 pub tabbing_identifier: Option<String>,
2913
2914 #[cfg(target_os = "macos")]
2915 pub use_display_p3: bool,
2916}
2917
2918#[derive(Debug, Copy, Clone, PartialEq)]
2920pub enum WindowBounds {
2921 Windowed(Bounds<Pixels>),
2923 Maximized(Bounds<Pixels>),
2926 Fullscreen(Bounds<Pixels>),
2929}
2930
2931impl Default for WindowBounds {
2932 fn default() -> Self {
2933 WindowBounds::Windowed(Bounds::default())
2934 }
2935}
2936
2937impl WindowBounds {
2938 pub fn get_bounds(&self) -> Bounds<Pixels> {
2940 match self {
2941 WindowBounds::Windowed(bounds) => *bounds,
2942 WindowBounds::Maximized(bounds) => *bounds,
2943 WindowBounds::Fullscreen(bounds) => *bounds,
2944 }
2945 }
2946
2947 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2949 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2950 }
2951}
2952
2953impl Default for WindowOptions {
2954 fn default() -> Self {
2955 Self {
2956 window_bounds: None,
2957 titlebar: Some(TitlebarOptions {
2958 title: Default::default(),
2959 appears_transparent: Default::default(),
2960 traffic_light_position: Default::default(),
2961 }),
2962 focus: true,
2963 show: true,
2964 kind: WindowKind::Normal,
2965 is_movable: true,
2966 app_owns_titlebar_drag: false,
2967 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2968 low_power_frame_interval: Some(Duration::from_micros(33_333)),
2969 thermal_frame_interval: Some(Duration::from_micros(16_667)),
2970 is_resizable: true,
2971 is_minimizable: true,
2972 display_id: None,
2973 window_background: WindowBackgroundAppearance::default(),
2974 icon: None,
2975 app_id: None,
2976 window_min_size: None,
2977 window_decorations: None,
2978 tabbing_identifier: None,
2979 use_display_p3: false,
2980 }
2981 }
2982}
2983
2984#[derive(Debug, Default)]
2986pub struct TitlebarOptions {
2987 pub title: Option<SharedString>,
2989
2990 pub appears_transparent: bool,
2993
2994 pub traffic_light_position: Option<Point<Pixels>>,
2996}
2997
2998#[derive(Clone, Debug, PartialEq, Eq)]
3000pub enum WindowKind {
3001 Normal,
3003
3004 PopUp,
3007
3008 AnchoredPopup(popup::PopupOptions),
3015
3016 Floating,
3018
3019 #[cfg(all(target_os = "linux", feature = "wayland"))]
3022 LayerShell(layer_shell::LayerShellOptions),
3023
3024 Dialog,
3027}
3028
3029#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
3034pub enum WindowAppearance {
3035 #[default]
3039 Light,
3040
3041 VibrantLight,
3045
3046 Dark,
3050
3051 VibrantDark,
3055}
3056
3057#[derive(Copy, Clone, Debug, Default, PartialEq)]
3060pub enum WindowBackgroundAppearance {
3061 #[default]
3069 Opaque,
3070 Transparent,
3072 Blurred,
3076 MicaBackdrop,
3078 MicaAltBackdrop,
3080}
3081
3082#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
3084pub enum TextRenderingMode {
3085 #[default]
3087 PlatformDefault,
3088 Subpixel,
3090 Grayscale,
3092}
3093
3094#[derive(Clone, Debug)]
3096pub struct PathPromptOptions {
3097 pub files: bool,
3099 pub directories: bool,
3101 pub multiple: bool,
3103 pub prompt: Option<SharedString>,
3105}
3106
3107#[derive(Copy, Clone, Debug, PartialEq)]
3109pub enum PromptLevel {
3110 Info,
3112
3113 Warning,
3115
3116 Critical,
3118}
3119
3120#[derive(Clone, Debug, PartialEq)]
3122pub enum PromptButton {
3123 Ok(SharedString),
3125 Cancel(SharedString),
3127 Other(SharedString),
3129}
3130
3131impl PromptButton {
3132 pub fn new(label: impl Into<SharedString>) -> Self {
3134 PromptButton::Other(label.into())
3135 }
3136
3137 pub fn ok(label: impl Into<SharedString>) -> Self {
3139 PromptButton::Ok(label.into())
3140 }
3141
3142 pub fn cancel(label: impl Into<SharedString>) -> Self {
3144 PromptButton::Cancel(label.into())
3145 }
3146
3147 #[allow(dead_code)]
3149 pub fn is_cancel(&self) -> bool {
3150 matches!(self, PromptButton::Cancel(_))
3151 }
3152
3153 pub fn label(&self) -> &SharedString {
3155 match self {
3156 PromptButton::Ok(label) => label,
3157 PromptButton::Cancel(label) => label,
3158 PromptButton::Other(label) => label,
3159 }
3160 }
3161}
3162
3163impl From<&str> for PromptButton {
3164 fn from(value: &str) -> Self {
3165 match value.to_lowercase().as_str() {
3166 "ok" => PromptButton::Ok("OK".into()),
3167 "cancel" => PromptButton::Cancel("Cancel".into()),
3168 _ => PromptButton::Other(SharedString::from(value.to_owned())),
3169 }
3170 }
3171}
3172
3173#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3175pub enum CursorStyle {
3176 #[default]
3178 Arrow,
3179
3180 IBeam,
3183
3184 Crosshair,
3187
3188 ClosedHand,
3191
3192 OpenHand,
3195
3196 PointingHand,
3199
3200 ResizeLeft,
3203
3204 ResizeRight,
3207
3208 ResizeLeftRight,
3211
3212 ResizeUp,
3215
3216 ResizeDown,
3219
3220 ResizeUpDown,
3223
3224 ResizeUpLeftDownRight,
3227
3228 ResizeUpRightDownLeft,
3231
3232 ResizeColumn,
3235
3236 ResizeRow,
3239
3240 IBeamCursorForVerticalLayout,
3243
3244 OperationNotAllowed,
3247
3248 DragLink,
3251
3252 DragCopy,
3255
3256 ContextualMenu,
3259}
3260
3261#[derive(Clone, Debug, Eq, PartialEq)]
3263pub struct ClipboardItem {
3264 pub entries: Vec<ClipboardEntry>,
3266}
3267
3268#[derive(Clone, Debug, PartialEq, Eq)]
3273pub enum ClipboardReadError {
3274 Unavailable,
3278 Denied(String),
3281 UnsupportedContent,
3284}
3285
3286impl std::fmt::Display for ClipboardReadError {
3287 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3288 match self {
3289 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3290 Self::Denied(message) => {
3291 write!(formatter, "clipboard access was denied: {message}")
3292 }
3293 Self::UnsupportedContent => {
3294 formatter.write_str("the clipboard contents are unsupported")
3295 }
3296 }
3297 }
3298}
3299
3300impl std::error::Error for ClipboardReadError {}
3301
3302#[derive(Clone, Debug, Eq, PartialEq)]
3304pub enum ClipboardEntry {
3305 String(ClipboardString),
3307 Image(Image),
3309 ExternalPaths(crate::ExternalPaths),
3311}
3312
3313impl ClipboardItem {
3314 pub fn new_string(text: String) -> Self {
3316 Self {
3317 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3318 }
3319 }
3320
3321 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3323 Self {
3324 entries: vec![ClipboardEntry::String(ClipboardString {
3325 text,
3326 metadata: Some(metadata),
3327 })],
3328 }
3329 }
3330
3331 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3333 Self {
3334 entries: vec![ClipboardEntry::String(
3335 ClipboardString::new(text).with_json_metadata(metadata),
3336 )],
3337 }
3338 }
3339
3340 pub fn new_image(image: &Image) -> Self {
3342 Self {
3343 entries: vec![ClipboardEntry::Image(image.clone())],
3344 }
3345 }
3346
3347 pub fn text(&self) -> Option<String> {
3350 let mut answer = String::new();
3351
3352 for entry in self.entries.iter() {
3353 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3354 answer.push_str(text);
3355 }
3356 }
3357
3358 if answer.is_empty() {
3359 for entry in self.entries.iter() {
3360 if let ClipboardEntry::ExternalPaths(paths) = entry {
3361 for path in &paths.0 {
3362 use std::fmt::Write as _;
3363 _ = write!(answer, "{}", path.display());
3364 }
3365 }
3366 }
3367 }
3368
3369 if !answer.is_empty() {
3370 Some(answer)
3371 } else {
3372 None
3373 }
3374 }
3375
3376 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3378 pub fn metadata(&self) -> Option<&String> {
3379 match self.entries().first() {
3380 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3381 clipboard_string.metadata.as_ref()
3382 }
3383 _ => None,
3384 }
3385 }
3386
3387 pub fn entries(&self) -> &[ClipboardEntry] {
3389 &self.entries
3390 }
3391
3392 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3394 self.entries.into_iter()
3395 }
3396}
3397
3398impl From<ClipboardString> for ClipboardEntry {
3399 fn from(value: ClipboardString) -> Self {
3400 Self::String(value)
3401 }
3402}
3403
3404impl From<String> for ClipboardEntry {
3405 fn from(value: String) -> Self {
3406 Self::from(ClipboardString::from(value))
3407 }
3408}
3409
3410impl From<Image> for ClipboardEntry {
3411 fn from(value: Image) -> Self {
3412 Self::Image(value)
3413 }
3414}
3415
3416impl From<ClipboardEntry> for ClipboardItem {
3417 fn from(value: ClipboardEntry) -> Self {
3418 Self {
3419 entries: vec![value],
3420 }
3421 }
3422}
3423
3424impl From<String> for ClipboardItem {
3425 fn from(value: String) -> Self {
3426 Self::from(ClipboardEntry::from(value))
3427 }
3428}
3429
3430impl From<Image> for ClipboardItem {
3431 fn from(value: Image) -> Self {
3432 Self::from(ClipboardEntry::from(value))
3433 }
3434}
3435
3436#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3438pub enum ImageFormat {
3439 Png,
3444 Jpeg,
3446 Webp,
3448 Gif,
3450 Svg,
3452 Bmp,
3454 Tiff,
3456 Ico,
3458 Pnm,
3460}
3461
3462impl ImageFormat {
3463 pub const fn mime_type(self) -> &'static str {
3465 match self {
3466 ImageFormat::Png => "image/png",
3467 ImageFormat::Jpeg => "image/jpeg",
3468 ImageFormat::Webp => "image/webp",
3469 ImageFormat::Gif => "image/gif",
3470 ImageFormat::Svg => "image/svg+xml",
3471 ImageFormat::Bmp => "image/bmp",
3472 ImageFormat::Tiff => "image/tiff",
3473 ImageFormat::Ico => "image/ico",
3474 ImageFormat::Pnm => "image/x-portable-anymap",
3475 }
3476 }
3477
3478 pub const fn extension(self) -> &'static str {
3480 match self {
3481 ImageFormat::Png => "png",
3482 ImageFormat::Jpeg => "jpg",
3483 ImageFormat::Webp => "webp",
3484 ImageFormat::Gif => "gif",
3485 ImageFormat::Svg => "svg",
3486 ImageFormat::Bmp => "bmp",
3487 ImageFormat::Tiff => "tiff",
3488 ImageFormat::Ico => "ico",
3489 ImageFormat::Pnm => "pnm",
3490 }
3491 }
3492
3493 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3495 use strum::IntoEnumIterator;
3496 Self::iter()
3497 .find(|format| format.mime_type() == mime_type)
3498 .or_else(|| Self::from_mime_type_alias(mime_type))
3499 }
3500
3501 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3505 match mime_type {
3506 "image/jpg" => Some(Self::Jpeg),
3507 "image/tif" => Some(Self::Tiff),
3508 _ => None,
3509 }
3510 }
3511}
3512
3513#[derive(Clone, Debug, PartialEq, Eq)]
3515pub struct Image {
3516 pub format: ImageFormat,
3518 pub bytes: Vec<u8>,
3520 pub id: u64,
3522}
3523
3524pub(crate) fn decode_static_image(
3525 bytes: &[u8],
3526 format: image::ImageFormat,
3527) -> Result<SmallVec<[Frame; 1]>> {
3528 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3529 .into_decoder()
3530 .context("creating image decoder")?;
3531 decode_static_image_from_decoder(decoder)
3532}
3533
3534pub(crate) fn decode_static_image_from_decoder(
3535 mut decoder: impl image::ImageDecoder,
3536) -> Result<SmallVec<[Frame; 1]>> {
3537 let orientation = decoder
3538 .orientation()
3539 .context("reading decoder's orientation")?;
3540 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3541 image.apply_orientation(orientation);
3542
3543 let mut data = image.into_rgba8();
3544 for pixel in data.chunks_exact_mut(4) {
3545 pixel.swap(0, 2);
3546 }
3547
3548 Ok(SmallVec::from_elem(Frame::new(data), 1))
3549}
3550
3551impl Hash for Image {
3552 fn hash<H: Hasher>(&self, state: &mut H) {
3553 state.write_u64(self.id);
3554 }
3555}
3556
3557impl Image {
3558 pub fn empty() -> Self {
3560 Self::from_bytes(ImageFormat::Png, Vec::new())
3561 }
3562
3563 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3565 Self {
3566 id: hash(&bytes),
3567 format,
3568 bytes,
3569 }
3570 }
3571
3572 pub fn id(&self) -> u64 {
3574 self.id
3575 }
3576
3577 pub fn use_render_image(
3579 self: Arc<Self>,
3580 window: &mut Window,
3581 cx: &mut App,
3582 ) -> Option<Arc<RenderImage>> {
3583 ImageSource::Image(self)
3584 .use_data(None, window, cx)
3585 .and_then(|result| result.ok())
3586 }
3587
3588 pub fn get_render_image(
3590 self: Arc<Self>,
3591 window: &mut Window,
3592 cx: &mut App,
3593 ) -> Option<Arc<RenderImage>> {
3594 ImageSource::Image(self)
3595 .get_data(None, window, cx)
3596 .and_then(|result| result.ok())
3597 }
3598
3599 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3601 ImageSource::Image(self).remove_asset(cx);
3602 }
3603
3604 #[cfg(any(test, feature = "test-support"))]
3607 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3608 ImageSource::Image(self.clone()).is_asset_cached(cx)
3609 }
3610
3611 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3613 let frames = match self.format {
3614 ImageFormat::Gif => {
3615 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3616 let mut frames = SmallVec::new();
3617
3618 for frame in decoder.into_frames() {
3619 match frame {
3620 Ok(mut frame) => {
3621 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3623 pixel.swap(0, 2);
3624 }
3625 frames.push(frame);
3626 }
3627 Err(err) => {
3628 log::debug!("Skipping GIF frame due to decode error: {err}");
3629 }
3630 }
3631 }
3632
3633 if frames.is_empty() {
3634 anyhow::bail!("GIF could not be decoded: all frames failed");
3635 }
3636
3637 frames
3638 }
3639 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3640 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3641 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3642 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3643 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3644 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3645 ImageFormat::Svg => {
3646 return svg_renderer
3647 .render_single_frame(&self.bytes, 1.0)
3648 .map_err(Into::into);
3649 }
3650 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3651 };
3652
3653 Ok(Arc::new(RenderImage::new(frames)))
3654 }
3655
3656 pub fn format(&self) -> ImageFormat {
3658 self.format
3659 }
3660
3661 pub fn bytes(&self) -> &[u8] {
3663 self.bytes.as_slice()
3664 }
3665}
3666
3667#[derive(Clone, Debug, Eq, PartialEq)]
3669pub struct ClipboardString {
3670 pub text: String,
3672 pub metadata: Option<String>,
3674}
3675
3676impl ClipboardString {
3677 pub fn new(text: String) -> Self {
3679 Self {
3680 text,
3681 metadata: None,
3682 }
3683 }
3684
3685 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3688 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3689 self
3690 }
3691
3692 pub fn text(&self) -> &String {
3694 &self.text
3695 }
3696
3697 pub fn into_text(self) -> String {
3699 self.text
3700 }
3701
3702 pub fn metadata_json<T>(&self) -> Option<T>
3704 where
3705 T: for<'a> Deserialize<'a>,
3706 {
3707 self.metadata
3708 .as_ref()
3709 .and_then(|m| serde_json::from_str(m).ok())
3710 }
3711
3712 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3713 pub fn text_hash(text: &str) -> u64 {
3715 let mut hasher = SeaHasher::new();
3716 text.hash(&mut hasher);
3717 hasher.finish()
3718 }
3719}
3720
3721impl From<String> for ClipboardString {
3722 fn from(value: String) -> Self {
3723 Self {
3724 text: value,
3725 metadata: None,
3726 }
3727 }
3728}
3729
3730#[cfg(test)]
3731mod image_tests {
3732 use super::*;
3733 use std::sync::Arc;
3734
3735 #[test]
3736 fn test_image_to_image_data_applies_exif_orientation() {
3737 let image = Image::from_bytes(
3738 ImageFormat::Jpeg,
3739 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3740 );
3741
3742 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3743
3744 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3745
3746 let bytes = render_image.as_bytes(0).unwrap();
3747 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3748 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3749 }
3750
3751 #[test]
3752 fn test_svg_image_to_image_data_converts_to_bgra() {
3753 let image = Image::from_bytes(
3754 ImageFormat::Svg,
3755 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3756<rect width="1" height="1" fill="#38BDF8"/>
3757</svg>"##
3758 .to_vec(),
3759 );
3760
3761 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3762 let bytes = render_image.as_bytes(0).unwrap();
3763
3764 for pixel in bytes.chunks_exact(4) {
3765 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3766 }
3767 }
3768}
3769
3770#[cfg(test)]
3771mod atlas_tests {
3772 use super::*;
3773
3774 const TILE_SIZE: Size<DevicePixels> = Size {
3775 width: DevicePixels(1),
3776 height: DevicePixels(1),
3777 };
3778
3779 #[derive(Default)]
3780 struct RecordingAtlasBackend {
3781 insert_calls: u32,
3782 fail_next_insert: bool,
3783 removed_tiles: Vec<AtlasTile>,
3784 }
3785
3786 impl AtlasBackend for RecordingAtlasBackend {
3787 fn insert(
3788 &mut self,
3789 kind: AtlasTextureKind,
3790 size: Size<DevicePixels>,
3791 _bytes: &[u8],
3792 ) -> Result<AtlasTile> {
3793 self.insert_calls += 1;
3794 if std::mem::take(&mut self.fail_next_insert) {
3795 anyhow::bail!("backend failed");
3796 }
3797 Ok(AtlasTile {
3798 texture_id: AtlasTextureId { index: 0, kind },
3799 tile_id: TileId(self.insert_calls),
3800 padding: 0,
3801 bounds: Bounds {
3802 origin: Point::default(),
3803 size,
3804 },
3805 })
3806 }
3807
3808 fn remove(&mut self, tile: AtlasTile) {
3809 self.removed_tiles.push(tile);
3810 }
3811 }
3812
3813 fn image_key(image_id: usize) -> AtlasKey {
3814 AtlasKey::Image(RenderImageParams {
3815 image_id: crate::ImageId(image_id),
3816 frame_index: 0,
3817 })
3818 }
3819
3820 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3821 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3822 }
3823
3824 #[test]
3825 fn only_successful_inserts_are_cached() -> Result<()> {
3826 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3827 let key = image_key(1);
3828
3829 assert_eq!(
3830 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3831 None
3832 );
3833 state
3834 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3835 .expect_err("builder error should propagate");
3836 assert!(!state.contains(&key));
3837 assert_eq!(state.backend.insert_calls, 0);
3838
3839 state.backend.fail_next_insert = true;
3840 state
3841 .get_or_insert_with(key.clone(), &mut build_tile)
3842 .expect_err("backend error should propagate");
3843 assert!(!state.contains(&key));
3844 assert_eq!(state.backend.insert_calls, 1);
3845
3846 let tile = state
3847 .get_or_insert_with(key.clone(), &mut build_tile)?
3848 .context("builder should produce a tile")?;
3849 assert_eq!(tile.texture_id.kind, key.texture_kind());
3850 assert_eq!(
3851 state.get_or_insert_with(key.clone(), &mut || {
3852 anyhow::bail!("cache hit must not call the builder")
3853 })?,
3854 Some(tile)
3855 );
3856 assert!(state.contains(&key));
3857 assert_eq!(state.backend.insert_calls, 2);
3858 Ok(())
3859 }
3860
3861 #[test]
3862 fn remove_and_clear_invalidate_keys() -> Result<()> {
3863 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3864 let key = image_key(1);
3865 let other_key = image_key(2);
3866 let tile = state
3867 .get_or_insert_with(key.clone(), &mut build_tile)?
3868 .context("builder should produce a tile")?;
3869 state
3870 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3871 .context("builder should produce another tile")?;
3872
3873 state.remove(&key);
3874 state.remove(&key);
3875 assert!(!state.contains(&key));
3876 assert!(state.contains(&other_key));
3877 assert_eq!(state.backend.removed_tiles, vec![tile]);
3878
3879 let mut reset_calls = 0;
3880 state.clear(|_| reset_calls += 1);
3881 assert_eq!(reset_calls, 1);
3882 assert!(!state.contains(&other_key));
3883 assert_eq!(state.backend.removed_tiles, vec![tile]);
3884 Ok(())
3885 }
3886}
3887
3888#[cfg(test)]
3889mod frame_signal_tests {
3890 use super::*;
3891
3892 #[cfg(feature = "profiler")]
3893 #[test]
3894 fn coalesced_signals_retain_the_earliest_until_drained() {
3895 for (first_source, later_source) in [
3896 (
3897 FrameRequestSource::NativeCallback,
3898 FrameRequestSource::LocalSchedule,
3899 ),
3900 (
3901 FrameRequestSource::LocalSchedule,
3902 FrameRequestSource::NativeCallback,
3903 ),
3904 ] {
3905 let signal = PlatformFrameSignal::new();
3906 let first =
3907 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3908 assert_eq!(signal.take(), None);
3909 signal.record(first + Duration::from_millis(16), later_source);
3910 signal.record(first, first_source);
3911 signal.record(first + Duration::from_millis(32), later_source);
3912 assert_eq!(signal.take(), Some((first, first_source)));
3913 assert_eq!(signal.take(), None);
3914 let next = first + Duration::from_millis(48);
3915 signal.record(next, later_source);
3916 assert_eq!(signal.take(), Some((next, later_source)));
3917 }
3918 }
3919
3920 #[cfg(not(feature = "profiler"))]
3921 #[test]
3922 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3923 let captured = PlatformFrameSignal::capture(|| {
3924 panic!("profiler-disabled builds must not evaluate the capture closure");
3925 });
3926 assert_eq!(captured, None);
3927 let signal = PlatformFrameSignal::new();
3928 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3929 assert_eq!(signal.take(), None);
3930 }
3931}
3932
3933#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3934mod tests {
3935 use super::*;
3936 use std::collections::HashSet;
3937
3938 #[test]
3939 fn test_refresh_interval_from_hz() {
3940 assert_eq!(
3941 refresh_interval_from_hz(50.0),
3942 Some(Duration::from_millis(20))
3943 );
3944 assert_eq!(
3945 refresh_interval_from_hz(120.0),
3946 Some(Duration::from_secs(1) / 120)
3947 );
3948 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3949 assert_eq!(refresh_interval_from_hz(unknown), None);
3950 }
3951 }
3952
3953 #[test]
3954 fn test_window_button_layout_parse_standard() {
3955 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3956 assert_eq!(
3957 layout.left,
3958 [
3959 Some(WindowButton::Close),
3960 Some(WindowButton::Minimize),
3961 None
3962 ]
3963 );
3964 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3965 }
3966
3967 #[test]
3968 fn test_window_button_layout_parse_right_only() {
3969 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3970 assert_eq!(layout.left, [None, None, None]);
3971 assert_eq!(
3972 layout.right,
3973 [
3974 Some(WindowButton::Minimize),
3975 Some(WindowButton::Maximize),
3976 Some(WindowButton::Close)
3977 ]
3978 );
3979 }
3980
3981 #[test]
3982 fn test_window_button_layout_parse_left_only() {
3983 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3984 assert_eq!(
3985 layout.left,
3986 [
3987 Some(WindowButton::Close),
3988 Some(WindowButton::Minimize),
3989 Some(WindowButton::Maximize)
3990 ]
3991 );
3992 assert_eq!(layout.right, [None, None, None]);
3993 }
3994
3995 #[test]
3996 fn test_window_button_layout_parse_with_whitespace() {
3997 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3998 assert_eq!(
3999 layout.left,
4000 [
4001 Some(WindowButton::Close),
4002 Some(WindowButton::Minimize),
4003 None
4004 ]
4005 );
4006 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4007 }
4008
4009 #[test]
4010 fn test_window_button_layout_parse_empty() {
4011 let layout = WindowButtonLayout::parse("").unwrap();
4012 assert_eq!(layout.left, [None, None, None]);
4013 assert_eq!(layout.right, [None, None, None]);
4014 }
4015
4016 #[test]
4017 fn test_window_button_layout_parse_intentionally_empty() {
4018 let layout = WindowButtonLayout::parse(":").unwrap();
4019 assert_eq!(layout.left, [None, None, None]);
4020 assert_eq!(layout.right, [None, None, None]);
4021 }
4022
4023 #[test]
4024 fn test_window_button_layout_parse_invalid_buttons() {
4025 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
4026 assert_eq!(
4027 layout.left,
4028 [
4029 Some(WindowButton::Close),
4030 Some(WindowButton::Minimize),
4031 None
4032 ]
4033 );
4034 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4035 }
4036
4037 #[test]
4038 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
4039 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
4040 assert_eq!(
4041 layout.right,
4042 [
4043 Some(WindowButton::Close),
4044 Some(WindowButton::Minimize),
4045 None
4046 ]
4047 );
4048 assert_eq!(layout.format(), ":close,minimize");
4049 }
4050
4051 #[test]
4052 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
4053 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
4054 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
4055 assert_eq!(
4056 layout.right,
4057 [
4058 Some(WindowButton::Maximize),
4059 Some(WindowButton::Minimize),
4060 None
4061 ]
4062 );
4063
4064 let button_ids: Vec<_> = layout
4065 .left
4066 .iter()
4067 .chain(layout.right.iter())
4068 .flatten()
4069 .map(WindowButton::id)
4070 .collect();
4071 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
4072 assert_eq!(unique_button_ids.len(), button_ids.len());
4073 assert_eq!(layout.format(), "close:maximize,minimize");
4074 }
4075
4076 #[test]
4077 fn test_window_button_layout_parse_gnome_style() {
4078 let layout = WindowButtonLayout::parse("close").unwrap();
4079 assert_eq!(layout.left, [None, None, None]);
4080 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
4081 }
4082
4083 #[test]
4084 fn test_window_button_layout_parse_elementary_style() {
4085 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
4086 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
4087 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4088 }
4089
4090 #[test]
4091 fn test_window_button_layout_round_trip() {
4092 let cases = [
4093 "close:minimize,maximize",
4094 "minimize,maximize,close:",
4095 ":close",
4096 "close:",
4097 "close:maximize",
4098 ":",
4099 ];
4100
4101 for case in cases {
4102 let layout = WindowButtonLayout::parse(case).unwrap();
4103 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
4104 }
4105 }
4106
4107 #[test]
4108 fn test_window_button_layout_linux_default() {
4109 let layout = WindowButtonLayout::linux_default();
4110 assert_eq!(layout.left, [None, None, None]);
4111 assert_eq!(
4112 layout.right,
4113 [
4114 Some(WindowButton::Minimize),
4115 Some(WindowButton::Maximize),
4116 Some(WindowButton::Close)
4117 ]
4118 );
4119
4120 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
4121 assert_eq!(round_tripped, layout);
4122 }
4123
4124 #[test]
4125 fn test_window_button_layout_parse_all_invalid() {
4126 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
4127 }
4128}