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 gpu_specs(&self) -> Option<GpuSpecs>;
1477
1478 fn update_ime_position(&self, _bounds: Bounds<Pixels>);
1479
1480 fn insets(&self) -> WindowInsets {
1485 WindowInsets::default()
1486 }
1487
1488 fn on_insets_changed(&self, _callback: Box<dyn FnMut(WindowInsets)>) {}
1494
1495 fn set_back_handler(&self, _callback: Box<dyn FnMut()>) {}
1498
1499 fn set_back_enabled(&self, _enabled: bool) {}
1502
1503 fn show_soft_keyboard(&self) {}
1505
1506 fn hide_soft_keyboard(&self) {}
1508
1509 fn text_input_state_changed(&self, _change: TextInputStateChange) {}
1511
1512 fn play_system_bell(&self) {}
1513
1514 fn a11y_init(&self, _callbacks: A11yCallbacks) {}
1516
1517 fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {}
1519
1520 fn a11y_update_window_bounds(&self) {}
1522
1523 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1524 fn as_test(&mut self) -> Option<&mut TestWindow> {
1525 None
1526 }
1527
1528 #[cfg(any(test, feature = "test-support"))]
1531 fn render_to_linear_p3(&self, _scene: &Scene) -> Result<LinearP3Image> {
1532 anyhow::bail!("linear P3 readback is unsupported on this backend")
1533 }
1534
1535 #[cfg(any(test, feature = "test-support"))]
1539 fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
1540 anyhow::bail!("render_to_image not implemented for this platform")
1541 }
1542}
1543
1544#[cfg(any(test, feature = "test-support"))]
1548#[derive(Debug)]
1549pub struct LinearP3Image {
1550 pub size: Size<DevicePixels>,
1552 pub pixels: Vec<[u16; 4]>,
1554}
1555
1556#[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1558pub trait PlatformHeadlessRenderer {
1559 fn render_scene_to_image(
1561 &mut self,
1562 scene: &Scene,
1563 size: Size<DevicePixels>,
1564 ) -> Result<RgbaImage>;
1565
1566 fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> Result<()>;
1572
1573 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
1575}
1576
1577#[doc(hidden)]
1580pub type RunnableVariant = Runnable<RunnableMeta>;
1581
1582#[doc(hidden)]
1583pub type TimerResolutionGuard = gpui_util::Deferred<Box<dyn FnOnce() + Send>>;
1584
1585#[doc(hidden)]
1586pub enum TasksIncluded {
1587 OnlyCompleted,
1588 CompletedAndRunning,
1589}
1590
1591#[doc(hidden)]
1594pub trait PlatformDispatcher: Send + Sync {
1595 fn is_main_thread(&self) -> bool;
1596 fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
1597 fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
1598 fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
1599
1600 fn dispatch_on_main_thread_when_idle(
1601 &self,
1602 runnable: RunnableVariant,
1603 timeout: Option<Duration>,
1604 ) {
1605 let _ = timeout;
1606 self.dispatch_on_main_thread(runnable, Priority::Low);
1607 }
1608
1609 fn idle_time_remaining(&self) -> Option<Duration> {
1610 None
1611 }
1612
1613 fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
1614
1615 fn now(&self) -> Instant {
1616 Instant::now()
1617 }
1618
1619 fn increase_timer_resolution(&self) -> TimerResolutionGuard {
1620 gpui_util::defer(Box::new(|| {}))
1621 }
1622
1623 fn prevent_app_nap(&self, _reason: &str) -> ActivityGuard {
1624 ActivityGuard::noop()
1625 }
1626
1627 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1628 fn as_test(&self) -> Option<&TestDispatcher> {
1629 None
1630 }
1631
1632 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1635 fn as_threaded(&self) -> Option<&ThreadedDispatcher> {
1636 None
1637 }
1638}
1639
1640#[expect(missing_docs)]
1641pub trait PlatformTextSystem: Send + Sync {
1642 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
1643 fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
1645 fn all_font_names(&self) -> Vec<String>;
1647 fn font_id(&self, descriptor: &Font) -> Result<FontId>;
1649 fn font_capabilities(&self) -> crate::FontCapabilities {
1651 crate::FontCapabilities::default()
1652 }
1653 fn font_table(
1662 &self,
1663 _font_id: FontId,
1664 _tag: crate::FontTag,
1665 ) -> Result<crate::FontQuery<Option<Vec<u8>>>> {
1666 Ok(crate::FontQuery::Unsupported)
1667 }
1668 fn font_axes(
1671 &self,
1672 font_id: FontId,
1673 ) -> Result<crate::FontQuery<Vec<crate::VariationAxisInfo>>> {
1674 crate::text_system::font_inspection::query_axes(|tag| self.font_table(font_id, tag))
1675 }
1676 fn opentype_features(
1681 &self,
1682 font_id: FontId,
1683 ) -> Result<crate::FontQuery<Vec<crate::OpenTypeFeatureInfo>>> {
1684 crate::text_system::font_inspection::query_features(|tag| self.font_table(font_id, tag))
1685 }
1686 fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
1688 fn font_metrics(&self, font_id: FontId) -> FontMetrics;
1690 fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
1692 fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
1694 fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
1696 fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
1698 fn rasterize_glyph(
1700 &self,
1701 params: &RenderGlyphParams,
1702 raster_bounds: Bounds<DevicePixels>,
1703 ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
1704 fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
1706 fn shape_paragraph(
1708 &self,
1709 _request: crate::ParagraphRequest<'_>,
1710 ) -> Result<Arc<dyn crate::PlatformParagraphShape>> {
1711 anyhow::bail!("paragraph layout is unsupported by this text backend")
1712 }
1713 fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
1715 -> TextRenderingMode;
1716 fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
1718 0
1719 }
1720}
1721
1722#[expect(missing_docs)]
1723pub struct NoopTextSystem;
1724
1725#[expect(missing_docs)]
1726impl NoopTextSystem {
1727 #[allow(dead_code)]
1728 pub fn new() -> Self {
1729 Self
1730 }
1731}
1732
1733impl PlatformTextSystem for NoopTextSystem {
1734 fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1735 Ok(())
1736 }
1737
1738 fn all_font_names(&self) -> Vec<String> {
1739 Vec::new()
1740 }
1741
1742 fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
1743 Ok(FontId(1))
1744 }
1745
1746 fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
1747 FontMetrics {
1748 units_per_em: 1000,
1749 ascent: 1025.0,
1750 descent: -275.0,
1751 line_gap: 0.0,
1752 underline_position: -95.0,
1753 underline_thickness: 60.0,
1754 cap_height: 698.0,
1755 x_height: 516.0,
1756 bounding_box: Bounds {
1757 origin: Point {
1758 x: -260.0,
1759 y: -245.0,
1760 },
1761 size: Size {
1762 width: 1501.0,
1763 height: 1364.0,
1764 },
1765 },
1766 }
1767 }
1768
1769 fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1770 Ok(Bounds {
1771 origin: Point { x: 54.0, y: 0.0 },
1772 size: size(392.0, 528.0),
1773 })
1774 }
1775
1776 fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1777 Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1778 }
1779
1780 fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1781 Some(GlyphId(ch.len_utf16() as u32))
1782 }
1783
1784 fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1785 Ok(Default::default())
1786 }
1787
1788 fn rasterize_glyph(
1789 &self,
1790 _params: &RenderGlyphParams,
1791 raster_bounds: Bounds<DevicePixels>,
1792 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1793 Ok((raster_bounds.size, Vec::new()))
1794 }
1795
1796 fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1797 let mut position = px(0.);
1798 let metrics = self.font_metrics(FontId(0));
1799 let em_width = font_size
1800 * self
1801 .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1802 .unwrap()
1803 .width
1804 / metrics.units_per_em as f32;
1805 let mut glyphs = Vec::new();
1806 for (ix, c) in text.char_indices() {
1807 if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1808 glyphs.push(ShapedGlyph {
1809 id: glyph,
1810 position: point(position, px(0.)),
1811 index: ix,
1812 is_emoji: glyph.0 == 2,
1813 });
1814 if glyph.0 == 2 {
1815 position += em_width * 2.0;
1816 } else {
1817 position += em_width;
1818 }
1819 } else {
1820 position += em_width
1821 }
1822 }
1823 let mut runs = Vec::default();
1824 if !glyphs.is_empty() {
1825 runs.push(ShapedRun {
1826 font_id: FontId(0),
1827 glyphs,
1828 });
1829 } else {
1830 position = px(0.);
1831 }
1832
1833 LineLayout {
1834 font_size,
1835 width: position,
1836 ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1837 descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1838 runs,
1839 len: text.len(),
1840 }
1841 }
1842
1843 fn recommended_rendering_mode(
1844 &self,
1845 _font_id: FontId,
1846 _font_size: Pixels,
1847 ) -> TextRenderingMode {
1848 TextRenderingMode::Grayscale
1849 }
1850}
1851
1852#[allow(dead_code)]
1857pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1858 const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1859 [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], ];
1873
1874 const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1875 const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1876
1877 let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1878 let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1879
1880 [
1881 ratios[0] * NORM13,
1882 ratios[1] * NORM24,
1883 ratios[2] * NORM13,
1884 ratios[3] * NORM24,
1885 ]
1886}
1887
1888#[derive(PartialEq, Eq, Hash, Clone)]
1889#[expect(missing_docs)]
1890pub enum AtlasKey {
1891 Glyph(RenderGlyphParams),
1892 Svg(RenderSvgParams),
1893 Image(RenderImageParams),
1894}
1895
1896impl AtlasKey {
1897 pub fn texture_kind(&self) -> AtlasTextureKind {
1899 match self {
1900 AtlasKey::Glyph(params) => {
1901 if params.is_emoji {
1902 AtlasTextureKind::Polychrome
1903 } else if params.subpixel_rendering {
1904 AtlasTextureKind::Subpixel
1905 } else {
1906 AtlasTextureKind::Monochrome
1907 }
1908 }
1909 AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1910 AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1911 }
1912 }
1913}
1914
1915impl From<RenderGlyphParams> for AtlasKey {
1916 fn from(params: RenderGlyphParams) -> Self {
1917 Self::Glyph(params)
1918 }
1919}
1920
1921impl From<RenderSvgParams> for AtlasKey {
1922 fn from(params: RenderSvgParams) -> Self {
1923 Self::Svg(params)
1924 }
1925}
1926
1927impl From<RenderImageParams> for AtlasKey {
1928 fn from(params: RenderImageParams) -> Self {
1929 Self::Image(params)
1930 }
1931}
1932
1933#[expect(missing_docs)]
1934pub trait PlatformAtlas {
1935 fn get_or_insert_with<'a>(
1937 &self,
1938 key: AtlasKey,
1939 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1940 ) -> Result<Option<AtlasTile>>;
1941 fn remove(&self, key: &AtlasKey);
1942
1943 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
1944 fn contains(&self, _key: &AtlasKey) -> bool {
1945 false
1946 }
1947}
1948
1949#[doc(hidden)]
1950pub trait AtlasBackend {
1951 fn insert(
1952 &mut self,
1953 kind: AtlasTextureKind,
1954 size: Size<DevicePixels>,
1955 bytes: &[u8],
1956 ) -> Result<AtlasTile>;
1957
1958 fn remove(&mut self, tile: AtlasTile);
1959}
1960
1961#[doc(hidden)]
1962pub struct AtlasState<Backend> {
1963 tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
1964 pub backend: Backend,
1965}
1966
1967impl<Backend> AtlasState<Backend> {
1968 pub fn new(backend: Backend) -> Self {
1969 Self {
1970 tiles_by_key: FxHashMap::default(),
1971 backend,
1972 }
1973 }
1974
1975 pub fn contains(&self, key: &AtlasKey) -> bool {
1976 self.tiles_by_key.contains_key(key)
1977 }
1978
1979 pub fn clear(&mut self, reset_backend: impl FnOnce(&mut Backend)) {
1980 self.tiles_by_key.clear();
1981 reset_backend(&mut self.backend);
1982 }
1983}
1984
1985impl<Backend: Default> Default for AtlasState<Backend> {
1986 fn default() -> Self {
1987 Self::new(Backend::default())
1988 }
1989}
1990
1991impl<Backend: AtlasBackend> AtlasState<Backend> {
1992 pub fn get_or_insert_with<'a>(
1993 &mut self,
1994 key: AtlasKey,
1995 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1996 ) -> Result<Option<AtlasTile>> {
1997 match self.tiles_by_key.entry(key) {
1998 Entry::Occupied(entry) => Ok(Some(*entry.get())),
1999 Entry::Vacant(entry) => {
2000 profiling::scope!("new tile");
2001 let Some((size, bytes)) = build()? else {
2002 return Ok(None);
2003 };
2004 let tile = self
2005 .backend
2006 .insert(entry.key().texture_kind(), size, &bytes)?;
2007 entry.insert(tile);
2008 Ok(Some(tile))
2009 }
2010 }
2011 }
2012
2013 pub fn remove(&mut self, key: &AtlasKey) {
2014 if let Some(tile) = self.tiles_by_key.remove(key) {
2015 self.backend.remove(tile);
2016 }
2017 }
2018}
2019
2020#[derive(Default)]
2024pub struct HeadlessAtlas(parking_lot::Mutex<AtlasState<HeadlessAtlasBackend>>);
2025
2026#[doc(hidden)]
2027#[derive(Default)]
2028pub struct HeadlessAtlasBackend {
2029 next_id: u32,
2030}
2031
2032impl AtlasBackend for HeadlessAtlasBackend {
2033 fn insert(
2034 &mut self,
2035 kind: AtlasTextureKind,
2036 size: Size<DevicePixels>,
2037 _bytes: &[u8],
2038 ) -> Result<AtlasTile> {
2039 self.next_id += 1;
2040 let texture_id = self.next_id;
2041 self.next_id += 1;
2042 let tile_id = self.next_id;
2043 Ok(AtlasTile {
2044 texture_id: AtlasTextureId {
2045 index: texture_id,
2046 kind,
2047 },
2048 tile_id: TileId(tile_id),
2049 padding: 0,
2050 bounds: Bounds {
2051 origin: Point::default(),
2052 size,
2053 },
2054 })
2055 }
2056
2057 fn remove(&mut self, _tile: AtlasTile) {}
2058}
2059
2060impl PlatformAtlas for HeadlessAtlas {
2061 fn get_or_insert_with<'a>(
2062 &self,
2063 key: AtlasKey,
2064 build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
2065 ) -> Result<Option<AtlasTile>> {
2066 self.0.lock().get_or_insert_with(key, build)
2067 }
2068
2069 fn remove(&self, key: &AtlasKey) {
2070 self.0.lock().remove(key);
2071 }
2072
2073 #[cfg(any(test, feature = "test-support", feature = "bench-support"))]
2074 fn contains(&self, key: &AtlasKey) -> bool {
2075 self.0.lock().contains(key)
2076 }
2077}
2078
2079#[doc(hidden)]
2080pub struct AtlasTextureList<T> {
2081 pub textures: Vec<Option<T>>,
2082 pub free_list: Vec<usize>,
2083}
2084
2085impl<T> Default for AtlasTextureList<T> {
2086 fn default() -> Self {
2087 Self {
2088 textures: Vec::default(),
2089 free_list: Vec::default(),
2090 }
2091 }
2092}
2093
2094impl<T> ops::Index<usize> for AtlasTextureList<T> {
2095 type Output = Option<T>;
2096
2097 fn index(&self, index: usize) -> &Self::Output {
2098 &self.textures[index]
2099 }
2100}
2101
2102impl<T> AtlasTextureList<T> {
2103 #[allow(unused)]
2104 pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
2105 self.free_list.clear();
2106 self.textures.drain(..)
2107 }
2108
2109 #[allow(dead_code)]
2110 pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
2111 self.textures.iter_mut().flatten()
2112 }
2113}
2114
2115#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2116#[repr(C)]
2117#[expect(missing_docs)]
2118pub struct AtlasTile {
2119 pub texture_id: AtlasTextureId,
2121 pub tile_id: TileId,
2123 pub padding: u32,
2125 pub bounds: Bounds<DevicePixels>,
2127}
2128
2129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2130#[repr(C)]
2131#[expect(missing_docs)]
2132pub struct AtlasTextureId {
2133 pub index: u32,
2136 pub kind: AtlasTextureKind,
2138}
2139
2140#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2141#[repr(C)]
2142#[cfg_attr(
2143 all(
2144 any(target_os = "linux", target_os = "freebsd"),
2145 not(any(feature = "x11", feature = "wayland"))
2146 ),
2147 allow(dead_code)
2148)]
2149#[expect(missing_docs)]
2150pub enum AtlasTextureKind {
2151 Monochrome = 0,
2152 Polychrome = 1,
2153 Subpixel = 2,
2154}
2155
2156#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2157#[repr(C)]
2158#[expect(missing_docs)]
2159pub struct TileId(pub u32);
2160
2161impl From<etagere::AllocId> for TileId {
2162 fn from(id: etagere::AllocId) -> Self {
2163 Self(id.serialize())
2164 }
2165}
2166
2167impl From<TileId> for etagere::AllocId {
2168 fn from(id: TileId) -> Self {
2169 Self::deserialize(id.0)
2170 }
2171}
2172
2173#[expect(missing_docs)]
2174pub struct PlatformInputHandler {
2175 cx: AsyncWindowContext,
2176 handler: Box<dyn InputHandler>,
2177 focus_id: Option<crate::FocusId>,
2178}
2179
2180#[expect(missing_docs)]
2181#[cfg_attr(
2182 all(
2183 any(target_os = "linux", target_os = "freebsd"),
2184 not(any(feature = "x11", feature = "wayland"))
2185 ),
2186 allow(dead_code)
2187)]
2188impl PlatformInputHandler {
2189 pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
2190 Self {
2191 cx,
2192 handler,
2193 focus_id: None,
2194 }
2195 }
2196
2197 pub(crate) fn with_focus(mut self, focus_id: crate::FocusId) -> Self {
2198 self.focus_id = Some(focus_id);
2199 self
2200 }
2201
2202 pub fn focus_id(&self) -> Option<crate::FocusId> {
2207 self.focus_id
2208 }
2209
2210 pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
2211 self.cx
2212 .update(|window, cx| {
2213 self.handler
2214 .selected_text_range(ignore_disabled_input, window, cx)
2215 })
2216 .ok()
2217 .flatten()
2218 }
2219
2220 #[cfg_attr(target_os = "windows", allow(dead_code))]
2221 pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
2222 self.cx
2223 .update(|window, cx| self.handler.marked_text_range(window, cx))
2224 .ok()
2225 .flatten()
2226 }
2227
2228 #[cfg_attr(
2229 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2230 allow(dead_code)
2231 )]
2232 pub fn text_for_range(
2233 &mut self,
2234 range_utf16: Range<usize>,
2235 adjusted: &mut Option<Range<usize>>,
2236 ) -> Option<String> {
2237 self.cx
2238 .update(|window, cx| {
2239 self.handler
2240 .text_for_range(range_utf16, adjusted, window, cx)
2241 })
2242 .ok()
2243 .flatten()
2244 }
2245
2246 pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
2247 self.cx
2248 .update(|window, cx| {
2249 self.handler
2250 .replace_text_in_range(replacement_range, text, window, cx);
2251 })
2252 .ok();
2253 }
2254
2255 pub fn replace_and_mark_text_in_range(
2256 &mut self,
2257 range_utf16: Option<Range<usize>>,
2258 new_text: &str,
2259 new_selected_range: Option<Range<usize>>,
2260 ) {
2261 self.cx
2262 .update(|window, cx| {
2263 self.handler.replace_and_mark_text_in_range(
2264 range_utf16,
2265 new_text,
2266 new_selected_range,
2267 window,
2268 cx,
2269 )
2270 })
2271 .ok();
2272 }
2273
2274 #[cfg_attr(target_os = "windows", allow(dead_code))]
2275 pub fn unmark_text(&mut self) {
2276 self.cx
2277 .update(|window, cx| self.handler.unmark_text(window, cx))
2278 .ok();
2279 }
2280
2281 pub fn paste(&mut self, item: ClipboardItem) {
2282 self.cx
2283 .update(|window, cx| self.handler.paste(item, window, cx))
2284 .ok();
2285 }
2286
2287 pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
2288 self.cx
2289 .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
2290 .ok()
2291 .flatten()
2292 }
2293
2294 pub fn selection_bounds_for_range(&mut self, range_utf16: Range<usize>) -> Vec<Bounds<Pixels>> {
2295 self.cx
2296 .update(|window, cx| {
2297 self.handler
2298 .selection_bounds_for_range(range_utf16, window, cx)
2299 })
2300 .unwrap_or_default()
2301 }
2302
2303 #[allow(dead_code)]
2304 pub fn apple_press_and_hold_enabled(&mut self) -> bool {
2305 self.handler.apple_press_and_hold_enabled()
2306 }
2307
2308 pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
2309 self.handler.replace_text_in_range(None, input, window, cx);
2310 }
2311
2312 pub fn compute_ime_candidate_bounds(
2313 marked_range: Option<Range<usize>>,
2314 selection: &UTF16Selection,
2315 mut bounds_for_range: impl FnMut(Range<usize>) -> Option<Bounds<Pixels>>,
2316 ) -> Option<Bounds<Pixels>> {
2317 if let Some(marked_range) = marked_range {
2318 let mut line_start = marked_range.start;
2320
2321 let caret = selection.range.end;
2325 if let Some(caret_bounds) = bounds_for_range(caret..caret) {
2326 for i in (marked_range.start..caret).rev() {
2327 if let Some(b) = bounds_for_range(i..i) {
2328 if (b.origin.y - caret_bounds.origin.y).abs() > px(0.1) {
2329 line_start = i + 1;
2330 break;
2331 }
2332 }
2333 }
2334 }
2335 bounds_for_range(line_start..line_start)
2336 } else {
2337 let offset = if selection.reversed {
2339 selection.range.start
2340 } else {
2341 selection.range.end
2342 };
2343 bounds_for_range(offset..offset)
2344 }
2345 }
2346
2347 pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
2348 let marked_range = self.handler.marked_text_range(window, cx);
2349 let selection = self.handler.selected_text_range(true, window, cx)?;
2350 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2351 self.handler.bounds_for_range(range, window, cx)
2352 })
2353 }
2354
2355 pub fn ime_candidate_bounds(&mut self) -> Option<Bounds<Pixels>> {
2356 let marked_range = self.marked_text_range();
2357 let selection = self.selected_text_range(true)?;
2358 Self::compute_ime_candidate_bounds(marked_range, &selection, |range| {
2359 self.bounds_for_range(range)
2360 })
2361 }
2362
2363 #[allow(unused)]
2364 pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
2365 self.cx
2366 .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
2367 .ok()
2368 .flatten()
2369 }
2370
2371 pub fn set_selected_text_range(&mut self, range_utf16: Range<usize>) {
2373 self.cx
2374 .update(|window, cx| {
2375 self.handler
2376 .set_selected_text_range(range_utf16, window, cx)
2377 })
2378 .ok();
2379 }
2380
2381 pub fn element_bounds(&mut self) -> Option<Bounds<Pixels>> {
2383 self.cx
2384 .update(|window, cx| self.handler.element_bounds(window, cx))
2385 .ok()
2386 .flatten()
2387 }
2388
2389 pub fn text_length_utf16(&mut self) -> Option<usize> {
2391 self.cx
2392 .update(|window, cx| self.handler.text_length_utf16(window, cx))
2393 .ok()
2394 .flatten()
2395 }
2396
2397 #[allow(dead_code)]
2398 pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
2399 self.handler.accepts_text_input(window, cx)
2400 }
2401
2402 #[allow(dead_code)]
2403 pub fn query_accepts_text_input(&mut self) -> bool {
2404 self.cx
2405 .update(|window, cx| self.handler.accepts_text_input(window, cx))
2406 .unwrap_or(true)
2407 }
2408
2409 pub fn query_accepts_focused_text_input(&mut self) -> bool {
2414 let focus_id = self.focus_id;
2415 self.cx
2416 .update(|window, cx| {
2417 focus_id.is_some()
2418 && window.focus == focus_id
2419 && self.handler.accepts_text_input(window, cx)
2420 })
2421 .unwrap_or(false)
2422 }
2423
2424 pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
2430 self.cx
2431 .update(|window, cx| {
2432 !window.has_pending_keystrokes()
2434 && self.handler.prefers_ime_for_printable_keys(window, cx)
2435 })
2436 .unwrap_or(false)
2437 }
2438
2439 pub fn text_input_configuration(
2441 &mut self,
2442 window: &mut Window,
2443 cx: &mut App,
2444 ) -> TextInputConfiguration {
2445 self.handler.text_input_configuration(window, cx)
2446 }
2447
2448 pub fn text_input_editable_range(&mut self) -> Option<Range<usize>> {
2450 self.cx
2451 .update(|window, cx| self.handler.text_input_editable_range(window, cx))
2452 .ok()
2453 .flatten()
2454 }
2455}
2456
2457#[derive(Debug)]
2460pub struct UTF16Selection {
2461 pub range: Range<usize>,
2464 pub reversed: bool,
2467}
2468
2469pub trait InputHandler: 'static {
2474 fn selected_text_range(
2479 &mut self,
2480 ignore_disabled_input: bool,
2481 window: &mut Window,
2482 cx: &mut App,
2483 ) -> Option<UTF16Selection>;
2484
2485 fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
2490
2491 fn text_for_range(
2496 &mut self,
2497 range_utf16: Range<usize>,
2498 adjusted_range: &mut Option<Range<usize>>,
2499 window: &mut Window,
2500 cx: &mut App,
2501 ) -> Option<String>;
2502
2503 fn replace_text_in_range(
2508 &mut self,
2509 replacement_range: Option<Range<usize>>,
2510 text: &str,
2511 window: &mut Window,
2512 cx: &mut App,
2513 );
2514
2515 fn replace_and_mark_text_in_range(
2522 &mut self,
2523 range_utf16: Option<Range<usize>>,
2524 new_text: &str,
2525 new_selected_range: Option<Range<usize>>,
2526 window: &mut Window,
2527 cx: &mut App,
2528 );
2529
2530 fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
2533
2534 fn paste(&mut self, item: ClipboardItem, window: &mut Window, cx: &mut App) {
2541 if let Some(text) = item.text() {
2542 self.replace_text_in_range(None, &text, window, cx);
2543 }
2544 }
2545
2546 fn bounds_for_range(
2551 &mut self,
2552 range_utf16: Range<usize>,
2553 window: &mut Window,
2554 cx: &mut App,
2555 ) -> Option<Bounds<Pixels>>;
2556
2557 fn selection_bounds_for_range(
2564 &mut self,
2565 _range_utf16: Range<usize>,
2566 _window: &mut Window,
2567 _cx: &mut App,
2568 ) -> Vec<Bounds<Pixels>> {
2569 Vec::new()
2570 }
2571
2572 fn character_index_for_point(
2576 &mut self,
2577 point: Point<Pixels>,
2578 window: &mut Window,
2579 cx: &mut App,
2580 ) -> Option<usize>;
2581
2582 fn set_selected_text_range(
2592 &mut self,
2593 _range_utf16: Range<usize>,
2594 _window: &mut Window,
2595 _cx: &mut App,
2596 ) {
2597 }
2598
2599 fn element_bounds(&mut self, _window: &mut Window, _cx: &mut App) -> Option<Bounds<Pixels>> {
2606 None
2607 }
2608
2609 fn text_length_utf16(&mut self, _window: &mut Window, _cx: &mut App) -> Option<usize> {
2611 None
2612 }
2613
2614 #[allow(dead_code)]
2619 fn apple_press_and_hold_enabled(&mut self) -> bool {
2620 true
2621 }
2622
2623 fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2625 true
2626 }
2627
2628 fn text_input_editable_range(
2639 &mut self,
2640 _window: &mut Window,
2641 _cx: &mut App,
2642 ) -> Option<Range<usize>> {
2643 None
2644 }
2645
2646 fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
2655 false
2656 }
2657
2658 fn text_input_configuration(
2664 &mut self,
2665 _window: &mut Window,
2666 _cx: &mut App,
2667 ) -> TextInputConfiguration {
2668 TextInputConfiguration::default()
2669 }
2670}
2671
2672#[derive(Clone, Debug, Default, PartialEq, Eq)]
2682pub struct TextInputConfiguration {
2683 pub autocorrect: bool,
2685 pub autocapitalize: Autocapitalize,
2687 pub suggestions: bool,
2689 pub input_action: TextInputAction,
2691}
2692
2693#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2695pub enum Autocapitalize {
2696 #[default]
2698 None,
2699 Words,
2701 Sentences,
2703 Characters,
2705}
2706
2707#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2717pub enum TextInputAction {
2718 #[default]
2720 Unspecified,
2721 Enter,
2723 Done,
2725 Go,
2727 Next,
2729 Previous,
2731 Search,
2733 Send,
2735}
2736
2737#[derive(Debug)]
2739pub struct WindowOptions {
2740 pub window_bounds: Option<WindowBounds>,
2744
2745 pub titlebar: Option<TitlebarOptions>,
2747
2748 pub focus: bool,
2750
2751 pub show: bool,
2753
2754 pub kind: WindowKind,
2756
2757 pub is_movable: bool,
2761
2762 pub app_owns_titlebar_drag: bool,
2774
2775 pub inactive_frame_interval: Option<Duration>,
2796
2797 pub is_resizable: bool,
2799
2800 pub is_minimizable: bool,
2802
2803 pub display_id: Option<DisplayId>,
2806
2807 pub window_background: WindowBackgroundAppearance,
2809
2810 pub app_id: Option<String>,
2812
2813 pub window_min_size: Option<Size<Pixels>>,
2815
2816 pub window_decorations: Option<WindowDecorations>,
2819
2820 pub icon: Option<Arc<image::RgbaImage>>,
2822
2823 pub tabbing_identifier: Option<String>,
2825
2826 pub use_display_p3: bool,
2831}
2832
2833#[derive(Debug)]
2835#[cfg_attr(
2836 all(
2837 any(target_os = "linux", target_os = "freebsd"),
2838 not(any(feature = "x11", feature = "wayland"))
2839 ),
2840 allow(dead_code)
2841)]
2842#[allow(missing_docs)]
2843pub struct WindowParams {
2844 pub bounds: Bounds<Pixels>,
2845
2846 #[cfg_attr(feature = "wayland", allow(dead_code))]
2848 pub titlebar: Option<TitlebarOptions>,
2849
2850 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2852 pub kind: WindowKind,
2853
2854 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2856 pub is_movable: bool,
2857
2858 #[cfg_attr(
2860 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2861 allow(dead_code)
2862 )]
2863 pub app_owns_titlebar_drag: bool,
2864
2865 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2867 pub is_resizable: bool,
2868
2869 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2871 pub is_minimizable: bool,
2872
2873 #[cfg_attr(
2874 any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
2875 allow(dead_code)
2876 )]
2877 pub focus: bool,
2878
2879 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2880 pub show: bool,
2881
2882 #[cfg_attr(feature = "wayland", allow(dead_code))]
2884 pub icon: Option<Arc<image::RgbaImage>>,
2885
2886 #[cfg_attr(feature = "wayland", allow(dead_code))]
2887 pub display_id: Option<DisplayId>,
2888
2889 #[cfg_attr(feature = "wayland", allow(dead_code))]
2890 pub app_id: Option<String>,
2891
2892 pub window_min_size: Option<Size<Pixels>>,
2893
2894 #[cfg(target_os = "macos")]
2895 pub tabbing_identifier: Option<String>,
2896
2897 #[cfg(target_os = "macos")]
2898 pub use_display_p3: bool,
2899}
2900
2901#[derive(Debug, Copy, Clone, PartialEq)]
2903pub enum WindowBounds {
2904 Windowed(Bounds<Pixels>),
2906 Maximized(Bounds<Pixels>),
2909 Fullscreen(Bounds<Pixels>),
2912}
2913
2914impl Default for WindowBounds {
2915 fn default() -> Self {
2916 WindowBounds::Windowed(Bounds::default())
2917 }
2918}
2919
2920impl WindowBounds {
2921 pub fn get_bounds(&self) -> Bounds<Pixels> {
2923 match self {
2924 WindowBounds::Windowed(bounds) => *bounds,
2925 WindowBounds::Maximized(bounds) => *bounds,
2926 WindowBounds::Fullscreen(bounds) => *bounds,
2927 }
2928 }
2929
2930 pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
2932 WindowBounds::Windowed(Bounds::centered(None, size, cx))
2933 }
2934}
2935
2936impl Default for WindowOptions {
2937 fn default() -> Self {
2938 Self {
2939 window_bounds: None,
2940 titlebar: Some(TitlebarOptions {
2941 title: Default::default(),
2942 appears_transparent: Default::default(),
2943 traffic_light_position: Default::default(),
2944 }),
2945 focus: true,
2946 show: true,
2947 kind: WindowKind::Normal,
2948 is_movable: true,
2949 app_owns_titlebar_drag: false,
2950 inactive_frame_interval: Some(Duration::from_micros(33_333)),
2951 is_resizable: true,
2952 is_minimizable: true,
2953 display_id: None,
2954 window_background: WindowBackgroundAppearance::default(),
2955 icon: None,
2956 app_id: None,
2957 window_min_size: None,
2958 window_decorations: None,
2959 tabbing_identifier: None,
2960 use_display_p3: false,
2961 }
2962 }
2963}
2964
2965#[derive(Debug, Default)]
2967pub struct TitlebarOptions {
2968 pub title: Option<SharedString>,
2970
2971 pub appears_transparent: bool,
2974
2975 pub traffic_light_position: Option<Point<Pixels>>,
2977}
2978
2979#[derive(Clone, Debug, PartialEq, Eq)]
2981pub enum WindowKind {
2982 Normal,
2984
2985 PopUp,
2988
2989 AnchoredPopup(popup::PopupOptions),
2996
2997 Floating,
2999
3000 #[cfg(all(target_os = "linux", feature = "wayland"))]
3003 LayerShell(layer_shell::LayerShellOptions),
3004
3005 Dialog,
3008}
3009
3010#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
3015pub enum WindowAppearance {
3016 #[default]
3020 Light,
3021
3022 VibrantLight,
3026
3027 Dark,
3031
3032 VibrantDark,
3036}
3037
3038#[derive(Copy, Clone, Debug, Default, PartialEq)]
3041pub enum WindowBackgroundAppearance {
3042 #[default]
3050 Opaque,
3051 Transparent,
3053 Blurred,
3057 MicaBackdrop,
3059 MicaAltBackdrop,
3061}
3062
3063#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
3065pub enum TextRenderingMode {
3066 #[default]
3068 PlatformDefault,
3069 Subpixel,
3071 Grayscale,
3073}
3074
3075#[derive(Clone, Debug)]
3077pub struct PathPromptOptions {
3078 pub files: bool,
3080 pub directories: bool,
3082 pub multiple: bool,
3084 pub prompt: Option<SharedString>,
3086}
3087
3088#[derive(Copy, Clone, Debug, PartialEq)]
3090pub enum PromptLevel {
3091 Info,
3093
3094 Warning,
3096
3097 Critical,
3099}
3100
3101#[derive(Clone, Debug, PartialEq)]
3103pub enum PromptButton {
3104 Ok(SharedString),
3106 Cancel(SharedString),
3108 Other(SharedString),
3110}
3111
3112impl PromptButton {
3113 pub fn new(label: impl Into<SharedString>) -> Self {
3115 PromptButton::Other(label.into())
3116 }
3117
3118 pub fn ok(label: impl Into<SharedString>) -> Self {
3120 PromptButton::Ok(label.into())
3121 }
3122
3123 pub fn cancel(label: impl Into<SharedString>) -> Self {
3125 PromptButton::Cancel(label.into())
3126 }
3127
3128 #[allow(dead_code)]
3130 pub fn is_cancel(&self) -> bool {
3131 matches!(self, PromptButton::Cancel(_))
3132 }
3133
3134 pub fn label(&self) -> &SharedString {
3136 match self {
3137 PromptButton::Ok(label) => label,
3138 PromptButton::Cancel(label) => label,
3139 PromptButton::Other(label) => label,
3140 }
3141 }
3142}
3143
3144impl From<&str> for PromptButton {
3145 fn from(value: &str) -> Self {
3146 match value.to_lowercase().as_str() {
3147 "ok" => PromptButton::Ok("OK".into()),
3148 "cancel" => PromptButton::Cancel("Cancel".into()),
3149 _ => PromptButton::Other(SharedString::from(value.to_owned())),
3150 }
3151 }
3152}
3153
3154#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
3156pub enum CursorStyle {
3157 #[default]
3159 Arrow,
3160
3161 IBeam,
3164
3165 Crosshair,
3168
3169 ClosedHand,
3172
3173 OpenHand,
3176
3177 PointingHand,
3180
3181 ResizeLeft,
3184
3185 ResizeRight,
3188
3189 ResizeLeftRight,
3192
3193 ResizeUp,
3196
3197 ResizeDown,
3200
3201 ResizeUpDown,
3204
3205 ResizeUpLeftDownRight,
3208
3209 ResizeUpRightDownLeft,
3212
3213 ResizeColumn,
3216
3217 ResizeRow,
3220
3221 IBeamCursorForVerticalLayout,
3224
3225 OperationNotAllowed,
3228
3229 DragLink,
3232
3233 DragCopy,
3236
3237 ContextualMenu,
3240}
3241
3242#[derive(Clone, Debug, Eq, PartialEq)]
3244pub struct ClipboardItem {
3245 pub entries: Vec<ClipboardEntry>,
3247}
3248
3249#[derive(Clone, Debug, PartialEq, Eq)]
3254pub enum ClipboardReadError {
3255 Unavailable,
3259 Denied(String),
3262 UnsupportedContent,
3265}
3266
3267impl std::fmt::Display for ClipboardReadError {
3268 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3269 match self {
3270 Self::Unavailable => formatter.write_str("the clipboard is unavailable"),
3271 Self::Denied(message) => {
3272 write!(formatter, "clipboard access was denied: {message}")
3273 }
3274 Self::UnsupportedContent => {
3275 formatter.write_str("the clipboard contents are unsupported")
3276 }
3277 }
3278 }
3279}
3280
3281impl std::error::Error for ClipboardReadError {}
3282
3283#[derive(Clone, Debug, Eq, PartialEq)]
3285pub enum ClipboardEntry {
3286 String(ClipboardString),
3288 Image(Image),
3290 ExternalPaths(crate::ExternalPaths),
3292}
3293
3294impl ClipboardItem {
3295 pub fn new_string(text: String) -> Self {
3297 Self {
3298 entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
3299 }
3300 }
3301
3302 pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
3304 Self {
3305 entries: vec![ClipboardEntry::String(ClipboardString {
3306 text,
3307 metadata: Some(metadata),
3308 })],
3309 }
3310 }
3311
3312 pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
3314 Self {
3315 entries: vec![ClipboardEntry::String(
3316 ClipboardString::new(text).with_json_metadata(metadata),
3317 )],
3318 }
3319 }
3320
3321 pub fn new_image(image: &Image) -> Self {
3323 Self {
3324 entries: vec![ClipboardEntry::Image(image.clone())],
3325 }
3326 }
3327
3328 pub fn text(&self) -> Option<String> {
3331 let mut answer = String::new();
3332
3333 for entry in self.entries.iter() {
3334 if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
3335 answer.push_str(text);
3336 }
3337 }
3338
3339 if answer.is_empty() {
3340 for entry in self.entries.iter() {
3341 if let ClipboardEntry::ExternalPaths(paths) = entry {
3342 for path in &paths.0 {
3343 use std::fmt::Write as _;
3344 _ = write!(answer, "{}", path.display());
3345 }
3346 }
3347 }
3348 }
3349
3350 if !answer.is_empty() {
3351 Some(answer)
3352 } else {
3353 None
3354 }
3355 }
3356
3357 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
3359 pub fn metadata(&self) -> Option<&String> {
3360 match self.entries().first() {
3361 Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
3362 clipboard_string.metadata.as_ref()
3363 }
3364 _ => None,
3365 }
3366 }
3367
3368 pub fn entries(&self) -> &[ClipboardEntry] {
3370 &self.entries
3371 }
3372
3373 pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
3375 self.entries.into_iter()
3376 }
3377}
3378
3379impl From<ClipboardString> for ClipboardEntry {
3380 fn from(value: ClipboardString) -> Self {
3381 Self::String(value)
3382 }
3383}
3384
3385impl From<String> for ClipboardEntry {
3386 fn from(value: String) -> Self {
3387 Self::from(ClipboardString::from(value))
3388 }
3389}
3390
3391impl From<Image> for ClipboardEntry {
3392 fn from(value: Image) -> Self {
3393 Self::Image(value)
3394 }
3395}
3396
3397impl From<ClipboardEntry> for ClipboardItem {
3398 fn from(value: ClipboardEntry) -> Self {
3399 Self {
3400 entries: vec![value],
3401 }
3402 }
3403}
3404
3405impl From<String> for ClipboardItem {
3406 fn from(value: String) -> Self {
3407 Self::from(ClipboardEntry::from(value))
3408 }
3409}
3410
3411impl From<Image> for ClipboardItem {
3412 fn from(value: Image) -> Self {
3413 Self::from(ClipboardEntry::from(value))
3414 }
3415}
3416
3417#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
3419pub enum ImageFormat {
3420 Png,
3425 Jpeg,
3427 Webp,
3429 Gif,
3431 Svg,
3433 Bmp,
3435 Tiff,
3437 Ico,
3439 Pnm,
3441}
3442
3443impl ImageFormat {
3444 pub const fn mime_type(self) -> &'static str {
3446 match self {
3447 ImageFormat::Png => "image/png",
3448 ImageFormat::Jpeg => "image/jpeg",
3449 ImageFormat::Webp => "image/webp",
3450 ImageFormat::Gif => "image/gif",
3451 ImageFormat::Svg => "image/svg+xml",
3452 ImageFormat::Bmp => "image/bmp",
3453 ImageFormat::Tiff => "image/tiff",
3454 ImageFormat::Ico => "image/ico",
3455 ImageFormat::Pnm => "image/x-portable-anymap",
3456 }
3457 }
3458
3459 pub const fn extension(self) -> &'static str {
3461 match self {
3462 ImageFormat::Png => "png",
3463 ImageFormat::Jpeg => "jpg",
3464 ImageFormat::Webp => "webp",
3465 ImageFormat::Gif => "gif",
3466 ImageFormat::Svg => "svg",
3467 ImageFormat::Bmp => "bmp",
3468 ImageFormat::Tiff => "tiff",
3469 ImageFormat::Ico => "ico",
3470 ImageFormat::Pnm => "pnm",
3471 }
3472 }
3473
3474 pub fn from_mime_type(mime_type: &str) -> Option<Self> {
3476 use strum::IntoEnumIterator;
3477 Self::iter()
3478 .find(|format| format.mime_type() == mime_type)
3479 .or_else(|| Self::from_mime_type_alias(mime_type))
3480 }
3481
3482 fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
3486 match mime_type {
3487 "image/jpg" => Some(Self::Jpeg),
3488 "image/tif" => Some(Self::Tiff),
3489 _ => None,
3490 }
3491 }
3492}
3493
3494#[derive(Clone, Debug, PartialEq, Eq)]
3496pub struct Image {
3497 pub format: ImageFormat,
3499 pub bytes: Vec<u8>,
3501 pub id: u64,
3503}
3504
3505pub(crate) fn decode_static_image(
3506 bytes: &[u8],
3507 format: image::ImageFormat,
3508) -> Result<SmallVec<[Frame; 1]>> {
3509 let decoder = image::ImageReader::with_format(Cursor::new(bytes), format)
3510 .into_decoder()
3511 .context("creating image decoder")?;
3512 decode_static_image_from_decoder(decoder)
3513}
3514
3515pub(crate) fn decode_static_image_from_decoder(
3516 mut decoder: impl image::ImageDecoder,
3517) -> Result<SmallVec<[Frame; 1]>> {
3518 let orientation = decoder
3519 .orientation()
3520 .context("reading decoder's orientation")?;
3521 let mut image = DynamicImage::from_decoder(decoder).context("decoding image")?;
3522 image.apply_orientation(orientation);
3523
3524 let mut data = image.into_rgba8();
3525 for pixel in data.chunks_exact_mut(4) {
3526 pixel.swap(0, 2);
3527 }
3528
3529 Ok(SmallVec::from_elem(Frame::new(data), 1))
3530}
3531
3532impl Hash for Image {
3533 fn hash<H: Hasher>(&self, state: &mut H) {
3534 state.write_u64(self.id);
3535 }
3536}
3537
3538impl Image {
3539 pub fn empty() -> Self {
3541 Self::from_bytes(ImageFormat::Png, Vec::new())
3542 }
3543
3544 pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
3546 Self {
3547 id: hash(&bytes),
3548 format,
3549 bytes,
3550 }
3551 }
3552
3553 pub fn id(&self) -> u64 {
3555 self.id
3556 }
3557
3558 pub fn use_render_image(
3560 self: Arc<Self>,
3561 window: &mut Window,
3562 cx: &mut App,
3563 ) -> Option<Arc<RenderImage>> {
3564 ImageSource::Image(self)
3565 .use_data(None, window, cx)
3566 .and_then(|result| result.ok())
3567 }
3568
3569 pub fn get_render_image(
3571 self: Arc<Self>,
3572 window: &mut Window,
3573 cx: &mut App,
3574 ) -> Option<Arc<RenderImage>> {
3575 ImageSource::Image(self)
3576 .get_data(None, window, cx)
3577 .and_then(|result| result.ok())
3578 }
3579
3580 pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
3582 ImageSource::Image(self).remove_asset(cx);
3583 }
3584
3585 #[cfg(any(test, feature = "test-support"))]
3588 pub fn is_asset_cached(self: &Arc<Self>, cx: &App) -> bool {
3589 ImageSource::Image(self.clone()).is_asset_cached(cx)
3590 }
3591
3592 pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
3594 let frames = match self.format {
3595 ImageFormat::Gif => {
3596 let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
3597 let mut frames = SmallVec::new();
3598
3599 for frame in decoder.into_frames() {
3600 match frame {
3601 Ok(mut frame) => {
3602 for pixel in frame.buffer_mut().chunks_exact_mut(4) {
3604 pixel.swap(0, 2);
3605 }
3606 frames.push(frame);
3607 }
3608 Err(err) => {
3609 log::debug!("Skipping GIF frame due to decode error: {err}");
3610 }
3611 }
3612 }
3613
3614 if frames.is_empty() {
3615 anyhow::bail!("GIF could not be decoded: all frames failed");
3616 }
3617
3618 frames
3619 }
3620 ImageFormat::Png => decode_static_image(&self.bytes, image::ImageFormat::Png)?,
3621 ImageFormat::Jpeg => decode_static_image(&self.bytes, image::ImageFormat::Jpeg)?,
3622 ImageFormat::Webp => decode_static_image(&self.bytes, image::ImageFormat::WebP)?,
3623 ImageFormat::Bmp => decode_static_image(&self.bytes, image::ImageFormat::Bmp)?,
3624 ImageFormat::Tiff => decode_static_image(&self.bytes, image::ImageFormat::Tiff)?,
3625 ImageFormat::Ico => decode_static_image(&self.bytes, image::ImageFormat::Ico)?,
3626 ImageFormat::Svg => {
3627 return svg_renderer
3628 .render_single_frame(&self.bytes, 1.0)
3629 .map_err(Into::into);
3630 }
3631 ImageFormat::Pnm => decode_static_image(&self.bytes, image::ImageFormat::Pnm)?,
3632 };
3633
3634 Ok(Arc::new(RenderImage::new(frames)))
3635 }
3636
3637 pub fn format(&self) -> ImageFormat {
3639 self.format
3640 }
3641
3642 pub fn bytes(&self) -> &[u8] {
3644 self.bytes.as_slice()
3645 }
3646}
3647
3648#[derive(Clone, Debug, Eq, PartialEq)]
3650pub struct ClipboardString {
3651 pub text: String,
3653 pub metadata: Option<String>,
3655}
3656
3657impl ClipboardString {
3658 pub fn new(text: String) -> Self {
3660 Self {
3661 text,
3662 metadata: None,
3663 }
3664 }
3665
3666 pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
3669 self.metadata = Some(serde_json::to_string(&metadata).unwrap());
3670 self
3671 }
3672
3673 pub fn text(&self) -> &String {
3675 &self.text
3676 }
3677
3678 pub fn into_text(self) -> String {
3680 self.text
3681 }
3682
3683 pub fn metadata_json<T>(&self) -> Option<T>
3685 where
3686 T: for<'a> Deserialize<'a>,
3687 {
3688 self.metadata
3689 .as_ref()
3690 .and_then(|m| serde_json::from_str(m).ok())
3691 }
3692
3693 #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
3694 pub fn text_hash(text: &str) -> u64 {
3696 let mut hasher = SeaHasher::new();
3697 text.hash(&mut hasher);
3698 hasher.finish()
3699 }
3700}
3701
3702impl From<String> for ClipboardString {
3703 fn from(value: String) -> Self {
3704 Self {
3705 text: value,
3706 metadata: None,
3707 }
3708 }
3709}
3710
3711#[cfg(test)]
3712mod image_tests {
3713 use super::*;
3714 use std::sync::Arc;
3715
3716 #[test]
3717 fn test_image_to_image_data_applies_exif_orientation() {
3718 let image = Image::from_bytes(
3719 ImageFormat::Jpeg,
3720 include_bytes!("../examples/image/exif-orientation-rotate-180.jpg").to_vec(),
3721 );
3722
3723 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3724
3725 assert_eq!(render_image.size(0), size(16.into(), 32.into()));
3726
3727 let bytes = render_image.as_bytes(0).unwrap();
3728 assert_eq!(&bytes[..4], &[255, 255, 255, 255]);
3729 assert_eq!(&bytes[(16 * 32 - 1) * 4..], &[0, 0, 0, 255]);
3730 }
3731
3732 #[test]
3733 fn test_svg_image_to_image_data_converts_to_bgra() {
3734 let image = Image::from_bytes(
3735 ImageFormat::Svg,
3736 br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
3737<rect width="1" height="1" fill="#38BDF8"/>
3738</svg>"##
3739 .to_vec(),
3740 );
3741
3742 let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
3743 let bytes = render_image.as_bytes(0).unwrap();
3744
3745 for pixel in bytes.chunks_exact(4) {
3746 assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
3747 }
3748 }
3749}
3750
3751#[cfg(test)]
3752mod atlas_tests {
3753 use super::*;
3754
3755 const TILE_SIZE: Size<DevicePixels> = Size {
3756 width: DevicePixels(1),
3757 height: DevicePixels(1),
3758 };
3759
3760 #[derive(Default)]
3761 struct RecordingAtlasBackend {
3762 insert_calls: u32,
3763 fail_next_insert: bool,
3764 removed_tiles: Vec<AtlasTile>,
3765 }
3766
3767 impl AtlasBackend for RecordingAtlasBackend {
3768 fn insert(
3769 &mut self,
3770 kind: AtlasTextureKind,
3771 size: Size<DevicePixels>,
3772 _bytes: &[u8],
3773 ) -> Result<AtlasTile> {
3774 self.insert_calls += 1;
3775 if std::mem::take(&mut self.fail_next_insert) {
3776 anyhow::bail!("backend failed");
3777 }
3778 Ok(AtlasTile {
3779 texture_id: AtlasTextureId { index: 0, kind },
3780 tile_id: TileId(self.insert_calls),
3781 padding: 0,
3782 bounds: Bounds {
3783 origin: Point::default(),
3784 size,
3785 },
3786 })
3787 }
3788
3789 fn remove(&mut self, tile: AtlasTile) {
3790 self.removed_tiles.push(tile);
3791 }
3792 }
3793
3794 fn image_key(image_id: usize) -> AtlasKey {
3795 AtlasKey::Image(RenderImageParams {
3796 image_id: crate::ImageId(image_id),
3797 frame_index: 0,
3798 })
3799 }
3800
3801 fn build_tile() -> Result<Option<(Size<DevicePixels>, Cow<'static, [u8]>)>> {
3802 Ok(Some((TILE_SIZE, Cow::Borrowed(&[0, 0, 0, 255]))))
3803 }
3804
3805 #[test]
3806 fn only_successful_inserts_are_cached() -> Result<()> {
3807 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3808 let key = image_key(1);
3809
3810 assert_eq!(
3811 state.get_or_insert_with(key.clone(), &mut || Ok(None))?,
3812 None
3813 );
3814 state
3815 .get_or_insert_with(key.clone(), &mut || anyhow::bail!("builder failed"))
3816 .expect_err("builder error should propagate");
3817 assert!(!state.contains(&key));
3818 assert_eq!(state.backend.insert_calls, 0);
3819
3820 state.backend.fail_next_insert = true;
3821 state
3822 .get_or_insert_with(key.clone(), &mut build_tile)
3823 .expect_err("backend error should propagate");
3824 assert!(!state.contains(&key));
3825 assert_eq!(state.backend.insert_calls, 1);
3826
3827 let tile = state
3828 .get_or_insert_with(key.clone(), &mut build_tile)?
3829 .context("builder should produce a tile")?;
3830 assert_eq!(tile.texture_id.kind, key.texture_kind());
3831 assert_eq!(
3832 state.get_or_insert_with(key.clone(), &mut || {
3833 anyhow::bail!("cache hit must not call the builder")
3834 })?,
3835 Some(tile)
3836 );
3837 assert!(state.contains(&key));
3838 assert_eq!(state.backend.insert_calls, 2);
3839 Ok(())
3840 }
3841
3842 #[test]
3843 fn remove_and_clear_invalidate_keys() -> Result<()> {
3844 let mut state = AtlasState::new(RecordingAtlasBackend::default());
3845 let key = image_key(1);
3846 let other_key = image_key(2);
3847 let tile = state
3848 .get_or_insert_with(key.clone(), &mut build_tile)?
3849 .context("builder should produce a tile")?;
3850 state
3851 .get_or_insert_with(other_key.clone(), &mut build_tile)?
3852 .context("builder should produce another tile")?;
3853
3854 state.remove(&key);
3855 state.remove(&key);
3856 assert!(!state.contains(&key));
3857 assert!(state.contains(&other_key));
3858 assert_eq!(state.backend.removed_tiles, vec![tile]);
3859
3860 let mut reset_calls = 0;
3861 state.clear(|_| reset_calls += 1);
3862 assert_eq!(reset_calls, 1);
3863 assert!(!state.contains(&other_key));
3864 assert_eq!(state.backend.removed_tiles, vec![tile]);
3865 Ok(())
3866 }
3867}
3868
3869#[cfg(test)]
3870mod frame_signal_tests {
3871 use super::*;
3872
3873 #[cfg(feature = "profiler")]
3874 #[test]
3875 fn coalesced_signals_retain_the_earliest_until_drained() {
3876 for (first_source, later_source) in [
3877 (
3878 FrameRequestSource::NativeCallback,
3879 FrameRequestSource::LocalSchedule,
3880 ),
3881 (
3882 FrameRequestSource::LocalSchedule,
3883 FrameRequestSource::NativeCallback,
3884 ),
3885 ] {
3886 let signal = PlatformFrameSignal::new();
3887 let first =
3888 PlatformFrameSignal::capture(Instant::now).expect("profiling captures requests");
3889 assert_eq!(signal.take(), None);
3890 signal.record(first + Duration::from_millis(16), later_source);
3891 signal.record(first, first_source);
3892 signal.record(first + Duration::from_millis(32), later_source);
3893 assert_eq!(signal.take(), Some((first, first_source)));
3894 assert_eq!(signal.take(), None);
3895 let next = first + Duration::from_millis(48);
3896 signal.record(next, later_source);
3897 assert_eq!(signal.take(), Some((next, later_source)));
3898 }
3899 }
3900
3901 #[cfg(not(feature = "profiler"))]
3902 #[test]
3903 fn disabled_profiling_does_not_capture_or_store_timestamps() {
3904 let captured = PlatformFrameSignal::capture(|| {
3905 panic!("profiler-disabled builds must not evaluate the capture closure");
3906 });
3907 assert_eq!(captured, None);
3908 let signal = PlatformFrameSignal::new();
3909 signal.record(Instant::now(), FrameRequestSource::LocalSchedule);
3910 assert_eq!(signal.take(), None);
3911 }
3912}
3913
3914#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
3915mod tests {
3916 use super::*;
3917 use std::collections::HashSet;
3918
3919 #[test]
3920 fn test_refresh_interval_from_hz() {
3921 assert_eq!(
3922 refresh_interval_from_hz(50.0),
3923 Some(Duration::from_millis(20))
3924 );
3925 assert_eq!(
3926 refresh_interval_from_hz(120.0),
3927 Some(Duration::from_secs(1) / 120)
3928 );
3929 for unknown in [0.0, -60.0, f64::NAN, f64::INFINITY] {
3930 assert_eq!(refresh_interval_from_hz(unknown), None);
3931 }
3932 }
3933
3934 #[test]
3935 fn test_window_button_layout_parse_standard() {
3936 let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
3937 assert_eq!(
3938 layout.left,
3939 [
3940 Some(WindowButton::Close),
3941 Some(WindowButton::Minimize),
3942 None
3943 ]
3944 );
3945 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3946 }
3947
3948 #[test]
3949 fn test_window_button_layout_parse_right_only() {
3950 let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
3951 assert_eq!(layout.left, [None, None, None]);
3952 assert_eq!(
3953 layout.right,
3954 [
3955 Some(WindowButton::Minimize),
3956 Some(WindowButton::Maximize),
3957 Some(WindowButton::Close)
3958 ]
3959 );
3960 }
3961
3962 #[test]
3963 fn test_window_button_layout_parse_left_only() {
3964 let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
3965 assert_eq!(
3966 layout.left,
3967 [
3968 Some(WindowButton::Close),
3969 Some(WindowButton::Minimize),
3970 Some(WindowButton::Maximize)
3971 ]
3972 );
3973 assert_eq!(layout.right, [None, None, None]);
3974 }
3975
3976 #[test]
3977 fn test_window_button_layout_parse_with_whitespace() {
3978 let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
3979 assert_eq!(
3980 layout.left,
3981 [
3982 Some(WindowButton::Close),
3983 Some(WindowButton::Minimize),
3984 None
3985 ]
3986 );
3987 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
3988 }
3989
3990 #[test]
3991 fn test_window_button_layout_parse_empty() {
3992 let layout = WindowButtonLayout::parse("").unwrap();
3993 assert_eq!(layout.left, [None, None, None]);
3994 assert_eq!(layout.right, [None, None, None]);
3995 }
3996
3997 #[test]
3998 fn test_window_button_layout_parse_intentionally_empty() {
3999 let layout = WindowButtonLayout::parse(":").unwrap();
4000 assert_eq!(layout.left, [None, None, None]);
4001 assert_eq!(layout.right, [None, None, None]);
4002 }
4003
4004 #[test]
4005 fn test_window_button_layout_parse_invalid_buttons() {
4006 let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
4007 assert_eq!(
4008 layout.left,
4009 [
4010 Some(WindowButton::Close),
4011 Some(WindowButton::Minimize),
4012 None
4013 ]
4014 );
4015 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4016 }
4017
4018 #[test]
4019 fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
4020 let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
4021 assert_eq!(
4022 layout.right,
4023 [
4024 Some(WindowButton::Close),
4025 Some(WindowButton::Minimize),
4026 None
4027 ]
4028 );
4029 assert_eq!(layout.format(), ":close,minimize");
4030 }
4031
4032 #[test]
4033 fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
4034 let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
4035 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
4036 assert_eq!(
4037 layout.right,
4038 [
4039 Some(WindowButton::Maximize),
4040 Some(WindowButton::Minimize),
4041 None
4042 ]
4043 );
4044
4045 let button_ids: Vec<_> = layout
4046 .left
4047 .iter()
4048 .chain(layout.right.iter())
4049 .flatten()
4050 .map(WindowButton::id)
4051 .collect();
4052 let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
4053 assert_eq!(unique_button_ids.len(), button_ids.len());
4054 assert_eq!(layout.format(), "close:maximize,minimize");
4055 }
4056
4057 #[test]
4058 fn test_window_button_layout_parse_gnome_style() {
4059 let layout = WindowButtonLayout::parse("close").unwrap();
4060 assert_eq!(layout.left, [None, None, None]);
4061 assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
4062 }
4063
4064 #[test]
4065 fn test_window_button_layout_parse_elementary_style() {
4066 let layout = WindowButtonLayout::parse("close:maximize").unwrap();
4067 assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
4068 assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
4069 }
4070
4071 #[test]
4072 fn test_window_button_layout_round_trip() {
4073 let cases = [
4074 "close:minimize,maximize",
4075 "minimize,maximize,close:",
4076 ":close",
4077 "close:",
4078 "close:maximize",
4079 ":",
4080 ];
4081
4082 for case in cases {
4083 let layout = WindowButtonLayout::parse(case).unwrap();
4084 assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
4085 }
4086 }
4087
4088 #[test]
4089 fn test_window_button_layout_linux_default() {
4090 let layout = WindowButtonLayout::linux_default();
4091 assert_eq!(layout.left, [None, None, None]);
4092 assert_eq!(
4093 layout.right,
4094 [
4095 Some(WindowButton::Minimize),
4096 Some(WindowButton::Maximize),
4097 Some(WindowButton::Close)
4098 ]
4099 );
4100
4101 let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
4102 assert_eq!(round_tripped, layout);
4103 }
4104
4105 #[test]
4106 fn test_window_button_layout_parse_all_invalid() {
4107 assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
4108 }
4109}