use crate::draw::{ClipRect, Color, Rect, Scene, ShaderId, TextAlign, TextDraw};
use crate::textures::{TextureId, TextureInfo};
use crate::widgets::WidgetState;
use serde::Deserialize;
use std::f32::consts::PI;
#[derive(Clone, Copy)]
pub struct StyleCtx<'a> {
pub registry: &'a StyleRegistry,
pub tex_registry: &'a dyn TextureInfo,
}
#[derive(Clone, Copy)]
pub struct Transition {
pub from: WidgetState,
pub to: WidgetState,
pub t: f32,
}
#[derive(Clone, Copy)]
pub struct DrawLabel<'a> {
pub label: Option<&'a str>,
pub z: f32,
pub clip: Option<ClipRect>,
pub alpha: f32,
}
#[derive(Debug, Clone, Deserialize)]
pub enum Shape {
Rectangle,
RoundedRectangle,
Pill,
Circle,
Polygon(Vec<[f32; 2]>),
}
#[derive(Debug, Clone)]
pub struct VisualState {
pub shape: Shape,
pub color: Color,
pub corner_radius: f32,
pub border_width: f32,
pub border_color: Color,
pub highlight_color: Color,
pub shadow_size: f32,
pub shadow_color: Color,
pub opacity: f32,
pub scale: f32,
pub shader: Option<ShaderId>,
pub texture: Option<TextureId>,
pub cast_shadow: bool,
pub text_color: Option<Color>,
pub font_size: Option<f32>,
pub text_align: TextAlign,
pub text_offset_x: f32,
pub text_offset_y: f32,
pub font: Option<String>,
pub bold: bool,
pub italic: bool,
}
impl Default for VisualState {
fn default() -> Self {
Self {
shape: Shape::Rectangle,
color: Color::rgba(0.0, 0.0, 0.0, 0.0),
corner_radius: 0.0,
border_width: 0.0,
border_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
highlight_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
shadow_size: 0.0,
shadow_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
opacity: 0.0,
scale: 1.0,
shader: None,
texture: None,
cast_shadow: true,
text_color: None,
font_size: None,
text_align: TextAlign::Center,
text_offset_x: 0.0,
text_offset_y: 0.0,
font: None,
bold: false,
italic: false,
}
}
}
impl VisualState {
pub fn lerp(&self, other: &Self, t: f32) -> Self {
Self {
shape: self.shape.clone(), color: lerp_color(self.color, other.color, t),
corner_radius: lerp_f32(self.corner_radius, other.corner_radius, t),
border_width: lerp_f32(self.border_width, other.border_width, t),
border_color: lerp_color(self.border_color, other.border_color, t),
highlight_color: lerp_color(self.highlight_color, other.highlight_color, t),
shadow_size: lerp_f32(self.shadow_size, other.shadow_size, t),
shadow_color: lerp_color(self.shadow_color, other.shadow_color, t),
opacity: lerp_f32(self.opacity, other.opacity, t),
scale: lerp_f32(self.scale, other.scale, t),
shader: other.shader,
texture: other.texture,
cast_shadow: other.cast_shadow,
text_color: other.text_color,
font_size: other.font_size,
text_align: other.text_align,
text_offset_x: other.text_offset_x,
text_offset_y: other.text_offset_y,
font: other.font.clone(),
bold: other.bold,
italic: other.italic,
}
}
}
pub struct Style {
pub shader: ShaderId,
pub textured: ShaderId,
pub idle: VisualState,
pub hovered: VisualState,
pub pressed: VisualState,
pub disabled: VisualState,
}
impl Style {
pub const fn resolve(&self, state: WidgetState) -> &VisualState {
if state.disabled {
&self.disabled
} else if state.pressed {
&self.pressed
} else if state.hovered {
&self.hovered
} else {
&self.idle
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StyleId(usize);
impl StyleId {
pub(crate) const fn new(index: usize) -> Self {
Self(index)
}
pub(crate) const fn index(self) -> usize {
self.0
}
}
pub struct StyleRegistry {
styles: Vec<Style>,
}
impl StyleRegistry {
pub const fn new() -> Self {
Self { styles: Vec::new() }
}
pub fn register(&mut self, style: Style) -> StyleId {
let id = StyleId::new(self.styles.len());
self.styles.push(style);
id
}
pub fn get(&self, id: StyleId) -> &Style {
&self.styles[id.index()]
}
pub fn draw(
&self,
id: StyleId,
rect: Rect,
state: WidgetState,
dl: DrawLabel<'_>,
scene: &mut Scene,
tex_registry: &dyn TextureInfo,
) {
let style = self.get(id);
let vs = style.resolve(state);
let state_arr = [
f32::from(u8::from(state.hovered)),
f32::from(u8::from(state.pressed)),
f32::from(u8::from(state.disabled)),
f32::from(u8::from(state.focused)),
];
draw_visual_state(
scene,
tex_registry,
style,
vs,
rect,
dl.z,
dl.clip,
state_arr,
);
if let Some(text) = dl.label {
let font_size = vs
.font_size
.unwrap_or_else(|| (rect.h * 0.45).clamp(10.0, 48.0));
let mut color = vs.text_color.unwrap_or(Color::WHITE);
color.a *= dl.alpha;
let tx = rect.x + vs.text_offset_x;
let ty = (rect.h - font_size).mul_add(0.5, rect.y) + vs.text_offset_y;
scene.push_text(&TextDraw {
text,
x: tx,
y: ty,
w: rect.w,
size: font_size,
color,
align: vs.text_align,
font: vs.font.as_deref(),
bold: vs.bold,
italic: vs.italic,
clip: dl.clip,
z: dl.z,
});
}
}
}
impl Default for StyleRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn push_component(
scene: &mut Scene,
ctx: StyleCtx<'_>,
style_id: StyleId,
tr: Transition,
rect: Rect,
z: f32,
clip: Option<ClipRect>,
) -> VisualState {
let style = ctx.registry.get(style_id);
let vs = style.resolve(tr.from).lerp(style.resolve(tr.to), tr.t);
let state_arr = [
lerp_f32(
f32::from(u8::from(tr.from.hovered)),
f32::from(u8::from(tr.to.hovered)),
tr.t,
),
lerp_f32(
f32::from(u8::from(tr.from.pressed)),
f32::from(u8::from(tr.to.pressed)),
tr.t,
),
lerp_f32(
f32::from(u8::from(tr.from.disabled)),
f32::from(u8::from(tr.to.disabled)),
tr.t,
),
lerp_f32(
f32::from(u8::from(tr.from.focused)),
f32::from(u8::from(tr.to.focused)),
tr.t,
),
];
draw_visual_state(
scene,
ctx.tex_registry,
style,
&vs,
rect,
z,
clip,
state_arr,
);
vs
}
pub fn draw_visual_state(
scene: &mut Scene,
tex_registry: &dyn TextureInfo,
style: &Style,
vs: &VisualState,
rect: Rect,
z: f32,
clip: Option<ClipRect>,
state: [f32; 4],
) {
let shader = vs.shader.unwrap_or(style.shader);
let (x, y, w, h) = (rect.x, rect.y, rect.w, rect.h);
let cx = w.mul_add(0.5, x);
let cy = h.mul_add(0.5, y);
let sw = w * vs.scale;
let sh = h * vs.scale;
let sx = sw.mul_add(-0.5, cx);
let sy = sh.mul_add(-0.5, cy);
let has_base = vs.shader.is_some() || vs.texture.is_none();
if has_base {
if vs.cast_shadow && vs.shadow_size > 0.0 {
let verts = shape_verts(
&vs.shape,
sx + vs.shadow_size,
sy + vs.shadow_size,
sw,
sh,
vs.corner_radius,
);
let mut col = vs.shadow_color;
col.a *= vs.opacity;
scene.push_widget(
verts,
shader,
col,
z - 0.1,
clip,
vs.corner_radius,
vs.border_width,
state,
);
}
let verts = shape_verts(&vs.shape, sx, sy, sw, sh, vs.corner_radius);
let mut col = vs.color;
col.a *= vs.opacity;
scene.push_widget(
verts,
shader,
col,
z,
clip,
vs.corner_radius,
vs.border_width,
state,
);
if vs.border_width > 0.0 {
let verts = border_verts(&vs.shape, sx, sy, sw, sh, vs.corner_radius, vs.border_width);
let mut col = vs.border_color;
col.a *= vs.opacity;
scene.push_widget(
verts,
shader,
col,
z + 0.01,
clip,
vs.corner_radius,
vs.border_width,
state,
);
}
if vs.highlight_color.a > 0.0 {
let verts = shape_verts(&vs.shape, sx, sy, sw, sh * 0.5, vs.corner_radius);
let mut col = vs.highlight_color;
col.a *= vs.opacity;
scene.push_widget(
verts,
shader,
col,
z + 0.02,
clip,
vs.corner_radius,
vs.border_width,
state,
);
}
}
if let Some(tex) = vs.texture
&& !tex_registry.is_hidden(tex)
{
let uv = tex_registry.current_uv_rect(tex);
scene.push_image_uv(
Rect::new(sx, sy, sw, sh),
style.textured,
tex,
z + 0.03,
clip,
Some(uv),
);
}
}
fn corner_segments(radius: f32) -> u32 {
((radius * 8.0) as u32).clamp(8, 256)
}
fn shape_verts(shape: &Shape, x: f32, y: f32, w: f32, h: f32, radius: f32) -> Vec<[f32; 2]> {
match shape {
Shape::Rectangle => rect_verts(x, y, w, h),
Shape::RoundedRectangle => rounded_rect_verts(x, y, w, h, radius.min(w / 2.0).min(h / 2.0)),
Shape::Pill => rounded_rect_verts(x, y, w, h, (h / 2.0).min(w / 2.0)),
Shape::Circle => rounded_rect_verts(x, y, w, h, (w / 2.0).min(h / 2.0)),
Shape::Polygon(rel_verts) => {
let abs: Vec<[f32; 2]> = rel_verts
.iter()
.map(|v| [v[0].mul_add(w, x), v[1].mul_add(h, y)])
.collect();
centroid_fan(&abs)
}
}
}
fn centroid_fan(verts: &[[f32; 2]]) -> Vec<[f32; 2]> {
if verts.len() < 3 {
return vec![];
}
let n = verts.len() as f32;
let cx = verts.iter().map(|v| v[0]).sum::<f32>() / n;
let cy = verts.iter().map(|v| v[1]).sum::<f32>() / n;
let centroid = [cx, cy];
let count = verts.len();
let mut out = Vec::with_capacity(count * 3);
for i in 0..count {
let next = (i + 1) % count;
out.push(centroid);
out.push(verts[i]);
out.push(verts[next]);
}
out
}
fn rect_verts(x: f32, y: f32, w: f32, h: f32) -> Vec<[f32; 2]> {
vec![
[x, y],
[x + w, y],
[x, y + h],
[x + w, y],
[x + w, y + h],
[x, y + h],
]
}
pub fn rounded_rect_verts(x: f32, y: f32, w: f32, h: f32, r: f32) -> Vec<[f32; 2]> {
let mut v = Vec::new();
let segs = corner_segments(r);
let tl = [x + r, y + r];
let tr = [x + w - r, y + r];
let br = [x + w - r, y + h - r];
let bl = [x + r, y + h - r];
push_rect(&mut v, x + r, y + r, r.mul_add(-2.0, w), r.mul_add(-2.0, h)); push_rect(&mut v, x + r, y, r.mul_add(-2.0, w), r); push_rect(&mut v, x + r, y + h - r, r.mul_add(-2.0, w), r); push_rect(&mut v, x, y + r, r, r.mul_add(-2.0, h)); push_rect(&mut v, x + w - r, y + r, r, r.mul_add(-2.0, h));
push_corner_fan(&mut v, tl, r, PI, PI * 1.5, segs); push_corner_fan(&mut v, tr, r, PI * 1.5, PI * 2.0, segs); push_corner_fan(&mut v, br, r, 0.0, PI * 0.5, segs); push_corner_fan(&mut v, bl, r, PI * 0.5, PI, segs);
v
}
fn push_rect(v: &mut Vec<[f32; 2]>, x: f32, y: f32, w: f32, h: f32) {
v.extend_from_slice(&[
[x, y],
[x + w, y],
[x, y + h],
[x + w, y],
[x + w, y + h],
[x, y + h],
]);
}
fn push_corner_fan(
v: &mut Vec<[f32; 2]>,
center: [f32; 2],
r: f32,
start: f32,
end: f32,
segs: u32,
) {
let [cx, cy] = center;
for i in 0..segs {
let t0 = i as f32 / segs as f32;
let t1 = (i + 1) as f32 / segs as f32;
let a0 = (end - start).mul_add(t0, start);
let a1 = (end - start).mul_add(t1, start);
v.push([cx, cy]);
v.push([r.mul_add(a0.cos(), cx), r.mul_add(a0.sin(), cy)]);
v.push([r.mul_add(a1.cos(), cx), r.mul_add(a1.sin(), cy)]);
}
}
fn border_verts(
shape: &Shape,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
bw: f32,
) -> Vec<[f32; 2]> {
let segs_per_corner = corner_segments(radius.min(w / 2.0).min(h / 2.0));
let segments = (segs_per_corner * 4) as usize;
let outer_r = match shape {
Shape::Rectangle => 0.0,
Shape::RoundedRectangle => radius.min(w / 2.0).min(h / 2.0),
Shape::Pill => (h / 2.0).min(w / 2.0),
Shape::Circle => (w / 2.0).min(h / 2.0),
Shape::Polygon(_) => return vec![],
};
let inner_r = (outer_r - bw).max(0.0);
let outer_pts = perimeter_points(x, y, w, h, outer_r, segments);
let inner_pts = perimeter_points(
x + bw,
y + bw,
bw.mul_add(-2.0, w),
bw.mul_add(-2.0, h),
inner_r,
segments,
);
let mut v = Vec::new();
let n = outer_pts.len();
for i in 0..n {
let next = (i + 1) % n;
let (o0, o1) = (outer_pts[i], outer_pts[next]);
let (i0, i1) = (inner_pts[i], inner_pts[next]);
v.push(o0);
v.push(o1);
v.push(i0);
v.push(o1);
v.push(i1);
v.push(i0);
}
v
}
fn perimeter_points(x: f32, y: f32, w: f32, h: f32, r: f32, segments: usize) -> Vec<[f32; 2]> {
let r = r.min(w / 2.0).min(h / 2.0).max(0.0);
let mut pts = Vec::new();
let corners = [
(x + w - r, y + r, PI * 1.5, PI * 2.0), (x + w - r, y + h - r, 0.0, PI * 0.5), (x + r, y + h - r, PI * 0.5, PI), (x + r, y + r, PI, PI * 1.5), ];
let segs_per_corner = (segments / 4).max(2);
for (cx, cy, start, end) in corners {
for i in 0..segs_per_corner {
let t = i as f32 / segs_per_corner as f32;
let a = (end - start).mul_add(t, start);
pts.push([cx + r * a.cos(), cy + r * a.sin()]);
}
}
pts
}
fn lerp_f32(a: f32, b: f32, t: f32) -> f32 {
(b - a).mul_add(t, a)
}
fn lerp_color(a: Color, b: Color, t: f32) -> Color {
Color::rgba(
lerp_f32(a.r, b.r, t),
lerp_f32(a.g, b.g, t),
lerp_f32(a.b, b.b, t),
lerp_f32(a.a, b.a, t),
)
}