灵符lingfu

ling-ui

↓ 0 downloads

Ling UI framework — retained-mode widgets and flex layout

lingfu install ling-ui
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (18)

src/widgets.rs
//! Professional vector UI toolkit — HUD overlays, meters/gauges, interface
//! controls and game UI, all emitted as resolved vector primitives so a single
//! widget can be multi-colour and the host can rasterize (AA fill + AA stroke)
//! however it likes. Pure, allocation-light, immediate-mode: call a function
//! each frame with the live params and draw the returned [`Draw`].
//!
//! Conventions: screen space, x→right, **y→down**, origin top-left. Colours are
//! packed `0x00RRGGBB` (the Ling framebuffer format). Interactive state
//! (hover/active/value) is passed in by the host; these functions are pure
//! geometry and never read input themselves.

use crate::holo;
use core::f32::consts::PI;

/// Packed `0x00RRGGBB` colour.
pub type Rgba = u32;

/// Resolved vector output: filled polygons + stroked polylines, each with its
/// own colour so one widget can mix theme slots (track vs fill vs accent).
#[derive(Default, Clone)]
pub struct Draw {
    pub fills: Vec<(Rgba, Vec<[f32; 2]>)>,
    pub strokes: Vec<(Rgba, Vec<[f32; 2]>)>,
}

impl Draw {
    fn new() -> Self {
        Self::default()
    }

    fn fill(&mut self, c: Rgba, poly: Vec<[f32; 2]>) {
        self.fills.push((c, poly));
    }

    fn stroke(&mut self, c: Rgba, pl: Vec<[f32; 2]>) {
        self.strokes.push((c, pl));
    }

    /// Stroke a list of disjoint segments `[x0,y0,x1,y1]` (e.g. from `holo`).
    #[allow(dead_code)] // helper kept for holo/segment widgets (not yet called)
    fn segs(&mut self, c: Rgba, segs: &[[f32; 4]]) {
        for s in segs {
            self.strokes.push((c, vec![[s[0], s[1]], [s[2], s[3]]]));
        }
    }

    fn rect_outline(&mut self, c: Rgba, x: f32, y: f32, w: f32, h: f32) {
        self.stroke(
            c,
            vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]],
        );
    }

    fn rect_fill(&mut self, c: Rgba, x: f32, y: f32, w: f32, h: f32) {
        self.fill(
            c,
            vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]],
        );
    }
}

// ── small helpers ─────────────────────────────────────────────────────────────

#[inline]
fn clamp01(v: f32) -> f32 {
    v.clamp(0.0, 1.0)
}

/// Mix two packed colours by `t∈[0,1]`.
pub fn mix(a: Rgba, b: Rgba, t: f32) -> Rgba {
    let t = clamp01(t);
    let lerp = |x: u32, y: u32| (x as f32 + (y as f32 - x as f32) * t) as u32 & 0xFF;
    let (ar, ag, ab) = (a >> 16 & 0xFF, a >> 8 & 0xFF, a & 0xFF);
    let (br, bg, bb) = (b >> 16 & 0xFF, b >> 8 & 0xFF, b & 0xFF);
    (lerp(ar, br) << 16) | (lerp(ag, bg) << 8) | lerp(ab, bb)
}

/// Scale a colour's brightness by `k` (clamped).
pub fn shade(c: Rgba, k: f32) -> Rgba {
    let f = |x: u32| ((x as f32 * k).clamp(0.0, 255.0)) as u32;
    (f(c >> 16 & 0xFF) << 16) | (f(c >> 8 & 0xFF) << 8) | f(c & 0xFF)
}

/// Arc polyline from `a0` to `a1` radians (y-down screen space), `n` segments.
fn arc(cx: f32, cy: f32, r: f32, a0: f32, a1: f32, n: usize) -> Vec<[f32; 2]> {
    let n = n.max(1);
    (0..=n)
        .map(|i| {
            let a = a0 + (a1 - a0) * i as f32 / n as f32;
            [cx + a.cos() * r, cy + a.sin() * r]
        })
        .collect()
}

/// Beveled (cut-corner) panel polygon — the holographic panel silhouette.
fn bevel_poly(x: f32, y: f32, w: f32, h: f32, b: f32) -> Vec<[f32; 2]> {
    let b = b.min(w * 0.5).min(h * 0.5);
    let (x1, y1) = (x + w, y + h);
    vec![
        [x + b, y],
        [x1 - b, y],
        [x1, y + b],
        [x1, y1 - b],
        [x1 - b, y1],
        [x + b, y1],
        [x, y1 - b],
        [x, y + b],
        [x + b, y],
    ]
}

// ════════════════════════════════════════════════════════════════════════════
// HUD / sci-fi overlays
// ════════════════════════════════════════════════════════════════════════════

