use cotis_defaults::colors::{
Color as CotisColor, ColorAttachmentPoint, ColorLayer, ColorPos, GradientStop, LinearGradient,
RadialGradient, RoundedRectGradient,
};
use cotis_defaults::render_commands::CornerRadii;
use raylib::drawing::{RaylibDraw, RaylibDrawHandle};
use raylib::ffi::{BeginScissorMode, EndScissorMode};
use raylib::math::{Rectangle as RlRectangle, Vector2};
use raylib::prelude::Font;
pub(crate) fn cotis_color_to_raylib(c: CotisColor) -> raylib::color::Color {
raylib::color::Color::new(c.r as u8, c.g as u8, c.b as u8, c.a as u8)
}
fn lerp_cotis_color(a: CotisColor, b: CotisColor, t: f32) -> CotisColor {
let t = t.clamp(0.0, 1.0);
CotisColor {
r: a.r + (b.r - a.r) * t,
g: a.g + (b.g - a.g) * t,
b: a.b + (b.b - a.b) * t,
a: a.a + (b.a - a.a) * t,
}
}
fn bilinear_quad_corners(
c_tl: CotisColor,
c_tr: CotisColor,
c_bl: CotisColor,
c_br: CotisColor,
u: f32,
v: f32,
) -> CotisColor {
let u = u.clamp(0.0, 1.0);
let v = v.clamp(0.0, 1.0);
let top = lerp_cotis_color(c_tl, c_tr, u);
let bottom = lerp_cotis_color(c_bl, c_br, u);
lerp_cotis_color(top, bottom, v)
}
fn sorted_stops(stops: &[GradientStop]) -> Vec<GradientStop> {
let mut v: Vec<GradientStop> = stops.to_vec();
v.sort_by(|a, b| {
a.offset
.partial_cmp(&b.offset)
.unwrap_or(std::cmp::Ordering::Equal)
});
v
}
pub(crate) fn sample_gradient_stops(stops: &[GradientStop], mut t: f32) -> CotisColor {
t = t.clamp(0.0, 1.0);
if stops.is_empty() {
return CotisColor::rgba(0.0, 0.0, 0.0, 0.0);
}
let stops = sorted_stops(stops);
if stops.len() == 1 {
return stops[0].color;
}
if t <= stops[0].offset {
return stops[0].color;
}
for i in 0..stops.len() - 1 {
let a = &stops[i];
let b = &stops[i + 1];
if t <= b.offset {
let span = (b.offset - a.offset).max(1e-6);
let u = (t - a.offset) / span;
return lerp_cotis_color(a.color, b.color, u);
}
}
stops[stops.len() - 1].color
}
fn anchor_offset(attachment: ColorAttachmentPoint, w: f32, h: f32) -> (f32, f32) {
match attachment {
ColorAttachmentPoint::TopLeft => (0.0, 0.0),
ColorAttachmentPoint::TopCenter => (w * 0.5, 0.0),
ColorAttachmentPoint::TopRight => (w, 0.0),
ColorAttachmentPoint::CenterLeft => (0.0, h * 0.5),
ColorAttachmentPoint::CenterCenter => (w * 0.5, h * 0.5),
ColorAttachmentPoint::CenterRight => (w, h * 0.5),
ColorAttachmentPoint::BottomLeft => (0.0, h),
ColorAttachmentPoint::BottomCenter => (w * 0.5, h),
ColorAttachmentPoint::BottomRight => (w, h),
}
}
fn color_pos_to_local_px(pos: &ColorPos, w: f32, h: f32) -> Vector2 {
let (ax, ay) = anchor_offset(pos.attachment_point, w, h);
Vector2::new(ax + pos.x * w, ay + pos.y * h)
}
fn linear_corner_t(grad: &LinearGradient, w: f32, h: f32, px: f32, py: f32) -> f32 {
let s = color_pos_to_local_px(&grad.start, w, h);
let e = color_pos_to_local_px(&grad.end, w, h);
let dx = e.x - s.x;
let dy = e.y - s.y;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-6 {
return 0.0;
}
let vx = px - s.x;
let vy = py - s.y;
((vx * dx + vy * dy) / len_sq).clamp(0.0, 1.0)
}
fn radial_corner_t(grad: &RadialGradient, w: f32, h: f32, px: f32, py: f32) -> f32 {
let c = color_pos_to_local_px(&grad.center, w, h);
let r = grad.radius * w.min(h).max(1.0);
if r < 1e-3 {
return 0.0;
}
let dx = px - c.x;
let dy = py - c.y;
((dx * dx + dy * dy).sqrt() / r).clamp(0.0, 1.0)
}
fn rounded_rect_corner_t(grad: &RoundedRectGradient, w: f32, h: f32, px: f32, py: f32) -> f32 {
let origin = color_pos_to_local_px(&grad.position, w, h);
let gw = (grad.width * w).max(1e-6);
let gh = (grad.height * h).max(1e-6);
let u = ((px - origin.x) / gw).clamp(0.0, 1.0);
let v = ((py - origin.y) / gh).clamp(0.0, 1.0);
(u + v) * 0.5
}
fn intersect_clip_rect(a: (f32, f32, f32, f32), b: (f32, f32, f32, f32)) -> (f32, f32, f32, f32) {
let ax2 = a.0 + a.2;
let ay2 = a.1 + a.3;
let bx2 = b.0 + b.2;
let by2 = b.1 + b.3;
let x = a.0.max(b.0);
let y = a.1.max(b.1);
let w = ax2.min(bx2) - x;
let h = ay2.min(by2) - y;
(x, y, w.max(0.0), h.max(0.0))
}
fn rounded_row_x_span(y_center: f32, w: f32, h: f32, r: f32) -> (f32, f32) {
let r = r.max(0.0).min(w * 0.5).min(h * 0.5);
if r < 1e-4 {
return (0.0, w);
}
let y = y_center.clamp(0.0, h);
if y < r {
let dy = r - y;
let dx = (r * r - dy * dy).max(0.0).sqrt();
let xmin = r - dx;
let xmax = w - r + dx;
(xmin, xmax)
} else if y > h - r {
let dy = r - (h - y);
let dx = (r * r - dy * dy).max(0.0).sqrt();
let xmin = r - dx;
let xmax = w - r + dx;
(xmin, xmax)
} else {
(0.0, w)
}
}
fn spans_close(a: (f32, f32), b: (f32, f32)) -> bool {
(a.0 - b.0).abs() < 0.01 && (a.1 - b.1).abs() < 0.01
}
#[allow(clippy::too_many_arguments)]
fn draw_gradient_rounded_with_scissor<'rl>(
d: &mut RaylibDrawHandle<'rl>,
x: f32,
y: f32,
w: f32,
h: f32,
r_px: f32,
parent_clip: Option<(f32, f32, f32, f32)>,
sample_tl: CotisColor,
sample_bl: CotisColor,
sample_br: CotisColor,
sample_tr: CotisColor,
) {
let w = w.max(1e-3);
let h = h.max(1e-3);
let row_count = h.ceil().max(1.0) as i32;
let mut iy = 0;
while iy < row_count {
let yc = (iy as f32 + 0.5).min(h - 1e-3);
let span0 = rounded_row_x_span(yc, w, h, r_px);
let mut run_end = iy + 1;
while run_end < row_count {
let yc2 = (run_end as f32 + 0.5).min(h - 1e-3);
let sp = rounded_row_x_span(yc2, w, h, r_px);
if !spans_close(span0, sp) {
break;
}
run_end += 1;
}
let strip_h = (run_end - iy) as f32;
let y_top = y + iy as f32;
let xmin = span0.0.max(0.0).min(w);
let xmax = span0.1.max(0.0).min(w);
if xmax <= xmin + 1e-4 {
iy = run_end;
continue;
}
let mut sx = x + xmin;
let mut sw = xmax - xmin;
let mut sy = y_top;
let mut sh = strip_h;
if let Some(pc) = parent_clip {
let inter = intersect_clip_rect(pc, (sx, sy, sw, sh));
if inter.2 < 1e-4 || inter.3 < 1e-4 {
iy = run_end;
continue;
}
sx = inter.0;
sy = inter.1;
sw = inter.2;
sh = inter.3;
}
let lx0 = sx - x;
let ly0 = sy - y;
let lx1 = lx0 + sw;
let ly1 = ly0 + sh;
let u0 = (lx0 / w).clamp(0.0, 1.0);
let u1 = (lx1 / w).clamp(0.0, 1.0);
let v0 = (ly0 / h).clamp(0.0, 1.0);
let v1 = (ly1 / h).clamp(0.0, 1.0);
let tl = bilinear_quad_corners(sample_tl, sample_tr, sample_bl, sample_br, u0, v0);
let tr = bilinear_quad_corners(sample_tl, sample_tr, sample_bl, sample_br, u1, v0);
let bl = bilinear_quad_corners(sample_tl, sample_tr, sample_bl, sample_br, u0, v1);
let br = bilinear_quad_corners(sample_tl, sample_tr, sample_bl, sample_br, u1, v1);
unsafe {
BeginScissorMode(sx as i32, sy as i32, sw.ceil() as i32, sh.ceil() as i32);
}
d.draw_rectangle_gradient_ex(
RlRectangle::new(sx, sy, sw, sh),
cotis_color_to_raylib(tl),
cotis_color_to_raylib(bl),
cotis_color_to_raylib(br),
cotis_color_to_raylib(tr),
);
iy = run_end;
}
unsafe {
if let Some(pc) = parent_clip {
BeginScissorMode(pc.0 as i32, pc.1 as i32, pc.2 as i32, pc.3 as i32);
} else {
EndScissorMode();
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_color_layer_in_rect<'rl>(
d: &mut RaylibDrawHandle<'rl>,
x: f32,
y: f32,
w: f32,
h: f32,
layer: &ColorLayer,
corner_radii: &CornerRadii,
parent_clip: Option<(f32, f32, f32, f32)>,
) {
match layer {
ColorLayer::Solid(c) => {
if c.a <= 0.0 {
return;
}
let rc = cotis_color_to_raylib(*c);
if corner_radii.top_left > 0.0 {
let radius = (corner_radii.top_left * 2.) / if w > h { h } else { w };
d.draw_rectangle_rounded(RlRectangle::new(x, y, w, h), radius, 8, rc);
} else {
d.draw_rectangle(x as i32, y as i32, w as i32, h as i32, rc);
}
}
ColorLayer::Linear(g) => {
let c_tl = sample_gradient_stops(&g.stops, linear_corner_t(g, w, h, 0.0, 0.0));
let c_bl = sample_gradient_stops(&g.stops, linear_corner_t(g, w, h, 0.0, h));
let c_br = sample_gradient_stops(&g.stops, linear_corner_t(g, w, h, w, h));
let c_tr = sample_gradient_stops(&g.stops, linear_corner_t(g, w, h, w, 0.0));
if corner_radii.top_left > 0.0 {
let r_px = corner_radii.top_left.max(0.0).min(w * 0.5).min(h * 0.5);
draw_gradient_rounded_with_scissor(
d,
x,
y,
w,
h,
r_px,
parent_clip,
c_tl,
c_bl,
c_br,
c_tr,
);
} else {
d.draw_rectangle_gradient_ex(
RlRectangle::new(x, y, w, h),
cotis_color_to_raylib(c_tl),
cotis_color_to_raylib(c_bl),
cotis_color_to_raylib(c_br),
cotis_color_to_raylib(c_tr),
);
}
}
ColorLayer::Radial(g) => {
let c_tl = sample_gradient_stops(&g.stops, radial_corner_t(g, w, h, 0.0, 0.0));
let c_bl = sample_gradient_stops(&g.stops, radial_corner_t(g, w, h, 0.0, h));
let c_br = sample_gradient_stops(&g.stops, radial_corner_t(g, w, h, w, h));
let c_tr = sample_gradient_stops(&g.stops, radial_corner_t(g, w, h, w, 0.0));
if corner_radii.top_left > 0.0 {
let r_px = corner_radii.top_left.max(0.0).min(w * 0.5).min(h * 0.5);
draw_gradient_rounded_with_scissor(
d,
x,
y,
w,
h,
r_px,
parent_clip,
c_tl,
c_bl,
c_br,
c_tr,
);
} else {
d.draw_rectangle_gradient_ex(
RlRectangle::new(x, y, w, h),
cotis_color_to_raylib(c_tl),
cotis_color_to_raylib(c_bl),
cotis_color_to_raylib(c_br),
cotis_color_to_raylib(c_tr),
);
}
}
ColorLayer::RoundedRect(g) => {
let c_tl = sample_gradient_stops(&g.stops, rounded_rect_corner_t(g, w, h, 0.0, 0.0));
let c_bl = sample_gradient_stops(&g.stops, rounded_rect_corner_t(g, w, h, 0.0, h));
let c_br = sample_gradient_stops(&g.stops, rounded_rect_corner_t(g, w, h, w, h));
let c_tr = sample_gradient_stops(&g.stops, rounded_rect_corner_t(g, w, h, w, 0.0));
let r_px = g
.corner_radius
.max(corner_radii.top_left)
.max(0.0)
.min(w * 0.5)
.min(h * 0.5);
draw_gradient_rounded_with_scissor(
d,
x,
y,
w,
h,
r_px,
parent_clip,
c_tl,
c_bl,
c_br,
c_tr,
);
}
ColorLayer::Layered(stack) => {
for sub in &stack.layers {
draw_color_layer_in_rect(d, x, y, w, h, sub, corner_radii, parent_clip);
}
}
}
}
pub(crate) fn text_color_at(layer: &ColorLayer, t: f32) -> CotisColor {
let t = t.clamp(0.0, 1.0);
match layer {
ColorLayer::Solid(c) => *c,
ColorLayer::Linear(g) => {
sample_gradient_stops(&g.stops, linear_corner_t(g, 1.0, 1.0, t, 0.5))
}
ColorLayer::Radial(g) => {
sample_gradient_stops(&g.stops, radial_corner_t(g, 1.0, 1.0, t, 0.5))
}
ColorLayer::RoundedRect(g) => sample_gradient_stops(&g.stops, t),
ColorLayer::Layered(stack) => stack
.layers
.last()
.map(|l| text_color_at(l, t))
.unwrap_or(CotisColor::rgb(0.0, 0.0, 0.0)),
}
}
pub(crate) fn draw_text_color_layer<'rl>(
d: &mut RaylibDrawHandle<'rl>,
font: &Font,
text: &str,
origin: Vector2,
font_size: f32,
letter_spacing: f32,
layer: &ColorLayer,
) {
if text.is_empty() {
return;
}
let scale = font_size / font.baseSize as f32;
let glyphs = unsafe { std::slice::from_raw_parts(font.glyphs, font.glyphCount as usize) };
let recs = unsafe { std::slice::from_raw_parts(font.recs, font.glyphCount as usize) };
let tw: f32 = text
.chars()
.map(|ch| {
let idx = (ch as usize).saturating_sub(32);
if idx >= font.glyphCount as usize {
return letter_spacing;
}
if glyphs[idx].advanceX != 0 {
glyphs[idx].advanceX as f32 * scale + letter_spacing
} else {
(recs[idx].width + glyphs[idx].offsetX as f32) * scale + letter_spacing
}
})
.sum();
let tw = tw.max(1.0);
let mut pen_x = origin.x;
for ch in text.chars() {
let idx = (ch as usize).saturating_sub(32);
let t = ((pen_x - origin.x) / tw).clamp(0.0, 1.0);
let tint = cotis_color_to_raylib(text_color_at(layer, t));
if idx < font.glyphCount as usize {
d.draw_text_codepoint(
font,
ch as i32,
Vector2::new(pen_x, origin.y),
font_size,
tint,
);
if glyphs[idx].advanceX != 0 {
pen_x += glyphs[idx].advanceX as f32 * scale + letter_spacing;
} else {
pen_x += (recs[idx].width + glyphs[idx].offsetX as f32) * scale + letter_spacing;
}
} else {
pen_x += letter_spacing;
}
}
}