use eframe::egui::{
self,
text::{LayoutJob, TextFormat},
};
pub(crate) const VISIBLE_SECS: f64 = 30.0; pub(crate) const FADE_SECS: f64 = 1.0; pub(crate) const CUTOFF_SECS: f64 = VISIBLE_SECS + FADE_SECS;
pub(crate) struct Glyph {
pub ch: char,
pub birth: f64, pub end_offset: usize, }
pub(crate) fn fade_factor(age: f64) -> f32 {
if age <= VISIBLE_SECS {
1.0
} else if age >= CUTOFF_SECS {
0.0
} else {
(1.0 - (age - VISIBLE_SECS) / FADE_SECS) as f32
}
}
pub(crate) fn quantize_alpha(factor: f32) -> u8 {
(factor.clamp(0.0, 1.0) * 255.0).round() as u8
}
pub(crate) const PRIMARY: egui::Color32 = egui::Color32::from_rgb(154, 220, 154);
pub(crate) const PRIMARY_DARKER: egui::Color32 = egui::Color32::from_rgb(21, 30, 21);
pub(crate) const OLDER_SCALE: f32 = 0.78;
pub(crate) fn darken(c: egui::Color32) -> egui::Color32 {
egui::Color32::from_rgb(
(c.r() as f32 * OLDER_SCALE) as u8,
(c.g() as f32 * OLDER_SCALE) as u8,
(c.b() as f32 * OLDER_SCALE) as u8,
)
}
pub(crate) const SHAKE_SECS: f64 = 0.4; pub(crate) const SHAKE_AMPLITUDE: f32 = 12.0; pub(crate) const SHAKE_FREQ: f64 = 47.0;
pub(crate) fn shake_offset(since: f64) -> f32 {
if since <= 0.0 || since >= SHAKE_SECS {
0.0
} else {
let decay = 1.0 - since / SHAKE_SECS; (SHAKE_AMPLITUDE as f64 * decay * (since * SHAKE_FREQ).sin()) as f32
}
}
pub(crate) fn clear_factor(age_since_clear: f64) -> f32 {
if age_since_clear <= 0.0 {
1.0
} else if age_since_clear >= FADE_SECS {
0.0
} else {
(1.0 - age_since_clear / FADE_SECS) as f32
}
}
pub(crate) fn glyph_factor(birth: f64, now: f64, visibility_head: f64) -> f32 {
let mut f = fade_factor(now - birth);
if birth < visibility_head {
f *= clear_factor(now - visibility_head);
}
f
}
pub(crate) fn runs(
glyphs: &[Glyph],
now: f64,
visibility_head: f64,
floor_byte: usize,
) -> Vec<(String, u8, bool)> {
let mut out: Vec<(String, u8, bool)> = Vec::new();
for g in glyphs {
let alpha = quantize_alpha(glyph_factor(g.birth, now, visibility_head));
let fresh = g.end_offset.saturating_sub(g.ch.len_utf8()) >= floor_byte;
match out.last_mut() {
Some((s, a, f)) if *a == alpha && *f == fresh => s.push(g.ch),
_ => out.push((g.ch.to_string(), alpha, fresh)),
}
}
out
}
pub(crate) fn word_count(s: &str) -> usize {
s.lines()
.filter(|line| !line.trim_start().starts_with("## "))
.flat_map(str::split_whitespace)
.count()
}
pub(crate) fn start_byte_of_second_to_last_word(s: &str) -> usize {
let (mut second_last, mut last, mut count, mut prev_ws) = (0usize, 0usize, 0u32, true);
for (i, ch) in s.char_indices() {
let ws = ch.is_whitespace();
if !ws && prev_ws {
second_last = last;
last = i;
count += 1;
}
prev_ws = ws;
}
if count >= 2 { second_last } else { 0 }
}
pub(crate) fn build_job(
glyphs: &[Glyph],
now: f64,
visibility_head: f64,
font: egui::FontId,
primary: egui::Color32,
max_width: f32,
floor_byte: usize,
) -> LayoutJob {
let mut job = LayoutJob::default();
job.wrap.max_width = max_width;
for (text, alpha, fresh) in runs(glyphs, now, visibility_head, floor_byte) {
let base = if fresh { primary } else { darken(primary) };
let color = base.gamma_multiply(alpha as f32 / 255.0);
job.append(
&text,
0.0,
TextFormat {
font_id: font.clone(),
color,
..Default::default()
},
);
}
job
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_count_counts_whitespace_tokens() {
assert_eq!(word_count(""), 0);
assert_eq!(word_count(" "), 0);
assert_eq!(word_count("hello"), 1);
assert_eq!(word_count("hello world"), 2);
assert_eq!(word_count(" multiple spaces\tand\nnewlines "), 4);
}
#[test]
fn word_count_skips_atx_headers() {
assert_eq!(word_count("## 2026-06-26 1:23:45pm PDT"), 0);
let doc = "\
## 2026-06-26 1:23:45pm PDT
hello world
## 2026-06-26 2:00:00pm PDT
three more words";
assert_eq!(word_count(doc), 5); assert_eq!(word_count(" ## indented header"), 0);
assert_eq!(word_count("### kept words here"), 4);
}
#[test]
fn start_byte_of_second_to_last_word_cases() {
assert_eq!(start_byte_of_second_to_last_word(""), 0);
assert_eq!(start_byte_of_second_to_last_word(" "), 0);
assert_eq!(start_byte_of_second_to_last_word("hello"), 0);
assert_eq!(start_byte_of_second_to_last_word("hello world"), 0);
assert_eq!(start_byte_of_second_to_last_word("foo bar baz"), 4);
assert_eq!(start_byte_of_second_to_last_word(" foo bar baz"), 6);
assert_eq!(start_byte_of_second_to_last_word("foo bar baz "), 4);
assert_eq!(start_byte_of_second_to_last_word("foo\n\nbar baz"), 5);
assert_eq!(start_byte_of_second_to_last_word("café déjà vu"), 6);
}
#[test]
fn delete_floor_ratchet_is_monotonic() {
let boot_size = 100u64;
let snapshots = [
"",
"a",
"a b",
"a b c",
"a b c d",
"a b", "a b c d e f",
];
let mut floor = boot_size;
let mut prev = floor;
for doc in snapshots {
let candidate = boot_size + start_byte_of_second_to_last_word(doc) as u64;
floor = floor.max(candidate);
assert!(floor >= prev, "floor decreased: {prev} -> {floor}");
assert!(floor >= boot_size);
prev = floor;
}
}
#[test]
fn fade_factor_thresholds() {
assert_eq!(fade_factor(0.0), 1.0);
assert_eq!(fade_factor(30.0), 1.0);
assert!((fade_factor(30.5) - 0.5).abs() < 1e-6);
assert_eq!(fade_factor(31.0), 0.0);
assert_eq!(fade_factor(100.0), 0.0);
}
#[test]
fn quantize_alpha_rounds() {
assert_eq!(quantize_alpha(1.0), 255);
assert_eq!(quantize_alpha(0.0), 0);
assert_eq!(quantize_alpha(0.5), 128);
}
#[test]
fn runs_coalesce_same_age() {
let glyphs = [
Glyph {
ch: 'a',
birth: 0.0,
end_offset: 0,
},
Glyph {
ch: 'b',
birth: 0.0,
end_offset: 0,
},
Glyph {
ch: 'c',
birth: 0.0,
end_offset: 0,
},
];
assert_eq!(
runs(&glyphs, 0.0, 0.0, usize::MAX),
vec![("abc".to_string(), 255, false)]
);
}
#[test]
fn runs_split_on_alpha_change() {
let glyphs = [
Glyph {
ch: 'x',
birth: 100.0,
end_offset: 0,
},
Glyph {
ch: 'y',
birth: 0.0,
end_offset: 0,
},
];
assert_eq!(
runs(&glyphs, 100.0, 0.0, usize::MAX),
vec![("x".to_string(), 255, false), ("y".to_string(), 0, false)]
);
}
#[test]
fn runs_split_on_fresh_boundary() {
let glyphs = [
Glyph {
ch: 'a',
birth: 0.0,
end_offset: 1,
},
Glyph {
ch: 'b',
birth: 0.0,
end_offset: 2,
},
Glyph {
ch: 'c',
birth: 0.0,
end_offset: 3,
},
Glyph {
ch: 'd',
birth: 0.0,
end_offset: 4,
},
];
assert_eq!(
runs(&glyphs, 0.0, 0.0, 2),
vec![
("ab".to_string(), 255, false),
("cd".to_string(), 255, true)
]
);
}
#[test]
fn clear_factor_immediate() {
assert_eq!(clear_factor(0.0), 1.0);
assert!((clear_factor(0.5) - 0.5).abs() < 1e-6);
assert_eq!(clear_factor(1.0), 0.0);
assert_eq!(clear_factor(5.0), 0.0);
}
#[test]
fn glyph_factor_unaffected_after_head() {
let head = 5.0;
let birth = 5.0;
assert_eq!(glyph_factor(birth, birth, head), 1.0);
}
#[test]
fn shake_offset_at_rest_outside_window() {
assert_eq!(shake_offset(-1.0), 0.0);
assert_eq!(shake_offset(0.0), 0.0);
assert_eq!(shake_offset(SHAKE_SECS), 0.0);
assert_eq!(shake_offset(SHAKE_SECS + 1.0), 0.0);
}
#[test]
fn shake_offset_bounded_and_decays() {
let mut nonzero = 0;
let mut t = 0.005_f64;
while t < SHAKE_SECS {
let env = SHAKE_AMPLITUDE * (1.0 - (t / SHAKE_SECS) as f32);
assert!(shake_offset(t).abs() <= env + 1e-3);
if shake_offset(t).abs() > 0.1 {
nonzero += 1;
}
t += 0.005;
}
assert!(nonzero > 0, "shake should be visibly nonzero mid-window");
}
#[test]
fn glyph_factor_clears_older_glyphs() {
let birth = 0.0;
let head = 10.0;
assert_eq!(glyph_factor(birth, 10.0, head), 1.0);
assert!((glyph_factor(birth, 10.5, head) - 0.5).abs() < 1e-6);
assert_eq!(glyph_factor(birth, 11.0, head), 0.0);
}
}