/// Circular radar with grid rings, cross axes and a sweeping wedge at `sweep`
/// radians. `blip` ∈ [0,1] fraction along the sweep where a contact pings.
pub fn radar(
    cx: f32,
    cy: f32,
    r: f32,
    sweep: f32,
    primary: Rgba,
    accent: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    for k in 1..=3 {
        d.stroke(track, arc(cx, cy, r * k as f32 / 3.0, 0.0, PI * 2.0, 48));
    }
    d.stroke(track, vec![[cx - r, cy], [cx + r, cy]]);
    d.stroke(track, vec![[cx, cy - r], [cx, cy + r]]);
    // sweep wedge
    let sw = 0.32;
    let mut wedge = vec![[cx, cy]];
    wedge.extend(arc(cx, cy, r, sweep - sw, sweep, 10));
    wedge.push([cx, cy]);
    d.fill(shade(accent, 0.5), wedge);
    d.stroke(
        accent,
        vec![[cx, cy], [cx + sweep.cos() * r, cy + sweep.sin() * r]],
    );
    // contact ping
    let pr = r * 0.62;
    let pa = sweep - 0.15;
    let (bx, by) = (cx + pa.cos() * pr, cy + pa.sin() * pr);
    d.fill(primary, arc(bx, by, 3.5, 0.0, PI * 2.0, 10));
    d
}

/// Horizontal compass strip centred on `heading` (degrees). Width `w`, ticks
/// every 15°, cardinal marks taller.
pub fn compass(x: f32, y: f32, w: f32, h: f32, heading: f32, primary: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    d.rect_outline(track, x, y, w, h);
    let cx = x + w * 0.5;
    let deg_per_px = 90.0 / w; // show ~90° across
    let start = (heading - w * 0.5 * deg_per_px).floor() as i32;
    let end = (heading + w * 0.5 * deg_per_px).ceil() as i32;
    let mut deg = start - (start.rem_euclid(15));
    while deg <= end {
        let px = cx + (deg as f32 - heading) / deg_per_px;
        if px >= x && px <= x + w {
            let card = deg.rem_euclid(90) == 0;
            let th = if card { h * 0.6 } else { h * 0.3 };
            d.stroke(
                if card { primary } else { track },
                vec![[px, y + h], [px, y + h - th]],
            );
        }
        deg += 15;
    }
    // centre marker
    d.stroke(
        primary,
        vec![
            [cx, y - 4.0],
            [cx - 5.0, y - 12.0],
            [cx + 5.0, y - 12.0],
            [cx, y - 4.0],
        ],
    );
    d
}

/// Aiming reticle: ring + tick gaps + centre dot. `spread` ∈ [0,1] opens the gaps.
pub fn reticle(cx: f32, cy: f32, r: f32, spread: f32, primary: Rgba) -> Draw {
    let mut d = Draw::new();
    let g = 0.25 + spread * 0.5;
    for q in 0..4 {
        let base = q as f32 * PI * 0.5 + PI * 0.25;
        d.stroke(
            primary,
            arc(cx, cy, r, base + g * 0.5, base + PI * 0.5 - g * 0.5, 10),
        );
    }
    let inner = r * 0.45 + spread * r * 0.4;
    for k in 0..4 {
        let a = k as f32 * PI * 0.5;
        d.stroke(
            primary,
            vec![
                [cx + a.cos() * inner, cy + a.sin() * inner],
                [cx + a.cos() * (inner + 8.0), cy + a.sin() * (inner + 8.0)],
            ],
        );
    }
    d.fill(primary, arc(cx, cy, 1.8, 0.0, PI * 2.0, 8));
    d
}

/// Animated target brackets that close in as `lock` → 1 (0 = wide, 1 = snug).
pub fn target(x: f32, y: f32, w: f32, h: f32, lock: f32, primary: Rgba, accent: Rgba) -> Draw {
    let mut d = Draw::new();
    let off = (1.0 - clamp01(lock)) * 14.0;
    let col = mix(primary, accent, clamp01(lock));
    let l = (w.min(h)) * 0.28;
    let (xx, yy, ww, hh) = (x - off, y - off, w + off * 2.0, h + off * 2.0);
    for seg in holo::corner_brackets(xx, yy, ww, hh, l) {
        d.stroke(col, vec![[seg[0], seg[1]], [seg[2], seg[3]]]);
    }
    if lock > 0.5 {
        d.stroke(col, vec![[x + w * 0.5, y - off - 6.0], [x + w * 0.5, y]]);
    }
    d
}

/// Sci-fi panel: beveled outline + faint fill + a header tab notch.
pub fn panel(x: f32, y: f32, w: f32, h: f32, bevel: f32, primary: Rgba, bg: Rgba) -> Draw {
    let mut d = Draw::new();
    d.fill(bg, bevel_poly(x, y, w, h, bevel));
    d.stroke(primary, bevel_poly(x, y, w, h, bevel));
    // header bar
    let hb = 16.0_f32.min(h * 0.25);
    d.stroke(primary, vec![[x + bevel, y + hb], [x + w - bevel, y + hb]]);
    d.fill(
        shade(primary, 0.35),
        vec![
            [x + bevel, y],
            [x + w * 0.4, y],
            [x + w * 0.4 - 6.0, y + hb],
            [x + bevel, y + hb],
        ],
    );
    d
}

