#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::prelude::{length, percent, Size, Style};
use llimphi_ui::llimphi_raster::kurbo::{Affine, Rect};
use llimphi_ui::llimphi_raster::peniko::{Color, Fill};
use llimphi_ui::View;
#[derive(Debug, Clone, Copy)]
pub struct TimelinePalette {
pub track: Color,
pub fill: Color,
pub knob: Color,
pub mark: Color,
pub height: f32,
pub radius: f64,
}
impl Default for TimelinePalette {
fn default() -> Self {
Self::from_theme(&llimphi_theme::Theme::dark())
}
}
impl TimelinePalette {
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
Self {
track: t.bg_button,
fill: t.accent,
knob: t.fg_text,
mark: Color::from_rgba8(255, 196, 84, 255),
height: 14.0,
radius: 7.0,
}
}
}
pub fn timeline_view<Msg, F>(progress: f32, palette: &TimelinePalette, on_seek: F) -> View<Msg>
where
Msg: 'static,
F: Fn(f32) -> Option<Msg> + Send + Sync + 'static,
{
timeline_view_marked(progress, &[], palette, on_seek)
}
pub fn timeline_view_marked<Msg, F>(
progress: f32,
marks: &[f32],
palette: &TimelinePalette,
on_seek: F,
) -> View<Msg>
where
Msg: 'static,
F: Fn(f32) -> Option<Msg> + Send + Sync + 'static,
{
let p = progress.clamp(0.0, 1.0);
let fill_color = palette.fill;
let knob_color = palette.knob;
let mark_color = palette.mark;
let marks: Vec<f32> = marks
.iter()
.copied()
.filter(|f| f.is_finite() && (0.0..=1.0).contains(f))
.collect();
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(palette.height),
},
..Default::default()
})
.fill(palette.track)
.radius(palette.radius)
.paint_with(move |scene, _ts, rect| {
if rect.w <= 2.0 || rect.h <= 2.0 {
return;
}
let pad: f32 = 2.0;
let x0 = rect.x + pad;
let y0 = rect.y + pad;
let w = (rect.w - 2.0 * pad).max(1.0);
let h = (rect.h - 2.0 * pad).max(1.0);
let fw = (w * p).max(0.0);
if fw > 0.5 {
let fill = Rect::new(x0 as f64, y0 as f64, (x0 + fw) as f64, (y0 + h) as f64);
scene.fill(Fill::NonZero, Affine::IDENTITY, fill_color, None, &fill);
}
let mw: f32 = 2.0;
for f in &marks {
let mx = x0 + w * f;
let mark = Rect::new(
(mx - mw * 0.5) as f64,
y0 as f64,
(mx + mw * 0.5) as f64,
(y0 + h) as f64,
);
scene.fill(Fill::NonZero, Affine::IDENTITY, mark_color, None, &mark);
}
let kx = x0 + fw;
let kw: f32 = 3.0;
let knob = Rect::new(
(kx - kw * 0.5) as f64,
y0 as f64,
(kx + kw * 0.5) as f64,
(y0 + h) as f64,
);
scene.fill(Fill::NonZero, Affine::IDENTITY, knob_color, None, &knob);
})
.on_click_at(move |lx, _ly, w, _h| {
if w <= 0.0 {
return None;
}
on_seek((lx / w).clamp(0.0, 1.0))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct Seek(f32);
#[test]
fn from_theme_usa_colores_semanticos() {
let t = llimphi_theme::Theme::dark();
let p = TimelinePalette::from_theme(&t);
assert_eq!(p.track, t.bg_button);
assert_eq!(p.fill, t.accent);
assert_eq!(p.knob, t.fg_text);
}
#[test]
fn construye_sin_panic_en_extremos() {
let pal = TimelinePalette::default();
let _ = timeline_view(-0.5, &pal, |f| Some(Seek(f)));
let _ = timeline_view(0.0, &pal, |f| Some(Seek(f)));
let _ = timeline_view(1.0, &pal, |f| Some(Seek(f)));
let _ = timeline_view(2.0, &pal, |f| Some(Seek(f)));
}
}