// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Composite surfaces: fill + outer shadows + inset shadows. //! //! A [`Surface`] is the richest kind of theme slot. It packages together a //! fill (possibly a gradient), an elevation stack, and inset shadows with //! their own blend modes. Widgets that only need a flat colour or a plain //! gradient do not construct [`Surface`] values directly; slot promotion //! (in `theme::slots`) hands them a [`Surface`] with empty decorations when //! they ask for one against a simpler slot. use super::paint::Paint; use super::shadow::{ InsetShadow, ShadowsRef }; use crate::types::Color; // ─── Surface ───────────────────────────────────────────────────────────────── /// A composite theme surface: fill, elevation, insets. /// /// All decorations are optional. A surface with `fill: Paint::Solid(...)`, /// no shadows and no insets behaves exactly like a flat-colour fill, which /// is why the promotion path from a `color` slot to a [`Surface`] is trivial. #[ derive( Debug, Clone, PartialEq ) ] pub struct Surface { /// What the surface is filled with. pub fill: Paint, /// Outer shadow stack. When `Some`, can reference another slot by name /// or inline the full list. pub shadows: Option, /// Inset shadows, in back-to-front order. Each entry carries its own /// [`crate::theme::BlendMode`]. pub inset_shadows: Vec, } impl Surface { /// Build a surface from just a [`Paint`], with no shadows or insets. pub fn from_paint( paint: Paint ) -> Self { Self { fill: paint, shadows: None, inset_shadows: Vec::new(), } } } impl From for Surface { fn from( p: Paint ) -> Self { Surface::from_paint( p ) } } impl From for Surface { fn from( c: Color ) -> Self { Surface::from_paint( Paint::Solid( c ) ) } } // ─── Tests ─────────────────────────────────────────────────────────────────── #[ cfg( test ) ] mod tests { use super::*; use crate::types::Color; use crate::theme::shadow::{ BlendMode, InsetShadow, Shadow, ShadowsRef }; #[ test ] fn surface_promotes_from_color() { let s: Surface = Color::hex( 0x04, 0xD9, 0xFE ).into(); assert_eq!( s.fill, Paint::Solid( Color::hex( 0x04, 0xD9, 0xFE ) ) ); assert!( s.shadows.is_none() ); assert!( s.inset_shadows.is_empty() ); } #[ test ] fn glass_accent_shape_composes() { let s = Surface { fill: Paint::Solid( Color::hex( 0x04, 0xD9, 0xFE ) ), shadows: Some( ShadowsRef::Named( "shadows-glass".to_string() ) ), inset_shadows: vec! [ InsetShadow { offset: [ -3.6, -3.6 ], blur: 13.5, spread: 0.0, color: Color::hex( 0x55, 0x55, 0x55 ), blend: BlendMode::PlusLighter }, InsetShadow { offset: [ 1.8, 1.8 ], blur: 1.8, spread: 0.0, color: Color::hex( 0x55, 0x55, 0x55 ), blend: BlendMode::PlusLighter }, InsetShadow { offset: [ 0.45, 0.45 ], blur: 0.9, spread: 0.0, color: Color::BLACK, blend: BlendMode::Overlay }, InsetShadow { offset: [ 1.8, 1.8 ], blur: 7.2, spread: 0.0, color: Color::rgba( 0.0, 0.0, 0.0, 0.15 ), blend: BlendMode::Normal }, ], }; assert_eq!( s.inset_shadows.len(), 4 ); assert_eq!( s.inset_shadows[0].blend, BlendMode::PlusLighter ); assert_eq!( s.inset_shadows[2].blend, BlendMode::Overlay ); let _ = Shadow { offset: [ 0.0, 0.0 ], blur: 9.0, spread: 0.0, color: Color::rgba( 33.0 / 255.0, 33.0 / 255.0, 33.0 / 255.0, 0.25 ), blend: BlendMode::Normal, }; } }