#![forbid(unsafe_code)]
use crate::borders::{BorderSet, BorderType};
use crate::sparkline::Sparkline;
use crate::{Widget, apply_style, clear_text_area, draw_text_span};
use ftui_core::geometry::Rect;
use ftui_render::cell::{Cell, PackedRgba};
use ftui_render::frame::Frame;
use ftui_runtime::transparency::TrafficLight;
use ftui_runtime::unified_evidence::DecisionDomain;
use ftui_style::Style;
const ZONE_GREEN: PackedRgba = PackedRgba::rgb(0, 180, 0);
const ZONE_YELLOW: PackedRgba = PackedRgba::rgb(200, 180, 0);
const ZONE_RED: PackedRgba = PackedRgba::rgb(200, 50, 50);
const FALLBACK_FG: PackedRgba = PackedRgba::rgb(255, 80, 80);
const FALLBACK_BG: PackedRgba = PackedRgba::rgb(80, 10, 10);
const REGIME_FG: PackedRgba = PackedRgba::rgb(255, 200, 100);
const DIM_FG: PackedRgba = PackedRgba::rgb(120, 120, 120);
const LABEL_FG: PackedRgba = PackedRgba::rgb(160, 180, 200);
#[derive(Debug, Clone, Copy)]
pub struct DomainSnapshot {
pub domain: DecisionDomain,
pub confidence: f64,
pub signal: TrafficLight,
pub in_fallback: bool,
pub regime_label: &'static str,
}
#[derive(Debug, Clone)]
pub struct DriftSnapshot {
pub domains: Vec<DomainSnapshot>,
pub frame_id: u64,
}
#[derive(Debug, Clone)]
pub struct DriftTimeline {
snapshots: Vec<DriftSnapshot>,
write_pos: usize,
len: usize,
capacity: usize,
}
impl DriftTimeline {
#[must_use]
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(1);
Self {
snapshots: Vec::with_capacity(capacity),
write_pos: 0,
len: 0,
capacity,
}
}
pub fn push(&mut self, snapshot: DriftSnapshot) {
if self.snapshots.len() < self.capacity {
self.snapshots.push(snapshot);
} else {
self.snapshots[self.write_pos] = snapshot;
}
self.write_pos = (self.write_pos + 1) % self.capacity;
self.len = (self.len + 1).min(self.capacity);
}
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn iter_chronological(&self) -> impl Iterator<Item = &DriftSnapshot> {
let start = if self.len < self.capacity {
0
} else {
self.write_pos
};
(0..self.len).map(move |i| {
let idx = (start + i) % self.capacity;
&self.snapshots[idx]
})
}
pub fn confidence_series(&self, domain: DecisionDomain) -> Vec<f64> {
self.iter_chronological()
.map(|snap| {
snap.domains
.iter()
.find(|d| d.domain == domain)
.map_or(0.0, |d| d.confidence)
})
.collect()
}
pub fn last_fallback_trigger(&self, domain: DecisionDomain) -> Option<usize> {
let series: Vec<bool> = self
.iter_chronological()
.map(|snap| {
snap.domains
.iter()
.find(|d| d.domain == domain)
.is_some_and(|d| d.in_fallback)
})
.collect();
if series.first().copied().unwrap_or(false) {
return Some(0);
}
(1..series.len())
.rev()
.find(|&i| series[i] && !series[i - 1])
}
#[must_use]
pub fn latest(&self) -> Option<&DriftSnapshot> {
if self.len == 0 {
return None;
}
let idx = if self.write_pos == 0 {
self.capacity - 1
} else {
self.write_pos - 1
};
self.snapshots.get(idx)
}
}
#[derive(Debug, Clone)]
pub struct DriftVisualization<'a> {
timeline: &'a DriftTimeline,
domains: Option<Vec<DecisionDomain>>,
border_type: BorderType,
style: Style,
show_regime_banner: bool,
fallback_threshold: f64,
caution_threshold: f64,
}
impl<'a> DriftVisualization<'a> {
#[must_use]
pub fn new(timeline: &'a DriftTimeline) -> Self {
Self {
timeline,
domains: None,
border_type: BorderType::Rounded,
style: Style::default(),
show_regime_banner: true,
fallback_threshold: 0.3,
caution_threshold: 0.7,
}
}
#[must_use]
pub fn domains(mut self, domains: Vec<DecisionDomain>) -> Self {
self.domains = Some(domains);
self
}
#[must_use]
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = border_type;
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn show_regime_banner(mut self, show: bool) -> Self {
self.show_regime_banner = show;
self
}
#[must_use]
pub fn fallback_threshold(mut self, t: f64) -> Self {
self.fallback_threshold = t;
self
}
#[must_use]
pub fn caution_threshold(mut self, t: f64) -> Self {
self.caution_threshold = t;
self
}
fn active_domains(&self) -> Vec<DecisionDomain> {
if let Some(ref domains) = self.domains {
return domains.clone();
}
if let Some(latest) = self.timeline.latest() {
latest.domains.iter().map(|d| d.domain).collect()
} else {
Vec::new()
}
}
fn confidence_color(&self, confidence: f64) -> PackedRgba {
let fallback = self.fallback_threshold.clamp(0.0, 1.0);
let caution = self.caution_threshold.clamp(fallback, 1.0);
if confidence >= caution {
ZONE_GREEN
} else if caution > fallback && confidence >= fallback {
let t = (confidence - fallback) / (caution - fallback);
lerp_color(ZONE_YELLOW, ZONE_GREEN, t)
} else {
let t = if fallback <= f64::EPSILON {
0.0
} else {
confidence / fallback
};
lerp_color(ZONE_RED, ZONE_YELLOW, t)
}
}
#[must_use]
pub fn min_height(&self) -> u16 {
let domains = self.active_domains();
let domain_rows = domains.len() as u16;
let mut h: u16 = 2; h += 1; h += domain_rows * 2; if self.show_regime_banner {
h += 1;
}
h
}
fn render_domain_row(
&self,
domain: DecisionDomain,
x: u16,
y: u16,
width: u16,
frame: &mut Frame,
) -> u16 {
let deg = frame.buffer.degradation;
let apply_styling = deg.apply_styling();
let max_x = x + width;
let label = domain.as_str();
let label_style = if apply_styling {
Style::new().fg(LABEL_FG)
} else {
Style::default()
};
let mut cx = draw_text_span(frame, x, y, label, label_style, max_x);
if let Some(latest) = self.timeline.latest()
&& let Some(ds) = latest.domains.iter().find(|d| d.domain == domain)
{
let conf_pct = format!(" {:.0}%", ds.confidence * 100.0);
let conf_color = self.confidence_color(ds.confidence);
let conf_style = if apply_styling {
Style::new().fg(conf_color).bold()
} else {
Style::default()
};
if cx < max_x {
cx = draw_text_span(frame, cx, y, " ", Style::default(), max_x);
}
cx = draw_text_span(frame, cx, y, &conf_pct, conf_style, max_x);
if ds.in_fallback {
let fb_style = if apply_styling {
Style::new().fg(FALLBACK_FG).bg(FALLBACK_BG).bold()
} else {
Style::default()
};
if cx < max_x {
cx = draw_text_span(frame, cx, y, " ", Style::default(), max_x);
}
cx = draw_text_span(frame, cx, y, " FALLBACK ", fb_style, max_x);
}
let _ = cx;
}
let series = self.timeline.confidence_series(domain);
if !series.is_empty() {
let sparkline_width = width.min(series.len() as u16);
let start = series.len().saturating_sub(sparkline_width as usize);
let visible = &series[start..];
let sparkline = Sparkline::new(visible)
.bounds(0.0, 1.0)
.gradient(ZONE_RED, ZONE_GREEN);
let spark_area = Rect::new(x, y + 1, sparkline_width, 1);
sparkline.render(spark_area, frame);
if let Some(trigger_idx) = self.timeline.last_fallback_trigger(domain) {
let visible_start = series.len().saturating_sub(sparkline_width as usize);
if trigger_idx >= visible_start {
let marker_x = x + (trigger_idx - visible_start) as u16;
if marker_x < max_x {
let mut cell = Cell::from_char('|');
if apply_styling {
apply_style(&mut cell, Style::new().fg(FALLBACK_FG).bold());
}
frame.buffer.set_fast(marker_x, y + 1, cell);
}
}
}
}
y + 2 }
fn render_regime_banner(&self, x: u16, y: u16, max_x: u16, frame: &mut Frame) {
let Some(latest) = self.timeline.latest() else {
return;
};
let apply_styling = frame.buffer.degradation.apply_styling();
let fallback_domain = latest.domains.iter().find(|d| d.in_fallback);
if let Some(ds) = fallback_domain {
let banner = format!(
" REGIME: {} -> deterministic ({}) ",
ds.domain.as_str(),
ds.regime_label,
);
let style = if apply_styling {
Style::new().fg(REGIME_FG).bg(FALLBACK_BG).bold()
} else {
Style::default()
};
draw_text_span(frame, x, y, &banner, style, max_x);
} else {
let style = if apply_styling {
Style::new().fg(DIM_FG)
} else {
Style::default()
};
draw_text_span(frame, x, y, "All domains: Bayesian (normal)", style, max_x);
}
}
}
impl Widget for DriftVisualization<'_> {
fn render(&self, area: Rect, frame: &mut Frame) {
if area.is_empty() {
return;
}
if area.width < 6 || area.height < 4 {
clear_text_area(frame, area, Style::default());
return;
}
let deg = frame.buffer.degradation;
if !deg.render_content() {
clear_text_area(frame, area, Style::default());
return;
}
let base_style = if deg.apply_styling() {
self.style
} else {
Style::default()
};
clear_text_area(frame, area, base_style);
if deg.render_decorative() {
let set = if deg.use_unicode_borders() {
self.border_type.to_border_set()
} else {
BorderSet::ASCII
};
let border_style = if deg.apply_styling() {
Style::new().fg(LABEL_FG)
} else {
Style::default()
};
render_border(area, frame, set, border_style);
}
let inner_x = area.x.saturating_add(1);
let inner_max_x = area.right().saturating_sub(1);
let inner_width = inner_max_x.saturating_sub(inner_x);
let mut y = area.y.saturating_add(1);
let max_y = area.bottom().saturating_sub(1);
if inner_width < 4 || y >= max_y {
return;
}
let title_style = if deg.apply_styling() {
Style::new().fg(LABEL_FG).bold()
} else {
Style::default()
};
draw_text_span(frame, inner_x, y, "Drift Monitor", title_style, inner_max_x);
y += 1;
let domains = self.active_domains();
for domain in &domains {
if y + 1 >= max_y {
break;
}
y = self.render_domain_row(*domain, inner_x, y, inner_width, frame);
}
if self.show_regime_banner && y < max_y {
self.render_regime_banner(inner_x, y, inner_max_x, frame);
}
}
fn is_essential(&self) -> bool {
false
}
}
fn lerp_color(a: PackedRgba, b: PackedRgba, t: f64) -> PackedRgba {
let t = t.clamp(0.0, 1.0) as f32;
let r = (a.r() as f32 * (1.0 - t) + b.r() as f32 * t).round() as u8;
let g = (a.g() as f32 * (1.0 - t) + b.g() as f32 * t).round() as u8;
let b_val = (a.b() as f32 * (1.0 - t) + b.b() as f32 * t).round() as u8;
PackedRgba::rgb(r, g, b_val)
}
fn render_border(area: Rect, frame: &mut Frame, set: BorderSet, style: Style) {
let border_cell = |c: char| -> Cell {
let mut cell = Cell::from_char(c);
apply_style(&mut cell, style);
cell
};
let right_x = area.right().saturating_sub(1);
let bottom_y = area.bottom().saturating_sub(1);
for x in area.x..area.right() {
frame
.buffer
.set_fast(x, area.y, border_cell(set.horizontal));
frame
.buffer
.set_fast(x, bottom_y, border_cell(set.horizontal));
}
for y in area.y..area.bottom() {
frame.buffer.set_fast(area.x, y, border_cell(set.vertical));
frame.buffer.set_fast(right_x, y, border_cell(set.vertical));
}
frame
.buffer
.set_fast(area.x, area.y, border_cell(set.top_left));
frame
.buffer
.set_fast(right_x, area.y, border_cell(set.top_right));
frame
.buffer
.set_fast(area.x, bottom_y, border_cell(set.bottom_left));
frame
.buffer
.set_fast(right_x, bottom_y, border_cell(set.bottom_right));
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::budget::DegradationLevel;
use ftui_render::cell::Cell;
use ftui_render::grapheme_pool::GraphemePool;
fn make_snapshot(frame_id: u64, confidence: f64, in_fallback: bool) -> DriftSnapshot {
DriftSnapshot {
domains: vec![
DomainSnapshot {
domain: DecisionDomain::DiffStrategy,
confidence,
signal: if confidence >= 0.7 {
TrafficLight::Green
} else if confidence >= 0.3 {
TrafficLight::Yellow
} else {
TrafficLight::Red
},
in_fallback,
regime_label: if in_fallback {
"deterministic"
} else {
"bayesian"
},
},
DomainSnapshot {
domain: DecisionDomain::ResizeCoalescing,
confidence: confidence * 0.9,
signal: TrafficLight::Green,
in_fallback: false,
regime_label: "bayesian",
},
],
frame_id,
}
}
fn make_drift_timeline() -> DriftTimeline {
let mut tl = DriftTimeline::new(60);
for i in 0..30 {
tl.push(make_snapshot(i, 0.85, false));
}
for i in 30..40 {
let conf = 0.85 - (i - 30) as f64 * 0.07;
tl.push(make_snapshot(i, conf, false));
}
for i in 40..50 {
tl.push(make_snapshot(i, 0.15, true));
}
for i in 50..60 {
let conf = 0.15 + (i - 50) as f64 * 0.07;
tl.push(make_snapshot(i, conf, false));
}
tl
}
#[test]
fn timeline_push_and_len() {
let mut tl = DriftTimeline::new(10);
assert!(tl.is_empty());
assert_eq!(tl.len(), 0);
tl.push(make_snapshot(0, 0.8, false));
assert_eq!(tl.len(), 1);
assert!(!tl.is_empty());
}
#[test]
fn timeline_wraps_at_capacity() {
let mut tl = DriftTimeline::new(5);
for i in 0..10 {
tl.push(make_snapshot(i, 0.5, false));
}
assert_eq!(tl.len(), 5);
assert_eq!(tl.latest().unwrap().frame_id, 9);
}
#[test]
fn timeline_chronological_order() {
let mut tl = DriftTimeline::new(5);
for i in 0..8 {
tl.push(make_snapshot(i, 0.5, false));
}
let ids: Vec<u64> = tl.iter_chronological().map(|s| s.frame_id).collect();
assert_eq!(ids, vec![3, 4, 5, 6, 7]);
}
#[test]
fn confidence_series_extraction() {
let tl = make_drift_timeline();
let series = tl.confidence_series(DecisionDomain::DiffStrategy);
assert_eq!(series.len(), 60);
assert!((series[0] - 0.85).abs() < 0.01);
assert!((series[40] - 0.15).abs() < 0.01);
}
#[test]
fn fallback_trigger_detection() {
let tl = make_drift_timeline();
let trigger = tl.last_fallback_trigger(DecisionDomain::DiffStrategy);
assert_eq!(trigger, Some(40));
}
#[test]
fn no_fallback_trigger_when_none() {
let mut tl = DriftTimeline::new(10);
for i in 0..10 {
tl.push(make_snapshot(i, 0.8, false));
}
assert!(
tl.last_fallback_trigger(DecisionDomain::DiffStrategy)
.is_none()
);
}
#[test]
fn fallback_trigger_at_start_of_visible_timeline() {
let mut tl = DriftTimeline::new(5);
for i in 0..5 {
tl.push(make_snapshot(i, 0.15, true));
}
assert_eq!(
tl.last_fallback_trigger(DecisionDomain::DiffStrategy),
Some(0)
);
}
#[test]
fn render_empty_timeline() {
let tl = DriftTimeline::new(60);
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 24, &mut pool);
viz.render(Rect::new(0, 0, 80, 24), &mut frame);
}
#[test]
fn render_populated_timeline() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 20, &mut pool);
viz.render(Rect::new(0, 0, 80, 20), &mut frame);
let mut found_title = false;
for x in 0..80 {
if let Some(cell) = frame.buffer.get(x, 1)
&& cell.content.as_char() == Some('D')
{
found_title = true;
break;
}
}
assert!(found_title, "should render title row");
}
#[test]
fn render_no_styling_drops_border_and_label_styles() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 20, &mut pool);
frame.buffer.degradation = DegradationLevel::NoStyling;
viz.render(Rect::new(0, 0, 80, 20), &mut frame);
let border = frame.buffer.get(0, 0).unwrap();
let border_default = Cell::from_char(border.content.as_char().unwrap());
assert_eq!(border.fg, border_default.fg);
assert_eq!(border.bg, border_default.bg);
assert_eq!(border.attrs, border_default.attrs);
let title = frame.buffer.get(1, 1).unwrap();
let title_default = Cell::from_char('D');
assert_eq!(title.content.as_char(), Some('D'));
assert_eq!(title.fg, title_default.fg);
assert_eq!(title.bg, title_default.bg);
assert_eq!(title.attrs, title_default.attrs);
let label = frame.buffer.get(1, 2).unwrap();
let label_default = Cell::from_char('d');
assert_eq!(label.content.as_char(), Some('d'));
assert_eq!(label.fg, label_default.fg);
assert_eq!(label.bg, label_default.bg);
assert_eq!(label.attrs, label_default.attrs);
}
#[test]
fn render_shows_fallback_indicator() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl).domains(vec![DecisionDomain::DiffStrategy]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 12, &mut pool);
viz.render(Rect::new(0, 0, 80, 12), &mut frame);
}
#[test]
fn render_regime_banner_in_fallback() {
let mut tl = DriftTimeline::new(10);
for i in 0..10 {
tl.push(make_snapshot(i, 0.15, true));
}
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 12, &mut pool);
viz.render(Rect::new(0, 0, 80, 12), &mut frame);
let mut found_regime = false;
for y in 0..12 {
let mut row = String::new();
for x in 0..80 {
if let Some(cell) = frame.buffer.get(x, y)
&& let Some(ch) = cell.content.as_char()
{
row.push(ch);
}
}
if row.contains("REGIME") {
found_regime = true;
break;
}
}
assert!(found_regime, "should show regime banner when in fallback");
}
#[test]
fn render_regime_banner_normal() {
let mut tl = DriftTimeline::new(10);
for i in 0..10 {
tl.push(make_snapshot(i, 0.85, false));
}
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 12, &mut pool);
viz.render(Rect::new(0, 0, 80, 12), &mut frame);
let mut found_normal = false;
for y in 0..12 {
let mut row = String::new();
for x in 0..80 {
if let Some(cell) = frame.buffer.get(x, y)
&& let Some(ch) = cell.content.as_char()
{
row.push(ch);
}
}
if row.contains("Bayesian") {
found_normal = true;
break;
}
}
assert!(found_normal, "should show normal regime banner");
}
#[test]
fn tiny_area_no_panic() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 3, &mut pool);
viz.render(Rect::new(0, 0, 5, 3), &mut frame);
}
#[test]
fn min_height_calculation() {
let mut tl = DriftTimeline::new(5);
tl.push(make_snapshot(0, 0.8, false)); let viz = DriftVisualization::new(&tl);
assert_eq!(viz.min_height(), 8);
}
#[test]
fn min_height_no_banner() {
let mut tl = DriftTimeline::new(5);
tl.push(make_snapshot(0, 0.8, false));
let viz = DriftVisualization::new(&tl).show_regime_banner(false);
assert_eq!(viz.min_height(), 7);
}
#[test]
fn confidence_color_zones() {
let tl = DriftTimeline::new(1);
let viz = DriftVisualization::new(&tl);
let green = viz.confidence_color(0.9);
assert_eq!(green, ZONE_GREEN);
let red_ish = viz.confidence_color(0.1);
assert!(red_ish.r() > 100);
assert!(red_ish.g() < 100);
}
#[test]
fn confidence_color_handles_degenerate_thresholds() {
let tl = DriftTimeline::new(1);
let viz = DriftVisualization::new(&tl)
.fallback_threshold(0.0)
.caution_threshold(0.0);
assert_eq!(viz.confidence_color(0.0), ZONE_GREEN);
let low = viz.confidence_color(-1.0);
assert!(low.r() >= ZONE_RED.r());
}
#[test]
fn builder_chain() {
let tl = DriftTimeline::new(10);
let viz = DriftVisualization::new(&tl)
.border_type(BorderType::Double)
.style(Style::new().bg(PackedRgba::rgb(10, 10, 10)))
.show_regime_banner(false)
.fallback_threshold(0.2)
.caution_threshold(0.8)
.domains(vec![DecisionDomain::DiffStrategy]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 24, &mut pool);
viz.render(Rect::new(0, 0, 80, 24), &mut frame);
}
#[test]
fn is_not_essential() {
let tl = DriftTimeline::new(1);
let viz = DriftVisualization::new(&tl);
assert!(!viz.is_essential());
}
#[test]
fn lerp_color_endpoints() {
let a = PackedRgba::rgb(0, 0, 0);
let b = PackedRgba::rgb(255, 255, 255);
assert_eq!(lerp_color(a, b, 0.0), a);
assert_eq!(lerp_color(a, b, 1.0), b);
}
#[test]
fn lerp_color_clamps() {
let a = PackedRgba::rgb(0, 0, 0);
let b = PackedRgba::rgb(255, 255, 255);
assert_eq!(lerp_color(a, b, -1.0), a);
assert_eq!(lerp_color(a, b, 2.0), b);
}
#[test]
fn render_with_single_domain_filter() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl).domains(vec![DecisionDomain::DiffStrategy]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 10, &mut pool);
viz.render(Rect::new(0, 0, 80, 10), &mut frame);
let mut found_diff = false;
for y in 0..10 {
let mut row = String::new();
for x in 0..80 {
if let Some(cell) = frame.buffer.get(x, y)
&& let Some(ch) = cell.content.as_char()
{
row.push(ch);
}
}
if row.contains("diff_strategy") {
found_diff = true;
break;
}
}
assert!(found_diff, "should show DiffStrategy domain");
}
#[test]
fn render_fallback_badge_on_label_row() {
let mut tl = DriftTimeline::new(5);
for i in 0..5 {
tl.push(make_snapshot(i, 0.1, true));
}
let viz = DriftVisualization::new(&tl).domains(vec![DecisionDomain::DiffStrategy]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 10, &mut pool);
viz.render(Rect::new(0, 0, 80, 10), &mut frame);
let mut found_fallback = false;
for y in 0..10 {
let mut row = String::new();
for x in 0..80 {
if let Some(cell) = frame.buffer.get(x, y)
&& let Some(ch) = cell.content.as_char()
{
row.push(ch);
}
}
if row.contains("FALLBACK") {
found_fallback = true;
break;
}
}
assert!(
found_fallback,
"should show FALLBACK badge when in fallback"
);
}
#[test]
fn render_clears_gap_before_confidence_badge() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl).domains(vec![DecisionDomain::DiffStrategy]);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 10, &mut pool);
frame.buffer.set_fast(14, 2, Cell::from_char('X'));
viz.render(Rect::new(0, 0, 80, 10), &mut frame);
assert_eq!(
frame.buffer.get(14, 2).unwrap().content.as_char(),
Some(' ')
);
}
#[test]
fn render_skeleton_clears_previous_visualization() {
let tl = make_drift_timeline();
let viz = DriftVisualization::new(&tl).show_regime_banner(false);
let area = Rect::new(0, 0, 80, 10);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 10, &mut pool);
viz.render(area, &mut frame);
frame.buffer.degradation = DegradationLevel::Skeleton;
viz.render(area, &mut frame);
for y in 0..area.height {
for x in 0..area.width {
assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some(' '));
}
}
}
#[test]
fn render_with_fewer_domains_clears_stale_rows() {
let tl = make_drift_timeline();
let full = DriftVisualization::new(&tl).show_regime_banner(false);
let filtered = DriftVisualization::new(&tl)
.domains(vec![DecisionDomain::DiffStrategy])
.show_regime_banner(false);
let area = Rect::new(0, 0, 80, 10);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(80, 10, &mut pool);
full.render(area, &mut frame);
filtered.render(area, &mut frame);
for y in 4..6u16 {
for x in 1..area.width.saturating_sub(1) {
assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some(' '));
}
}
}
}