flowrs-engine/crates/dpb-plot/src/chrome.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

235 lines
6.8 KiB
Rust

//! The chrome as data. Axes, tick labels, gridline positions, the readout,
//! markers' and spans' places and the direct-label anchors are handed to the
//! host, which draws them in the DOM: crisp at any zoom, themed by the page's
//! tokens, and read by assistive technology. Text wears the page's ink
//! tokens; a series' colour only marks its key (a short line beside its
//! name).
//!
//! Positions are CSS pixels from the plot area's top-left corner. Every
//! list keeps its capacity, so a frame writes the chrome without allocating;
//! `labels_changed` tells the host when text (not just positions) changed.
use crate::annot::Tint;
use crate::label::Label;
use crate::layout::Rect;
use crate::view::Mode;
use alloc::vec::Vec;
/// Most ticks an axis carries.
pub const MAX_TICKS: usize = 32;
/// One tick: where, and its label. Gridlines go at the same positions.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Tick {
/// Position along the axis (x for the time axis, y for a lane), CSS px.
pub pos: f32,
/// The label (in the axis's unit, prefix included in `Axis::unit`).
pub label: Label,
}
/// An axis.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Axis {
/// Ticks, in order.
pub ticks: Vec<Tick>,
/// The unit with its prefix ("mV", "ms", "m:ss").
pub unit: Label,
/// Whether any label or the unit changed since the last frame.
pub labels_changed: bool,
}
impl Axis {
/// An axis with room for [`MAX_TICKS`].
pub fn new() -> Axis {
Axis {
ticks: Vec::with_capacity(MAX_TICKS),
unit: Label::new(),
labels_changed: true,
}
}
/// Starts writing a frame's ticks.
pub fn begin(&mut self) -> AxisWriter<'_> {
AxisWriter {
axis: self,
n: 0,
changed: false,
}
}
}
/// Writes an axis's ticks in place, noting whether any label changed.
pub struct AxisWriter<'a> {
axis: &'a mut Axis,
n: usize,
changed: bool,
}
impl AxisWriter<'_> {
/// Adds a tick; `write` fills its label.
pub fn tick(&mut self, pos: f32, write: impl FnOnce(&mut Label)) {
if self.n >= MAX_TICKS {
return;
}
let mut l = Label::new();
write(&mut l);
if self.n < self.axis.ticks.len() {
let t = &mut self.axis.ticks[self.n];
if t.label != l {
self.changed = true;
t.label = l;
}
t.pos = pos;
} else {
self.axis.ticks.push(Tick { pos, label: l });
self.changed = true;
}
self.n += 1;
}
/// Sets the unit and finishes.
pub fn finish(self, unit: &Label) {
let changed = self.changed || self.n != self.axis.ticks.len() || self.axis.unit != *unit;
self.axis.ticks.truncate(self.n);
self.axis.unit = *unit;
self.axis.labels_changed = changed;
}
}
/// A lane's chrome.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct LaneChrome {
/// Where the lane is.
pub rect: Rect,
/// Its vertical axis (positions are y).
pub y: Axis,
/// Its range this frame.
pub lo: f64,
/// Its range this frame.
pub hi: f64,
/// The y of zero, if zero is in range (a baseline).
pub zero: Option<f32>,
}
/// One series at the cursor: the value first, the name after.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct ReadoutRow {
/// Lane.
pub lane: u16,
/// Series in the lane.
pub series: u16,
/// Its colour slot (for its key).
pub slot: u8,
/// The value at the cursor's sample ("—" when there is none).
pub value: Label,
/// The unit with prefix.
pub unit: Label,
/// Where the value is on screen (CSS px), NaN when none.
pub y: f32,
/// The value as a number.
pub v: f64,
/// When the cursor's pixel column holds more than one sample: their
/// least and greatest (empty labels otherwise).
pub col_min: Label,
/// See `col_min`.
pub col_max: Label,
}
/// The crosshair and its readout.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Readout {
/// Whether there is a cursor.
pub visible: bool,
/// Its x (CSS px), snapped to the focused lane's nearest sample.
pub x: f32,
/// Its page time.
pub t: f64,
/// Its time, written on the time axis's terms with finer decimals.
pub time: Label,
/// The time's unit.
pub time_unit: Label,
/// The lane the cursor snaps to.
pub focus_lane: u16,
/// Every series of every lane at that time.
pub rows: Vec<ReadoutRow>,
}
/// A marker or span on screen.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MarkChrome {
/// x of a marker, or a span's left edge (CSS px).
pub x0: f32,
/// A span's right edge (equal to `x0` for a marker).
pub x1: f32,
/// The host's id.
pub id: u32,
/// Its tint.
pub tint: Tint,
}
/// Where a series' newest visible value is, for a direct label.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct EndLabel {
/// Lane.
pub lane: u16,
/// Series in the lane.
pub series: u16,
/// y (CSS px), NaN when nothing is visible.
pub y: f32,
}
/// Everything the host draws around and over the canvas.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Chrome {
/// Frames made.
pub frame: u64,
/// The mode.
pub mode: Mode,
/// The window shown (page times).
pub window: (f64, f64),
/// The time axis (positions are x).
pub x: Axis,
/// The lanes, top to bottom.
pub lanes: Vec<LaneChrome>,
/// The cursor's readout.
pub readout: Readout,
/// Markers on screen.
pub markers: Vec<MarkChrome>,
/// Spans on screen.
pub spans: Vec<MarkChrome>,
/// Direct-label anchors.
pub ends: Vec<EndLabel>,
/// The scope's trigger x (CSS px) when one is shown.
pub trigger_x: Option<f32>,
/// The trigger's level as y in its lane (CSS px).
pub trigger_y: Option<f32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writing_notes_label_changes() {
let mut a = Axis::new();
let mut w = a.begin();
w.tick(1.0, |l| l.push_str("0"));
w.tick(2.0, |l| l.push_str("5"));
w.finish(&Label::of("s"));
assert!(a.labels_changed);
let cap = a.ticks.capacity();
// Same labels, new positions: only positions change.
let mut w = a.begin();
w.tick(1.5, |l| l.push_str("0"));
w.tick(2.5, |l| l.push_str("5"));
w.finish(&Label::of("s"));
assert!(!a.labels_changed);
assert_eq!(a.ticks[1].pos, 2.5);
let mut w = a.begin();
w.tick(1.5, |l| l.push_str("0"));
w.finish(&Label::of("s"));
assert!(a.labels_changed);
assert_eq!(a.ticks.len(), 1);
assert_eq!(a.ticks.capacity(), cap);
}
}