/// Horizontal scanline overlay inside a rect (`density` lines).
pub fn scanlines(x: f32, y: f32, w: f32, h: f32, density: usize, line: Rgba) -> Draw {
    let mut d = Draw::new();
    let n = density.max(1);
    for i in 0..n {
        let py = y + h * i as f32 / n as f32;
        d.stroke(line, vec![[x, py], [x + w, py]]);
    }
    d
}

// ════════════════════════════════════════════════════════════════════════════
// Meters & gauges
// ════════════════════════════════════════════════════════════════════════════

/// Linear bar: track outline + proportional fill (`value/max`).
pub fn bar(x: f32, y: f32, w: f32, h: f32, frac: f32, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    d.rect_outline(track, x, y, w, h);
    let fw = w * clamp01(frac);
    if fw > 0.5 {
        d.rect_fill(fill, x + 1.0, y + 1.0, (fw - 2.0).max(0.0), h - 2.0);
    }
    d
}

/// Segmented/notched bar — `segs` cells, `frac` lit proportionally.
#[allow(clippy::too_many_arguments)]
pub fn segbar(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    frac: f32,
    segs: usize,
    fill: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let n = segs.max(1);
    let gap = 2.0;
    let cw = (w - gap * (n as f32 - 1.0)) / n as f32;
    let lit = (clamp01(frac) * n as f32).round() as usize;
    for i in 0..n {
        let cx = x + i as f32 * (cw + gap);
        if i < lit {
            d.rect_fill(fill, cx, y, cw, h);
        } else {
            d.rect_outline(track, cx, y, cw, h);
        }
    }
    d
}

/// Radial dial gauge with tick marks and a needle. Sweeps 225°→ -45° (270° arc).
pub fn gauge(cx: f32, cy: f32, r: f32, frac: f32, needle: Rgba, accent: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    let a0 = PI * 0.75; // 135° (down-left)
    let a1 = PI * 0.25 + PI * 2.0; // wrap to up-right (270° sweep clockwise)
    let span = a1 - a0;
    d.stroke(track, arc(cx, cy, r, a0, a1, 60));
    // colored progress arc
    d.stroke(accent, arc(cx, cy, r, a0, a0 + span * clamp01(frac), 60));
    // ticks
    for i in 0..=10 {
        let a = a0 + span * i as f32 / 10.0;
        let (c, s) = (a.cos(), a.sin());
        d.stroke(
            track,
            vec![
                [cx + c * r, cy + s * r],
                [cx + c * (r - 7.0), cy + s * (r - 7.0)],
            ],
        );
    }
    // needle
    let a = a0 + span * clamp01(frac);
    d.stroke(
        needle,
        vec![
            [cx, cy],
            [cx + a.cos() * (r - 4.0), cy + a.sin() * (r - 4.0)],
        ],
    );
    d.fill(needle, arc(cx, cy, 3.0, 0.0, PI * 2.0, 8));
    d
}

/// Ring / arc progress meter (full circle minus a gap at the bottom).
pub fn ring(cx: f32, cy: f32, r: f32, frac: f32, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    let a0 = PI * 0.5 + 0.4;
    let a1 = PI * 0.5 - 0.4 + PI * 2.0;
    let span = a1 - a0;
    d.stroke(track, arc(cx, cy, r, a0, a1, 64));
    d.stroke(fill, arc(cx, cy, r, a0, a0 + span * clamp01(frac), 64));
    d
}

/// VU / spectrum bars from a list of levels (each 0..1).
pub fn vu(x: f32, y: f32, w: f32, h: f32, levels: &[f32], fill: Rgba, peak: Rgba) -> Draw {
    let mut d = Draw::new();
    let n = levels.len().max(1);
    let gap = 2.0;
    let bw = (w - gap * (n as f32 - 1.0)) / n as f32;
    for (i, &lv) in levels.iter().enumerate() {
        let bx = x + i as f32 * (bw + gap);
        let bh = h * clamp01(lv);
        let c = mix(fill, peak, clamp01(lv));
        d.rect_fill(c, bx, y + h - bh, bw, bh);
    }
    d
}

/// Sparkline polyline from a list of values (auto-scaled to [min,max]).
pub fn spark(x: f32, y: f32, w: f32, h: f32, vals: &[f32], line: Rgba) -> Draw {
    let mut d = Draw::new();
    if vals.len() < 2 {
        return d;
    }
    let (mut lo, mut hi) = (f32::MAX, f32::MIN);
    for &v in vals {
        lo = lo.min(v);
        hi = hi.max(v);
    }
    let rng = (hi - lo).max(1e-6);
    let pl: Vec<[f32; 2]> = vals
        .iter()
        .enumerate()
        .map(|(i, &v)| {
            [
                x + w * i as f32 / (vals.len() as f32 - 1.0),
                y + h - (v - lo) / rng * h,
            ]
        })
        .collect();
    d.stroke(line, pl);
    d
}

