use std::fmt::Write as _;
use crate::base::Rgba;
use super::cell::{Attrs, Cell};
use super::surface::Surface;
pub fn snapshot(s: &Surface) -> String {
let mut out = String::new();
let border: String = "-".repeat(s.width().max(0) as usize);
let _ = writeln!(out, "+{border}+");
for y in 0..s.height() {
out.push('|');
push_row_text(s, y, &mut out);
out.push_str("|\n");
}
let _ = writeln!(out, "+{border}+");
out
}
pub fn snapshot_styles(s: &Surface) -> String {
let mut out = snapshot(s);
for y in 0..s.height() {
let mut row_header = false;
let mut x = 0;
while x < s.width() {
let Some(&cell) = s.get(x, y) else { break };
let start = x;
while x < s.width() {
match s.get(x, y) {
Some(c) if same_paint(c, &cell) => x += 1,
_ => break,
}
}
if is_default_paint(&cell) {
continue; }
if !row_header {
let _ = writeln!(out, "row {y}:");
row_header = true;
}
let _ = write!(
out,
" {start}..{x} fg={} bg={}",
color(cell.fg),
color(cell.bg)
);
if !cell.attrs.is_empty() {
let _ = write!(out, " attrs={}", attrs(cell.attrs));
}
if !cell.ul.is_transparent() {
let _ = write!(out, " ul={}", color(cell.ul));
}
if cell.link != 0 {
match s.link_uri(cell.link) {
Some(uri) => {
let _ = write!(out, " link={uri}");
}
None => {
let _ = write!(out, " link=#{}", cell.link);
}
}
}
out.push('\n');
}
}
out
}
fn push_row_text(s: &Surface, y: i32, out: &mut String) {
for x in 0..s.width() {
let Some(c) = s.get(x, y) else { continue };
if c.is_continuation() {
continue; }
let g = s.glyph_str(c);
out.push_str(if g.is_empty() { " " } else { g });
}
}
fn same_paint(a: &Cell, b: &Cell) -> bool {
a.fg == b.fg && a.bg == b.bg && a.ul == b.ul && a.attrs == b.attrs && a.link == b.link
}
fn is_default_paint(c: &Cell) -> bool {
same_paint(c, &Cell::EMPTY)
}
fn color(c: Rgba) -> String {
if c.is_transparent() {
return "-".to_string();
}
if c.a == 255 {
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
} else {
format!("#{:02x}{:02x}{:02x}/{:02x}", c.r, c.g, c.b, c.a)
}
}
fn attrs(a: Attrs) -> String {
const LETTERS: [(Attrs, char); 9] = [
(Attrs::BOLD, 'B'),
(Attrs::DIM, 'd'),
(Attrs::ITALIC, 'I'),
(Attrs::UNDERLINE, 'U'),
(Attrs::UNDERCURL, '~'),
(Attrs::BLINK, 'k'),
(Attrs::REVERSE, 'R'),
(Attrs::HIDDEN, 'h'),
(Attrs::STRIKE, 'S'),
];
LETTERS
.iter()
.filter(|(bit, _)| a.contains(*bit))
.map(|&(_, ch)| ch)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::Size;
use crate::render::style::Style;
#[test]
fn grid_shows_chars_wide_pairs_and_border() {
let mut s = Surface::new(Size::new(8, 2), Cell::EMPTY);
s.draw_text(0, 0, "hi 世界", Style::new());
s.draw_text(0, 1, "x", Style::new());
let dump = snapshot(&s);
let lines: Vec<&str> = dump.lines().collect();
assert_eq!(lines[0], "+--------+");
assert_eq!(lines[1], "|hi 世界 |");
assert_eq!(lines[2], "|x |");
assert_eq!(lines[3], "+--------+");
}
#[test]
fn style_runs_annotate_only_styled_content() {
let mut s = Surface::new(Size::new(12, 2), Cell::EMPTY);
let link = s.register_link("https://x.io");
s.draw_text(0, 0, "err", Style::new().fg(Rgba::rgb(255, 0, 0)).bold());
s.draw_text(4, 0, "docs", Style::new().underline().link(link));
let dump = snapshot_styles(&s);
assert!(dump.contains("row 0:"), "{dump}");
assert!(dump.contains("0..3 fg=#ff0000 bg=- attrs=B"), "{dump}");
assert!(
dump.contains("4..8 fg=- bg=- attrs=U link=https://x.io"),
"{dump}"
);
assert!(!dump.contains("row 1:"), "unstyled row stays quiet: {dump}");
}
#[test]
fn translucent_and_ul_colors_render_readably() {
let mut s = Surface::new(Size::new(4, 1), Cell::EMPTY);
s.draw_text(
0,
0,
"ab",
Style::new()
.bg(Rgba::new(10, 20, 30, 128))
.underline()
.underline_color(Rgba::rgb(0, 0, 255)),
);
let dump = snapshot_styles(&s);
assert!(dump.contains("bg=#0a141e/80"), "{dump}");
assert!(dump.contains("ul=#0000ff"), "{dump}");
}
}