use frust::{Color, Theme};
pub mod anchored;
pub mod modal;
pub use anchored::{
ANCHORED_ENTER_SCALE, AnchoredOverlayView, AnchoredOverlayWidget, OVERLAY_ANCHOR_GAP,
OverlayAlign, OverlayAnchor, OverlayAnchorView, OverlayAnchorWidget, OverlayPlacement,
OverlaySide, anchored_overlay, overlay_anchor, place_anchored,
};
pub use modal::{
ModalDismiss, OVERLAY_DIALOG_MAX_WIDTH, OVERLAY_DIALOG_MIN_WIDTH, OVERLAY_EDGE_FRACTION,
OVERLAY_FLING_VELOCITY, OVERLAY_HANDLE_RESERVE, OVERLAY_SHEET_MAX_HEIGHT_FRACTION,
OVERLAY_SIDE_SHEET_MAX_WIDTH, OverlayBorder, OverlayCorners, OverlayEntrance, OverlayExtent,
OverlayGeometry, OverlayLimit, OverlayModalConfig, OverlayModalContent, OverlayModalView,
OverlayModalWidget, OverlayRole, overlay_modal, show_overlay_modal,
};
pub const OVERLAY_SCRIM_ALPHA: f32 = 0.32;
const FALLBACK_SCRIM: Color = Color::from_rgb8(0x00, 0x00, 0x00);
const FALLBACK_CONTAINER_LOW: Color = Color::from_rgb8(0xF7, 0xF2, 0xFA);
const FALLBACK_CONTAINER: Color = Color::from_rgb8(0xF3, 0xED, 0xF7);
const FALLBACK_CONTAINER_HIGH: Color = Color::from_rgb8(0xEC, 0xE6, 0xF0);
const FALLBACK_CONTAINER_HIGHEST: Color = Color::from_rgb8(0xE6, 0xE0, 0xE9);
const FALLBACK_ON_SURFACE: Color = Color::from_rgb8(0x1D, 0x1B, 0x20);
const FALLBACK_ON_SURFACE_VARIANT: Color = Color::from_rgb8(0x49, 0x45, 0x4F);
const FALLBACK_OUTLINE_VARIANT: Color = Color::from_rgb8(0xCA, 0xC4, 0xD0);
const FALLBACK_SHADOW: Color = Color::from_rgb8(0x00, 0x00, 0x00);
const FALLBACK_RADIUS: f64 = 28.0;
const FALLBACK_SHADOW_ALPHA: f32 = 0.3;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverlayContainer {
Low,
Standard,
#[default]
High,
Highest,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverlayElevation {
#[default]
None,
Level1,
Level2,
Level3,
}
pub fn shadow(theme: Option<&Theme>, level: OverlayElevation) -> Option<(f64, f64, Color)> {
let dp = match level {
OverlayElevation::None => return None,
OverlayElevation::Level1 => 1.0,
OverlayElevation::Level2 => 3.0,
OverlayElevation::Level3 => 6.0,
};
Some(match theme {
Some(theme) => {
let table = &theme.elevation;
let entry = match level {
OverlayElevation::None => unreachable!("returned above"),
OverlayElevation::Level1 => table.level1,
OverlayElevation::Level2 => table.level2,
OverlayElevation::Level3 => table.level3,
};
let spec = entry.shadow(theme.brightness);
(
spec.blur_std_dev,
spec.y_offset,
with_alpha(theme.scheme().shadow, spec.color_alpha),
)
}
None => (
dp,
dp / 2.0 + 1.0,
with_alpha(FALLBACK_SHADOW, FALLBACK_SHADOW_ALPHA),
),
})
}
pub(crate) fn with_alpha(color: Color, alpha: f32) -> Color {
let c = color.components;
Color::new([c[0], c[1], c[2], alpha])
}
pub fn scrim(theme: Option<&Theme>) -> Color {
let base = theme.map_or(FALLBACK_SCRIM, |t| t.scheme().scrim);
with_alpha(base, OVERLAY_SCRIM_ALPHA)
}
pub fn container(theme: Option<&Theme>, role: OverlayContainer) -> Color {
match (theme, role) {
(Some(t), OverlayContainer::Low) => t.scheme().surface_container_low,
(Some(t), OverlayContainer::Standard) => t.scheme().surface_container,
(Some(t), OverlayContainer::High) => t.scheme().surface_container_high,
(Some(t), OverlayContainer::Highest) => t.scheme().surface_container_highest,
(None, OverlayContainer::Low) => FALLBACK_CONTAINER_LOW,
(None, OverlayContainer::Standard) => FALLBACK_CONTAINER,
(None, OverlayContainer::High) => FALLBACK_CONTAINER_HIGH,
(None, OverlayContainer::Highest) => FALLBACK_CONTAINER_HIGHEST,
}
}
pub fn on_surface(theme: Option<&Theme>) -> Color {
theme.map_or(FALLBACK_ON_SURFACE, |t| t.scheme().on_surface)
}
pub fn on_surface_variant(theme: Option<&Theme>) -> Color {
theme.map_or(FALLBACK_ON_SURFACE_VARIANT, |t| {
t.scheme().on_surface_variant
})
}
pub fn outline_variant(theme: Option<&Theme>) -> Color {
theme.map_or(FALLBACK_OUTLINE_VARIANT, |t| t.scheme().outline_variant)
}
pub fn radius(theme: Option<&Theme>) -> f64 {
theme.map_or(FALLBACK_RADIUS, |t| t.shape.extra_large)
}
pub(crate) fn reduce_motion(theme: Option<&Theme>) -> bool {
theme.is_some_and(|t| t.motion.reduce_motion)
}
pub(crate) fn finite_or_zero(v: f64) -> f64 {
if v.is_finite() { v } else { 0.0 }
}
#[cfg(test)]
mod tests {
use super::*;
use frust::Brightness;
#[test]
fn every_accessor_folds_onto_its_material_role() {
let t = crate::baseline().with_brightness(Brightness::Light);
let s = t.scheme();
assert_eq!(scrim(Some(&t)), with_alpha(s.scrim, OVERLAY_SCRIM_ALPHA));
assert_eq!(
container(Some(&t), OverlayContainer::Low),
s.surface_container_low
);
assert_eq!(
container(Some(&t), OverlayContainer::Standard),
s.surface_container
);
assert_eq!(
container(Some(&t), OverlayContainer::High),
s.surface_container_high
);
assert_eq!(
container(Some(&t), OverlayContainer::Highest),
s.surface_container_highest
);
assert_eq!(on_surface(Some(&t)), s.on_surface);
assert_eq!(on_surface_variant(Some(&t)), s.on_surface_variant);
assert_eq!(outline_variant(Some(&t)), s.outline_variant);
assert_eq!(radius(Some(&t)), t.shape.extra_large);
}
#[test]
fn the_accessors_follow_a_live_brightness_flip() {
let light = crate::baseline().with_brightness(Brightness::Light);
let dark = crate::baseline().with_brightness(Brightness::Dark);
assert_ne!(
container(Some(&light), OverlayContainer::High),
container(Some(&dark), OverlayContainer::High)
);
assert_ne!(on_surface(Some(&light)), on_surface(Some(&dark)));
}
#[test]
fn the_unthemed_fallbacks_are_the_m3_baseline_light_values() {
let light = crate::baseline().with_brightness(Brightness::Light);
let s = light.scheme();
assert_eq!(
container(None, OverlayContainer::Low),
s.surface_container_low
);
assert_eq!(
container(None, OverlayContainer::High),
s.surface_container_high
);
assert_eq!(on_surface(None), s.on_surface);
assert_eq!(on_surface_variant(None), s.on_surface_variant);
assert_eq!(outline_variant(None), s.outline_variant);
assert_eq!(radius(None), light.shape.extra_large);
let unthemed = scrim(None);
assert_eq!(unthemed.components[..3], [0.0, 0.0, 0.0]);
assert!((unthemed.components[3] - OVERLAY_SCRIM_ALPHA).abs() < 1e-6);
assert_eq!(unthemed, scrim(Some(&light)));
}
#[test]
fn the_elevation_rungs_resolve_off_the_themes_own_table() {
let t = crate::baseline();
assert_eq!(shadow(Some(&t), OverlayElevation::None), None);
let (blur, y, color) = shadow(Some(&t), OverlayElevation::Level3).expect("a level-3 rung");
let spec = t.elevation.level3.shadow(t.brightness);
assert_eq!(blur, spec.blur_std_dev);
assert_eq!(y, spec.y_offset);
assert_eq!(color, with_alpha(t.scheme().shadow, spec.color_alpha));
let (sheet_blur, _, _) =
shadow(Some(&t), OverlayElevation::Level1).expect("a level-1 rung");
assert!(sheet_blur < blur);
}
#[test]
fn the_unthemed_elevation_fallback_matches_the_tables_own_mapping() {
let (blur, y, _) = shadow(None, OverlayElevation::Level3).expect("a level-3 rung");
assert_eq!(blur, 6.0);
assert_eq!(y, 4.0);
assert_eq!(shadow(None, OverlayElevation::None), None);
}
#[test]
fn a_non_material_theme_still_resolves_every_role() {
let bare = frust::Theme::neutral();
let _ = scrim(Some(&bare));
let _ = container(Some(&bare), OverlayContainer::High);
let _ = on_surface(Some(&bare));
let _ = radius(Some(&bare));
assert!(!reduce_motion(None));
}
#[test]
fn finite_or_zero_collapses_an_unbounded_axis() {
assert_eq!(finite_or_zero(400.0), 400.0);
assert_eq!(finite_or_zero(f64::INFINITY), 0.0);
}
}