use crate::material::{BlurStrength, CornerRadius, Opacity, Tint};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Scale(f32);
impl Scale {
pub fn new(factor: f32) -> Self {
Self(factor.max(f32::MIN_POSITIVE))
}
pub fn factor(self) -> f32 {
self.0
}
}
impl Default for Scale {
fn default() -> Self {
Self(1.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Region {
pub origin: [u32; 2],
pub size: [u32; 2],
pub scale: Scale,
}
impl Region {
pub fn clip_to(&self, source_extent: [u32; 2]) -> Option<Region> {
let [ox, oy] = self.origin;
let [w, h] = self.size;
let [ex, ey] = source_extent;
let x0 = ox.min(ex);
let y0 = oy.min(ey);
let x1 = ox.saturating_add(w).min(ex);
let y1 = oy.saturating_add(h).min(ey);
let clipped_w = x1.saturating_sub(x0);
let clipped_h = y1.saturating_sub(y0);
if clipped_w == 0 || clipped_h == 0 {
None
} else {
Some(Region {
origin: [x0, y0],
size: [clipped_w, clipped_h],
scale: self.scale,
})
}
}
}
impl std::fmt::Display for Region {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [x, y] = self.origin;
let [w, h] = self.size;
let scale = self.scale.factor();
write!(f, "origin ({x}, {y}), size {w}×{h}, scale {scale}")
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResolvedMask {
pub half_extents: [f32; 2],
pub corner_radius_px: f32,
}
impl ResolvedMask {
pub fn from_target(target: &Region, corner_radius: CornerRadius) -> Self {
let half_extents = [target.size[0] as f32 / 2.0, target.size[1] as f32 / 2.0];
let max_radius = half_extents[0].min(half_extents[1]);
let corner_radius_px =
(corner_radius.points() * target.scale.factor()).clamp(0.0, max_radius);
Self {
half_extents,
corner_radius_px,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlurRequest {
pub source_region: Region,
pub target_rect: Region,
pub strength: BlurStrength,
pub tint: Tint,
pub corner_radius: CornerRadius,
pub opacity: Opacity,
}
impl BlurRequest {
pub fn physical_blur_radius(&self) -> f32 {
self.strength.to_physical_radius(self.source_region.scale)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::material::LinearRgba;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
fn region(origin: [u32; 2], size: [u32; 2], scale: f32) -> Region {
Region {
origin,
size,
scale: Scale::new(scale),
}
}
#[test]
fn scale_new_floors_nonpositive_to_positive() {
assert!(Scale::new(0.0).factor() > 0.0);
assert!(Scale::new(-2.0).factor() > 0.0);
assert!(close(Scale::new(2.5).factor(), 2.5));
}
#[test]
fn scale_default_is_one() {
assert!(close(Scale::default().factor(), 1.0));
}
#[test]
fn clip_to_leaves_an_in_bounds_region_unchanged() {
let r = region([10, 10], [20, 20], 1.0);
assert_eq!(r.clip_to([100, 100]), Some(r));
}
#[test]
fn clip_to_clamps_a_partially_offscreen_region() {
let r = region([90, 90], [20, 20], 2.0);
let clipped = r
.clip_to([100, 100])
.expect("partial overlap is not a no-op");
assert_eq!(clipped.origin, [90, 90]);
assert_eq!(clipped.size, [10, 10]);
assert!(close(clipped.scale.factor(), 2.0));
}
#[test]
fn clip_to_is_none_when_origin_past_extent() {
let r = region([100, 0], [10, 10], 1.0);
assert_eq!(r.clip_to([100, 100]), None);
}
#[test]
fn clip_to_is_none_for_zero_area() {
let r = region([0, 0], [0, 10], 1.0);
assert_eq!(r.clip_to([100, 100]), None);
}
#[test]
fn clip_to_saturates_instead_of_overflowing() {
let r = region([u32::MAX - 1, 0], [10, 10], 1.0);
assert_eq!(r.clip_to([100, 100]), None);
}
#[test]
fn region_display_reads_as_a_sentence_fragment() {
let r = region([4, 8], [100, 60], 2.0);
assert_eq!(r.to_string(), "origin (4, 8), size 100×60, scale 2");
}
#[test]
fn resolved_mask_half_extents_are_half_the_size() {
let mask =
ResolvedMask::from_target(®ion([0, 0], [80, 40], 1.0), CornerRadius::new(8.0));
assert!(close(mask.half_extents[0], 40.0));
assert!(close(mask.half_extents[1], 20.0));
assert!(close(mask.corner_radius_px, 8.0));
}
#[test]
fn resolved_mask_scales_corner_radius_to_physical() {
let mask =
ResolvedMask::from_target(®ion([0, 0], [200, 200], 2.0), CornerRadius::new(8.0));
assert!(close(mask.corner_radius_px, 16.0));
}
#[test]
fn resolved_mask_clamps_radius_to_min_half_extent() {
let mask =
ResolvedMask::from_target(®ion([0, 0], [40, 100], 1.0), CornerRadius::new(999.0));
assert!(close(mask.corner_radius_px, 20.0));
}
#[test]
fn physical_blur_radius_resolves_against_the_source_scale() {
let request = BlurRequest {
source_region: region([0, 0], [100, 100], 2.0),
target_rect: region([0, 0], [80, 60], 1.0),
strength: BlurStrength::new(8.0),
tint: Tint::new(LinearRgba::new(0.1, 0.1, 0.12, 0.7)),
corner_radius: CornerRadius::new(10.0),
opacity: Opacity::default(),
};
assert!(close(request.physical_blur_radius(), 16.0));
}
#[test]
fn blur_request_constructs_by_named_fields() {
let r = region([0, 0], [100, 60], 1.0);
let request = BlurRequest {
source_region: r,
target_rect: r,
strength: BlurStrength::new(12.0),
tint: Tint::new(LinearRgba::new(0.1, 0.1, 0.12, 0.7)),
corner_radius: CornerRadius::new(10.0),
opacity: Opacity::default(),
};
assert_eq!(request.strength.points(), 12.0);
assert!(close(request.tint.color().a(), 0.7));
}
}