108 lines
3.7 KiB
Rust
108 lines
3.7 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
|
|
|
//! 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<ShadowsRef>,
|
|
/// Inset shadows, in back-to-front order. Each entry carries its own
|
|
/// [`crate::theme::BlendMode`].
|
|
pub inset_shadows: Vec<InsetShadow>,
|
|
}
|
|
|
|
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<Paint> for Surface
|
|
{
|
|
fn from( p: Paint ) -> Self { Surface::from_paint( p ) }
|
|
}
|
|
|
|
impl From<Color> 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,
|
|
};
|
|
}
|
|
}
|