use waterui::style::{FloatingStyle, Shadow as ViewShadow, Vector};
use waterui::{Environment, View, ViewExt as _};
use crate::color::Shadow;
const KEY_OPACITY: f32 = 0.19;
const AMBIENT_OPACITY: f32 = 0.039;
const KEY_SHADOWS: [ElevationShadow; 6] = [
ElevationShadow::new(0.0, 0.0),
ElevationShadow::new(0.5, 1.5),
ElevationShadow::new(0.85, 3.0),
ElevationShadow::new(1.25, 5.0),
ElevationShadow::new(1.85, 6.25),
ElevationShadow::new(2.75, 9.0),
];
const AMBIENT_SHADOWS: [ElevationShadow; 6] = [
ElevationShadow::new(0.0, 0.0),
ElevationShadow::new(0.0, 1.0),
ElevationShadow::new(0.25, 1.0),
ElevationShadow::new(0.3333, 1.5),
ElevationShadow::new(0.5, 1.75),
ElevationShadow::new(0.25, 3.0),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MaterialElevationLevel(u8);
impl MaterialElevationLevel {
pub const LEVEL0: Self = Self(0);
pub const LEVEL1: Self = Self(1);
pub const LEVEL2: Self = Self(2);
pub const LEVEL3: Self = Self(3);
pub const LEVEL4: Self = Self(4);
pub const LEVEL5: Self = Self(5);
#[must_use]
pub const fn new(level: u8) -> Self {
assert!(level <= 5, "Material elevation level must be in 0..=5");
Self(level)
}
const fn index(self) -> usize {
self.0 as usize
}
}
impl Default for MaterialElevationLevel {
fn default() -> Self {
Self::LEVEL0
}
}
#[derive(Debug)]
pub struct MaterialElevation<Content> {
level: MaterialElevationLevel,
corner_radius: f32,
content: Content,
}
impl<Content> MaterialElevation<Content> {
#[must_use]
pub const fn new(level: MaterialElevationLevel, corner_radius: f32, content: Content) -> Self {
Self {
level,
corner_radius,
content,
}
}
}
impl<Content> View for MaterialElevation<Content>
where
Content: View + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let tokens = ElevationTokens::for_level(self.level);
self.content
.shadow(tokens.ambient_shadow(self.corner_radius))
.shadow(tokens.key_shadow(self.corner_radius))
}
}
#[must_use]
pub const fn material_elevation<Content>(
level: MaterialElevationLevel,
corner_radius: f32,
content: Content,
) -> MaterialElevation<Content> {
MaterialElevation::new(level, corner_radius, content)
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct ElevationTokens {
key: ElevationShadow,
ambient: ElevationShadow,
}
impl ElevationTokens {
const fn for_level(level: MaterialElevationLevel) -> Self {
Self {
key: KEY_SHADOWS[level.index()],
ambient: AMBIENT_SHADOWS[level.index()],
}
}
fn key_shadow(self, corner_radius: f32) -> ViewShadow {
ViewShadow::new(
Shadow.with_opacity(self.key.opacity(KEY_OPACITY)).into(),
Vector::new(0.0, self.key.y),
self.key.blur,
corner_radius,
)
}
fn ambient_shadow(self, corner_radius: f32) -> ViewShadow {
ViewShadow::new(
Shadow
.with_opacity(self.ambient.opacity(AMBIENT_OPACITY))
.into(),
Vector::new(0.0, self.ambient.y),
self.ambient.blur,
corner_radius,
)
}
}
#[derive(Debug, Clone)]
pub(crate) struct LevelShadow {
pub color: waterui::color::Color,
pub offset_y: f32,
pub radius: f32,
}
pub(crate) fn shadows_for_level(level: MaterialElevationLevel) -> (LevelShadow, LevelShadow) {
let tokens = ElevationTokens::for_level(level);
(
LevelShadow {
color: Shadow.with_opacity(tokens.key.opacity(KEY_OPACITY)).into(),
offset_y: tokens.key.y,
radius: tokens.key.blur,
},
LevelShadow {
color: Shadow
.with_opacity(tokens.ambient.opacity(AMBIENT_OPACITY))
.into(),
offset_y: tokens.ambient.y,
radius: tokens.ambient.blur,
},
)
}
pub(crate) fn draw_shadows(
draw: &mut dyn crate::DrawContext,
rect: vello::kurbo::Rect,
radii: vello::kurbo::RoundedRectRadii,
level: MaterialElevationLevel,
colors: &crate::theme::colors::MaterialColorScheme,
) {
let tokens = ElevationTokens::for_level(level);
for (shadow, base_opacity) in [(tokens.key, KEY_OPACITY), (tokens.ambient, AMBIENT_OPACITY)] {
draw.draw_shadow(
rect,
radii,
vello::kurbo::Vec2::new(0.0, f64::from(shadow.y)),
f64::from(shadow.blur),
colors
.shadow
.peniko()
.with_alpha(shadow.opacity(base_opacity)),
);
}
}
pub(crate) fn apply_to_floating_style(style: &mut FloatingStyle, level: MaterialElevationLevel) {
let tokens = ElevationTokens::for_level(level);
style.key_shadow_color = Shadow.with_opacity(tokens.key.opacity(KEY_OPACITY)).into();
style.key_shadow_offset_y = tokens.key.y;
style.key_shadow_radius = tokens.key.blur;
style.ambient_shadow_color = Shadow
.with_opacity(tokens.ambient.opacity(AMBIENT_OPACITY))
.into();
style.ambient_shadow_offset_y = tokens.ambient.y;
style.ambient_shadow_radius = tokens.ambient.blur;
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct ElevationShadow {
y: f32,
blur: f32,
}
impl ElevationShadow {
const fn new(y: f32, blur: f32) -> Self {
Self { y, blur }
}
const fn opacity(self, opacity: f32) -> f32 {
if self.y == 0.0 && self.blur == 0.0 {
0.0
} else {
opacity
}
}
}
#[cfg(test)]
mod tests {
use super::{
AMBIENT_OPACITY, AMBIENT_SHADOWS, KEY_OPACITY, KEY_SHADOWS, MaterialElevationLevel,
};
#[test]
fn elevation_levels_match_compose_elevation_tokens() {
assert_eq!(KEY_OPACITY, 0.19);
assert_eq!(AMBIENT_OPACITY, 0.039);
assert_eq!(KEY_SHADOWS[0].opacity(KEY_OPACITY), 0.0);
assert_eq!(AMBIENT_SHADOWS[0].opacity(AMBIENT_OPACITY), 0.0);
assert_eq!(KEY_SHADOWS[0].y, 0.0);
assert_eq!(KEY_SHADOWS[1].y, 0.5);
assert_eq!(KEY_SHADOWS[2].blur, 3.0);
assert_eq!(KEY_SHADOWS[3].blur, 5.0);
assert_eq!(KEY_SHADOWS[4].y, 1.85);
assert_eq!(KEY_SHADOWS[5].y, 2.75);
assert_eq!(AMBIENT_SHADOWS[0].blur, 0.0);
assert_eq!(AMBIENT_SHADOWS[1].blur, 1.0);
assert_eq!(AMBIENT_SHADOWS[2].y, 0.25);
assert_eq!(AMBIENT_SHADOWS[3].y, 0.3333);
assert_eq!(AMBIENT_SHADOWS[4].blur, 1.75);
assert_eq!(AMBIENT_SHADOWS[5].blur, 3.0);
}
#[test]
#[should_panic(expected = "Material elevation level must be in 0..=5")]
fn elevation_level_rejects_out_of_range_values() {
let _ = MaterialElevationLevel::new(6);
}
}