use std::hash::{Hash, Hasher};
#[derive(Clone, Debug)]
pub struct RenderResult {
pub lines: Vec<String>,
pub hash: u64,
}
impl RenderResult {
#[must_use]
pub fn new(lines: Vec<String>) -> Self {
let hash = hash_lines(&lines);
Self { lines, hash }
}
#[must_use]
pub fn one(line: impl Into<String>) -> Self {
Self::new(vec![line.into()])
}
#[must_use]
pub fn empty() -> Self {
Self::new(Vec::new())
}
}
impl Default for RenderResult {
fn default() -> Self {
Self::empty()
}
}
fn hash_lines(lines: &[String]) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
lines.len().hash(&mut hasher);
for line in lines {
line.hash(&mut hasher);
}
hasher.finish()
}
pub trait Component: Send {
fn render(&self, width: u16) -> RenderResult;
fn revision(&self) -> u64 {
0
}
fn live_region(&self) -> LiveRegion {
LiveRegion::None
}
fn invalidate(&mut self) {}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LiveRegion {
#[default]
None,
Mutable {
start: usize,
},
Pinned {
start: usize,
},
}
impl LiveRegion {
#[must_use]
pub fn start(&self) -> Option<usize> {
match self {
LiveRegion::None => None,
LiveRegion::Mutable { start } | LiveRegion::Pinned { start } => Some(*start),
}
}
#[must_use]
pub fn is_pinned(&self) -> bool {
matches!(self, LiveRegion::Pinned { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TextComponent {
text: String,
}
impl Component for TextComponent {
fn render(&self, _width: u16) -> RenderResult {
RenderResult::new(vec![self.text.clone()])
}
}
#[test]
fn render_result_hash_stability() {
let r1 = RenderResult::new(vec!["hello".into(), "world".into()]);
let r2 = RenderResult::new(vec!["hello".into(), "world".into()]);
assert_eq!(r1.hash, r2.hash);
}
#[test]
fn render_result_hash_differs_on_change() {
let r1 = RenderResult::new(vec!["hello".into()]);
let r2 = RenderResult::new(vec!["world".into()]);
assert_ne!(r1.hash, r2.hash);
}
#[test]
fn render_result_hash_differs_on_length() {
let r1 = RenderResult::new(vec!["hello".into()]);
let r2 = RenderResult::new(vec!["hello".into(), "world".into()]);
assert_ne!(r1.hash, r2.hash);
}
#[test]
fn text_component_renders() {
let c = TextComponent { text: "hi".into() };
let r = c.render(80);
assert_eq!(r.lines, vec!["hi"]);
}
#[test]
fn default_live_region_is_none() {
let c = TextComponent { text: "hi".into() };
assert_eq!(c.live_region(), LiveRegion::None);
}
#[test]
fn live_region_start() {
assert_eq!(LiveRegion::None.start(), None);
assert_eq!(LiveRegion::Mutable { start: 5 }.start(), Some(5));
assert_eq!(LiveRegion::Pinned { start: 3 }.start(), Some(3));
}
#[test]
fn live_region_is_pinned() {
assert!(!LiveRegion::None.is_pinned());
assert!(!LiveRegion::Mutable { start: 0 }.is_pinned());
assert!(LiveRegion::Pinned { start: 0 }.is_pinned());
}
}