ltk/theme/
surface.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 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,
		};
	}
}