use crate::renderer::Renderer;
use crate::style::Style;
use crate::widget::{Color, Rect};
pub const GRADIENT_MAX_STOPS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GradientKind {
Linear {
angle_deg: i16,
},
Radial {
cx_frac: u8,
cy_frac: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GradientDesc<'a> {
pub kind: GradientKind,
pub stops: &'a [(u8, Color)],
}
impl<'a> GradientDesc<'a> {
pub const fn new(kind: GradientKind, stops: &'a [(u8, Color)]) -> Self {
Self { kind, stops }
}
pub fn color_at(&self, rect: Rect, x: i32, y: i32) -> Option<Color> {
if rect.width <= 0 || rect.height <= 0 || self.stops.is_empty() {
return None;
}
Some(self.color_at_fraction(gradient_position(self.kind, rect, x, y)))
}
fn color_at_fraction(&self, t: u8) -> Color {
let stops = &self.stops[..self.stops.len().min(GRADIENT_MAX_STOPS)];
if stops.len() == 1 {
return stops[0].1;
}
let mut lower: Option<(u8, Color)> = None;
let mut upper: Option<(u8, Color)> = None;
for &(pos, color) in stops {
if pos <= t && lower.is_none_or(|(best, _)| pos >= best) {
lower = Some((pos, color));
}
if pos >= t && upper.is_none_or(|(best, _)| pos <= best) {
upper = Some((pos, color));
}
}
match (lower, upper) {
(Some((lp, lc)), Some((up, uc))) if up != lp => {
let num = i32::from(t.saturating_sub(lp));
let den = i32::from(up - lp);
lc.lerp(uc, num, den)
}
(Some((_, c)), _) => c,
(_, Some((_, c))) => c,
(None, None) => Color(0, 0, 0, 0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowDesc {
pub offset_x: i16,
pub offset_y: i16,
pub spread: u8,
pub blur: u8,
pub color: Color,
}
fn isqrt(n: u32) -> u32 {
if n == 0 {
return 0;
}
let mut x = n;
let mut y = x.div_ceil(2);
while y < x {
x = y;
y = (x + n / x) / 2;
}
x
}
fn gradient_position(kind: GradientKind, rect: Rect, x: i32, y: i32) -> u8 {
match kind {
GradientKind::Linear { angle_deg } => linear_gradient_position(angle_deg, rect, x, y),
GradientKind::Radial { cx_frac, cy_frac } => {
radial_gradient_position(cx_frac, cy_frac, rect, x, y)
}
}
}
fn linear_gradient_position(angle_deg: i16, rect: Rect, x: i32, y: i32) -> u8 {
let xf = axis_fraction(x - rect.x, rect.width);
let yf = axis_fraction(y - rect.y, rect.height);
match snapped_octant(angle_deg) {
0 => xf,
1 => avg_u8(xf, yf),
2 => yf,
3 => avg_u8(255u8.saturating_sub(xf), yf),
4 => 255u8.saturating_sub(xf),
5 => avg_u8(255u8.saturating_sub(xf), 255u8.saturating_sub(yf)),
6 => 255u8.saturating_sub(yf),
_ => avg_u8(xf, 255u8.saturating_sub(yf)),
}
}
fn radial_gradient_position(cx_frac: u8, cy_frac: u8, rect: Rect, x: i32, y: i32) -> u8 {
let cx = rect.x + fraction_to_axis(cx_frac, rect.width);
let cy = rect.y + fraction_to_axis(cy_frac, rect.height);
let dx = (x - cx).unsigned_abs();
let dy = (y - cy).unsigned_abs();
let dist = isqrt(dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy)));
let corners = [
(rect.x, rect.y),
(rect.x + rect.width - 1, rect.y),
(rect.x, rect.y + rect.height - 1),
(rect.x + rect.width - 1, rect.y + rect.height - 1),
];
let mut max_dist = 1u32;
for &(corner_x, corner_y) in &corners {
let dx = (corner_x - cx).unsigned_abs();
let dy = (corner_y - cy).unsigned_abs();
max_dist = max_dist.max(isqrt(
dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy)),
));
}
((dist.min(max_dist) * 255) / max_dist) as u8
}
fn snapped_octant(angle_deg: i16) -> u8 {
let angle = i32::from(angle_deg).rem_euclid(360);
(((angle + 22) / 45) % 8) as u8
}
fn axis_fraction(pos: i32, len: i32) -> u8 {
if len <= 1 {
return 255;
}
((pos.clamp(0, len - 1) * 255) / (len - 1)) as u8
}
fn fraction_to_axis(frac: u8, len: i32) -> i32 {
if len <= 1 {
return 0;
}
(i32::from(frac) * (len - 1)) / 255
}
fn avg_u8(a: u8, b: u8) -> u8 {
((u16::from(a) + u16::from(b)) / 2) as u8
}
fn arc_dx(r: i32, dy: i32) -> (i32, u8) {
let r4 = r as u32 * 4;
let dy4 = dy as u32 * 4 + 2; let sq = r4 * r4;
let dysq = dy4 * dy4;
if dysq >= sq {
return (0, 0);
}
let dx4 = isqrt(sq - dysq);
let dx_int = (dx4 / 4) as i32;
let frac = (dx4 % 4) as u8 * 64; (dx_int, frac)
}
pub fn fill_rounded_rect(renderer: &mut dyn Renderer, rect: Rect, color: Color, radius: u8) {
let r = radius as i32;
if r == 0 {
renderer.fill_rect(rect, color);
return;
}
let r = r.min(rect.width / 2).min(rect.height / 2);
if r <= 0 {
renderer.fill_rect(rect, color);
return;
}
if rect.height - 2 * r > 0 {
renderer.fill_rect(
Rect {
x: rect.x,
y: rect.y + r,
width: rect.width,
height: rect.height - 2 * r,
},
color,
);
}
if rect.width - 2 * r > 0 {
renderer.fill_rect(
Rect {
x: rect.x + r,
y: rect.y,
width: rect.width - 2 * r,
height: r,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + r,
y: rect.y + rect.height - r,
width: rect.width - 2 * r,
height: r,
},
color,
);
}
let base_alpha = color.3 as u16;
for dy in 0..r {
let (dx_int, frac) = arc_dx(r, r - 1 - dy);
if dx_int > 0 {
renderer.fill_rect(
Rect {
x: rect.x + r - dx_int,
y: rect.y + dy,
width: dx_int,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - r,
y: rect.y + dy,
width: dx_int,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + r - dx_int,
y: rect.y + rect.height - 1 - dy,
width: dx_int,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - r,
y: rect.y + rect.height - 1 - dy,
width: dx_int,
height: 1,
},
color,
);
}
if frac > 0 {
let aa_alpha = ((frac as u16 * base_alpha) / 255) as u8;
let aa = Color(color.0, color.1, color.2, aa_alpha);
renderer.blend_rect(
Rect {
x: rect.x + r - dx_int - 1,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - r + dx_int,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + r - dx_int - 1,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - r + dx_int,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
}
}
}
pub fn draw_rounded_border(
renderer: &mut dyn Renderer,
rect: Rect,
color: Color,
border_width: u8,
radius: u8,
) {
let bw = border_width as i32;
if bw == 0 {
return;
}
let r = radius as i32;
if r == 0 {
draw_border_straight(renderer, rect, color, border_width);
return;
}
let rout = r.min(rect.width / 2).min(rect.height / 2);
if rout <= 0 {
draw_border_straight(renderer, rect, color, border_width);
return;
}
let rin = (rout - bw).max(0);
let base_alpha = color.3 as u16;
for dy in 0..rout {
let axis_dy = rout - 1 - dy;
let (out_dx, out_frac) = arc_dx(rout, axis_dy);
let (in_dx, in_frac) = if rin > 0 {
let (d, f) = arc_dx(rin, axis_dy);
(d, f)
} else {
(0i32, 0u8)
};
let ring_w = out_dx - in_dx;
if ring_w > 0 {
renderer.fill_rect(
Rect {
x: rect.x + rout - out_dx,
y: rect.y + dy,
width: ring_w,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - rout + in_dx,
y: rect.y + dy,
width: ring_w,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rout - out_dx,
y: rect.y + rect.height - 1 - dy,
width: ring_w,
height: 1,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - rout + in_dx,
y: rect.y + rect.height - 1 - dy,
width: ring_w,
height: 1,
},
color,
);
}
if out_frac > 0 {
let aa_alpha = ((out_frac as u16 * base_alpha) / 255) as u8;
let aa = Color(color.0, color.1, color.2, aa_alpha);
renderer.blend_rect(
Rect {
x: rect.x + rout - out_dx - 1,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - rout + out_dx,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rout - out_dx - 1,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - rout + out_dx,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
}
if in_dx > 0 && in_frac > 0 {
let aa_alpha = (((255 - in_frac as u16) * base_alpha) / 255) as u8;
let aa = Color(color.0, color.1, color.2, aa_alpha);
renderer.blend_rect(
Rect {
x: rect.x + rout - in_dx,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - rout + in_dx - 1,
y: rect.y + dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rout - in_dx,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
renderer.blend_rect(
Rect {
x: rect.x + rect.width - rout + in_dx - 1,
y: rect.y + rect.height - 1 - dy,
width: 1,
height: 1,
},
aa,
);
}
}
let straight_h = rect.height - 2 * rout;
if straight_h > 0 {
renderer.fill_rect(
Rect {
x: rect.x,
y: rect.y + rout,
width: bw,
height: straight_h,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - bw,
y: rect.y + rout,
width: bw,
height: straight_h,
},
color,
);
}
let straight_w = rect.width - 2 * rout;
if straight_w > 0 {
renderer.fill_rect(
Rect {
x: rect.x + rout,
y: rect.y,
width: straight_w,
height: bw,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rout,
y: rect.y + rect.height - bw,
width: straight_w,
height: bw,
},
color,
);
}
}
pub fn draw_border_straight(renderer: &mut dyn Renderer, rect: Rect, color: Color, width: u8) {
let w = width as i32;
if w == 0 {
return;
}
renderer.fill_rect(
Rect {
x: rect.x,
y: rect.y,
width: rect.width,
height: w,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x,
y: rect.y + rect.height - w,
width: rect.width,
height: w,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x,
y: rect.y + w,
width: w,
height: rect.height - 2 * w,
},
color,
);
renderer.fill_rect(
Rect {
x: rect.x + rect.width - w,
y: rect.y + w,
width: w,
height: rect.height - 2 * w,
},
color,
);
}
pub fn draw_widget_bg(renderer: &mut dyn Renderer, rect: Rect, style: &Style) {
let bg = style.bg_color.with_alpha(style.alpha);
if bg.3 != 0 {
if style.radius > 0 {
fill_rounded_rect(renderer, rect, bg, style.radius);
} else if bg.3 == 255 {
renderer.fill_rect(rect, bg);
} else {
renderer.blend_rect(rect, bg);
}
}
if style.border_width > 0 {
let border = style.border_color.with_alpha(style.alpha);
if border.3 != 0 {
draw_rounded_border(renderer, rect, border, style.border_width, style.radius);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct RecordRenderer {
fill_rects: alloc::vec::Vec<(Rect, Color)>,
blend_rects: alloc::vec::Vec<(Rect, Color)>,
}
impl RecordRenderer {
fn new() -> Self {
Self {
fill_rects: alloc::vec::Vec::new(),
blend_rects: alloc::vec::Vec::new(),
}
}
}
impl Renderer for RecordRenderer {
fn fill_rect(&mut self, rect: Rect, color: Color) {
self.fill_rects.push((rect, color));
}
fn blend_rect(&mut self, rect: Rect, color: Color) {
self.blend_rects.push((rect, color));
}
fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
}
struct CountRenderer {
fills: u32,
blends: u32,
}
impl Renderer for CountRenderer {
fn fill_rect(&mut self, _rect: Rect, _color: Color) {
self.fills += 1;
}
fn blend_rect(&mut self, _rect: Rect, _color: Color) {
self.blends += 1;
}
fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
}
#[test]
fn zero_radius_single_fill() {
let mut r = CountRenderer {
fills: 0,
blends: 0,
};
let rect = Rect {
x: 0,
y: 0,
width: 100,
height: 50,
};
fill_rounded_rect(&mut r, rect, Color(0, 0, 0, 255), 0);
assert_eq!(r.fills, 1);
assert_eq!(r.blends, 0);
}
#[test]
fn radius_clamped_for_pill_shape() {
let mut r = CountRenderer {
fills: 0,
blends: 0,
};
let rect = Rect {
x: 0,
y: 0,
width: 40,
height: 20,
};
fill_rounded_rect(&mut r, rect, Color(0, 0, 0, 255), 30);
assert!(r.fills > 1, "expected corners, got {} fills", r.fills);
}
#[test]
fn aa_fringe_produces_blend_calls() {
let mut r = CountRenderer {
fills: 0,
blends: 0,
};
let rect = Rect {
x: 0,
y: 0,
width: 100,
height: 100,
};
fill_rounded_rect(&mut r, rect, Color(255, 0, 0, 255), 10);
assert!(r.blends > 0, "expected AA blend calls, got 0");
}
#[test]
fn rounded_border_produces_ring() {
let mut r = RecordRenderer::new();
let rect = Rect {
x: 0,
y: 0,
width: 60,
height: 60,
};
draw_rounded_border(&mut r, rect, Color(0, 0, 0, 255), 2, 8);
assert!(!r.fill_rects.is_empty(), "expected border fills");
}
#[test]
fn straight_border_four_strips() {
let mut r = CountRenderer {
fills: 0,
blends: 0,
};
let rect = Rect {
x: 0,
y: 0,
width: 100,
height: 50,
};
draw_border_straight(&mut r, rect, Color(0, 0, 0, 255), 2);
assert_eq!(r.fills, 4);
}
#[test]
fn draw_widget_bg_uses_radius() {
let mut r = CountRenderer {
fills: 0,
blends: 0,
};
let rect = Rect {
x: 0,
y: 0,
width: 80,
height: 40,
};
let style = Style {
bg_color: Color(100, 100, 100, 255),
border_color: Color(0, 0, 0, 255),
border_width: 1,
alpha: 255,
radius: 6,
};
draw_widget_bg(&mut r, rect, &style);
assert!(r.fills > 1);
}
#[test]
fn linear_gradient_samples_cardinal_axis() {
let stops = [(0, Color(0, 0, 0, 255)), (255, Color(255, 0, 0, 255))];
let gradient = GradientDesc::new(GradientKind::Linear { angle_deg: 0 }, &stops);
let rect = Rect {
x: 10,
y: 20,
width: 3,
height: 2,
};
assert_eq!(gradient.color_at(rect, 10, 20), Some(Color(0, 0, 0, 255)));
assert_eq!(gradient.color_at(rect, 11, 20), Some(Color(127, 0, 0, 255)));
assert_eq!(gradient.color_at(rect, 12, 20), Some(Color(255, 0, 0, 255)));
}
#[test]
fn gradient_stops_do_not_need_sorting() {
let stops = [(255, Color(255, 0, 0, 255)), (0, Color(0, 0, 0, 255))];
let gradient = GradientDesc::new(GradientKind::Linear { angle_deg: 90 }, &stops);
let rect = Rect {
x: 0,
y: 0,
width: 2,
height: 3,
};
assert_eq!(gradient.color_at(rect, 0, 0), Some(Color(0, 0, 0, 255)));
assert_eq!(gradient.color_at(rect, 0, 1), Some(Color(127, 0, 0, 255)));
assert_eq!(gradient.color_at(rect, 0, 2), Some(Color(255, 0, 0, 255)));
}
#[test]
fn radial_gradient_reaches_outer_stop_at_corner() {
let stops = [(0, Color(0, 0, 0, 255)), (255, Color(0, 0, 255, 255))];
let gradient = GradientDesc::new(
GradientKind::Radial {
cx_frac: 128,
cy_frac: 128,
},
&stops,
);
let rect = Rect {
x: 0,
y: 0,
width: 5,
height: 5,
};
assert_eq!(gradient.color_at(rect, 2, 2), Some(Color(0, 0, 0, 255)));
assert_eq!(gradient.color_at(rect, 0, 0), Some(Color(0, 0, 255, 255)));
}
}