Skip to main content

iced_window_chrome/
lib.rs

1//! Native-only window chrome patches for [`iced`].
2//!
3//! This crate keeps the runtime integration small:
4//! - call [`apply`] or [`apply_to_latest`] when you want to patch a live window
5//! - install [`subscription`] to watch later-opened windows
6//! - forward emitted [`Event`] values back into [`handle`] in your update loop
7//!
8//! The extra `subscription` + `handle` hop is a real `iced` runtime constraint:
9//! subscriptions can observe window openings, but they cannot directly execute
10//! `window::run` side effects on their own.
11
12mod settings;
13
14#[cfg(target_os = "linux")]
15mod linux;
16#[cfg(target_os = "macos")]
17mod macos;
18#[cfg(target_os = "windows")]
19mod windows;
20
21use iced::{Subscription, Task, window};
22use std::fmt;
23
24pub use settings::{
25    CaptionButtons, ChromeSettings, LinuxChromeSettings, MacosChromeSettings,
26    MacosTitlebarSeparatorStyle, WindowCornerPreference, WindowsBackdrop, WindowsChromeSettings,
27};
28
29/// The current Windows runtime version.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct WindowsVersion {
32    pub major: u32,
33    pub minor: u32,
34    pub build: u32,
35}
36
37impl WindowsVersion {
38    /// Returns `true` for Windows 11 builds and newer.
39    pub fn is_windows_11_or_newer(self) -> bool {
40        self.major > 10 || (self.major == 10 && self.build >= 22_000)
41    }
42}
43
44impl fmt::Display for WindowsVersion {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "{}.{}.{}", self.major, self.minor, self.build)
47    }
48}
49
50/// Windows runtime support flags for version-dependent chrome features.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct WindowsCapabilities {
53    pub version: WindowsVersion,
54    pub corner_preference: bool,
55    pub border_color: bool,
56    pub title_background_color: bool,
57    pub title_text_color: bool,
58    pub system_backdrop: bool,
59}
60
61impl WindowsCapabilities {
62    /// Returns `true` when the Windows 11 DWM visual chrome APIs are available.
63    pub fn supports_dwm_visuals(self) -> bool {
64        self.corner_preference
65            && self.border_color
66            && self.title_background_color
67            && self.title_text_color
68    }
69
70    /// Returns `true` when the newer system backdrop material API is available.
71    pub fn supports_system_backdrop(self) -> bool {
72        self.system_backdrop
73    }
74}
75
76/// Convenience result type for native patch operations.
77pub type Result<T> = std::result::Result<T, Error>;
78
79/// Errors produced while trying to patch a live native window.
80#[derive(Debug)]
81pub enum Error {
82    NoWindowAvailable,
83    WindowHandle(raw_window_handle::HandleError),
84    UnsupportedWindowHandle(&'static str),
85    Windows(&'static str),
86    Macos(&'static str),
87    Linux(&'static str),
88}
89
90impl fmt::Display for Error {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::NoWindowAvailable => {
94                write!(f, "no live iced window was available to patch")
95            }
96            Self::WindowHandle(error) => {
97                write!(f, "failed to access the native window handle: {error}")
98            }
99            Self::UnsupportedWindowHandle(kind) => {
100                write!(f, "unsupported native window handle: {kind}")
101            }
102            Self::Windows(message) => write!(f, "Windows API error: {message}"),
103            Self::Macos(message) => write!(f, "macOS AppKit error: {message}"),
104            Self::Linux(message) => write!(f, "Linux/X11 error: {message}"),
105        }
106    }
107}
108
109impl std::error::Error for Error {}
110
111/// Subscription payloads that should be fed back into [`handle`].
112#[derive(Debug, Clone, PartialEq, Hash)]
113pub enum Event {
114    ApplyToWindow {
115        id: window::Id,
116        settings: ChromeSettings,
117    },
118}
119
120/// Apply a chrome patch to the given live `iced` window.
121pub fn apply<Message>(id: window::Id, settings: ChromeSettings) -> Task<Message>
122where
123    Message: Send + 'static,
124{
125    apply_result(id, settings).discard()
126}
127
128/// Apply a chrome patch to the given live `iced` window and return the native result.
129pub fn apply_result(id: window::Id, settings: ChromeSettings) -> Task<Result<()>> {
130    window::run(id, move |native| apply_native(native, &settings))
131}
132
133/// Apply a chrome patch to the most recently opened live `iced` window.
134pub fn apply_to_latest<Message>(settings: ChromeSettings) -> Task<Message>
135where
136    Message: Send + 'static,
137{
138    apply_to_latest_result(settings).discard()
139}
140
141/// Apply a chrome patch to the most recently opened live `iced` window and return the native result.
142pub fn apply_to_latest_result(settings: ChromeSettings) -> Task<Result<()>> {
143    window::latest().then(move |id| match id {
144        Some(id) => apply_result(id, settings.clone()),
145        None => Task::done(Err(Error::NoWindowAvailable)),
146    })
147}
148
149/// Subscribe to later-opened windows so they can be patched in your update loop.
150pub fn subscription(settings: ChromeSettings) -> Subscription<Event> {
151    window::open_events().with(settings).map(map_open_event)
152}
153
154/// Handle a crate [`Event`] emitted by [`subscription`].
155pub fn handle<Message>(event: Event) -> Task<Message>
156where
157    Message: Send + 'static,
158{
159    handle_result(event).discard()
160}
161
162/// Handle a crate [`Event`] emitted by [`subscription`] and return the native result.
163pub fn handle_result(event: Event) -> Task<Result<()>> {
164    match event {
165        Event::ApplyToWindow { id, settings } => apply_result(id, settings),
166    }
167}
168
169/// Returns Windows runtime capabilities when running on Windows.
170#[cfg(target_os = "windows")]
171pub fn current_windows_capabilities() -> Option<WindowsCapabilities> {
172    windows::current_capabilities()
173}
174
175/// Returns Windows runtime capabilities when running on Windows.
176#[cfg(not(target_os = "windows"))]
177pub fn current_windows_capabilities() -> Option<WindowsCapabilities> {
178    None
179}
180
181#[cfg(target_os = "windows")]
182fn apply_native(window: &dyn iced::window::Window, settings: &ChromeSettings) -> Result<()> {
183    use raw_window_handle::RawWindowHandle;
184
185    let handle = window.window_handle().map_err(Error::WindowHandle)?;
186
187    match handle.as_raw() {
188        RawWindowHandle::Win32(handle) => windows::apply(handle, settings),
189        _ => Err(Error::UnsupportedWindowHandle("non-Win32")),
190    }
191}
192
193#[cfg(target_os = "macos")]
194fn apply_native(window: &dyn iced::window::Window, settings: &ChromeSettings) -> Result<()> {
195    use raw_window_handle::RawWindowHandle;
196
197    let handle = window.window_handle().map_err(Error::WindowHandle)?;
198
199    match handle.as_raw() {
200        RawWindowHandle::AppKit(handle) => macos::apply(handle, settings),
201        _ => Err(Error::UnsupportedWindowHandle("non-AppKit")),
202    }
203}
204
205#[cfg(target_os = "linux")]
206fn apply_native(window: &dyn iced::window::Window, settings: &ChromeSettings) -> Result<()> {
207    use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
208
209    let window_handle = window.window_handle().map_err(Error::WindowHandle)?;
210    let display_handle = window.display_handle().map_err(Error::WindowHandle)?;
211
212    match (display_handle.as_raw(), window_handle.as_raw()) {
213        (RawDisplayHandle::Xlib(display), RawWindowHandle::Xlib(handle)) => {
214            linux::apply_xlib(display, handle, settings)
215        }
216        (RawDisplayHandle::Wayland(_), RawWindowHandle::Wayland(_)) => {
217            linux::apply_wayland(settings)
218        }
219        _ => Err(Error::UnsupportedWindowHandle("non-Xlib Linux")),
220    }
221}
222
223#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
224fn apply_native(_window: &dyn iced::window::Window, _settings: &ChromeSettings) -> Result<()> {
225    Ok(())
226}
227
228fn map_open_event((settings, id): (ChromeSettings, window::Id)) -> Event {
229    Event::ApplyToWindow { id, settings }
230}