teksilo-core 0.20.0

Core of the Teksilo GUI framework — widget trait, arena, layout engine, event dispatch, focus, signals and theming.
Documentation
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech

//! IntUI preset — JetBrains-flavored starting-point design language.
//!
//! Surfaces are role-differentiated (not elevation-based). 1 dp universal
//! borders. Focus rings drawn outside the control with a 2 dp gap.
//! Tooltip background is dark in both light and dark variants
//! (intentional Int UI house style).
//!
//! These constructors aggregate raw token data from `teksilo-tokens` with
//! the [`Theme`] struct. `ComponentStyles` was
//! removed; every widget now reads `pub const`s on its
//! `recipe_<widget>_style` (themable) or owning widget module
//! (group-4 composites). Theme-wide style installs go through
//! `style_slots: ComponentStyleSlots`.

use teksilo_tokens::{
    ColorTokens, InputTokens, LayoutTokens, MotionTokens, OsThemeColors, ShapeTokens,
    TypographyTokens,
};

use crate::styles::{ComponentStyleSlots, Theme, ThemeAppearance, ThemeExtensions, ThemeId};

/// IntUI light theme.
pub fn light() -> Theme {
    Theme {
        id: ThemeId::new("intui.light"),
        appearance: ThemeAppearance::Light,
        colors: ColorTokens::light_default(),
        layout: LayoutTokens::default(),
        typography: TypographyTokens::default(),
        shape: ShapeTokens::light_default(),
        motion: MotionTokens::default(),
        input: InputTokens::default(),
        style_slots: ComponentStyleSlots::default(),
        extensions: ThemeExtensions::new(),
    }
}

/// IntUI dark theme. Shadows use ~4× stronger alphas than the light
/// variant so they read against dark surfaces.
pub fn dark() -> Theme {
    Theme {
        id: ThemeId::new("intui.dark"),
        appearance: ThemeAppearance::Dark,
        colors: ColorTokens::dark_default(),
        layout: LayoutTokens::default(),
        typography: TypographyTokens::default(),
        shape: ShapeTokens::dark_default(),
        motion: MotionTokens::default(),
        input: InputTokens::default(),
        style_slots: ComponentStyleSlots::default(),
        extensions: ThemeExtensions::new(),
    }
}

/// IntUI coloured by the desktop: the [`light`] or [`dark`] theme, chosen by
/// `dark`, with its colours replaced by
/// [`ColorTokens::from_os_colors`] over `os`.
///
/// This is the theme `ThemeMode::Native` falls back to when the app gives it
/// no theme of its own. Everything but the colours (geometry, typography,
/// motion, input, style slots, extensions, id) is the chosen preset's, so the
/// id stays `intui.light` or `intui.dark` until the caller sets another.
///
/// `dark` picks only the preset under the colours. The colours follow
/// `os.color_scheme`: [`ColorTokens::from_os_colors`] starts from the light
/// or dark defaults that preference names (light when it states none), so a
/// caller normally passes `dark` as `os.color_scheme.is_dark()`.
pub fn from_os(os: &OsThemeColors, dark: bool) -> Theme {
    let base = if dark { self::dark() } else { light() };
    Theme {
        colors: ColorTokens::from_os_colors(os),
        ..base
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn light_reports_light_appearance() {
        assert_eq!(light().appearance, ThemeAppearance::Light);
        assert!(light().appearance.is_light());
        assert!(!light().is_dark());
    }

    #[test]
    fn dark_reports_dark_appearance() {
        assert_eq!(dark().appearance, ThemeAppearance::Dark);
        assert!(dark().appearance.is_dark());
        assert!(dark().is_dark());
    }

    #[test]
    fn light_and_dark_have_distinct_surfaces() {
        assert_ne!(light().colors.surface_main, dark().colors.surface_main);
    }

    #[test]
    fn dark_shadows_are_stronger_than_light() {
        // Int UI v2 §3 — dark theme shadows use higher alphas.
        let l = light();
        let d = dark();
        assert!(d.shape.shadow_lg.color.a() > l.shape.shadow_lg.color.a());
    }

    #[test]
    fn from_os_keeps_the_preset_and_takes_the_report_colours() {
        use teksilo_tokens::{Color, ColorSchemePreference};

        let mut os = OsThemeColors::new(ColorSchemePreference::Dark);
        os.window_bg = Some(Color::from_rgb(0.1, 0.2, 0.3));
        let theme = from_os(&os, true);
        assert_eq!(theme.colors, ColorTokens::from_os_colors(&os));
        assert_eq!(
            Theme {
                colors: dark().colors,
                ..theme
            },
            dark()
        );

        // A report with no colours is the preset itself.
        let plain = OsThemeColors::new(ColorSchemePreference::Light);
        assert_eq!(from_os(&plain, false), light());
    }
}