iced-window-chrome 0.1.0

Native-only window chrome patches for iced on Windows, macOS, and Linux
Documentation
use crate::{
    ChromeSettings, Error, Result, WindowCornerPreference, WindowsBackdrop, WindowsCapabilities,
    WindowsChromeSettings, WindowsVersion,
};

use iced::Color;
use raw_window_handle::Win32WindowHandle;

use std::ffi::c_void;
use std::mem::size_of;
use std::ptr::null_mut;

use windows_sys::Wdk::System::SystemServices::RtlGetVersion;
use windows_sys::Win32::Foundation::{GetLastError, HWND, STATUS_SUCCESS, SetLastError};
use windows_sys::Win32::Graphics::Dwm::{
    DWMSBT_AUTO, DWMSBT_MAINWINDOW, DWMSBT_NONE, DWMSBT_TABBEDWINDOW, DWMSBT_TRANSIENTWINDOW,
    DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_TEXT_COLOR,
    DWMWA_WINDOW_CORNER_PREFERENCE, DwmSetWindowAttribute,
};
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
use windows_sys::Win32::UI::WindowsAndMessaging::{
    DrawMenuBar, EnableMenuItem, GWL_STYLE, GetSystemMenu, GetWindowLongPtrW, MF_BYCOMMAND,
    MF_ENABLED, MF_GRAYED, SC_CLOSE, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER,
    SetWindowLongPtrW, SetWindowPos, WS_CAPTION, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_SYSMENU,
    WS_THICKFRAME,
};

const DWMWA_COLOR_DEFAULT: u32 = 0xFFFF_FFFF;
const DWMWA_COLOR_NONE: u32 = 0xFFFF_FFFE;
const DWMWCP_DEFAULT: u32 = 0;
const DWMWCP_DONOTROUND: u32 = 1;
const DWMWCP_ROUND: u32 = 2;
const DWMWCP_ROUNDSMALL: u32 = 3;

pub fn current_capabilities() -> Option<WindowsCapabilities> {
    let mut info = OSVERSIONINFOW {
        dwOSVersionInfoSize: size_of::<OSVERSIONINFOW>() as u32,
        ..OSVERSIONINFOW::default()
    };

    let status = unsafe { RtlGetVersion(&mut info) };
    if status != STATUS_SUCCESS {
        return None;
    }

    let version = WindowsVersion {
        major: info.dwMajorVersion,
        minor: info.dwMinorVersion,
        build: info.dwBuildNumber,
    };
    let supports_dwm_visuals = version.is_windows_11_or_newer();
    let supports_system_backdrop =
        version.major > 10 || (version.major == 10 && version.build >= 22_621);

    Some(WindowsCapabilities {
        version,
        corner_preference: supports_dwm_visuals,
        border_color: supports_dwm_visuals,
        title_background_color: supports_dwm_visuals,
        title_text_color: supports_dwm_visuals,
        system_backdrop: supports_system_backdrop,
    })
}

pub fn apply(handle: Win32WindowHandle, settings: &ChromeSettings) -> Result<()> {
    let hwnd = handle.hwnd.get() as HWND;

    unsafe {
        apply_style_bits(hwnd, &settings.windows)?;
        apply_system_menu(hwnd, &settings.windows)?;
        apply_dwm_attributes(hwnd, &settings.windows)?;
    }

    Ok(())
}

unsafe fn apply_style_bits(hwnd: HWND, settings: &WindowsChromeSettings) -> Result<()> {
    let mut style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) as u32 };

    style = with_flag(style, WS_CAPTION, settings.caption);
    style = with_flag(style, WS_THICKFRAME, settings.border);
    style = with_flag(style, WS_MINIMIZEBOX, settings.buttons.minimize);
    style = with_flag(style, WS_MAXIMIZEBOX, settings.buttons.maximize);

    let wants_system_menu =
        settings.buttons.close || settings.buttons.minimize || settings.buttons.maximize;
    style = with_flag(style, WS_SYSMENU, wants_system_menu);

    unsafe { SetLastError(0) };
    let previous = unsafe { SetWindowLongPtrW(hwnd, GWL_STYLE, style as isize) };
    if previous == 0 && unsafe { GetLastError() } != 0 {
        return Err(Error::Windows("SetWindowLongPtrW"));
    }

    let ok = unsafe {
        SetWindowPos(
            hwnd,
            null_mut(),
            0,
            0,
            0,
            0,
            SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER,
        )
    };
    if ok == 0 {
        return Err(Error::Windows("SetWindowPos"));
    }

    Ok(())
}