/// Battery cell with bezel, terminal nub and proportional charge fill.
#[allow(clippy::too_many_arguments)]
pub fn battery(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    frac: f32,
    fill: Rgba,
    track: Rgba,
    warn: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let bw = w - 4.0;
    d.rect_outline(track, x, y, bw, h);
    d.rect_fill(track, x + bw, y + h * 0.3, 4.0, h * 0.4); // terminal
    let f = clamp01(frac);
    let c = if f < 0.25 { warn } else { fill };
    if f > 0.0 {
        d.rect_fill(c, x + 2.0, y + 2.0, (bw - 4.0) * f, h - 4.0);
    }
    d
}

// ════════════════════════════════════════════════════════════════════════════
// Interface controls  (host supplies hover/active/value; widget = geometry)
// ════════════════════════════════════════════════════════════════════════════

/// Beveled button background (label drawn by the host). `hover`/`active` brighten.
#[allow(clippy::too_many_arguments)]
pub fn button(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    hover: bool,
    active: bool,
    primary: Rgba,
    bg: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let glow = if active {
        0.55
    } else if hover {
        0.3
    } else {
        0.12
    };
    d.fill(shade(primary, glow), bevel_poly(x, y, w, h, 8.0));
    d.stroke(
        if hover {
            mix(primary, 0xFFFFFF, 0.4)
        } else {
            primary
        },
        bevel_poly(x, y, w, h, 8.0),
    );
    let _ = bg;
    d
}

/// Toggle switch; `on` slides the knob and recolours the rail.
pub fn toggle(x: f32, y: f32, w: f32, h: f32, on: bool, on_col: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    let r = h * 0.5;
    let rail = if on { on_col } else { track };
    // rounded rail
    let mut p = arc(x + r, y + r, r, PI * 0.5, PI * 1.5, 12);
    p.extend(arc(x + w - r, y + r, r, PI * 1.5, PI * 2.5, 12));
    p.push(p[0]);
    d.stroke(rail, p);
    let kx = if on { x + w - r } else { x + r };
    d.fill(rail, arc(kx, y + r, r - 2.0, 0.0, PI * 2.0, 16));
    d
}

/// Horizontal slider: track + filled portion + draggable knob at `frac`.
pub fn slider(x: f32, y: f32, w: f32, frac: f32, hover: bool, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    d.stroke(track, vec![[x, y], [x + w, y]]);
    let kx = x + w * clamp01(frac);
    d.stroke(fill, vec![[x, y], [kx, y]]);
    let kr = if hover { 8.0 } else { 6.0 };
    d.fill(fill, arc(kx, y, kr, 0.0, PI * 2.0, 16));
    d.stroke(mix(fill, 0xFFFFFF, 0.5), arc(kx, y, kr, 0.0, PI * 2.0, 16));
    d
}

/// Checkbox; `checked` draws a tick.
pub fn checkbox(
    x: f32,
    y: f32,
    s: f32,
    checked: bool,
    hover: bool,
    primary: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    d.rect_outline(
        if hover {
            mix(primary, 0xFFFFFF, 0.4)
        } else {
            track
        },
        x,
        y,
        s,
        s,
    );
    if checked {
        d.stroke(
            primary,
            vec![
                [x + s * 0.22, y + s * 0.55],
                [x + s * 0.42, y + s * 0.75],
                [x + s * 0.8, y + s * 0.25],
            ],
        );
    }
    d
}

/// Tab strip: `count` tabs, `active` highlighted. Returns per-tab rects via geometry.
#[allow(clippy::too_many_arguments)]
pub fn tabs(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    count: usize,
    active: usize,
    hover: i32,
    primary: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let n = count.max(1);
    let tw = w / n as f32;
    for i in 0..n {
        let tx = x + i as f32 * tw;
        if i == active {
            d.fill(
                shade(primary, 0.3),
                vec![
                    [tx, y],
                    [tx + tw, y],
                    [tx + tw, y + h],
                    [tx, y + h],
                    [tx, y],
                ],
            );
            d.stroke(
                primary,
                vec![[tx, y + h], [tx, y], [tx + tw, y], [tx + tw, y + h]],
            );
        } else {
            let c = if hover == i as i32 {
                mix(track, primary, 0.5)
            } else {
                track
            };
            d.stroke(c, vec![[tx, y + h], [tx + tw, y + h]]);
        }
    }
    d
}

/// Progress bar with subtle internal chevrons.
pub fn progress(x: f32, y: f32, w: f32, h: f32, frac: f32, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    d.rect_outline(track, x, y, w, h);
    let fw = (w - 2.0) * clamp01(frac);
    if fw > 0.5 {
        d.rect_fill(fill, x + 1.0, y + 1.0, fw, h - 2.0);
    }
    d
}

