Skip to main content

rgpui/
platform.rs

1mod app_menu;
2mod keyboard;
3mod keystroke;
4
5#[cfg(all(target_os = "linux", feature = "wayland"))]
6#[expect(missing_docs)]
7pub mod layer_shell;
8
9#[cfg(any(test, feature = "test-support"))]
10mod test;
11
12#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
13mod visual_test;
14
15#[cfg(all(
16    feature = "screen-capture",
17    any(target_os = "windows", target_os = "linux", target_os = "freebsd",)
18))]
19pub mod scap_screen_capture;
20
21#[cfg(all(
22    any(target_os = "windows", target_os = "linux"),
23    feature = "screen-capture"
24))]
25pub(crate) type PlatformScreenCaptureFrame = scap::frame::Frame;
26#[cfg(not(feature = "screen-capture"))]
27pub(crate) type PlatformScreenCaptureFrame = ();
28#[cfg(all(target_os = "macos", feature = "screen-capture"))]
29pub(crate) type PlatformScreenCaptureFrame = core_video::image_buffer::CVImageBuffer;
30
31use crate::scheduler::Instant;
32pub use crate::scheduler::RunnableMeta;
33use crate::{
34    Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
35    DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
36    ForegroundExecutor, GlyphId, GpuSpecs, Hsla, ImageSource, Keymap, LineLayout, Pixels,
37    PlatformInput, Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams,
38    RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer,
39    SystemWindowTab, Task, ThreadTaskTimings, Tray, TrayIconEvent, TrayMenuItem, Window,
40    WindowControlArea, hash, point, px, size,
41};
42use anyhow::Result;
43#[cfg(any(target_os = "linux", target_os = "freebsd"))]
44use anyhow::bail;
45use async_task::Runnable;
46use futures::channel::oneshot;
47#[cfg(any(test, feature = "test-support"))]
48use image::RgbaImage;
49use image::codecs::gif::GifDecoder;
50use image::{AnimationDecoder as _, Frame};
51use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
52use schemars::JsonSchema;
53use seahash::SeaHasher;
54use serde::{Deserialize, Serialize};
55use smallvec::SmallVec;
56use std::borrow::Cow;
57use std::hash::{Hash, Hasher};
58use std::io::Cursor;
59use std::ops;
60use std::time::Duration;
61use std::{
62    fmt::{self, Debug},
63    ops::Range,
64    path::{Path, PathBuf},
65    rc::Rc,
66    sync::Arc,
67};
68use strum::EnumIter;
69use uuid::Uuid;
70
71pub use app_menu::*;
72pub use keyboard::*;
73pub use keystroke::*;
74
75#[cfg(any(test, feature = "test-support"))]
76pub(crate) use test::*;
77
78#[cfg(any(test, feature = "test-support"))]
79pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream};
80
81#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
82pub use visual_test::VisualTestPlatform;
83
84// TODO(jk): return an enum instead of a string
85/// 猜测当前 Linux 桌面环境使用的显示服务器类型
86/// 不会实际尝试连接显示服务器
87#[cfg(any(target_os = "linux", target_os = "freebsd"))]
88#[inline]
89pub fn guess_compositor() -> &'static str {
90    if std::env::var_os("ZED_HEADLESS").is_some() {
91        return "Headless";
92    }
93
94    #[cfg(feature = "wayland")]
95    let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
96    #[cfg(not(feature = "wayland"))]
97    let wayland_display: Option<std::ffi::OsString> = None;
98
99    #[cfg(feature = "x11")]
100    let x11_display = std::env::var_os("DISPLAY");
101    #[cfg(not(feature = "x11"))]
102    let x11_display: Option<std::ffi::OsString> = None;
103
104    let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
105    let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
106
107    if use_wayland {
108        "Wayland"
109    } else if use_x11 {
110        "X11"
111    } else {
112        "Headless"
113    }
114}
115
116/// 平台抽象层 trait,定义了与操作系统交互所需的所有接口
117/// 各平台(Windows、macOS、Linux、Web)需实现此 trait 以提供平台特定功能
118#[expect(missing_docs)]
119pub trait Platform: 'static {
120    fn background_executor(&self) -> BackgroundExecutor;
121    fn foreground_executor(&self) -> ForegroundExecutor;
122    fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
123
124    fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
125    fn quit(&self);
126    fn restart(&self, binary_path: Option<PathBuf>);
127    fn activate(&self, ignoring_other_apps: bool);
128    fn hide(&self);
129    fn hide_other_apps(&self);
130    fn unhide_other_apps(&self);
131
132    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
133    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
134    fn active_window(&self) -> Option<AnyWindowHandle>;
135    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
136        None
137    }
138
139    fn is_screen_capture_supported(&self) -> bool {
140        false
141    }
142
143    fn screen_capture_sources(
144        &self,
145    ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
146        let (sources_tx, sources_rx) = oneshot::channel();
147        sources_tx
148            .send(Err(anyhow::anyhow!(
149                "gpui was compiled without the screen-capture feature"
150            )))
151            .ok();
152        sources_rx
153    }
154
155    fn open_window(
156        &self,
157        handle: AnyWindowHandle,
158        options: WindowParams,
159    ) -> anyhow::Result<Box<dyn PlatformWindow>>;
160
161    /// 返回应用窗口的外观
162    fn window_appearance(&self) -> WindowAppearance;
163
164    /// 返回窗口按钮布局配置(部分平台支持)
165    fn button_layout(&self) -> Option<WindowButtonLayout> {
166        None
167    }
168
169    fn open_url(&self, url: &str);
170    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>);
171    fn register_url_scheme(&self, url: &str) -> Task<Result<()>>;
172
173    fn prompt_for_paths(
174        &self,
175        options: PathPromptOptions,
176    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
177    fn prompt_for_new_path(
178        &self,
179        directory: &Path,
180        suggested_name: Option<&str>,
181    ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
182    fn can_select_mixed_files_and_dirs(&self) -> bool;
183    fn reveal_path(&self, path: &Path);
184    fn open_with_system(&self, path: &Path);
185
186    fn on_quit(&self, callback: Box<dyn FnMut()>);
187    fn on_reopen(&self, callback: Box<dyn FnMut()>);
188
189    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
190    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
191        None
192    }
193
194    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
195    fn perform_dock_menu_action(&self, _action: usize) {}
196    fn add_recent_document(&self, _path: &Path) {}
197    fn update_jump_list(
198        &self,
199        _menus: Vec<MenuItem>,
200        _entries: Vec<SmallVec<[PathBuf; 2]>>,
201    ) -> Task<Vec<SmallVec<[PathBuf; 2]>>> {
202        Task::ready(Vec::new())
203    }
204
205    // 系统托盘相关方法
206    /// 设置系统托盘图标
207    fn set_tray_icon(&self, _icon: Option<&[u8]>) {}
208    /// 设置系统托盘菜单项
209    fn set_tray_menu(&self, _menu: Vec<TrayMenuItem>) {}
210    /// 设置系统托盘工具提示文本
211    fn set_tray_tooltip(&self, _tooltip: &str) {}
212    /// 启用或禁用托盘面板模式
213    fn set_tray_panel_mode(&self, _enabled: bool) {}
214    /// 获取托盘图标的屏幕边界坐标
215    fn get_tray_icon_bounds(&self) -> Option<Bounds<Pixels>> {
216        None
217    }
218    /// 注册托盘图标事件回调
219    fn on_tray_icon_event(&self, _callback: Box<dyn FnMut(TrayIconEvent)>) {}
220    /// 注册托盘菜单项点击事件回调
221    fn on_tray_menu_action(&self, _callback: Box<dyn FnMut(SharedString)>) {}
222
223    // 保留旧的 set_tray 方法以向后兼容
224    fn set_tray(&self, _tray: Tray, _menu: Option<Vec<MenuItem>>, _keymap: &Keymap) {}
225
226    fn set_keep_alive_without_windows(&self, _keep_alive: bool) {}
227
228    /// 注册全局快捷键
229    fn register_global_hotkey(&self, _id: u32, _keystroke: &Keystroke) -> Result<()> {
230        Err(anyhow::anyhow!(
231            "Global hotkeys not supported on this platform"
232        ))
233    }
234
235    /// 注销全局快捷键
236    fn unregister_global_hotkey(&self, _id: u32) {}
237
238    /// 注册全局快捷键事件回调
239    fn on_global_hotkey(&self, _callback: Box<dyn FnMut(u32)>) {}
240
241    /// 显示系统原生通知
242    fn show_notification(&self, _title: &str, _body: &str) -> Result<()> {
243        Err(anyhow::anyhow!(
244            "Notifications not supported on this platform"
245        ))
246    }
247
248    /// 设置开机自启动
249    fn set_auto_launch(&self, _app_id: &str, _enabled: bool) -> Result<()> {
250        Err(anyhow::anyhow!(
251            "Auto launch not supported on this platform"
252        ))
253    }
254
255    /// 检查开机自启动是否已启用
256    fn is_auto_launch_enabled(&self, _app_id: &str) -> bool {
257        false
258    }
259
260    /// 获取当前聚焦窗口信息
261    fn focused_window_info(&self) -> Option<FocusedWindowInfo> {
262        None
263    }
264
265    /// 获取辅助功能权限状态
266    fn accessibility_status(&self) -> PermissionStatus {
267        PermissionStatus::Granted
268    }
269
270    /// 请求辅助功能权限
271    fn request_accessibility_permission(&self) {}
272
273    /// 获取麦克风权限状态
274    fn microphone_status(&self) -> PermissionStatus {
275        PermissionStatus::Granted
276    }
277
278    /// 请求麦克风权限
279    fn request_microphone_permission(&self, callback: Box<dyn FnOnce(bool)>) {
280        callback(true);
281    }
282
283    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
284    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
285    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
286
287    fn thermal_state(&self) -> ThermalState;
288    fn on_thermal_state_change(&self, callback: Box<dyn FnMut()>);
289
290    fn compositor_name(&self) -> &'static str {
291        ""
292    }
293    fn app_path(&self) -> Result<PathBuf>;
294    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
295
296    fn set_cursor_style(&self, style: CursorStyle);
297
298    /// Hides the mouse cursor until the user moves the mouse over one of
299    /// this application's windows.
300    fn hide_cursor_until_mouse_moves(&self);
301
302    /// Returns whether the mouse cursor is currently visible.
303    fn is_cursor_visible(&self) -> bool;
304
305    fn should_auto_hide_scrollbars(&self) -> bool;
306
307    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
308    fn write_to_clipboard(&self, item: ClipboardItem);
309
310    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
311    fn read_from_primary(&self) -> Option<ClipboardItem>;
312    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
313    fn write_to_primary(&self, item: ClipboardItem);
314
315    #[cfg(target_os = "macos")]
316    fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
317    #[cfg(target_os = "macos")]
318    fn write_to_find_pasteboard(&self, item: ClipboardItem);
319
320    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
321    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
322    fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
323
324    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
325    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
326    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
327
328    /// 注册系统电源事件回调
329    fn on_system_power_event(&self, _callback: Box<dyn FnMut(SystemPowerEvent)>) {}
330    /// 启动电源阻止器,阻止系统进入省电模式
331    fn start_power_save_blocker(&self, _kind: PowerSaveBlockerKind) -> Option<u32> {
332        None
333    }
334    /// 停止电源阻止器
335    fn stop_power_save_blocker(&self, _id: u32) {}
336    /// 获取系统空闲时间
337    fn system_idle_time(&self) -> Option<Duration> {
338        None
339    }
340    /// 获取当前网络状态
341    fn network_status(&self) -> NetworkStatus {
342        NetworkStatus::Online
343    }
344    /// 注册网络状态变更回调
345    fn on_network_status_change(&self, _callback: Box<dyn FnMut(NetworkStatus)>) {}
346    /// 注册媒体键事件回调
347    fn on_media_key_event(&self, _callback: Box<dyn FnMut(MediaKeyEvent)>) {}
348    /// 请求用户注意力(如弹跳 Dock 图标)
349    fn request_user_attention(&self, _attention_type: AttentionType) {}
350    /// 取消用户注意力请求
351    fn cancel_user_attention(&self) {}
352    /// 设置 Dock 徽章(如 macOS 上的未读计数)
353    fn set_dock_badge(&self, _label: Option<&str>) {}
354    /// 在指定位置显示上下文菜单
355    fn show_context_menu(
356        &self,
357        _position: Point<Pixels>,
358        _items: Vec<TrayMenuItem>,
359        _callback: Box<dyn FnMut(SharedString)>,
360    ) {
361    }
362    /// 显示原生对话框
363    fn show_dialog(&self, _options: DialogOptions) -> oneshot::Receiver<usize> {
364        let (tx, rx) = oneshot::channel();
365        tx.send(0).ok();
366        rx
367    }
368    /// 获取操作系统信息
369    fn os_info(&self) -> OsInfo {
370        OsInfo {
371            name: std::env::consts::OS.into(),
372            arch: std::env::consts::ARCH.into(),
373            version: String::new().into(),
374            locale: String::new().into(),
375            hostname: String::new().into(),
376        }
377    }
378    /// 获取生物识别认证状态
379    fn biometric_status(&self) -> BiometricStatus {
380        BiometricStatus::Unavailable
381    }
382    /// 发起生物识别认证
383    fn authenticate_biometric(&self, _reason: &str, callback: Box<dyn FnOnce(bool) + Send>) {
384        callback(false);
385    }
386}
387
388/// 平台显示器句柄 trait,表示物理显示器或笔记本屏幕
389pub trait PlatformDisplay: Debug {
390    /// 获取显示器 ID
391    fn id(&self) -> DisplayId;
392
393    /// 返回可在系统重启后持久使用的稳定标识符
394    fn uuid(&self) -> Result<Uuid>;
395
396    /// 获取显示器边界
397    fn bounds(&self) -> Bounds<Pixels>;
398
399    /// 获取显示器可见边界,排除任务栏/停靠栏区域
400    /// 这是窗口可以放置而不会被遮挡的可用区域
401    /// 如果未重写则默认为完整显示器边界
402    fn visible_bounds(&self) -> Bounds<Pixels> {
403        self.bounds()
404    }
405
406    /// 获取在此显示器上放置窗口的默认边界
407    fn default_bounds(&self) -> Bounds<Pixels> {
408        let bounds = self.bounds();
409        let center = bounds.center();
410        let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size);
411
412        let offset = clipped_window_size / 2.0;
413        let origin = point(center.x - offset.width, center.y - offset.height);
414        Bounds::new(origin, clipped_window_size)
415    }
416}
417
418/// 系统热状态,表示 CPU/GPU 的热限制情况
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum ThermalState {
421    /// 系统无热限制
422    Nominal,
423    /// 系统轻微受限,应减少非必要工作
424    Fair,
425    /// 系统中度受限,应减少 CPU/GPU 密集型工作
426    Serious,
427    /// 系统严重受限,应最小化所有资源使用
428    Critical,
429}
430
431/// 屏幕捕获源的元数据
432#[derive(Clone)]
433pub struct SourceMetadata {
434    /// 屏幕的不透明标识符
435    pub id: u64,
436    /// 源的人类可读标签
437    pub label: Option<SharedString>,
438    /// 此源是否为主显示器
439    pub is_main: Option<bool>,
440    /// 源的视频分辨率
441    pub resolution: Size<DevicePixels>,
442}
443
444/// 可捕获的屏幕视频内容源
445pub trait ScreenCaptureSource {
446    /// 返回此源的元数据
447    fn metadata(&self) -> Result<SourceMetadata>;
448
449    /// 开始从此源捕获视频,对每一帧调用给定的回调
450    fn stream(
451        &self,
452        foreground_executor: &ForegroundExecutor,
453        frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
454    ) -> oneshot::Receiver<Result<Box<dyn ScreenCaptureStream>>>;
455}
456
457/// 从屏幕捕获的视频流
458pub trait ScreenCaptureStream {
459    /// 返回此源的元数据
460    fn metadata(&self) -> Result<SourceMetadata>;
461}
462
463/// 从屏幕捕获的视频帧
464pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
465
466/// 硬件显示器的不透明标识符
467#[derive(PartialEq, Eq, Hash, Copy, Clone)]
468pub struct DisplayId(pub(crate) u64);
469
470impl DisplayId {
471    /// 从原始平台显示器标识符创建新的 `DisplayId`
472    pub fn new(id: u64) -> Self {
473        Self(id)
474    }
475}
476
477impl From<u64> for DisplayId {
478    fn from(id: u64) -> Self {
479        Self(id)
480    }
481}
482
483impl From<DisplayId> for u64 {
484    fn from(id: DisplayId) -> Self {
485        id.0
486    }
487}
488
489impl Debug for DisplayId {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        write!(f, "DisplayId({})", self.0)
492    }
493}
494
495/// 窗口可调整大小的边缘位置
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497pub enum ResizeEdge {
498    /// 上边缘
499    Top,
500    /// 右上角
501    TopRight,
502    /// 右边缘
503    Right,
504    /// 右下角
505    BottomRight,
506    /// 下边缘
507    Bottom,
508    /// 左下角
509    BottomLeft,
510    /// 左边缘
511    Left,
512    /// 左上角
513    TopLeft,
514}
515
516/// 窗口装饰类型,决定使用服务端还是客户端装饰
517#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
518pub enum WindowDecorations {
519    #[default]
520    /// 服务端装饰(由窗口管理器绘制)
521    Server,
522    /// 客户端装饰(由应用自行绘制)
523    Client,
524}
525
526/// 描述窗口当前的装饰配置
527#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
528pub enum Decorations {
529    /// 窗口配置为使用服务端装饰
530    #[default]
531    Server,
532    /// 窗口配置为使用客户端装饰
533    Client {
534        /// 边缘平铺状态
535        tiling: Tiling,
536    },
537}
538
539/// 平台支持的窗口控制按钮
540#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
541pub struct WindowControls {
542    /// 是否支持全屏
543    pub fullscreen: bool,
544    /// 是否支持最大化
545    pub maximize: bool,
546    /// 是否支持最小化
547    pub minimize: bool,
548    /// 是否支持窗口菜单
549    pub window_menu: bool,
550}
551
552impl Default for WindowControls {
553    fn default() -> Self {
554        // 默认假设支持所有控制按钮
555        Self {
556            fullscreen: true,
557            maximize: true,
558            minimize: true,
559            window_menu: true,
560        }
561    }
562}
563
564/// 标题栏中的窗口控制按钮类型
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
566pub enum WindowButton {
567    /// 最小化按钮
568    Minimize,
569    /// 最大化按钮
570    Maximize,
571    /// 关闭按钮
572    Close,
573}
574
575impl WindowButton {
576    /// 返回渲染此按钮时使用的稳定元素 ID
577    pub fn id(&self) -> &'static str {
578        match self {
579            WindowButton::Minimize => "minimize",
580            WindowButton::Maximize => "maximize",
581            WindowButton::Close => "close",
582        }
583    }
584
585    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
586    fn index(&self) -> usize {
587        match self {
588            WindowButton::Minimize => 0,
589            WindowButton::Maximize => 1,
590            WindowButton::Close => 2,
591        }
592    }
593}
594
595/// 标题栏每侧最多可放置的按钮数量
596pub const MAX_BUTTONS_PER_SIDE: usize = 3;
597
598/// 描述标题栏每侧出现哪些控制按钮
599///
600/// 在 Linux 上,此信息从桌面环境的配置中读取
601/// (例如 GNOME 的 `gtk-decoration-layout` gsetting)
602#[derive(Debug, Clone, Copy, PartialEq, Eq)]
603pub struct WindowButtonLayout {
604    /// 标题栏左侧的按钮
605    pub left: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
606    /// 标题栏右侧的按钮
607    pub right: [Option<WindowButton>; MAX_BUTTONS_PER_SIDE],
608}
609
610#[cfg(any(target_os = "linux", target_os = "freebsd"))]
611impl WindowButtonLayout {
612    /// 返回 Linux 标题栏的内置回退按钮布局
613    pub fn linux_default() -> Self {
614        Self {
615            left: [None; MAX_BUTTONS_PER_SIDE],
616            right: [
617                Some(WindowButton::Minimize),
618                Some(WindowButton::Maximize),
619                Some(WindowButton::Close),
620            ],
621        }
622    }
623
624    /// 解析 GNOME 风格的 `button-layout` 字符串(例如 `"close,minimize:maximize"`)
625    pub fn parse(layout_string: &str) -> Result<Self> {
626        fn parse_side(
627            s: &str,
628            seen_buttons: &mut [bool; MAX_BUTTONS_PER_SIDE],
629            unrecognized: &mut Vec<String>,
630        ) -> [Option<WindowButton>; MAX_BUTTONS_PER_SIDE] {
631            let mut result = [None; MAX_BUTTONS_PER_SIDE];
632            let mut i = 0;
633            for name in s.split(',') {
634                let trimmed = name.trim();
635                if trimmed.is_empty() {
636                    continue;
637                }
638                let button = match trimmed {
639                    "minimize" => Some(WindowButton::Minimize),
640                    "maximize" => Some(WindowButton::Maximize),
641                    "close" => Some(WindowButton::Close),
642                    other => {
643                        unrecognized.push(other.to_string());
644                        None
645                    }
646                };
647                if let Some(button) = button {
648                    if seen_buttons[button.index()] {
649                        continue;
650                    }
651                    if let Some(slot) = result.get_mut(i) {
652                        *slot = Some(button);
653                        seen_buttons[button.index()] = true;
654                        i += 1;
655                    }
656                }
657            }
658            result
659        }
660
661        let (left_str, right_str) = layout_string.split_once(':').unwrap_or(("", layout_string));
662        let mut unrecognized = Vec::new();
663        let mut seen_buttons = [false; MAX_BUTTONS_PER_SIDE];
664        let layout = Self {
665            left: parse_side(left_str, &mut seen_buttons, &mut unrecognized),
666            right: parse_side(right_str, &mut seen_buttons, &mut unrecognized),
667        };
668
669        if !unrecognized.is_empty()
670            && layout.left.iter().all(Option::is_none)
671            && layout.right.iter().all(Option::is_none)
672        {
673            bail!(
674                "button layout string {:?} contains no valid buttons (unrecognized: {})",
675                layout_string,
676                unrecognized.join(", ")
677            );
678        }
679
680        Ok(layout)
681    }
682
683    /// 将布局格式转换回 GNOME 风格的 `button-layout` 字符串
684    #[cfg(test)]
685    pub fn format(&self) -> String {
686        fn format_side(buttons: &[Option<WindowButton>; MAX_BUTTONS_PER_SIDE]) -> String {
687            buttons
688                .iter()
689                .flatten()
690                .map(|button| match button {
691                    WindowButton::Minimize => "minimize",
692                    WindowButton::Maximize => "maximize",
693                    WindowButton::Close => "close",
694                })
695                .collect::<Vec<_>>()
696                .join(",")
697        }
698
699        format!("{}:{}", format_side(&self.left), format_side(&self.right))
700    }
701}
702
703/// 描述窗口哪些边缘当前被平铺
704#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
705pub struct Tiling {
706    /// 上边缘是否被平铺
707    pub top: bool,
708    /// 左边缘是否被平铺
709    pub left: bool,
710    /// 右边缘是否被平铺
711    pub right: bool,
712    /// 下边缘是否被平铺
713    pub bottom: bool,
714}
715
716impl Tiling {
717    /// 初始化所有边缘都被平铺的 [`Tiling`]
718    pub fn tiled() -> Self {
719        Self {
720            top: true,
721            left: true,
722            right: true,
723            bottom: true,
724        }
725    }
726
727    /// 是否有任何边缘被平铺
728    pub fn is_tiled(&self) -> bool {
729        self.top || self.left || self.right || self.bottom
730    }
731}
732
733#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
734#[expect(missing_docs)]
735pub struct RequestFrameOptions {
736    /// 是否需要呈现
737    pub require_presentation: bool,
738    /// 为 true 时强制刷新所有渲染状态
739    pub force_render: bool,
740}
741
742/// 平台窗口 trait,定义了窗口操作的核心接口
743/// 各平台窗口实现需实现此 trait
744#[expect(missing_docs)]
745pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
746    fn bounds(&self) -> Bounds<Pixels>;
747    fn is_maximized(&self) -> bool;
748    fn window_bounds(&self) -> WindowBounds;
749    fn content_size(&self) -> Size<Pixels>;
750    fn resize(&mut self, size: Size<Pixels>);
751    /// 设置窗口位置(保持大小不变)
752    fn set_position(&mut self, position: Point<Pixels>);
753    fn scale_factor(&self) -> f32;
754    fn appearance(&self) -> WindowAppearance;
755    fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
756    fn mouse_position(&self) -> Point<Pixels>;
757    fn modifiers(&self) -> Modifiers;
758    fn capslock(&self) -> Capslock;
759    fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
760    fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
761    fn prompt(
762        &self,
763        level: PromptLevel,
764        msg: &str,
765        detail: Option<&str>,
766        answers: &[PromptButton],
767    ) -> Option<oneshot::Receiver<usize>>;
768    fn activate(&self);
769    fn is_active(&self) -> bool;
770    fn is_hovered(&self) -> bool;
771    fn background_appearance(&self) -> WindowBackgroundAppearance;
772    fn set_title(&mut self, title: &str);
773    fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
774    fn minimize(&self);
775    fn hide(&self);
776    fn zoom(&self);
777    fn toggle_fullscreen(&self);
778    fn is_fullscreen(&self) -> bool;
779    fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>);
780    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
781    fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
782    fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
783    fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
784    fn on_moved(&self, callback: Box<dyn FnMut()>);
785    fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
786    fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>);
787    fn on_close(&self, callback: Box<dyn FnOnce()>);
788    fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
789    fn on_button_layout_changed(&self, _callback: Box<dyn FnMut()>) {}
790    fn draw(&self, scene: &Scene);
791    fn completed_frame(&self) {}
792    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
793    fn is_subpixel_rendering_supported(&self) -> bool;
794
795    // macOS specific methods
796    fn get_title(&self) -> String {
797        String::new()
798    }
799    fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
800        None
801    }
802    fn tab_bar_visible(&self) -> bool {
803        false
804    }
805    fn set_edited(&mut self, _edited: bool) {}
806    fn set_document_path(&self, _path: Option<&std::path::Path>) {}
807    fn show_character_palette(&self) {}
808    fn titlebar_double_click(&self) {}
809    fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
810    fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
811    fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
812    fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
813    fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
814    fn merge_all_windows(&self) {}
815    fn move_tab_to_new_window(&self) {}
816    fn toggle_window_tab_overview(&self) {}
817    fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
818
819    #[cfg(target_os = "windows")]
820    fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND;
821
822    // Linux specific methods
823    fn inner_window_bounds(&self) -> WindowBounds {
824        self.window_bounds()
825    }
826    fn request_decorations(&self, _decorations: WindowDecorations) {}
827    fn show_window_menu(&self, _position: Point<Pixels>) {}
828    fn start_window_move(&self) {}
829    fn start_window_resize(&self, _edge: ResizeEdge) {}
830    fn window_decorations(&self) -> Decorations {
831        Decorations::Server
832    }
833    fn set_app_id(&mut self, _app_id: &str) {}
834    fn map_window(&mut self) -> anyhow::Result<()> {
835        Ok(())
836    }
837    fn window_controls(&self) -> WindowControls {
838        WindowControls::default()
839    }
840    fn set_client_inset(&self, _inset: Pixels) {}
841    fn gpu_specs(&self) -> Option<GpuSpecs>;
842
843    fn update_ime_position(&self, _bounds: Bounds<Pixels>);
844
845    fn play_system_bell(&self) {}
846
847    /// 显示窗口(与 hide 相对)
848    fn show(&self) {}
849    /// 检查窗口当前是否可见
850    fn is_visible(&self) -> bool {
851        true
852    }
853    /// 设置任务栏/程序坞进度条状态
854    fn set_progress_bar(&self, _state: ProgressBarState) {}
855
856    /// 设置窗口是否允许鼠标事件穿透到后面的窗口
857    fn set_mouse_passthrough(&self, _passthrough: bool) {}
858
859    /// 获取窗口扩展样式(GWL_EXSTYLE),仅 Windows 有效
860    fn window_extended_style(&self) -> u32 {
861        0
862    }
863
864    /// 设置窗口扩展样式(GWL_EXSTYLE),仅 Windows 有效
865    /// 调用者负责确保样式的合法性,不正确的样式可能导致窗口行为异常
866    fn set_window_extended_style(&self, _style: u32) {}
867
868    /// 设置标题栏和边框是否可见
869    fn set_titlebar_visible(&self, _visible: bool) {}
870
871    #[cfg(any(test, feature = "test-support"))]
872    fn as_test(&mut self) -> Option<&mut TestWindow> {
873        None
874    }
875
876    /// 将场景渲染到纹理并返回 RGBA 图像的像素数据
877    /// 不会将帧呈现到屏幕 - 用于视觉测试,在不显示窗口的情况下捕获渲染内容
878    #[cfg(any(test, feature = "test-support"))]
879    fn render_to_image(&self, _scene: &Scene) -> Result<RgbaImage> {
880        anyhow::bail!("render_to_image not implemented for this platform")
881    }
882}
883
884/// 无头窗口的渲染器,可生成真实的渲染输出
885#[cfg(any(test, feature = "test-support"))]
886pub trait PlatformHeadlessRenderer {
887    /// 渲染场景并返回 RGBA 图像
888    fn render_scene_to_image(
889        &mut self,
890        scene: &Scene,
891        size: Size<DevicePixels>,
892    ) -> Result<RgbaImage>;
893
894    /// 返回此渲染器使用的精灵图集
895    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
896}
897
898/// 带元数据的可运行任务类型别名
899#[doc(hidden)]
900pub type RunnableVariant = Runnable<RunnableMeta>;
901
902#[doc(hidden)]
903pub type TimerResolutionGuard = crate::rgpui_util::Deferred<Box<dyn FnOnce() + Send>>;
904
905/// 平台分发器 trait,负责任务调度和执行
906/// 此类型公开是为了让测试宏可以生成和使用它,但不属于公共 API
907#[doc(hidden)]
908pub trait PlatformDispatcher: Send + Sync {
909    fn get_all_timings(&self) -> Vec<ThreadTaskTimings>;
910    fn get_current_thread_timings(&self) -> ThreadTaskTimings;
911    fn is_main_thread(&self) -> bool;
912    fn dispatch(&self, runnable: RunnableVariant, priority: Priority);
913    fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority);
914    fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant);
915
916    fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>);
917
918    fn now(&self) -> Instant {
919        Instant::now()
920    }
921
922    fn increase_timer_resolution(&self) -> TimerResolutionGuard {
923        crate::defer(Box::new(|| {}))
924    }
925
926    #[cfg(any(test, feature = "test-support"))]
927    fn as_test(&self) -> Option<&TestDispatcher> {
928        None
929    }
930}
931
932/// 平台文本系统 trait,负责字体加载、字形渲染和文本布局
933#[expect(missing_docs)]
934pub trait PlatformTextSystem: Send + Sync {
935    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
936    /// 获取所有可用字体名称
937    fn all_font_names(&self) -> Vec<String>;
938    /// 获取字体描述符对应的字体 ID
939    fn font_id(&self, descriptor: &Font) -> Result<FontId>;
940    /// 获取字体度量信息
941    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
942    /// 获取字形的排版边界
943    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
944    /// 获取字形的前进宽度
945    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
946    /// 获取字符对应的字形 ID
947    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
948    /// 获取字形的栅格边界
949    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
950    /// 栅格化字形
951    fn rasterize_glyph(
952        &self,
953        params: &RenderGlyphParams,
954        raster_bounds: Bounds<DevicePixels>,
955    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
956    /// 使用给定的字体运行信息布局一行文本
957    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
958    /// 返回给定字体和大小的推荐文本渲染模式
959    fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
960    -> TextRenderingMode;
961    /// 返回使用给定颜色绘制字形时应使用的膨胀级别
962    fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
963        0
964    }
965}
966
967#[expect(missing_docs)]
968pub struct NoopTextSystem;
969
970#[expect(missing_docs)]
971impl NoopTextSystem {
972    #[allow(dead_code)]
973    pub fn new() -> Self {
974        Self
975    }
976}
977
978impl PlatformTextSystem for NoopTextSystem {
979    fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
980        Ok(())
981    }
982
983    fn all_font_names(&self) -> Vec<String> {
984        Vec::new()
985    }
986
987    fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
988        Ok(FontId(1))
989    }
990
991    fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
992        FontMetrics {
993            units_per_em: 1000,
994            ascent: 1025.0,
995            descent: -275.0,
996            line_gap: 0.0,
997            underline_position: -95.0,
998            underline_thickness: 60.0,
999            cap_height: 698.0,
1000            x_height: 516.0,
1001            bounding_box: Bounds {
1002                origin: Point {
1003                    x: -260.0,
1004                    y: -245.0,
1005                },
1006                size: Size {
1007                    width: 1501.0,
1008                    height: 1364.0,
1009                },
1010            },
1011        }
1012    }
1013
1014    fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
1015        Ok(Bounds {
1016            origin: Point { x: 54.0, y: 0.0 },
1017            size: size(392.0, 528.0),
1018        })
1019    }
1020
1021    fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1022        Ok(size(600.0 * glyph_id.0 as f32, 0.0))
1023    }
1024
1025    fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
1026        Some(GlyphId(ch.len_utf16() as u32))
1027    }
1028
1029    fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1030        Ok(Default::default())
1031    }
1032
1033    fn rasterize_glyph(
1034        &self,
1035        _params: &RenderGlyphParams,
1036        raster_bounds: Bounds<DevicePixels>,
1037    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1038        Ok((raster_bounds.size, Vec::new()))
1039    }
1040
1041    fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
1042        let mut position = px(0.);
1043        let metrics = self.font_metrics(FontId(0));
1044        let em_width = font_size
1045            * self
1046                .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
1047                .unwrap()
1048                .width
1049            / metrics.units_per_em as f32;
1050        let mut glyphs = Vec::new();
1051        for (ix, c) in text.char_indices() {
1052            if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
1053                glyphs.push(ShapedGlyph {
1054                    id: glyph,
1055                    position: point(position, px(0.)),
1056                    index: ix,
1057                    is_emoji: glyph.0 == 2,
1058                });
1059                if glyph.0 == 2 {
1060                    position += em_width * 2.0;
1061                } else {
1062                    position += em_width;
1063                }
1064            } else {
1065                position += em_width
1066            }
1067        }
1068        let mut runs = Vec::default();
1069        if !glyphs.is_empty() {
1070            runs.push(ShapedRun {
1071                font_id: FontId(0),
1072                glyphs,
1073            });
1074        } else {
1075            position = px(0.);
1076        }
1077
1078        LineLayout {
1079            font_size,
1080            width: position,
1081            ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
1082            descent: font_size * (metrics.descent / metrics.units_per_em as f32),
1083            runs,
1084            len: text.len(),
1085        }
1086    }
1087
1088    fn recommended_rendering_mode(
1089        &self,
1090        _font_id: FontId,
1091        _font_size: Pixels,
1092    ) -> TextRenderingMode {
1093        TextRenderingMode::Grayscale
1094    }
1095}
1096
1097// 改编自 https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp
1098// Copyright (c) Microsoft Corporation.
1099// Licensed under the MIT license.
1100/// 计算子像素文本渲染的伽马校正比率
1101#[allow(dead_code)]
1102pub fn get_gamma_correction_ratios(gamma: f32) -> [f32; 4] {
1103    const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
1104        [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0
1105        [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1
1106        [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2
1107        [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3
1108        [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4
1109        [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5
1110        [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6
1111        [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7
1112        [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8
1113        [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9
1114        [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0
1115        [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1
1116        [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2
1117    ];
1118
1119    const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
1120    const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
1121
1122    let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
1123    let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
1124
1125    [
1126        ratios[0] * NORM13,
1127        ratios[1] * NORM24,
1128        ratios[2] * NORM13,
1129        ratios[3] * NORM24,
1130    ]
1131}
1132
1133#[derive(PartialEq, Eq, Hash, Clone)]
1134#[expect(missing_docs)]
1135pub enum AtlasKey {
1136    Glyph(RenderGlyphParams),
1137    Svg(RenderSvgParams),
1138    Image(RenderImageParams),
1139}
1140
1141impl AtlasKey {
1142    #[cfg_attr(
1143        all(
1144            any(target_os = "linux", target_os = "freebsd"),
1145            not(any(feature = "x11", feature = "wayland"))
1146        ),
1147        allow(dead_code)
1148    )]
1149    /// 返回此图集键对应的纹理类型
1150    pub fn texture_kind(&self) -> AtlasTextureKind {
1151        match self {
1152            AtlasKey::Glyph(params) => {
1153                if params.is_emoji {
1154                    AtlasTextureKind::Polychrome
1155                } else if params.subpixel_rendering {
1156                    AtlasTextureKind::Subpixel
1157                } else {
1158                    AtlasTextureKind::Monochrome
1159                }
1160            }
1161            AtlasKey::Svg(_) => AtlasTextureKind::Monochrome,
1162            AtlasKey::Image(_) => AtlasTextureKind::Polychrome,
1163        }
1164    }
1165}
1166
1167impl From<RenderGlyphParams> for AtlasKey {
1168    fn from(params: RenderGlyphParams) -> Self {
1169        Self::Glyph(params)
1170    }
1171}
1172
1173impl From<RenderSvgParams> for AtlasKey {
1174    fn from(params: RenderSvgParams) -> Self {
1175        Self::Svg(params)
1176    }
1177}
1178
1179impl From<RenderImageParams> for AtlasKey {
1180    fn from(params: RenderImageParams) -> Self {
1181        Self::Image(params)
1182    }
1183}
1184
1185/// 平台图集 trait,用于管理纹理图集的插入和移除
1186#[expect(missing_docs)]
1187pub trait PlatformAtlas {
1188    fn get_or_insert_with<'a>(
1189        &self,
1190        key: &AtlasKey,
1191        build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
1192    ) -> Result<Option<AtlasTile>>;
1193    fn remove(&self, key: &AtlasKey);
1194}
1195
1196#[doc(hidden)]
1197pub struct AtlasTextureList<T> {
1198    pub textures: Vec<Option<T>>,
1199    pub free_list: Vec<usize>,
1200}
1201
1202impl<T> Default for AtlasTextureList<T> {
1203    fn default() -> Self {
1204        Self {
1205            textures: Vec::default(),
1206            free_list: Vec::default(),
1207        }
1208    }
1209}
1210
1211impl<T> ops::Index<usize> for AtlasTextureList<T> {
1212    type Output = Option<T>;
1213
1214    fn index(&self, index: usize) -> &Self::Output {
1215        &self.textures[index]
1216    }
1217}
1218
1219impl<T> AtlasTextureList<T> {
1220    #[allow(unused)]
1221    pub fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
1222        self.free_list.clear();
1223        self.textures.drain(..)
1224    }
1225
1226    #[allow(dead_code)]
1227    pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
1228        self.textures.iter_mut().flatten()
1229    }
1230}
1231
1232#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1233#[repr(C)]
1234#[expect(missing_docs)]
1235pub struct AtlasTile {
1236    /// 此图块所属的纹理
1237    pub texture_id: AtlasTextureId,
1238    /// 图块在其纹理中的唯一 ID
1239    pub tile_id: TileId,
1240    /// 图块内容周围的填充像素
1241    pub padding: u32,
1242    /// 图块在纹理中的边界
1243    pub bounds: Bounds<DevicePixels>,
1244}
1245
1246#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1247#[repr(C)]
1248#[expect(missing_docs)]
1249pub struct AtlasTextureId {
1250    // 使用 u32 而非 usize 以兼容 Metal 着色语言
1251    /// 此纹理在图集中的索引
1252    pub index: u32,
1253    /// 此纹理存储的内容类型
1254    pub kind: AtlasTextureKind,
1255}
1256
1257#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1258#[repr(C)]
1259#[cfg_attr(
1260    all(
1261        any(target_os = "linux", target_os = "freebsd"),
1262        not(any(feature = "x11", feature = "wayland"))
1263    ),
1264    allow(dead_code)
1265)]
1266#[expect(missing_docs)]
1267pub enum AtlasTextureKind {
1268    Monochrome = 0,
1269    Polychrome = 1,
1270    Subpixel = 2,
1271}
1272
1273#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1274#[repr(C)]
1275#[expect(missing_docs)]
1276pub struct TileId(pub u32);
1277
1278impl From<etagere::AllocId> for TileId {
1279    fn from(id: etagere::AllocId) -> Self {
1280        Self(id.serialize())
1281    }
1282}
1283
1284impl From<TileId> for etagere::AllocId {
1285    fn from(id: TileId) -> Self {
1286        Self::deserialize(id.0)
1287    }
1288}
1289
1290#[expect(missing_docs)]
1291pub struct PlatformInputHandler {
1292    cx: AsyncWindowContext,
1293    handler: Box<dyn InputHandler>,
1294}
1295
1296#[expect(missing_docs)]
1297#[cfg_attr(
1298    all(
1299        any(target_os = "linux", target_os = "freebsd"),
1300        not(any(feature = "x11", feature = "wayland"))
1301    ),
1302    allow(dead_code)
1303)]
1304impl PlatformInputHandler {
1305    pub fn new(cx: AsyncWindowContext, handler: Box<dyn InputHandler>) -> Self {
1306        Self { cx, handler }
1307    }
1308
1309    pub fn selected_text_range(&mut self, ignore_disabled_input: bool) -> Option<UTF16Selection> {
1310        self.cx
1311            .update(|window, cx| {
1312                self.handler
1313                    .selected_text_range(ignore_disabled_input, window, cx)
1314            })
1315            .ok()
1316            .flatten()
1317    }
1318
1319    #[cfg_attr(target_os = "windows", allow(dead_code))]
1320    pub fn marked_text_range(&mut self) -> Option<Range<usize>> {
1321        self.cx
1322            .update(|window, cx| self.handler.marked_text_range(window, cx))
1323            .ok()
1324            .flatten()
1325    }
1326
1327    #[cfg_attr(
1328        any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1329        allow(dead_code)
1330    )]
1331    pub fn text_for_range(
1332        &mut self,
1333        range_utf16: Range<usize>,
1334        adjusted: &mut Option<Range<usize>>,
1335    ) -> Option<String> {
1336        self.cx
1337            .update(|window, cx| {
1338                self.handler
1339                    .text_for_range(range_utf16, adjusted, window, cx)
1340            })
1341            .ok()
1342            .flatten()
1343    }
1344
1345    pub fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str) {
1346        self.cx
1347            .update(|window, cx| {
1348                self.handler
1349                    .replace_text_in_range(replacement_range, text, window, cx);
1350            })
1351            .ok();
1352    }
1353
1354    pub fn replace_and_mark_text_in_range(
1355        &mut self,
1356        range_utf16: Option<Range<usize>>,
1357        new_text: &str,
1358        new_selected_range: Option<Range<usize>>,
1359    ) {
1360        self.cx
1361            .update(|window, cx| {
1362                self.handler.replace_and_mark_text_in_range(
1363                    range_utf16,
1364                    new_text,
1365                    new_selected_range,
1366                    window,
1367                    cx,
1368                )
1369            })
1370            .ok();
1371    }
1372
1373    #[cfg_attr(target_os = "windows", allow(dead_code))]
1374    pub fn unmark_text(&mut self) {
1375        self.cx
1376            .update(|window, cx| self.handler.unmark_text(window, cx))
1377            .ok();
1378    }
1379
1380    pub fn bounds_for_range(&mut self, range_utf16: Range<usize>) -> Option<Bounds<Pixels>> {
1381        self.cx
1382            .update(|window, cx| self.handler.bounds_for_range(range_utf16, window, cx))
1383            .ok()
1384            .flatten()
1385    }
1386
1387    #[allow(dead_code)]
1388    pub fn apple_press_and_hold_enabled(&mut self) -> bool {
1389        self.handler.apple_press_and_hold_enabled()
1390    }
1391
1392    pub fn dispatch_input(&mut self, input: &str, window: &mut Window, cx: &mut App) {
1393        self.handler.replace_text_in_range(None, input, window, cx);
1394    }
1395
1396    pub fn selected_bounds(&mut self, window: &mut Window, cx: &mut App) -> Option<Bounds<Pixels>> {
1397        let selection = self.handler.selected_text_range(true, window, cx)?;
1398        self.handler.bounds_for_range(
1399            if selection.reversed {
1400                selection.range.start..selection.range.start
1401            } else {
1402                selection.range.end..selection.range.end
1403            },
1404            window,
1405            cx,
1406        )
1407    }
1408
1409    #[allow(unused)]
1410    pub fn character_index_for_point(&mut self, point: Point<Pixels>) -> Option<usize> {
1411        self.cx
1412            .update(|window, cx| self.handler.character_index_for_point(point, window, cx))
1413            .ok()
1414            .flatten()
1415    }
1416
1417    #[allow(dead_code)]
1418    pub fn accepts_text_input(&mut self, window: &mut Window, cx: &mut App) -> bool {
1419        self.handler.accepts_text_input(window, cx)
1420    }
1421
1422    #[allow(dead_code)]
1423    pub fn query_accepts_text_input(&mut self) -> bool {
1424        self.cx
1425            .update(|window, cx| self.handler.accepts_text_input(window, cx))
1426            .unwrap_or(true)
1427    }
1428
1429    #[allow(dead_code)]
1430    pub fn query_prefers_ime_for_printable_keys(&mut self) -> bool {
1431        self.cx
1432            .update(|window, cx| self.handler.prefers_ime_for_printable_keys(window, cx))
1433            .unwrap_or(false)
1434    }
1435}
1436
1437/// 文本缓冲区中表示选区的结构,使用 UTF16 字符
1438/// 与普通的 Range 不同,因为选区的头部可能在尾部之前
1439#[derive(Debug)]
1440pub struct UTF16Selection {
1441    /// 此选区对应的文档范围(UTF-16 字符)
1442    pub range: Range<usize>,
1443    /// 选区头部是否在范围的起始位置(true),还是末尾位置(false)
1444    pub reversed: bool,
1445}
1446
1447/// Zed 处理平台 IME 系统文本输入的接口
1448/// 当前为 NSTextInputClient API 的 1:1 映射:
1449///
1450/// <https://developer.apple.com/documentation/appkit/nstextinputclient>
1451pub trait InputHandler: 'static {
1452    /// 获取用户当前选中的文本范围(如有)
1453    /// 对应 [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
1454    ///
1455    /// 返回值为 UTF-16 字符范围,从 0 到文档长度
1456    fn selected_text_range(
1457        &mut self,
1458        ignore_disabled_input: bool,
1459        window: &mut Window,
1460        cx: &mut App,
1461    ) -> Option<UTF16Selection>;
1462
1463    /// 获取当前标记文本的范围(如有)
1464    /// 对应 [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
1465    ///
1466    /// 返回值为 UTF-16 字符范围,从 0 到文档长度
1467    fn marked_text_range(&mut self, window: &mut Window, cx: &mut App) -> Option<Range<usize>>;
1468
1469    /// 获取给定文档范围的 UTF-16 文本
1470    /// 对应 [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
1471    ///
1472    /// range_utf16 为 UTF-16 字符范围
1473    fn text_for_range(
1474        &mut self,
1475        range_utf16: Range<usize>,
1476        adjusted_range: &mut Option<Range<usize>>,
1477        window: &mut Window,
1478        cx: &mut App,
1479    ) -> Option<String>;
1480
1481    /// 用给定文本替换指定文档范围的内容
1482    /// 对应 [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
1483    ///
1484    /// replacement_range 为 UTF-16 字符范围
1485    fn replace_text_in_range(
1486        &mut self,
1487        replacement_range: Option<Range<usize>>,
1488        text: &str,
1489        window: &mut Window,
1490        cx: &mut App,
1491    );
1492
1493    /// 用给定文本替换指定文档范围的内容,并将文本标记为 IME '组合' 状态
1494    /// 对应 [setMarkedText(_:selectedRange:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438246-setmarkedtext)
1495    ///
1496    /// range_utf16 为 UTF-16 字符范围
1497    /// new_selected_range 为 UTF-16 字符范围
1498    fn replace_and_mark_text_in_range(
1499        &mut self,
1500        range_utf16: Option<Range<usize>>,
1501        new_text: &str,
1502        new_selected_range: Option<Range<usize>>,
1503        window: &mut Window,
1504        cx: &mut App,
1505    );
1506
1507    /// 从文档中移除 IME '组合' 状态
1508    /// 对应 [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
1509    fn unmark_text(&mut self, window: &mut Window, cx: &mut App);
1510
1511    /// 获取给定文档范围在屏幕坐标系中的边界
1512    /// 对应 [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
1513    ///
1514    /// 用于定位 IME 候选窗口
1515    fn bounds_for_range(
1516        &mut self,
1517        range_utf16: Range<usize>,
1518        window: &mut Window,
1519        cx: &mut App,
1520    ) -> Option<Bounds<Pixels>>;
1521
1522    /// 获取给定点对应的字符偏移(UTF16 字符)
1523    ///
1524    /// 对应 [characterIndexForPoint:](https://developer.apple.com/documentation/appkit/nstextinputclient/characterindex(for:))
1525    fn character_index_for_point(
1526        &mut self,
1527        point: Point<Pixels>,
1528        window: &mut Window,
1529        cx: &mut App,
1530    ) -> Option<usize>;
1531
1532    /// 允许输入上下文选择接收原始按键重复事件而非发送到平台
1533    /// TODO: 理想情况下我们应该能够在 NSUserDefaults 中设置 ApplePressAndHoldEnabled
1534    /// (iTerm 就是这样做的),但目前不起作用
1535    #[allow(dead_code)]
1536    fn apple_press_and_hold_enabled(&mut self) -> bool {
1537        true
1538    }
1539
1540    /// 返回此处理器是否接受文本输入
1541    fn accepts_text_input(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1542        true
1543    }
1544
1545    /// 返回在非 ASCII 输入源(如日文、韩文、中文 IME)激活时,
1546    /// 可打印按键是否应在按键绑定匹配之前路由到 IME。
1547    /// 这防止了像 `jj` 这样的多击按键绑定拦截 IME 应该组合的按键。
1548    ///
1549    /// 默认为 `false`。编辑器根据是否期望字符输入来覆盖此值
1550    /// (例如 Vim 插入模式返回 `true`,普通模式返回 `false`)。
1551    /// 终端保持默认 `false` 以便原始按键能到达终端进程。
1552    fn prefers_ime_for_printable_keys(&mut self, _window: &mut Window, _cx: &mut App) -> bool {
1553        false
1554    }
1555}
1556
1557/// 创建新窗口时可配置的选项
1558#[derive(Debug)]
1559pub struct WindowOptions {
1560    /// 指定窗口在屏幕坐标系中的状态和边界
1561    /// - `None`: 继承当前边界
1562    /// - `Some(WindowBounds)`: 使用指定的状态和恢复大小打开窗口
1563    pub window_bounds: Option<WindowBounds>,
1564
1565    /// 窗口标题栏配置
1566    pub titlebar: Option<TitlebarOptions>,
1567
1568    /// 创建时是否获取焦点
1569    pub focus: bool,
1570
1571    /// 创建时是否显示
1572    pub show: bool,
1573
1574    /// 窗口类型
1575    pub kind: WindowKind,
1576
1577    /// 用户是否可移动窗口
1578    pub is_movable: bool,
1579
1580    /// 用户是否可调整窗口大小
1581    pub is_resizable: bool,
1582
1583    /// 用户是否可最小化窗口
1584    pub is_minimizable: bool,
1585
1586    /// 创建窗口的显示器,为 None 时使用主显示器
1587    pub display_id: Option<DisplayId>,
1588
1589    /// 窗口背景外观
1590    pub window_background: WindowBackgroundAppearance,
1591
1592    /// 窗口应用标识符,桌面环境可用于分组应用
1593    pub app_id: Option<String>,
1594
1595    /// 窗口最小尺寸
1596    pub window_min_size: Option<Size<Pixels>>,
1597
1598    /// 使用客户端还是服务端装饰,仅 Wayland 有效
1599    /// 注意:此设置可能被忽略
1600    pub window_decorations: Option<WindowDecorations>,
1601
1602    /// 窗口图标(仅 X11)
1603    pub icon: Option<Arc<image::RgbaImage>>,
1604
1605    /// 标签组名称,允许在 macOS 10.12+ 上将窗口作为原生标签打开
1606    /// 具有相同标签标识符的窗口会被分组
1607    pub tabbing_identifier: Option<String>,
1608
1609    /// 是否允许鼠标事件穿透到后面的窗口
1610    pub mouse_passthrough: bool,
1611}
1612
1613/// 创建新窗口时传递给平台的参数
1614#[derive(Debug)]
1615#[cfg_attr(
1616    all(
1617        any(target_os = "linux", target_os = "freebsd"),
1618        not(any(feature = "x11", feature = "wayland"))
1619    ),
1620    allow(dead_code)
1621)]
1622#[allow(missing_docs)]
1623pub struct WindowParams {
1624    pub bounds: Bounds<Pixels>,
1625
1626    /// The titlebar configuration of the window
1627    #[cfg_attr(feature = "wayland", allow(dead_code))]
1628    pub titlebar: Option<TitlebarOptions>,
1629
1630    /// The kind of window to create
1631    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1632    pub kind: WindowKind,
1633
1634    /// Whether the window should be movable by the user
1635    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1636    pub is_movable: bool,
1637
1638    /// Whether the window should be resizable by the user
1639    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1640    pub is_resizable: bool,
1641
1642    /// Whether the window should be minimized by the user
1643    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1644    pub is_minimizable: bool,
1645
1646    #[cfg_attr(
1647        any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
1648        allow(dead_code)
1649    )]
1650    pub focus: bool,
1651
1652    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1653    pub show: bool,
1654
1655    /// 窗口图标(仅 X11)
1656    #[cfg_attr(feature = "wayland", allow(dead_code))]
1657    pub icon: Option<Arc<image::RgbaImage>>,
1658
1659    #[cfg_attr(feature = "wayland", allow(dead_code))]
1660    pub display_id: Option<DisplayId>,
1661
1662    pub window_min_size: Option<Size<Pixels>>,
1663    #[cfg(target_os = "macos")]
1664    pub tabbing_identifier: Option<String>,
1665    #[cfg_attr(
1666        any(target_os = "linux", target_os = "freebsd", target_os = "macos"),
1667        allow(dead_code)
1668    )]
1669    pub window_decorations: WindowDecorations,
1670
1671    /// 是否允许鼠标事件穿透到后面的窗口
1672    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
1673    pub mouse_passthrough: bool,
1674}
1675
1676/// 表示窗口的打开状态
1677#[derive(Debug, Copy, Clone, PartialEq)]
1678pub enum WindowBounds {
1679    /// 窗口以给定边界打开
1680    Windowed(Bounds<Pixels>),
1681    /// 窗口以最大化状态打开,边界为恢复大小
1682    Maximized(Bounds<Pixels>),
1683    /// 窗口以全屏状态打开,边界为恢复大小
1684    Fullscreen(Bounds<Pixels>),
1685}
1686
1687impl Default for WindowBounds {
1688    fn default() -> Self {
1689        WindowBounds::Windowed(Bounds::default())
1690    }
1691}
1692
1693impl WindowBounds {
1694    /// 获取内部边界
1695    pub fn get_bounds(&self) -> Bounds<Pixels> {
1696        match self {
1697            WindowBounds::Windowed(bounds) => *bounds,
1698            WindowBounds::Maximized(bounds) => *bounds,
1699            WindowBounds::Fullscreen(bounds) => *bounds,
1700        }
1701    }
1702
1703    /// 创建在屏幕上居中的窗口边界
1704    pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
1705        WindowBounds::Windowed(Bounds::centered(None, size, cx))
1706    }
1707}
1708
1709impl Default for WindowOptions {
1710    fn default() -> Self {
1711        Self {
1712            window_bounds: None,
1713            titlebar: Some(TitlebarOptions {
1714                title: Default::default(),
1715                appears_transparent: Default::default(),
1716                traffic_light_position: Default::default(),
1717            }),
1718            focus: true,
1719            show: true,
1720            kind: WindowKind::Normal,
1721            is_movable: true,
1722            is_resizable: true,
1723            is_minimizable: true,
1724            display_id: None,
1725            window_background: WindowBackgroundAppearance::default(),
1726            icon: None,
1727            app_id: None,
1728            window_min_size: None,
1729            window_decorations: None,
1730            tabbing_identifier: None,
1731            mouse_passthrough: false,
1732        }
1733    }
1734}
1735
1736/// 窗口标题栏可配置的选项
1737#[derive(Debug, Default)]
1738pub struct TitlebarOptions {
1739    /// 窗口的初始标题
1740    pub title: Option<SharedString>,
1741
1742    /// 是否隐藏系统默认标题栏以使用自定义绘制的标题栏(仅 macOS 和 Windows)
1743    /// Linux 请参考 [`WindowOptions::window_decorations`]
1744    pub appears_transparent: bool,
1745
1746    /// macOS 红绿灯按钮的位置
1747    pub traffic_light_position: Option<Point<Pixels>>,
1748}
1749
1750/// 要创建的窗口类型
1751#[derive(Clone, Debug, PartialEq)]
1752pub enum WindowKind {
1753    /// 普通应用窗口
1754    Normal,
1755
1756    /// 始终在其他窗口上方的窗口,通常用于警告或弹出窗口
1757    /// 请谨慎使用!
1758    PopUp,
1759
1760    /// 浮动在父窗口上方的窗口
1761    Floating,
1762
1763    /// Wayland LayerShell 窗口,用于绘制叠加层或背景
1764    /// 适用于坞、通知或壁纸等应用
1765    #[cfg(all(target_os = "linux", feature = "wayland"))]
1766    LayerShell(layer_shell::LayerShellOptions),
1767
1768    /// 浮动在父窗口上方并阻止与其交互的窗口
1769    /// 直到模态窗口关闭
1770    Dialog,
1771
1772    /// 覆盖窗口,用于全局覆盖层、屏幕标注或透明 UI
1773    /// 特性:始终置顶、无装饰
1774    /// 鼠标穿透通过 [`WindowOptions::mouse_passthrough`] 控制
1775    /// 透明度通过 [`WindowOptions::window_background`] 控制
1776    Overlay,
1777}
1778
1779/// 窗口外观,由操作系统定义
1780///
1781/// 在 macOS 上,这对应命名的 [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance) 值
1782#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1783pub enum WindowAppearance {
1784    /// 浅色外观
1785    ///
1786    /// 在 macOS 上对应 `aqua` 外观
1787    #[default]
1788    Light,
1789
1790    /// 浅色鲜艳外观
1791    ///
1792    /// 在 macOS 上对应 `NSAppearanceNameVibrantLight` 外观
1793    VibrantLight,
1794
1795    /// 深色外观
1796    ///
1797    /// 在 macOS 上对应 `darkAqua` 外观
1798    Dark,
1799
1800    /// 深色鲜艳外观
1801    ///
1802    /// 在 macOS 上对应 `NSAppearanceNameVibrantDark` 外观
1803    VibrantDark,
1804}
1805
1806/// 窗口背景的外观,当没有内容或内容透明时使用
1807#[derive(Copy, Clone, Debug, Default, PartialEq)]
1808pub enum WindowBackgroundAppearance {
1809    /// 不透明
1810    ///
1811    /// 这告知窗口管理器不需要绘制此窗口后面的内容
1812    ///
1813    /// 实际颜色取决于系统,主题应定义完全不透明的背景色
1814    #[default]
1815    Opaque,
1816    /// 纯 Alpha 透明
1817    Transparent,
1818    /// 透明,但窗口后面的内容会被模糊
1819    ///
1820    /// 并非所有平台都支持
1821    Blurred,
1822    /// 云母背景材质,仅 Windows 11 支持
1823    MicaBackdrop,
1824    /// 云母 Alt 背景材质,仅 Windows 11 支持
1825    MicaAltBackdrop,
1826}
1827
1828/// 绘制字形时使用的文本渲染模式
1829#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1830pub enum TextRenderingMode {
1831    /// 使用平台默认的文本渲染模式
1832    #[default]
1833    PlatformDefault,
1834    /// 使用子像素(ClearType 风格)文本渲染
1835    Subpixel,
1836    /// 使用灰度文本渲染
1837    Grayscale,
1838}
1839
1840/// 文件对话框提示的可配置选项
1841#[derive(Clone, Debug)]
1842pub struct PathPromptOptions {
1843    /// 是否允许选择文件
1844    pub files: bool,
1845    /// 是否允许选择目录
1846    pub directories: bool,
1847    /// 是否允许多选
1848    pub multiple: bool,
1849    /// 选择路径时显示给用户的提示文字
1850    pub prompt: Option<SharedString>,
1851}
1852
1853/// 提示框的样式级别
1854#[derive(Copy, Clone, Debug, PartialEq)]
1855pub enum PromptLevel {
1856    /// 通知用户某些信息
1857    Info,
1858
1859    /// 警告用户存在潜在问题
1860    Warning,
1861
1862    /// 发生了严重问题
1863    Critical,
1864}
1865
1866/// 提示框按钮
1867#[derive(Clone, Debug, PartialEq)]
1868pub enum PromptButton {
1869    /// 确定按钮
1870    Ok(SharedString),
1871    /// 取消按钮
1872    Cancel(SharedString),
1873    /// 其他按钮
1874    Other(SharedString),
1875}
1876
1877impl PromptButton {
1878    /// 创建带标签的按钮
1879    pub fn new(label: impl Into<SharedString>) -> Self {
1880        PromptButton::Other(label.into())
1881    }
1882
1883    /// 创建确定按钮
1884    pub fn ok(label: impl Into<SharedString>) -> Self {
1885        PromptButton::Ok(label.into())
1886    }
1887
1888    /// 创建取消按钮
1889    pub fn cancel(label: impl Into<SharedString>) -> Self {
1890        PromptButton::Cancel(label.into())
1891    }
1892
1893    /// 返回此按钮是否为取消按钮
1894    #[allow(dead_code)]
1895    pub fn is_cancel(&self) -> bool {
1896        matches!(self, PromptButton::Cancel(_))
1897    }
1898
1899    /// 返回按钮标签
1900    pub fn label(&self) -> &SharedString {
1901        match self {
1902            PromptButton::Ok(label) => label,
1903            PromptButton::Cancel(label) => label,
1904            PromptButton::Other(label) => label,
1905        }
1906    }
1907}
1908
1909impl From<&str> for PromptButton {
1910    fn from(value: &str) -> Self {
1911        match value.to_lowercase().as_str() {
1912            "ok" => PromptButton::Ok("Ok".into()),
1913            "cancel" => PromptButton::Cancel("Cancel".into()),
1914            _ => PromptButton::Other(SharedString::from(value.to_owned())),
1915        }
1916    }
1917}
1918
1919/// 鼠标指针样式
1920#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
1921pub enum CursorStyle {
1922    /// 默认箭头指针
1923    #[default]
1924    Arrow,
1925
1926    /// 文本输入光标
1927    /// 对应 CSS cursor 值 `text`
1928    IBeam,
1929
1930    /// 十字准星光标
1931    /// 对应 CSS cursor 值 `crosshair`
1932    Crosshair,
1933
1934    /// 闭合手形光标
1935    /// 对应 CSS cursor 值 `grabbing`
1936    ClosedHand,
1937
1938    /// 张开手形光标
1939    /// 对应 CSS cursor 值 `grab`
1940    OpenHand,
1941
1942    /// 指向手形光标
1943    /// 对应 CSS cursor 值 `pointer`
1944    PointingHand,
1945
1946    /// 向左调整大小光标
1947    /// 对应 CSS cursor 值 `w-resize`
1948    ResizeLeft,
1949
1950    /// 向右调整大小光标
1951    /// 对应 CSS cursor 值 `e-resize`
1952    ResizeRight,
1953
1954    /// 左右调整大小光标
1955    /// 对应 CSS cursor 值 `ew-resize`
1956    ResizeLeftRight,
1957
1958    /// 向上调整大小光标
1959    /// 对应 CSS cursor 值 `n-resize`
1960    ResizeUp,
1961
1962    /// 向下调整大小光标
1963    /// 对应 CSS cursor 值 `s-resize`
1964    ResizeDown,
1965
1966    /// 上下调整大小光标
1967    /// 对应 CSS cursor 值 `ns-resize`
1968    ResizeUpDown,
1969
1970    /// 左上-右下调整大小光标
1971    /// 对应 CSS cursor 值 `nesw-resize`
1972    ResizeUpLeftDownRight,
1973
1974    /// 右上-左下调整大小光标
1975    /// 对应 CSS cursor 值 `nwse-resize`
1976    ResizeUpRightDownLeft,
1977
1978    /// 表示项目/列可水平调整大小的光标
1979    /// 对应 CSS cursor 值 `col-resize`
1980    ResizeColumn,
1981
1982    /// 表示项目/行可垂直调整大小的光标
1983    /// 对应 CSS cursor 值 `row-resize`
1984    ResizeRow,
1985
1986    /// 垂直布局的文本输入光标
1987    /// 对应 CSS cursor 值 `vertical-text`
1988    IBeamCursorForVerticalLayout,
1989
1990    /// 表示操作不允许的光标
1991    /// 对应 CSS cursor 值 `not-allowed`
1992    OperationNotAllowed,
1993
1994    /// 表示操作将产生链接的光标
1995    /// 对应 CSS cursor 值 `alias`
1996    DragLink,
1997
1998    /// 表示操作将产生副本的光标
1999    /// 对应 CSS cursor 值 `copy`
2000    DragCopy,
2001
2002    /// 表示操作将产生上下文菜单的光标
2003    /// 对应 CSS cursor 值 `context-menu`
2004    ContextualMenu,
2005}
2006
2007/// 应复制到剪贴板的项目
2008#[derive(Clone, Debug, Eq, PartialEq)]
2009pub struct ClipboardItem {
2010    /// 此剪贴板项目中的条目
2011    pub entries: Vec<ClipboardEntry>,
2012}
2013
2014/// 剪贴板条目类型,可以是文本、图片或外部文件路径
2015#[derive(Clone, Debug, Eq, PartialEq)]
2016pub enum ClipboardEntry {
2017    /// 文本条目
2018    String(ClipboardString),
2019    /// 图片条目
2020    Image(Image),
2021    /// 外部文件路径条目
2022    ExternalPaths(crate::ExternalPaths),
2023}
2024
2025impl ClipboardItem {
2026    /// 创建不带元数据的文本剪贴板项目
2027    pub fn new_string(text: String) -> Self {
2028        Self {
2029            entries: vec![ClipboardEntry::String(ClipboardString::new(text))],
2030        }
2031    }
2032
2033    /// 创建带元数据的文本剪贴板项目
2034    pub fn new_string_with_metadata(text: String, metadata: String) -> Self {
2035        Self {
2036            entries: vec![ClipboardEntry::String(ClipboardString {
2037                text,
2038                metadata: Some(metadata),
2039            })],
2040        }
2041    }
2042
2043    /// 创建带 JSON 元数据的文本剪贴板项目
2044    pub fn new_string_with_json_metadata<T: Serialize>(text: String, metadata: T) -> Self {
2045        Self {
2046            entries: vec![ClipboardEntry::String(
2047                ClipboardString::new(text).with_json_metadata(metadata),
2048            )],
2049        }
2050    }
2051
2052    /// 创建不带元数据的图片剪贴板项目
2053    pub fn new_image(image: &Image) -> Self {
2054        Self {
2055            entries: vec![ClipboardEntry::Image(image.clone())],
2056        }
2057    }
2058
2059    /// 拼接项目中所有文本条目
2060    /// 如果没有文本条目则返回 None
2061    pub fn text(&self) -> Option<String> {
2062        let mut answer = String::new();
2063
2064        for entry in self.entries.iter() {
2065            if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
2066                answer.push_str(text);
2067            }
2068        }
2069
2070        if answer.is_empty() {
2071            for entry in self.entries.iter() {
2072                if let ClipboardEntry::ExternalPaths(paths) = entry {
2073                    for path in &paths.0 {
2074                        use std::fmt::Write as _;
2075                        _ = write!(answer, "{}", path.display());
2076                    }
2077                }
2078            }
2079        }
2080
2081        if !answer.is_empty() {
2082            Some(answer)
2083        } else {
2084            None
2085        }
2086    }
2087
2088    /// 如果项目是单个文本条目,返回其元数据
2089    #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
2090    pub fn metadata(&self) -> Option<&String> {
2091        match self.entries().first() {
2092            Some(ClipboardEntry::String(clipboard_string)) if self.entries.len() == 1 => {
2093                clipboard_string.metadata.as_ref()
2094            }
2095            _ => None,
2096        }
2097    }
2098
2099    /// 获取项目的条目
2100    pub fn entries(&self) -> &[ClipboardEntry] {
2101        &self.entries
2102    }
2103
2104    /// 获取项目条目的所有权版本
2105    pub fn into_entries(self) -> impl Iterator<Item = ClipboardEntry> {
2106        self.entries.into_iter()
2107    }
2108}
2109
2110impl From<ClipboardString> for ClipboardEntry {
2111    fn from(value: ClipboardString) -> Self {
2112        Self::String(value)
2113    }
2114}
2115
2116impl From<String> for ClipboardEntry {
2117    fn from(value: String) -> Self {
2118        Self::from(ClipboardString::from(value))
2119    }
2120}
2121
2122impl From<Image> for ClipboardEntry {
2123    fn from(value: Image) -> Self {
2124        Self::Image(value)
2125    }
2126}
2127
2128impl From<ClipboardEntry> for ClipboardItem {
2129    fn from(value: ClipboardEntry) -> Self {
2130        Self {
2131            entries: vec![value],
2132        }
2133    }
2134}
2135
2136impl From<String> for ClipboardItem {
2137    fn from(value: String) -> Self {
2138        Self::from(ClipboardEntry::from(value))
2139    }
2140}
2141
2142impl From<Image> for ClipboardItem {
2143    fn from(value: Image) -> Self {
2144        Self::from(ClipboardEntry::from(value))
2145    }
2146}
2147
2148/// One of the editor's supported image formats (e.g. PNG, JPEG) - used when dealing with images in the clipboard
2149#[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter, Hash)]
2150pub enum ImageFormat {
2151    // Sorted from most to least likely to be pasted into an editor,
2152    // which matters when we iterate through them trying to see if
2153    // clipboard content matches them.
2154    /// .png
2155    Png,
2156    /// .jpeg or .jpg
2157    Jpeg,
2158    /// .webp
2159    Webp,
2160    /// .gif
2161    Gif,
2162    /// .svg
2163    Svg,
2164    /// .bmp
2165    Bmp,
2166    /// .tif or .tiff
2167    Tiff,
2168    /// .ico
2169    Ico,
2170    /// Netpbm image formats (.pbm, .ppm, .pgm).
2171    Pnm,
2172}
2173
2174impl ImageFormat {
2175    /// Returns the mime type for the ImageFormat
2176    pub const fn mime_type(self) -> &'static str {
2177        match self {
2178            ImageFormat::Png => "image/png",
2179            ImageFormat::Jpeg => "image/jpeg",
2180            ImageFormat::Webp => "image/webp",
2181            ImageFormat::Gif => "image/gif",
2182            ImageFormat::Svg => "image/svg+xml",
2183            ImageFormat::Bmp => "image/bmp",
2184            ImageFormat::Tiff => "image/tiff",
2185            ImageFormat::Ico => "image/ico",
2186            ImageFormat::Pnm => "image/x-portable-anymap",
2187        }
2188    }
2189
2190    /// Returns the ImageFormat for the given mime type, including known aliases.
2191    pub fn from_mime_type(mime_type: &str) -> Option<Self> {
2192        use strum::IntoEnumIterator;
2193        Self::iter()
2194            .find(|format| format.mime_type() == mime_type)
2195            .or_else(|| Self::from_mime_type_alias(mime_type))
2196    }
2197
2198    /// Non-canonical mime types that some producers use in the wild.
2199    /// Unlike `mime_type()` which returns the single canonical form,
2200    /// these are legacy or shortened variants we still need to recognize.
2201    fn from_mime_type_alias(mime_type: &str) -> Option<Self> {
2202        match mime_type {
2203            "image/jpg" => Some(Self::Jpeg),
2204            "image/tif" => Some(Self::Tiff),
2205            _ => None,
2206        }
2207    }
2208}
2209
2210/// An image, with a format and certain bytes
2211#[derive(Clone, Debug, PartialEq, Eq)]
2212pub struct Image {
2213    /// The image format the bytes represent (e.g. PNG)
2214    pub format: ImageFormat,
2215    /// The raw image bytes
2216    pub bytes: Vec<u8>,
2217    /// The unique ID for the image
2218    pub id: u64,
2219}
2220
2221impl Hash for Image {
2222    fn hash<H: Hasher>(&self, state: &mut H) {
2223        state.write_u64(self.id);
2224    }
2225}
2226
2227impl Image {
2228    /// An empty image containing no data
2229    pub fn empty() -> Self {
2230        Self::from_bytes(ImageFormat::Png, Vec::new())
2231    }
2232
2233    /// Create an image from a format and bytes
2234    pub fn from_bytes(format: ImageFormat, bytes: Vec<u8>) -> Self {
2235        Self {
2236            id: hash(&bytes),
2237            format,
2238            bytes,
2239        }
2240    }
2241
2242    /// Get this image's ID
2243    pub fn id(&self) -> u64 {
2244        self.id
2245    }
2246
2247    /// Use the GPUI `use_asset` API to make this image renderable
2248    pub fn use_render_image(
2249        self: Arc<Self>,
2250        window: &mut Window,
2251        cx: &mut App,
2252    ) -> Option<Arc<RenderImage>> {
2253        ImageSource::Image(self)
2254            .use_data(None, window, cx)
2255            .and_then(|result| result.ok())
2256    }
2257
2258    /// Use the GPUI `get_asset` API to make this image renderable
2259    pub fn get_render_image(
2260        self: Arc<Self>,
2261        window: &mut Window,
2262        cx: &mut App,
2263    ) -> Option<Arc<RenderImage>> {
2264        ImageSource::Image(self)
2265            .get_data(None, window, cx)
2266            .and_then(|result| result.ok())
2267    }
2268
2269    /// Use the GPUI `remove_asset` API to drop this image, if possible.
2270    pub fn remove_asset(self: Arc<Self>, cx: &mut App) {
2271        ImageSource::Image(self).remove_asset(cx);
2272    }
2273
2274    /// Convert the clipboard image to an `ImageData` object.
2275    pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
2276        fn frames_for_image(
2277            bytes: &[u8],
2278            format: image::ImageFormat,
2279        ) -> Result<SmallVec<[Frame; 1]>> {
2280            let mut data = image::load_from_memory_with_format(bytes, format)?.into_rgba8();
2281
2282            // Convert from RGBA to BGRA.
2283            for pixel in data.chunks_exact_mut(4) {
2284                pixel.swap(0, 2);
2285            }
2286
2287            Ok(SmallVec::from_elem(Frame::new(data), 1))
2288        }
2289
2290        let frames = match self.format {
2291            ImageFormat::Gif => {
2292                let decoder = GifDecoder::new(Cursor::new(&self.bytes))?;
2293                let mut frames = SmallVec::new();
2294
2295                for frame in decoder.into_frames() {
2296                    match frame {
2297                        Ok(mut frame) => {
2298                            // Convert from RGBA to BGRA.
2299                            for pixel in frame.buffer_mut().chunks_exact_mut(4) {
2300                                pixel.swap(0, 2);
2301                            }
2302                            frames.push(frame);
2303                        }
2304                        Err(err) => {
2305                            log::debug!("Skipping GIF frame due to decode error: {err}");
2306                        }
2307                    }
2308                }
2309
2310                if frames.is_empty() {
2311                    anyhow::bail!("GIF could not be decoded: all frames failed");
2312                }
2313
2314                frames
2315            }
2316            ImageFormat::Png => frames_for_image(&self.bytes, image::ImageFormat::Png)?,
2317            ImageFormat::Jpeg => frames_for_image(&self.bytes, image::ImageFormat::Jpeg)?,
2318            ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?,
2319            ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
2320            ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
2321            ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
2322            ImageFormat::Svg => {
2323                return svg_renderer
2324                    .render_single_frame(&self.bytes, 1.0)
2325                    .map_err(Into::into);
2326            }
2327            ImageFormat::Pnm => frames_for_image(&self.bytes, image::ImageFormat::Pnm)?,
2328        };
2329
2330        Ok(Arc::new(RenderImage::new(frames)))
2331    }
2332
2333    /// Get the format of the clipboard image
2334    pub fn format(&self) -> ImageFormat {
2335        self.format
2336    }
2337
2338    /// Get the raw bytes of the clipboard image
2339    pub fn bytes(&self) -> &[u8] {
2340        self.bytes.as_slice()
2341    }
2342}
2343
2344/// A clipboard item that should be copied to the clipboard
2345#[derive(Clone, Debug, Eq, PartialEq)]
2346pub struct ClipboardString {
2347    /// The text content.
2348    pub text: String,
2349    /// Optional metadata associated with this clipboard string.
2350    pub metadata: Option<String>,
2351}
2352
2353impl ClipboardString {
2354    /// Create a new clipboard string with the given text
2355    pub fn new(text: String) -> Self {
2356        Self {
2357            text,
2358            metadata: None,
2359        }
2360    }
2361
2362    /// Return a new clipboard item with the metadata replaced by the given metadata,
2363    /// after serializing it as JSON.
2364    pub fn with_json_metadata<T: Serialize>(mut self, metadata: T) -> Self {
2365        self.metadata = Some(serde_json::to_string(&metadata).unwrap());
2366        self
2367    }
2368
2369    /// Get the text of the clipboard string
2370    pub fn text(&self) -> &String {
2371        &self.text
2372    }
2373
2374    /// Get the owned text of the clipboard string
2375    pub fn into_text(self) -> String {
2376        self.text
2377    }
2378
2379    /// Get the metadata of the clipboard string, formatted as JSON
2380    pub fn metadata_json<T>(&self) -> Option<T>
2381    where
2382        T: for<'a> Deserialize<'a>,
2383    {
2384        self.metadata
2385            .as_ref()
2386            .and_then(|m| serde_json::from_str(m).ok())
2387    }
2388
2389    #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
2390    /// Compute a hash of the given text for clipboard change detection.
2391    pub fn text_hash(text: &str) -> u64 {
2392        let mut hasher = SeaHasher::new();
2393        text.hash(&mut hasher);
2394        hasher.finish()
2395    }
2396}
2397
2398impl From<String> for ClipboardString {
2399    fn from(value: String) -> Self {
2400        Self {
2401            text: value,
2402            metadata: None,
2403        }
2404    }
2405}
2406
2407/// 语义化窗口位置,用于相对于屏幕定位窗口
2408#[derive(Debug, Clone, PartialEq)]
2409pub enum WindowPosition {
2410    /// 在主显示器居中显示窗口
2411    Center,
2412    /// 在指定显示器上居中
2413    CenterOnDisplay(DisplayId),
2414    /// 在托盘图标区域上方居中显示
2415    TrayCenter(Bounds<Pixels>),
2416    /// 定位在右上角
2417    TopRight {
2418        /// 距离屏幕边缘的边距
2419        margin: Pixels,
2420    },
2421    /// 定位在右下角
2422    BottomRight {
2423        /// 距离屏幕边缘的边距
2424        margin: Pixels,
2425    },
2426    /// 定位在左上角
2427    TopLeft {
2428        /// 距离屏幕边缘的边距
2429        margin: Pixels,
2430    },
2431    /// 定位在左下角
2432    BottomLeft {
2433        /// 距离屏幕边缘的边距
2434        margin: Pixels,
2435    },
2436}
2437
2438/// 当前系统聚焦窗口的信息
2439#[derive(Debug, Clone)]
2440pub struct FocusedWindowInfo {
2441    /// 应用程序名称
2442    pub app_name: String,
2443    /// 窗口标题
2444    pub window_title: String,
2445    /// macOS 专属:应用 Bundle ID
2446    pub bundle_id: Option<String>,
2447    /// 进程 ID
2448    pub pid: Option<u32>,
2449}
2450
2451/// 权限类型枚举
2452#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2453pub enum PermissionType {
2454    /// 辅助功能权限
2455    Accessibility,
2456    /// 屏幕录制权限
2457    ScreenCapture,
2458    /// 输入监控权限
2459    InputMonitoring,
2460}
2461
2462/// 权限状态枚举
2463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2464pub enum PermissionStatus {
2465    /// 权限已授予
2466    Granted,
2467    /// 权限被拒绝
2468    Denied,
2469    /// 权限尚未确定(用户未做出选择)
2470    NotDetermined,
2471}
2472
2473/// 全局快捷键事件
2474#[derive(Debug, Clone)]
2475pub struct GlobalHotKeyEvent {
2476    /// 快捷键的唯一标识符
2477    pub id: u32,
2478}
2479
2480/// 系统电源状态变更事件
2481#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2482pub enum SystemPowerEvent {
2483    /// 系统即将休眠
2484    Suspend,
2485    /// 系统已从休眠中恢复
2486    Resume,
2487    /// 屏幕已锁定
2488    LockScreen,
2489    /// 屏幕已解锁
2490    UnlockScreen,
2491    /// 系统正在关机
2492    Shutdown,
2493}
2494
2495/// 电源阻止器类型,用于阻止系统进入省电模式
2496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2497pub enum PowerSaveBlockerKind {
2498    /// 阻止应用被挂起
2499    PreventAppSuspension,
2500    /// 阻止显示器进入睡眠
2501    PreventDisplaySleep,
2502}
2503
2504/// 当前网络连接状态
2505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2506pub enum NetworkStatus {
2507    /// 系统有网络连接
2508    Online,
2509    /// 系统无网络连接
2510    Offline,
2511}
2512
2513/// 媒体键事件,来自硬件媒体键或系统媒体控件
2514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2515pub enum MediaKeyEvent {
2516    /// 播放媒体
2517    Play,
2518    /// 暂停媒体
2519    Pause,
2520    /// 切换播放/暂停
2521    PlayPause,
2522    /// 停止媒体播放
2523    Stop,
2524    /// 跳转到下一曲目
2525    NextTrack,
2526    /// 跳转到上一曲目
2527    PreviousTrack,
2528}
2529
2530/// 请求用户注意力的类型
2531#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2532pub enum AttentionType {
2533    /// 信息性请求(例如弹跳 Dock 图标一次)
2534    Informational,
2535    /// 关键请求(例如持续弹跳 Dock 图标)
2536    Critical,
2537}
2538
2539/// 任务栏/程序坞进度条状态
2540#[derive(Debug, Clone, Copy, PartialEq)]
2541pub enum ProgressBarState {
2542    /// 不显示进度条
2543    None,
2544    /// 显示不确定进度条
2545    Indeterminate,
2546    /// 普通进度条,值为 0.0 到 1.0
2547    Normal(f64),
2548    /// 错误进度条,值为 0.0 到 1.0
2549    Error(f64),
2550    /// 暂停进度条,值为 0.0 到 1.0
2551    Paused(f64),
2552}
2553
2554/// 原生对话框类型
2555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2556pub enum DialogKind {
2557    /// 信息对话框
2558    Info,
2559    /// 警告对话框
2560    Warning,
2561    /// 错误对话框
2562    Error,
2563}
2564
2565/// 原生对话框的选项
2566#[derive(Debug, Clone)]
2567pub struct DialogOptions {
2568    /// 对话框类型
2569    pub kind: DialogKind,
2570    /// 对话框标题
2571    pub title: SharedString,
2572    /// 对话框主消息
2573    pub message: SharedString,
2574    /// 消息下方的可选详细信息
2575    pub detail: Option<SharedString>,
2576    /// 对话框按钮标签
2577    pub buttons: Vec<SharedString>,
2578}
2579
2580/// 操作系统信息
2581#[derive(Debug, Clone)]
2582pub struct OsInfo {
2583    /// 操作系统名称(如 "linux")
2584    pub name: SharedString,
2585    /// 操作系统版本
2586    pub version: SharedString,
2587    /// CPU 架构(如 "x86_64")
2588    pub arch: SharedString,
2589    /// 系统区域设置(如 "en-US")
2590    pub locale: SharedString,
2591    /// 系统主机名
2592    pub hostname: SharedString,
2593}
2594
2595/// 可用的生物识别认证类型
2596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2597pub enum BiometricKind {
2598    /// macOS Touch ID
2599    TouchId,
2600    /// Windows Hello
2601    WindowsHello,
2602    /// 通用指纹识别器
2603    Fingerprint,
2604}
2605
2606/// 生物识别认证的可用性状态
2607#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2608pub enum BiometricStatus {
2609    /// 生物识别认证可用,指定了具体类型
2610    Available(BiometricKind),
2611    /// 生物识别认证不可用
2612    Unavailable,
2613}
2614
2615#[cfg(test)]
2616mod image_tests {
2617    use super::*;
2618    use std::sync::Arc;
2619
2620    #[test]
2621    fn test_svg_image_to_image_data_converts_to_bgra() {
2622        let image = Image::from_bytes(
2623            ImageFormat::Svg,
2624            br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1">
2625<rect width="1" height="1" fill="#38BDF8"/>
2626</svg>"##
2627                .to_vec(),
2628        );
2629
2630        let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
2631        let bytes = render_image.as_bytes(0).unwrap();
2632
2633        for pixel in bytes.chunks_exact(4) {
2634            assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
2635        }
2636    }
2637}
2638
2639#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
2640mod tests {
2641    use super::*;
2642    use std::collections::HashSet;
2643
2644    #[test]
2645    fn test_window_button_layout_parse_standard() {
2646        let layout = WindowButtonLayout::parse("close,minimize:maximize").unwrap();
2647        assert_eq!(
2648            layout.left,
2649            [
2650                Some(WindowButton::Close),
2651                Some(WindowButton::Minimize),
2652                None
2653            ]
2654        );
2655        assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2656    }
2657
2658    #[test]
2659    fn test_window_button_layout_parse_right_only() {
2660        let layout = WindowButtonLayout::parse("minimize,maximize,close").unwrap();
2661        assert_eq!(layout.left, [None, None, None]);
2662        assert_eq!(
2663            layout.right,
2664            [
2665                Some(WindowButton::Minimize),
2666                Some(WindowButton::Maximize),
2667                Some(WindowButton::Close)
2668            ]
2669        );
2670    }
2671
2672    #[test]
2673    fn test_window_button_layout_parse_left_only() {
2674        let layout = WindowButtonLayout::parse("close,minimize,maximize:").unwrap();
2675        assert_eq!(
2676            layout.left,
2677            [
2678                Some(WindowButton::Close),
2679                Some(WindowButton::Minimize),
2680                Some(WindowButton::Maximize)
2681            ]
2682        );
2683        assert_eq!(layout.right, [None, None, None]);
2684    }
2685
2686    #[test]
2687    fn test_window_button_layout_parse_with_whitespace() {
2688        let layout = WindowButtonLayout::parse(" close , minimize : maximize ").unwrap();
2689        assert_eq!(
2690            layout.left,
2691            [
2692                Some(WindowButton::Close),
2693                Some(WindowButton::Minimize),
2694                None
2695            ]
2696        );
2697        assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2698    }
2699
2700    #[test]
2701    fn test_window_button_layout_parse_empty() {
2702        let layout = WindowButtonLayout::parse("").unwrap();
2703        assert_eq!(layout.left, [None, None, None]);
2704        assert_eq!(layout.right, [None, None, None]);
2705    }
2706
2707    #[test]
2708    fn test_window_button_layout_parse_intentionally_empty() {
2709        let layout = WindowButtonLayout::parse(":").unwrap();
2710        assert_eq!(layout.left, [None, None, None]);
2711        assert_eq!(layout.right, [None, None, None]);
2712    }
2713
2714    #[test]
2715    fn test_window_button_layout_parse_invalid_buttons() {
2716        let layout = WindowButtonLayout::parse("close,invalid,minimize:maximize,foo").unwrap();
2717        assert_eq!(
2718            layout.left,
2719            [
2720                Some(WindowButton::Close),
2721                Some(WindowButton::Minimize),
2722                None
2723            ]
2724        );
2725        assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2726    }
2727
2728    #[test]
2729    fn test_window_button_layout_parse_deduplicates_same_side_buttons() {
2730        let layout = WindowButtonLayout::parse("close,close,minimize").unwrap();
2731        assert_eq!(
2732            layout.right,
2733            [
2734                Some(WindowButton::Close),
2735                Some(WindowButton::Minimize),
2736                None
2737            ]
2738        );
2739        assert_eq!(layout.format(), ":close,minimize");
2740    }
2741
2742    #[test]
2743    fn test_window_button_layout_parse_deduplicates_buttons_across_sides() {
2744        let layout = WindowButtonLayout::parse("close:maximize,close,minimize").unwrap();
2745        assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2746        assert_eq!(
2747            layout.right,
2748            [
2749                Some(WindowButton::Maximize),
2750                Some(WindowButton::Minimize),
2751                None
2752            ]
2753        );
2754
2755        let button_ids: Vec<_> = layout
2756            .left
2757            .iter()
2758            .chain(layout.right.iter())
2759            .flatten()
2760            .map(WindowButton::id)
2761            .collect();
2762        let unique_button_ids = button_ids.iter().copied().collect::<HashSet<_>>();
2763        assert_eq!(unique_button_ids.len(), button_ids.len());
2764        assert_eq!(layout.format(), "close:maximize,minimize");
2765    }
2766
2767    #[test]
2768    fn test_window_button_layout_parse_gnome_style() {
2769        let layout = WindowButtonLayout::parse("close").unwrap();
2770        assert_eq!(layout.left, [None, None, None]);
2771        assert_eq!(layout.right, [Some(WindowButton::Close), None, None]);
2772    }
2773
2774    #[test]
2775    fn test_window_button_layout_parse_elementary_style() {
2776        let layout = WindowButtonLayout::parse("close:maximize").unwrap();
2777        assert_eq!(layout.left, [Some(WindowButton::Close), None, None]);
2778        assert_eq!(layout.right, [Some(WindowButton::Maximize), None, None]);
2779    }
2780
2781    #[test]
2782    fn test_window_button_layout_round_trip() {
2783        let cases = [
2784            "close:minimize,maximize",
2785            "minimize,maximize,close:",
2786            ":close",
2787            "close:",
2788            "close:maximize",
2789            ":",
2790        ];
2791
2792        for case in cases {
2793            let layout = WindowButtonLayout::parse(case).unwrap();
2794            assert_eq!(layout.format(), case, "Round-trip failed for: {}", case);
2795        }
2796    }
2797
2798    #[test]
2799    fn test_window_button_layout_linux_default() {
2800        let layout = WindowButtonLayout::linux_default();
2801        assert_eq!(layout.left, [None, None, None]);
2802        assert_eq!(
2803            layout.right,
2804            [
2805                Some(WindowButton::Minimize),
2806                Some(WindowButton::Maximize),
2807                Some(WindowButton::Close)
2808            ]
2809        );
2810
2811        let round_tripped = WindowButtonLayout::parse(&layout.format()).unwrap();
2812        assert_eq!(round_tripped, layout);
2813    }
2814
2815    #[test]
2816    fn test_window_button_layout_parse_all_invalid() {
2817        assert!(WindowButtonLayout::parse("asdfghjkl").is_err());
2818    }
2819}