use oxiui_core::Color;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BorderStyle {
None,
Solid,
Dashed,
Dotted,
Double,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BorderSpec {
pub width: f32,
pub style: BorderStyle,
pub color: Color,
}
impl BorderSpec {
pub const fn solid(width: f32, color: Color) -> Self {
Self {
width,
style: BorderStyle::Solid,
color,
}
}
pub const fn none() -> Self {
Self {
width: 0.0,
style: BorderStyle::None,
color: Color(0, 0, 0, 0),
}
}
pub fn is_invisible(&self) -> bool {
self.style == BorderStyle::None || self.width <= 0.0 || self.color.3 == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BorderSpecs {
pub top: BorderSpec,
pub right: BorderSpec,
pub bottom: BorderSpec,
pub left: BorderSpec,
}
impl BorderSpecs {
pub const fn uniform(spec: BorderSpec) -> Self {
Self {
top: spec,
right: spec,
bottom: spec,
left: spec,
}
}
pub const fn none() -> Self {
Self::uniform(BorderSpec::none())
}
pub const fn solid(width: f32, color: Color) -> Self {
Self::uniform(BorderSpec::solid(width, color))
}
pub fn is_invisible(&self) -> bool {
self.top.is_invisible()
&& self.right.is_invisible()
&& self.bottom.is_invisible()
&& self.left.is_invisible()
}
pub fn is_uniform(&self) -> bool {
self.top == self.right && self.right == self.bottom && self.bottom == self.left
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShadowSpec {
pub offset_x: f32,
pub offset_y: f32,
pub blur: f32,
pub spread: f32,
pub color: Color,
pub inset: bool,
}
impl ShadowSpec {
pub const fn drop(offset_y: f32, blur: f32, color: Color) -> Self {
Self {
offset_x: 0.0,
offset_y,
blur,
spread: 0.0,
color,
inset: false,
}
}
pub fn new(offset_x: f32, offset_y: f32, blur_radius: f32, color_rgba: [u8; 4]) -> Self {
Self {
offset_x,
offset_y,
blur: blur_radius,
spread: 0.0,
color: Color(color_rgba[0], color_rgba[1], color_rgba[2], color_rgba[3]),
inset: false,
}
}
pub fn drop_shadow(offset_x: f32, offset_y: f32, blur: f32) -> Self {
Self::new(offset_x, offset_y, blur, [0, 0, 0, 160])
}
pub fn with_spread(mut self, spread: f32) -> Self {
self.spread = spread;
self
}
pub fn with_inset(mut self, inset: bool) -> Self {
self.inset = inset;
self
}
pub fn to_pixel_color(&self) -> u32 {
let Color(r, g, b, a) = self.color;
((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
}
pub fn is_invisible(&self) -> bool {
self.color.3 == 0
}
}
pub fn elevation_to_shadow(elevation: f32) -> ShadowSpec {
if elevation <= 0.0 {
return ShadowSpec {
offset_x: 0.0,
offset_y: 0.0,
blur: 0.0,
spread: 0.0,
color: Color(0, 0, 0, 0),
inset: false,
};
}
let e = elevation.max(0.0);
let blur = e * 2.0;
let offset_y = e * 1.0;
let alpha = (40.0 + e * 5.0).min(160.0) as u8;
ShadowSpec {
offset_x: 0.0,
offset_y,
blur,
spread: 0.0,
color: Color(0, 0, 0, alpha),
inset: false,
}
}
pub fn elevation_shadow(level: usize) -> Option<ShadowSpec> {
match level {
0 => None,
1 => Some(ShadowSpec::drop(1.0, 2.0, Color(0, 0, 0, 56))),
2 => Some(ShadowSpec::drop(2.0, 4.0, Color(0, 0, 0, 64))),
3 => Some(ShadowSpec::drop(4.0, 8.0, Color(0, 0, 0, 72))),
4 => Some(ShadowSpec::drop(8.0, 16.0, Color(0, 0, 0, 84))),
_ => Some(ShadowSpec::drop(12.0, 24.0, Color(0, 0, 0, 96))),
}
}
pub fn elevation_shadows(elevation: u32) -> Vec<ShadowSpec> {
if elevation == 0 {
return vec![
ShadowSpec {
offset_x: 0.0,
offset_y: 0.0,
blur: 0.0,
spread: 0.0,
color: Color(0, 0, 0, 0),
inset: false,
},
ShadowSpec {
offset_x: 0.0,
offset_y: 0.0,
blur: 0.0,
spread: 0.0,
color: Color(0, 0, 0, 0),
inset: false,
},
];
}
let dp = elevation as f32;
let ambient_blur = (dp * 3.0).max(1.0);
let ambient_y = dp * 0.5;
let ambient_alpha = ((12.0 + dp * 1.5).min(60.0)) as u8;
let key_blur = (dp * 1.5).max(1.0);
let key_y = dp * 1.0;
let key_alpha = ((20.0 + dp * 2.5).min(100.0)) as u8;
vec![
ShadowSpec {
offset_x: 0.0,
offset_y: ambient_y,
blur: ambient_blur,
spread: 0.0,
color: Color(0, 0, 0, ambient_alpha),
inset: false,
},
ShadowSpec {
offset_x: 0.0,
offset_y: key_y,
blur: key_blur,
spread: 0.0,
color: Color(0, 0, 0, key_alpha),
inset: false,
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn border_visibility() {
assert!(BorderSpec::none().is_invisible());
assert!(!BorderSpec::solid(1.0, Color(255, 255, 255, 255)).is_invisible());
assert!(BorderSpec::solid(2.0, Color(255, 255, 255, 0)).is_invisible());
}
#[test]
fn shadow_visibility() {
assert!(!ShadowSpec::drop(2.0, 4.0, Color(0, 0, 0, 100)).is_invisible());
assert!(ShadowSpec::drop(2.0, 4.0, Color(0, 0, 0, 0)).is_invisible());
}
#[test]
fn elevation_grows_with_level() {
assert!(elevation_shadow(0).is_none());
let s1 = elevation_shadow(1).expect("level 1 has a shadow");
let s5 = elevation_shadow(5).expect("level 5 has a shadow");
assert!(s5.blur > s1.blur);
assert!(s5.offset_y > s1.offset_y);
assert_eq!(elevation_shadow(99), elevation_shadow(5));
}
#[test]
fn elevation_shadow_count() {
let stack = elevation_shadows(4);
assert_eq!(stack.len(), 2, "must return exactly 2 ShadowSpec values");
}
#[test]
fn elevation_zero_returns_invisible_pair() {
let stack = elevation_shadows(0);
assert_eq!(stack.len(), 2);
assert!(stack[0].is_invisible());
assert!(stack[1].is_invisible());
}
#[test]
fn elevation_shadows_increases_with_level() {
let stack_low = elevation_shadows(2);
let stack_high = elevation_shadows(8);
assert!(
stack_high[0].blur > stack_low[0].blur,
"ambient blur must increase: {} vs {}",
stack_high[0].blur,
stack_low[0].blur,
);
}
#[test]
fn border_style_double_exists() {
let b = BorderSpec {
width: 2.0,
style: BorderStyle::Double,
color: Color(0, 0, 0, 255),
};
assert_eq!(b.style, BorderStyle::Double);
}
}