/// Tooltip / callout bubble with a pointer (label drawn by host).
pub fn tooltip(x: f32, y: f32, w: f32, h: f32, primary: Rgba, bg: Rgba) -> Draw {
    let mut d = Draw::new();
    d.fill(bg, bevel_poly(x, y, w, h, 6.0));
    d.stroke(primary, bevel_poly(x, y, w, h, 6.0));
    d.fill(
        bg,
        vec![
            [x + 12.0, y + h],
            [x + 22.0, y + h],
            [x + 14.0, y + h + 8.0],
        ],
    );
    d.stroke(
        primary,
        vec![
            [x + 12.0, y + h],
            [x + 14.0, y + h + 8.0],
            [x + 22.0, y + h],
        ],
    );
    d
}

/// Stepper: [ - value + ] frame (host draws value). Returns the two button rects' geometry.
#[allow(clippy::too_many_arguments)]
pub fn stepper(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    hover_minus: bool,
    hover_plus: bool,
    primary: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let bw = h;
    d.fill(
        shade(primary, if hover_minus { 0.4 } else { 0.12 }),
        bevel_poly(x, y, bw, h, 5.0),
    );
    d.stroke(primary, bevel_poly(x, y, bw, h, 5.0));
    d.stroke(
        primary,
        vec![[x + bw * 0.3, y + h * 0.5], [x + bw * 0.7, y + h * 0.5]],
    );
    let px = x + w - bw;
    d.fill(
        shade(primary, if hover_plus { 0.4 } else { 0.12 }),
        bevel_poly(px, y, bw, h, 5.0),
    );
    d.stroke(primary, bevel_poly(px, y, bw, h, 5.0));
    d.stroke(
        primary,
        vec![[px + bw * 0.3, y + h * 0.5], [px + bw * 0.7, y + h * 0.5]],
    );
    d.stroke(
        primary,
        vec![[px + bw * 0.5, y + h * 0.3], [px + bw * 0.5, y + h * 0.7]],
    );
    d.rect_outline(track, x + bw + 2.0, y, w - bw * 2.0 - 4.0, h);
    d
}

// ════════════════════════════════════════════════════════════════════════════
// Game UI
// ════════════════════════════════════════════════════════════════════════════

/// Health bar: notched fill that shifts colour low→high and can pulse (host
/// passes a 0..1 `pulse`).
#[allow(clippy::too_many_arguments)]
pub fn healthbar(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    frac: f32,
    pulse: f32,
    full: Rgba,
    low: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    d.rect_outline(track, x, y, w, h);
    let f = clamp01(frac);
    let mut c = mix(low, full, f);
    if f < 0.3 {
        c = mix(c, 0xFFFFFF, pulse * 0.6);
    }
    if f > 0.0 {
        d.rect_fill(c, x + 1.0, y + 1.0, (w - 2.0) * f, h - 2.0);
    }
    for i in 1..10 {
        let nx = x + w * i as f32 / 10.0;
        d.stroke(track, vec![[nx, y], [nx, y + h]]);
    }
    d
}

/// Radial cooldown wipe (clockwise from top). `frac` = remaining 1→0.
pub fn cooldown(cx: f32, cy: f32, r: f32, frac: f32, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    d.stroke(track, arc(cx, cy, r, 0.0, PI * 2.0, 48));
    let a0 = -PI * 0.5;
    let mut wedge = vec![[cx, cy]];
    wedge.extend(arc(cx, cy, r, a0, a0 + PI * 2.0 * clamp01(frac), 48));
    wedge.push([cx, cy]);
    d.fill(shade(fill, 0.55), wedge);
    d
}

/// 7-segment style vector number for `value` with `digits` places.
#[allow(clippy::too_many_arguments)]
pub fn counter(
    x: f32,
    y: f32,
    dw: f32,
    dh: f32,
    value: i64,
    digits: usize,
    on: Rgba,
    off: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let segmap: [u8; 10] = [
        0b1111110, 0b0110000, 0b1101101, 0b1111001, 0b0110011, 0b1011011, 0b1011111, 0b1110000,
        0b1111111, 0b1111011,
    ];
    let gap = dw * 0.35;
    let v = value.unsigned_abs();
    for i in 0..digits {
        let digit = ((v / 10u64.pow((digits - 1 - i) as u32)) % 10) as usize;
        let mask = segmap[digit];
        let dx = x + i as f32 * (dw + gap);
        seven_seg(&mut d, dx, y, dw, dh, mask, on, off);
    }
    d
}

