uzor-text 1.5.1

Pretext-pattern text layout engine for uzor — greedy word-wrap, rich multi-run spans, InlineBox, and a mixed-run baseline pass over the cosmic-text shaper (Arc 2 Phase 1+2; kinetics/ascii/hyphenation land in later phases).
//! Dashboard tiles: label, value, delta, tofu sparkline.

use super::{empty, ink, tofu, Play};
use crate::ascii::{Cell, CellShader, Coord, Cursor, GridContext};

#[derive(Clone, Copy, Debug)]
pub struct KpiTile {
    pub label: &'static str,
    pub value: &'static str,
    pub delta: &'static str,
    pub up: bool,
    pub spark: &'static [f64],
}

pub struct Kpi<'a> {
    pub tiles: &'a [KpiTile],
    pub play: Play,
}

impl CellShader for Kpi<'_> {
    fn main(&self, coord: Coord, ctx: &GridContext, cursor: &Cursor) -> Cell {
        if self.tiles.is_empty() || ctx.cols < 10 || ctx.rows < 5 {
            return empty();
        }
        let n = self.tiles.len();
        let tw = (ctx.cols / n).max(4);
        let i = coord.x / tw;
        if i >= n {
            return empty();
        }
        let tile = self.tiles[i];
        let lx = coord.x - i * tw;
        if lx == 0 || lx + 1 == tw {
            return empty();
        }
        let hover = cursor.inside && (cursor.x as usize) / tw == i;
        let _ = self.play.motion(ctx.time, cursor.intensity);
        if coord.y == 1 {
            return glyph(tile.label, lx.saturating_sub(1), hover, 200.0, 0.45);
        }
        if coord.y == 2 {
            return glyph(tile.value, lx.saturating_sub(1), hover, 160.0, 0.62);
        }
        if coord.y == 3 {
            return glyph(
                tile.delta,
                lx.saturating_sub(1),
                hover,
                if tile.up { 140.0 } else { 8.0 },
                0.5,
            );
        }
        if coord.y + 2 >= ctx.rows || tile.spark.len() < 2 {
            return empty();
        }
        let inner = tw.saturating_sub(2).max(1);
        let col = lx.saturating_sub(1);
        let si = (col * (tile.spark.len() - 1)) / inner;
        let (lo, hi) = min_max(tile.spark);
        let y = tile.spark[si];
        let u = ((y - lo) / (hi - lo).max(1e-6)).clamp(0.0, 1.0);
        let spark_rows = ctx.rows.saturating_sub(5).max(1);
        let row = coord.y.saturating_sub(4);
        if row >= spark_rows {
            return empty();
        }
        let want = ((1.0 - u) * (spark_rows.saturating_sub(1) as f64)).round() as usize;
        if row != want {
            return empty();
        }
        tofu(hover, 175.0, 0.45)
    }
}

fn glyph(s: &str, i: usize, hover: bool, hue: f64, lit: f64) -> Cell {
    match s.chars().nth(i) {
        Some(ch) => ink(ch, hover, hue, lit),
        None => empty(),
    }
}

fn min_max(ys: &[f64]) -> (f64, f64) {
    let mut lo = f64::INFINITY;
    let mut hi = f64::NEG_INFINITY;
    for &y in ys {
        lo = lo.min(y);
        hi = hi.max(y);
    }
    if !lo.is_finite() {
        (0.0, 1.0)
    } else {
        (lo, hi)
    }
}