unsafe fn apply_system_menu(hwnd: HWND, settings: &WindowsChromeSettings) -> Result<()> {
    let wants_system_menu =
        settings.buttons.close || settings.buttons.minimize || settings.buttons.maximize;
    if !wants_system_menu {
        return Ok(());
    }

    let system_menu = unsafe { GetSystemMenu(hwnd, 0) };
    if system_menu.is_null() {
        return Err(Error::Windows("GetSystemMenu"));
    }

    let menu_state = if settings.buttons.close {
        MF_BYCOMMAND | MF_ENABLED
    } else {
        MF_BYCOMMAND | MF_GRAYED
    };

    unsafe { EnableMenuItem(system_menu, SC_CLOSE, menu_state) };

    if unsafe { DrawMenuBar(hwnd) } == 0 {
        return Err(Error::Windows("DrawMenuBar"));
    }

    Ok(())
}

unsafe fn apply_dwm_attributes(hwnd: HWND, settings: &WindowsChromeSettings) -> Result<()> {
    let Some(capabilities) = current_capabilities() else {
        return Ok(());
    };

    let corner_preference = settings
        .corner_preference
        .unwrap_or(WindowCornerPreference::Default);
    if capabilities.corner_preference {
        let corner_value = match corner_preference {
            WindowCornerPreference::Default => DWMWCP_DEFAULT,
            WindowCornerPreference::DoNotRound => DWMWCP_DONOTROUND,
            WindowCornerPreference::Round => DWMWCP_ROUND,
            WindowCornerPreference::RoundSmall => DWMWCP_ROUNDSMALL,
        };
        unsafe { set_dwm_attribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE as u32, &corner_value)? };
    }

    if capabilities.border_color {
        let border_value = if !settings.border {
            DWMWA_COLOR_NONE
        } else {
            settings
                .border_color
                .map(colorref)
                .unwrap_or(DWMWA_COLOR_DEFAULT)
        };
        unsafe { set_dwm_attribute(hwnd, DWMWA_BORDER_COLOR as u32, &border_value)? };
    }

    if capabilities.title_background_color {
        let title_background = settings
            .title_background_color
            .map(colorref)
            .unwrap_or(DWMWA_COLOR_DEFAULT);
        unsafe { set_dwm_attribute(hwnd, DWMWA_CAPTION_COLOR as u32, &title_background)? };
    }

    if capabilities.title_text_color {
        let title_text = settings
            .title_text_color
            .map(colorref)
            .unwrap_or(DWMWA_COLOR_DEFAULT);
        unsafe { set_dwm_attribute(hwnd, DWMWA_TEXT_COLOR as u32, &title_text)? };
    }

    if capabilities.system_backdrop {
        let backdrop = settings.backdrop.map(backdrop_type).unwrap_or(DWMSBT_AUTO);
        unsafe { set_dwm_attribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE as u32, &backdrop)? };
    }

    Ok(())
}

unsafe fn set_dwm_attribute<T>(hwnd: HWND, attribute: u32, value: &T) -> Result<()> {
    let result = unsafe {
        DwmSetWindowAttribute(
            hwnd,
            attribute,
            value as *const T as *const c_void,
            size_of::<T>() as u32,
        )
    };

    if result != 0 {
        return Err(Error::Windows("DwmSetWindowAttribute"));
    }

    Ok(())
}

fn with_flag(style: u32, flag: u32, enabled: bool) -> u32 {
    if enabled { style | flag } else { style & !flag }
}

fn colorref(color: Color) -> u32 {
    let [red, green, blue, _] = color.into_rgba8();
    u32::from(red) | (u32::from(green) << 8) | (u32::from(blue) << 16)
}

fn backdrop_type(backdrop: WindowsBackdrop) -> i32 {
    match backdrop {
        WindowsBackdrop::None => DWMSBT_NONE,
        WindowsBackdrop::Mica => DWMSBT_MAINWINDOW,
        WindowsBackdrop::Acrylic => DWMSBT_TRANSIENTWINDOW,
        WindowsBackdrop::MicaAlt => DWMSBT_TABBEDWINDOW,
    }
}