#[allow(clippy::too_many_arguments)]
fn seven_seg(d: &mut Draw, x: f32, y: f32, w: f32, h: f32, mask: u8, on: Rgba, off: Rgba) {
    let t = w * 0.12; // segment thickness
    let mid = y + h * 0.5;
    // segments: a(top) b(tr) c(br) d(bottom) e(bl) f(tl) g(mid) — bit6..bit0
    let bars = [
        (6, [x + t, y, w - 2.0 * t, t]),                       // a
        (5, [x + w - t, y + t, t, h * 0.5 - 1.5 * t]),         // b
        (4, [x + w - t, mid + 0.5 * t, t, h * 0.5 - 1.5 * t]), // c
        (3, [x + t, y + h - t, w - 2.0 * t, t]),               // d
        (2, [x, mid + 0.5 * t, t, h * 0.5 - 1.5 * t]),         // e
        (1, [x, y + t, t, h * 0.5 - 1.5 * t]),                 // f
        (0, [x + t, mid - 0.5 * t, w - 2.0 * t, t]),           // g
    ];
    for (bit, r) in bars {
        let c = if mask & (1 << bit) != 0 { on } else { off };
        d.rect_fill(c, r[0], r[1], r[2], r[3]);
    }
}

/// Minimap frame with corner brackets (host plots blips separately).
pub fn minimap(x: f32, y: f32, w: f32, h: f32, primary: Rgba, bg: Rgba) -> Draw {
    let mut d = Draw::new();
    d.fill(
        bg,
        vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h], [x, y]],
    );
    for seg in holo::corner_brackets(x, y, w, h, 14.0) {
        d.stroke(primary, vec![[seg[0], seg[1]], [seg[2], seg[3]]]);
    }
    d
}

/// Virtual D-pad / joystick; `dir` 0=none,1=up,2=right,3=down,4=left highlights.
pub fn dpad(cx: f32, cy: f32, r: f32, dir: i32, primary: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    let arm = r * 0.45;
    let dirs = [(0.0, -1.0), (1.0, 0.0), (0.0, 1.0), (-1.0, 0.0)];
    d.stroke(track, arc(cx, cy, r, 0.0, PI * 2.0, 32));
    for (i, (dx, dy)) in dirs.iter().enumerate() {
        let on = dir == i as i32 + 1;
        let c = if on { primary } else { track };
        let bx = cx + dx * r * 0.55;
        let by = cy + dy * r * 0.55;
        let tri = vec![
            [
                bx + dx * arm + dy * arm * 0.4,
                by + dy * arm + dx * arm * 0.4,
            ],
            [
                bx + dx * arm - dy * arm * 0.4,
                by + dy * arm - dx * arm * 0.4,
            ],
            [bx - dx * arm * 0.3, by - dy * arm * 0.3],
            [
                bx + dx * arm + dy * arm * 0.4,
                by + dy * arm + dx * arm * 0.4,
            ],
        ];
        if on {
            d.fill(c, tri.clone());
        }
        d.stroke(c, tri);
    }
    d
}

/// Item slot grid (`cols`×`rows`), `sel` index highlighted.
#[allow(clippy::too_many_arguments)]
pub fn slotgrid(
    x: f32,
    y: f32,
    cols: usize,
    rows: usize,
    cell: f32,
    sel: i32,
    primary: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let gap = 4.0;
    for r in 0..rows {
        for c in 0..cols {
            let idx = (r * cols + c) as i32;
            let cx = x + c as f32 * (cell + gap);
            let cy = y + r as f32 * (cell + gap);
            if idx == sel {
                d.fill(
                    shade(primary, 0.3),
                    vec![
                        [cx, cy],
                        [cx + cell, cy],
                        [cx + cell, cy + cell],
                        [cx, cy + cell],
                        [cx, cy],
                    ],
                );
                d.rect_outline(primary, cx, cy, cell, cell);
            } else {
                d.rect_outline(track, cx, cy, cell, cell);
            }
        }
    }
    d
}

/// Full-screen damage / alert vignette — `intensity` ∈ [0,1] thickens the border glow.
pub fn vignette(w: f32, h: f32, intensity: f32, col: Rgba) -> Draw {
    let mut d = Draw::new();
    let t = clamp01(intensity);
    let layers = (1.0 + t * 10.0) as usize;
    for i in 0..layers {
        let inset = i as f32 * 3.0;
        let c = shade(col, t * (1.0 - i as f32 / layers as f32));
        d.rect_outline(
            c,
            inset,
            inset,
            (w - inset * 2.0).max(0.0),
            (h - inset * 2.0).max(0.0),
        );
    }
    d
}

// ════════════════════════════════════════════════════════════════════════════
// Faux-3D in 2D space  (screen-space rotate + perspective divide, no camera)
// ════════════════════════════════════════════════════════════════════════════

#[inline]
fn proj3(p: [f32; 3], spin: f32, tilt: f32, cx: f32, cy: f32, scale: f32) -> [f32; 2] {
    // rotate around Y (spin) then X (tilt), simple perspective divide
    let (cs, sn) = (spin.cos(), spin.sin());
    let (ct, st) = (tilt.cos(), tilt.sin());
    let x1 = p[0] * cs + p[2] * sn;
    let z1 = -p[0] * sn + p[2] * cs;
    let y2 = p[1] * ct - z1 * st;
    let z2 = p[1] * st + z1 * ct;
    let f = 3.0 / (3.0 + z2);
    [cx + x1 * scale * f, cy + y2 * scale * f]
}

