//! 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, /// 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, } /// 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, } /// 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, /// The cursor's readout. pub readout: Readout, /// Markers on screen. pub markers: Vec, /// Spans on screen. pub spans: Vec, /// Direct-label anchors. pub ends: Vec, /// The scope's trigger x (CSS px) when one is shown. pub trigger_x: Option, /// The trigger's level as y in its lane (CSS px). pub trigger_y: Option, } #[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); } }