use oxiui_core::{
geometry::{Point, Rect},
paint::{DrawList, GradientStop},
Color,
};
use oxiui_theme::{BorderSpec, BorderSpecs, BorderStyle, ExtendedPalette, ShadowSpec};
use crate::gpu::shadow::ShadowDesc;
pub fn shadow_spec_to_desc(rect: Rect, spec: &ShadowSpec) -> Option<ShadowDesc> {
if spec.is_invisible() {
return None;
}
let spread = spec.spread;
let shadow_rect = Rect::new(
rect.left() + spec.offset_x - spread,
rect.top() + spec.offset_y - spread,
(rect.width() + 2.0 * spread).max(0.0),
(rect.height() + 2.0 * spread).max(0.0),
);
Some(ShadowDesc {
shadow_rect,
color: spec.color,
blur_radius: spec.blur,
})
}
pub fn push_shadow_spec(list: &mut DrawList, rect: Rect, spec: &ShadowSpec) {
if spec.is_invisible() {
return;
}
let spread = spec.spread;
let effective_rect = Rect::new(
rect.left() - spread,
rect.top() - spread,
(rect.width() + 2.0 * spread).max(0.0),
(rect.height() + 2.0 * spread).max(0.0),
);
list.push_shadow(
effective_rect,
Point::new(spec.offset_x, spec.offset_y),
spec.blur,
spec.color,
);
}
pub fn push_border_spec(list: &mut DrawList, rect: Rect, spec: &BorderSpec) {
if spec.is_invisible() {
return;
}
match spec.style {
BorderStyle::None => {}
BorderStyle::Solid => {
list.push_stroke_rect(rect, spec.width, spec.color);
}
BorderStyle::Dashed => {
let dash_len = (spec.width * 3.0).max(4.0);
let gap_len = (spec.width * 2.0).max(2.0);
let tl = Point::new(rect.left(), rect.top());
let tr = Point::new(rect.right(), rect.top());
let br = Point::new(rect.right(), rect.bottom());
let bl = Point::new(rect.left(), rect.bottom());
list.push_line_dashed(tl, tr, dash_len, gap_len, spec.color);
list.push_line_dashed(tr, br, dash_len, gap_len, spec.color);
list.push_line_dashed(br, bl, dash_len, gap_len, spec.color);
list.push_line_dashed(bl, tl, dash_len, gap_len, spec.color);
}
BorderStyle::Dotted => {
let dot = spec.width;
let gap = spec.width;
let tl = Point::new(rect.left(), rect.top());
let tr = Point::new(rect.right(), rect.top());
let br = Point::new(rect.right(), rect.bottom());
let bl = Point::new(rect.left(), rect.bottom());
list.push_line_dashed(tl, tr, dot, gap, spec.color);
list.push_line_dashed(tr, br, dot, gap, spec.color);
list.push_line_dashed(br, bl, dot, gap, spec.color);
list.push_line_dashed(bl, tl, dot, gap, spec.color);
}
BorderStyle::Double => {
let stroke_w = (spec.width / 3.0).max(1.0);
let half_gap = stroke_w;
let inset = stroke_w + half_gap;
let inner = Rect::new(
rect.left() + inset,
rect.top() + inset,
(rect.width() - 2.0 * inset).max(0.0),
(rect.height() - 2.0 * inset).max(0.0),
);
list.push_stroke_rect(rect, stroke_w, spec.color);
if inner.width() > 0.0 && inner.height() > 0.0 {
list.push_stroke_rect(inner, stroke_w, spec.color);
}
}
}
}
pub fn push_border_specs(list: &mut DrawList, rect: Rect, specs: &BorderSpecs) {
if specs.is_invisible() {
return;
}
if specs.is_uniform() {
push_border_spec(list, rect, &specs.top);
return;
}
let tl = Point::new(rect.left(), rect.top());
let tr = Point::new(rect.right(), rect.top());
let br = Point::new(rect.right(), rect.bottom());
let bl = Point::new(rect.left(), rect.bottom());
push_edge_line(list, tl, tr, &specs.top);
push_edge_line(list, tr, br, &specs.right);
push_edge_line(list, br, bl, &specs.bottom);
push_edge_line(list, bl, tl, &specs.left);
}
fn push_edge_line(list: &mut DrawList, from: Point, to: Point, spec: &BorderSpec) {
if spec.is_invisible() {
return;
}
match spec.style {
BorderStyle::None => {}
BorderStyle::Solid => {
list.push_line_thick(from, to, spec.width, spec.color);
}
BorderStyle::Dashed => {
let dash = (spec.width * 3.0).max(4.0);
let gap = (spec.width * 2.0).max(2.0);
list.push_line_dashed(from, to, dash, gap, spec.color);
}
BorderStyle::Dotted => {
let dot = spec.width;
let gap = spec.width;
list.push_line_dashed(from, to, dot, gap, spec.color);
}
BorderStyle::Double => {
let thin = (spec.width / 3.0).max(1.0);
list.push_line_thick(from, to, thin, spec.color);
list.push_line_thick(from, to, thin, spec.color);
}
}
}
pub fn primary_gradient_stops(palette: &ExtendedPalette) -> Vec<GradientStop> {
vec![
GradientStop::new(0.0, palette.base.background),
GradientStop::new(1.0, palette.base.primary),
]
}
pub fn surface_gradient_stops(palette: &ExtendedPalette) -> Vec<GradientStop> {
vec![
GradientStop::new(0.0, palette.base.background),
GradientStop::new(1.0, palette.base.surface),
]
}
pub fn status_gradient_stops(palette: &ExtendedPalette) -> Vec<GradientStop> {
vec![
GradientStop::new(0.0, palette.error),
GradientStop::new(0.5, palette.warning),
GradientStop::new(1.0, palette.success),
]
}
pub fn outline_gradient_stops(palette: &ExtendedPalette) -> Vec<GradientStop> {
vec![
GradientStop::new(0.0, palette.surface_variant),
GradientStop::new(1.0, palette.outline),
]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GradientDirection {
Horizontal,
Vertical,
Diagonal,
}
pub fn push_theme_gradient(
list: &mut DrawList,
rect: Rect,
stops: &[GradientStop],
direction: GradientDirection,
) {
if stops.is_empty() {
return;
}
let (start, end) = match direction {
GradientDirection::Horizontal => (
Point::new(rect.left(), rect.top()),
Point::new(rect.right(), rect.top()),
),
GradientDirection::Vertical => (
Point::new(rect.left(), rect.top()),
Point::new(rect.left(), rect.bottom()),
),
GradientDirection::Diagonal => (
Point::new(rect.left(), rect.top()),
Point::new(rect.right(), rect.bottom()),
),
};
list.push_gradient_linear(rect, start, end, stops.to_vec());
}
pub fn elevation_shadow_desc(rect: Rect, elevation: f32) -> Option<ShadowDesc> {
if elevation <= 0.0 {
return None;
}
let spec = oxiui_theme::elevation_to_shadow(elevation);
shadow_spec_to_desc(rect, &spec)
}
pub fn elevation_level_shadow_desc(rect: Rect, level: usize) -> Option<ShadowDesc> {
let spec = oxiui_theme::elevation_shadow(level)?;
shadow_spec_to_desc(rect, &spec)
}
pub fn push_elevation_shadows(list: &mut DrawList, rect: Rect, elevation: u32) {
let stack = oxiui_theme::elevation_shadows(elevation);
for spec in &stack {
push_shadow_spec(list, rect, spec);
}
}
pub fn tinted_color(color: Color, alpha_factor: f32) -> Color {
let a = (color.3 as f32 * alpha_factor.clamp(0.0, 1.0)).round() as u8;
Color(color.0, color.1, color.2, a)
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::{geometry::Rect, paint::DrawList, Color, Palette};
use oxiui_theme::ExtendedPalette;
fn test_palette() -> ExtendedPalette {
ExtendedPalette::derive(
Palette {
background: Color(26, 27, 38, 255),
surface: Color(36, 40, 59, 255),
primary: Color(122, 162, 247, 255),
on_primary: Color(26, 27, 38, 255),
text: Color(192, 202, 245, 255),
muted: Color(86, 95, 137, 255),
},
true,
)
}
fn test_rect() -> Rect {
Rect::new(10.0, 10.0, 100.0, 50.0)
}
#[test]
fn shadow_spec_to_desc_invisible_returns_none() {
let spec = ShadowSpec::new(0.0, 0.0, 0.0, [0, 0, 0, 0]);
assert!(shadow_spec_to_desc(test_rect(), &spec).is_none());
}
#[test]
fn shadow_spec_to_desc_visible_returns_some() {
let spec = ShadowSpec::drop_shadow(2.0, 4.0, 8.0);
let desc = shadow_spec_to_desc(test_rect(), &spec);
assert!(desc.is_some());
let d = desc.unwrap();
assert!((d.shadow_rect.left() - (test_rect().left() + spec.offset_x)).abs() < 1e-6);
assert!((d.shadow_rect.top() - (test_rect().top() + spec.offset_y)).abs() < 1e-6);
assert!((d.blur_radius - spec.blur).abs() < 1e-6);
}
#[test]
fn shadow_spec_to_desc_with_positive_spread_grows_rect() {
let spec = ShadowSpec::drop_shadow(0.0, 0.0, 4.0).with_spread(5.0);
let desc = shadow_spec_to_desc(test_rect(), &spec).unwrap();
let expected_w = test_rect().width() + 2.0 * 5.0;
assert!((desc.shadow_rect.width() - expected_w).abs() < 1e-6);
}
#[test]
fn shadow_spec_to_desc_with_negative_spread_shrinks_rect() {
let spec = ShadowSpec::drop_shadow(0.0, 0.0, 2.0).with_spread(-3.0);
let desc = shadow_spec_to_desc(test_rect(), &spec).unwrap();
let expected_w = (test_rect().width() - 6.0).max(0.0);
assert!((desc.shadow_rect.width() - expected_w).abs() < 1e-6);
}
#[test]
fn push_shadow_spec_invisible_pushes_nothing() {
let mut list = DrawList::new();
let spec = ShadowSpec::new(0.0, 0.0, 0.0, [0, 0, 0, 0]);
push_shadow_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 0);
}
#[test]
fn push_shadow_spec_visible_pushes_one_command() {
let mut list = DrawList::new();
let spec = ShadowSpec::drop_shadow(1.0, 2.0, 4.0);
push_shadow_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 1);
}
#[test]
fn primary_gradient_stops_has_two_stops() {
let p = test_palette();
let stops = primary_gradient_stops(&p);
assert_eq!(stops.len(), 2);
assert_eq!(stops[0].offset, 0.0);
assert_eq!(stops[1].offset, 1.0);
assert_eq!(stops[0].color, p.base.background);
assert_eq!(stops[1].color, p.base.primary);
}
#[test]
fn surface_gradient_stops_has_two_stops() {
let p = test_palette();
let stops = surface_gradient_stops(&p);
assert_eq!(stops.len(), 2);
assert_eq!(stops[0].color, p.base.background);
assert_eq!(stops[1].color, p.base.surface);
}
#[test]
fn status_gradient_stops_has_three_stops() {
let p = test_palette();
let stops = status_gradient_stops(&p);
assert_eq!(stops.len(), 3);
assert_eq!(stops[0].color, p.error);
assert_eq!(stops[1].color, p.warning);
assert_eq!(stops[2].color, p.success);
}
#[test]
fn outline_gradient_stops_has_two_stops() {
let p = test_palette();
let stops = outline_gradient_stops(&p);
assert_eq!(stops.len(), 2);
}
#[test]
fn push_theme_gradient_horizontal_pushes_one_command() {
let mut list = DrawList::new();
let p = test_palette();
let stops = primary_gradient_stops(&p);
push_theme_gradient(
&mut list,
test_rect(),
&stops,
GradientDirection::Horizontal,
);
assert_eq!(list.len(), 1);
}
#[test]
fn push_theme_gradient_vertical_pushes_one_command() {
let mut list = DrawList::new();
let p = test_palette();
let stops = surface_gradient_stops(&p);
push_theme_gradient(&mut list, test_rect(), &stops, GradientDirection::Vertical);
assert_eq!(list.len(), 1);
}
#[test]
fn push_theme_gradient_diagonal_pushes_one_command() {
let mut list = DrawList::new();
let p = test_palette();
let stops = status_gradient_stops(&p);
push_theme_gradient(&mut list, test_rect(), &stops, GradientDirection::Diagonal);
assert_eq!(list.len(), 1);
}
#[test]
fn push_theme_gradient_empty_stops_pushes_nothing() {
let mut list = DrawList::new();
push_theme_gradient(&mut list, test_rect(), &[], GradientDirection::Horizontal);
assert_eq!(list.len(), 0);
}
#[test]
fn elevation_shadow_desc_zero_returns_none() {
assert!(elevation_shadow_desc(test_rect(), 0.0).is_none());
}
#[test]
fn elevation_shadow_desc_positive_returns_some() {
let desc = elevation_shadow_desc(test_rect(), 4.0);
assert!(desc.is_some());
let d = desc.unwrap();
assert!(d.blur_radius > 0.0);
}
#[test]
fn elevation_level_shadow_desc_level_zero_returns_none() {
assert!(elevation_level_shadow_desc(test_rect(), 0).is_none());
}
#[test]
fn elevation_level_shadow_desc_level_three_returns_some() {
let desc = elevation_level_shadow_desc(test_rect(), 3);
assert!(desc.is_some());
}
#[test]
fn push_elevation_shadows_zero_pushes_nothing() {
let mut list = DrawList::new();
push_elevation_shadows(&mut list, test_rect(), 0);
assert_eq!(list.len(), 0);
}
#[test]
fn push_elevation_shadows_four_pushes_two_shadows() {
let mut list = DrawList::new();
push_elevation_shadows(&mut list, test_rect(), 4);
assert_eq!(list.len(), 2);
}
#[test]
fn tinted_color_full_alpha_is_noop() {
let c = Color(255, 128, 64, 200);
let t = tinted_color(c, 1.0);
assert_eq!(t, c);
}
#[test]
fn tinted_color_zero_alpha_makes_transparent() {
let c = Color(255, 128, 64, 200);
let t = tinted_color(c, 0.0);
assert_eq!(t.3, 0);
}
#[test]
fn tinted_color_half_alpha_halves_alpha_channel() {
let c = Color(255, 128, 64, 200);
let t = tinted_color(c, 0.5);
assert_eq!(t.3, 100);
assert_eq!(t.0, c.0);
assert_eq!(t.1, c.1);
assert_eq!(t.2, c.2);
}
#[test]
fn push_border_spec_invisible_pushes_nothing() {
let mut list = DrawList::new();
let spec = BorderSpec::none();
push_border_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 0);
}
#[test]
fn push_border_spec_solid_pushes_one_command() {
let mut list = DrawList::new();
let spec = BorderSpec::solid(2.0, Color(0, 0, 0, 255));
push_border_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 1);
}
#[test]
fn push_border_spec_dashed_pushes_four_lines() {
let mut list = DrawList::new();
let spec = BorderSpec {
width: 1.0,
style: BorderStyle::Dashed,
color: Color(255, 0, 0, 255),
};
push_border_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 4);
}
#[test]
fn push_border_spec_dotted_pushes_four_lines() {
let mut list = DrawList::new();
let spec = BorderSpec {
width: 1.0,
style: BorderStyle::Dotted,
color: Color(0, 255, 0, 255),
};
push_border_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 4);
}
#[test]
fn push_border_spec_double_pushes_two_strokes() {
let mut list = DrawList::new();
let spec = BorderSpec {
width: 6.0,
style: BorderStyle::Double,
color: Color(0, 0, 255, 255),
};
push_border_spec(&mut list, test_rect(), &spec);
assert_eq!(list.len(), 2);
}
#[test]
fn push_border_specs_invisible_pushes_nothing() {
let mut list = DrawList::new();
let specs = BorderSpecs::none();
push_border_specs(&mut list, test_rect(), &specs);
assert_eq!(list.len(), 0);
}
#[test]
fn push_border_specs_uniform_pushes_one_command() {
let mut list = DrawList::new();
let specs = BorderSpecs::solid(1.0, Color(50, 50, 50, 255));
push_border_specs(&mut list, test_rect(), &specs);
assert_eq!(list.len(), 1);
}
#[test]
fn push_border_specs_non_uniform_pushes_per_edge() {
use oxiui_theme::{BorderSpec, BorderSpecs};
let mut list = DrawList::new();
let specs = BorderSpecs {
top: BorderSpec::solid(1.0, Color(0, 0, 0, 255)),
right: BorderSpec::solid(2.0, Color(0, 0, 0, 255)),
bottom: BorderSpec::none(),
left: BorderSpec::none(),
};
push_border_specs(&mut list, test_rect(), &specs);
assert_eq!(list.len(), 2);
}
}