/// Spinning wireframe ring gauge — a torus-ish band that rotates in 2D space,
/// with `frac` lighting a leading arc.
pub fn gauge3d(cx: f32, cy: f32, r: f32, frac: f32, spin: f32, fill: Rgba, track: Rgba) -> Draw {
    let mut d = Draw::new();
    let n = 48;
    let lit = (clamp01(frac) * n as f32) as usize;
    let mut prev = proj3([1.0, 0.0, 0.0], spin, 0.9, cx, cy, r);
    for i in 1..=n {
        let a = i as f32 / n as f32 * PI * 2.0;
        let p = proj3([a.cos(), 0.0, a.sin()], spin, 0.9, cx, cy, r);
        let c = if i <= lit { fill } else { track };
        d.stroke(c, vec![prev, p]);
        prev = p;
    }
    // spokes for depth cue
    for k in 0..8 {
        let a = k as f32 / 8.0 * PI * 2.0;
        let o = proj3([a.cos(), 0.0, a.sin()], spin, 0.9, cx, cy, r);
        let inn = proj3([a.cos() * 0.7, 0.0, a.sin() * 0.7], spin, 0.9, cx, cy, r);
        d.stroke(shade(track, 0.7), vec![inn, o]);
    }
    d
}

/// Extruded / isometric panel — a beveled face lifted off the surface by `depth`.
pub fn panel3d(x: f32, y: f32, w: f32, h: f32, depth: f32, primary: Rgba, bg: Rgba) -> Draw {
    let mut d = Draw::new();
    let dx = depth * 0.7;
    let dy = -depth * 0.7;
    let front = [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
    let back: Vec<[f32; 2]> = front.iter().map(|p| [p[0] + dx, p[1] + dy]).collect();
    // side faces
    d.fill(
        shade(primary, 0.18),
        vec![front[1], back[1], back[2], front[2], front[1]],
    );
    d.fill(
        shade(primary, 0.28),
        vec![front[0], back[0], back[1], front[1], front[0]],
    );
    for i in 0..4 {
        d.stroke(shade(primary, 0.6), vec![front[i], back[i]]);
    }
    d.stroke(
        primary,
        back.iter()
            .cloned()
            .chain(std::iter::once(back[0]))
            .collect(),
    );
    d.fill(
        bg,
        front
            .iter()
            .cloned()
            .chain(std::iter::once(front[0]))
            .collect(),
    );
    d.stroke(
        primary,
        front
            .iter()
            .cloned()
            .chain(std::iter::once(front[0]))
            .collect(),
    );
    d
}

/// Perspective radar dish — concentric rings tilted into the screen, with a sweep.
pub fn radar3d(
    cx: f32,
    cy: f32,
    r: f32,
    tilt: f32,
    sweep: f32,
    primary: Rgba,
    track: Rgba,
) -> Draw {
    let mut d = Draw::new();
    for k in 1..=3 {
        let rr = r * k as f32 / 3.0;
        let mut pl = Vec::new();
        for i in 0..=40 {
            let a = i as f32 / 40.0 * PI * 2.0;
            pl.push(proj3(
                [a.cos() * rr / r, 0.0, a.sin() * rr / r],
                0.0,
                tilt,
                cx,
                cy,
                r,
            ));
        }
        d.stroke(track, pl);
    }
    let o = proj3([sweep.cos(), 0.0, sweep.sin()], 0.0, tilt, cx, cy, r);
    let c = proj3([0.0, 0.0, 0.0], 0.0, tilt, cx, cy, r);
    d.stroke(primary, vec![c, o]);
    d
}

// ════════════════════════════════════════════════════════════════════════════
// Application UI rendering routines
// ════════════════════════════════════════════════════════════════════════════

use crate::dock::SplitDirection;
use crate::menu::{MenuBar, MenuPopup};
use crate::palette::CommandPalette;
use crate::property::{Toggle, ValueDrag};

/// Context menu popup container background and border outline.
pub fn menu_popup_draw(popup: &MenuPopup, bg: Rgba, border: Rgba, accent: Rgba) -> Draw {
    let mut d = Draw::new();
    d.rect_fill(bg, popup.x, popup.y, popup.width, popup.height);
    d.rect_outline(border, popup.x, popup.y, popup.width, popup.height);

    if let Some(idx) = popup.hovered_index {
        let item_y = popup.y + 8.0 + idx as f32 * 24.0;
        d.rect_fill(
            shade(accent, 0.4),
            popup.x + 2.0,
            item_y,
            popup.width - 4.0,
            22.0,
        );
    }
    d
}

/// Top application menu bar background strip and active category highlight.
pub fn menu_bar_draw(
    bar: &MenuBar,
    rect: [f32; 4],
    bg: Rgba,
    active_bg: Rgba,
    border: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let [x, y, w, h] = rect;
    d.rect_fill(bg, x, y, w, h);
    d.stroke(border, vec![[x, y + h], [x + w, y + h]]);

    if let Some(active) = bar.active_category {
        let cat_x = x + active as f32 * 64.0 + 4.0;
        d.rect_fill(active_bg, cat_x, y + 2.0, 60.0, h - 4.0);
    }
    d
}

/// Draggable splitter bar handle with grip indicators.
pub fn splitter_draw(
    rect: [f32; 4],
    direction: SplitDirection,
    is_hovered: bool,
    color: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let [x, y, w, h] = rect;
    let col = if is_hovered { shade(color, 1.4) } else { color };
    d.rect_fill(col, x, y, w, h);

    if direction == SplitDirection::Horizontal {
        let cx = x + w * 0.5;
        let cy = y + h * 0.5;
        d.stroke(shade(color, 0.5), vec![[cx, cy - 10.0], [cx, cy + 10.0]]);
    } else {
        let cx = x + w * 0.5;
        let cy = y + h * 0.5;
        d.stroke(shade(color, 0.5), vec![[cx - 10.0, cy], [cx + 10.0, cy]]);
    }
    d
}

/// Blender-style numeric scrubber box with value fill bar and outline.
pub fn value_drag_draw(
    drag: &ValueDrag,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    primary: Rgba,
    bg: Rgba,
) -> Draw {
    let mut d = Draw::new();
    d.rect_fill(bg, x, y, w, h);

    let norm = ((drag.value - drag.min) / (drag.max - drag.min).max(1e-5)).clamp(0.0, 1.0);
    let fill_w = (w - 4.0) * norm;
    if fill_w > 0.0 {
        d.rect_fill(shade(primary, 0.4), x + 2.0, y + 2.0, fill_w, h - 4.0);
    }

    let border_col = if drag.is_dragging {
        primary
    } else {
        shade(primary, 0.6)
    };
    d.rect_outline(border_col, x, y, w, h);
    d
}

/// Toggle switch box (pill track + indicator knob).
pub fn toggle_draw(
    toggle: &Toggle,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    active_color: Rgba,
    bg: Rgba,
) -> Draw {
    let mut d = Draw::new();
    let track_col = if toggle.value { active_color } else { bg };
    d.rect_fill(track_col, x, y, w, h);
    d.rect_outline(active_color, x, y, w, h);

    let knob_w = h - 4.0;
    let knob_x = if toggle.value {
        x + w - knob_w - 2.0
    } else {
        x + 2.0
    };
    d.rect_fill(0xFFFFFF, knob_x, y + 2.0, knob_w, knob_w);
    d
}

/// Command palette overlay launcher window geometry.
pub fn command_palette_draw(
    palette: &CommandPalette,
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    bg: Rgba,
    primary: Rgba,
) -> Draw {
    let mut d = Draw::new();
    if !palette.is_open {
        return d;
    }

    d.rect_fill(bg, x, y, w, h);
    d.rect_outline(primary, x, y, w, h);

    // Search header box
    d.rect_outline(shade(primary, 0.5), x + 8.0, y + 8.0, w - 16.0, 28.0);

    // Highlight selected search result item
    let filtered_count = palette.filtered_items().len();
    if filtered_count > 0 && palette.selected_index < filtered_count {
        let sel_y = y + 44.0 + palette.selected_index as f32 * 26.0;
        d.rect_fill(shade(primary, 0.3), x + 8.0, sel_y, w - 16.0, 24.0);
    }

    d
}

/// Styled tooltip card background.
pub fn tooltip_draw(x: f32, y: f32, w: f32, h: f32, bg: Rgba, border: Rgba) -> Draw {
    let mut d = Draw::new();
    d.rect_fill(bg, x, y, w, h);
    d.rect_outline(border, x, y, w, h);
    d
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bar_fill_is_proportional() {
        let empty = bar(0.0, 0.0, 100.0, 10.0, 0.0, 0xFFFFFF, 0x222222);
        let half = bar(0.0, 0.0, 100.0, 10.0, 0.5, 0xFFFFFF, 0x222222);
        assert!(empty.fills.is_empty());
        assert_eq!(half.fills.len(), 1);
        // ~half width (minus the 2px inset)
        let poly = &half.fills[0].1;
        let wpx = poly[1][0] - poly[0][0];
        assert!((wpx - 48.0).abs() < 2.0, "got {wpx}");
    }

    #[test]
    fn segbar_lights_cells() {
        let d = segbar(0.0, 0.0, 100.0, 10.0, 0.5, 10, 0xFFFFFF, 0x222222);
        assert_eq!(d.fills.len(), 5); // 5 of 10 lit
    }

    #[test]
    fn counter_renders_digits() {
        let d = counter(0.0, 0.0, 12.0, 20.0, 42, 3, 0xFFFFFF, 0x111111);
        // 3 digits × 7 segments each
        assert_eq!(d.fills.len(), 21);
    }

    #[test]
    fn mix_and_shade() {
        assert_eq!(mix(0x000000, 0xFFFFFF, 0.5) & 0xFF, 0x7F);
        assert_eq!(shade(0x808080, 2.0), 0xFFFFFF);
    }
}

← Back to index