use crate::config::colors::{AnsiColor, ColorRgb};
pub const PLACEHOLDER: char = '\u{10EEEE}';
pub const DIACRITICS: &[char] = &[
'\u{0305}',
'\u{030D}',
'\u{030E}',
'\u{0310}',
'\u{0312}',
'\u{033D}',
'\u{033E}',
'\u{033F}',
'\u{0346}',
'\u{034A}',
'\u{034B}',
'\u{034C}',
'\u{0350}',
'\u{0351}',
'\u{0352}',
'\u{0357}',
'\u{035B}',
'\u{0363}',
'\u{0364}',
'\u{0365}',
'\u{0366}',
'\u{0367}',
'\u{0368}',
'\u{0369}',
'\u{036A}',
'\u{036B}',
'\u{036C}',
'\u{036D}',
'\u{036E}',
'\u{036F}',
'\u{0483}',
'\u{0484}',
'\u{0485}',
'\u{0486}',
'\u{0487}',
'\u{0592}',
'\u{0593}',
'\u{0594}',
'\u{0595}',
'\u{0597}',
'\u{0598}',
'\u{0599}',
'\u{059C}',
'\u{059D}',
'\u{059E}',
'\u{059F}',
'\u{05A0}',
'\u{05A1}',
'\u{05A8}',
'\u{05A9}',
'\u{05AB}',
'\u{05AC}',
'\u{05AF}',
'\u{05C4}',
'\u{0610}',
'\u{0611}',
'\u{0612}',
'\u{0613}',
'\u{0614}',
'\u{0615}',
'\u{0616}',
'\u{0617}',
'\u{0657}',
'\u{0658}',
'\u{0659}',
'\u{065A}',
'\u{065B}',
'\u{065D}',
'\u{065E}',
'\u{06D6}',
'\u{06D7}',
'\u{06D8}',
'\u{06D9}',
'\u{06DA}',
'\u{06DB}',
'\u{06DC}',
'\u{06DF}',
'\u{06E0}',
'\u{06E1}',
'\u{06E2}',
'\u{06E4}',
'\u{06E7}',
'\u{06E8}',
'\u{06EB}',
'\u{06EC}',
'\u{0730}',
'\u{0732}',
'\u{0733}',
'\u{0735}',
'\u{0736}',
'\u{073A}',
'\u{073D}',
'\u{073F}',
'\u{0740}',
'\u{0741}',
'\u{0743}',
'\u{0745}',
'\u{0747}',
'\u{0749}',
'\u{074A}',
'\u{07EB}',
'\u{07EC}',
'\u{07ED}',
'\u{07EE}',
'\u{07EF}',
'\u{07F0}',
'\u{07F1}',
'\u{07F3}',
'\u{0816}',
'\u{0817}',
'\u{0818}',
'\u{0819}',
'\u{081B}',
'\u{081C}',
'\u{081D}',
'\u{081E}',
'\u{081F}',
'\u{0820}',
'\u{0821}',
'\u{0822}',
'\u{0823}',
'\u{0825}',
'\u{0826}',
'\u{0827}',
'\u{0829}',
'\u{082A}',
'\u{082B}',
'\u{082C}',
'\u{082D}',
'\u{0951}',
'\u{0953}',
'\u{0954}',
'\u{0F82}',
'\u{0F83}',
'\u{0F86}',
'\u{0F87}',
'\u{135D}',
'\u{135E}',
'\u{135F}',
'\u{17DD}',
'\u{193A}',
'\u{1A17}',
'\u{1A75}',
'\u{1A76}',
'\u{1A77}',
'\u{1A78}',
'\u{1A79}',
'\u{1A7A}',
'\u{1A7B}',
'\u{1A7C}',
'\u{1B6B}',
'\u{1B6D}',
'\u{1B6E}',
'\u{1B6F}',
'\u{1B70}',
'\u{1B71}',
'\u{1B72}',
'\u{1B73}',
'\u{1CD0}',
'\u{1CD1}',
'\u{1CD2}',
'\u{1CDA}',
'\u{1CDB}',
'\u{1CE0}',
'\u{1DC0}',
'\u{1DC1}',
'\u{1DC3}',
'\u{1DC4}',
'\u{1DC5}',
'\u{1DC6}',
'\u{1DC7}',
'\u{1DC8}',
'\u{1DC9}',
'\u{1DCB}',
'\u{1DCC}',
'\u{1DD1}',
'\u{1DD2}',
'\u{1DD3}',
'\u{1DD4}',
'\u{1DD5}',
'\u{1DD6}',
'\u{1DD7}',
'\u{1DD8}',
'\u{1DD9}',
'\u{1DDA}',
'\u{1DDB}',
'\u{1DDC}',
'\u{1DDD}',
'\u{1DDE}',
'\u{1DDF}',
'\u{1DE0}',
'\u{1DE1}',
'\u{1DE2}',
'\u{1DE3}',
'\u{1DE4}',
'\u{1DE5}',
'\u{1DE6}',
'\u{1DFE}',
'\u{20D0}',
'\u{20D1}',
'\u{20D4}',
'\u{20D5}',
'\u{20D6}',
'\u{20D7}',
'\u{20DB}',
'\u{20DC}',
'\u{20E1}',
'\u{20E7}',
'\u{20E9}',
'\u{20F0}',
'\u{2CEF}',
'\u{2CF0}',
'\u{2CF1}',
'\u{2DE0}',
'\u{2DE1}',
'\u{2DE2}',
'\u{2DE3}',
'\u{2DE4}',
'\u{2DE5}',
'\u{2DE6}',
'\u{2DE7}',
'\u{2DE8}',
'\u{2DE9}',
'\u{2DEA}',
'\u{2DEB}',
'\u{2DEC}',
'\u{2DED}',
'\u{2DEE}',
'\u{2DEF}',
'\u{2DF0}',
'\u{2DF1}',
'\u{2DF2}',
'\u{2DF3}',
'\u{2DF4}',
'\u{2DF5}',
'\u{2DF6}',
'\u{2DF7}',
'\u{2DF8}',
'\u{2DF9}',
'\u{2DFA}',
'\u{2DFB}',
'\u{2DFC}',
'\u{2DFD}',
'\u{2DFE}',
'\u{2DFF}',
'\u{A66F}',
'\u{A67C}',
'\u{A67D}',
'\u{A6F0}',
'\u{A6F1}',
'\u{A8E0}',
'\u{A8E1}',
'\u{A8E2}',
'\u{A8E3}',
'\u{A8E4}',
'\u{A8E5}',
'\u{A8E6}',
'\u{A8E7}',
'\u{A8E8}',
'\u{A8E9}',
'\u{A8EA}',
'\u{A8EB}',
'\u{A8EC}',
'\u{A8ED}',
'\u{A8EE}',
'\u{A8EF}',
'\u{A8F0}',
'\u{A8F1}',
'\u{AAB0}',
'\u{AAB2}',
'\u{AAB3}',
'\u{AAB7}',
'\u{AAB8}',
'\u{AABE}',
'\u{AABF}',
'\u{AAC1}',
'\u{FE20}',
'\u{FE21}',
'\u{FE22}',
'\u{FE23}',
'\u{FE24}',
'\u{FE25}',
'\u{FE26}',
'\u{10A0F}',
'\u{10A38}',
'\u{1D185}',
'\u{1D186}',
'\u{1D187}',
'\u{1D188}',
'\u{1D189}',
'\u{1D1AA}',
'\u{1D1AB}',
'\u{1D1AC}',
'\u{1D1AD}',
'\u{1D242}',
'\u{1D243}',
'\u{1D244}',
];
pub fn index_to_diacritic(index: u32) -> Option<char> {
DIACRITICS.get(index as usize).copied()
}
pub fn diacritic_to_index(c: char) -> Option<u32> {
DIACRITICS.iter().position(|&d| d == c).map(|i| i as u32)
}
pub fn id_to_rgb(id: u32) -> ColorRgb {
ColorRgb {
r: ((id >> 16) & 0xFF) as u8,
g: ((id >> 8) & 0xFF) as u8,
b: (id & 0xFF) as u8,
}
}
pub fn rgb_to_id(rgb: ColorRgb) -> u32 {
((rgb.r as u32) << 16) | ((rgb.g as u32) << 8) | (rgb.b as u32)
}
pub fn encode_placeholder(row: u32, col: u32, image_id_high: Option<u8>) -> String {
let mut result = String::from('\u{10EEEE}');
if let Some(d) = index_to_diacritic(row) {
result.push(d);
}
if let Some(d) = index_to_diacritic(col) {
result.push(d);
}
if let Some(high) = image_id_high {
if let Some(d) = index_to_diacritic(high as u32) {
result.push(d);
}
}
result
}
pub fn decode_placeholder(s: &str) -> Option<(u32, u32, Option<u8>)> {
let mut chars = s.chars();
if chars.next()? != '\u{10EEEE}' {
return None;
}
let row = chars.next().and_then(diacritic_to_index)?;
let col = chars.next().and_then(diacritic_to_index)?;
let high =
chars.next().and_then(|c| {
diacritic_to_index(c).and_then(|idx| {
if idx <= 255 {
Some(idx as u8)
} else {
None
}
})
});
Some((row, col, high))
}
fn color_to_id(color: AnsiColor) -> u32 {
match color {
AnsiColor::Indexed(n) => n as u32,
AnsiColor::Spec(rgb) => rgb_to_id(rgb),
AnsiColor::Named(_) => 0,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IncompletePlacement {
pub image_id_low: u32,
pub image_id_high: Option<u8>,
pub placement_id: u32,
pub row: Option<u32>,
pub col: Option<u32>,
pub width: u32,
}
impl IncompletePlacement {
pub fn from_cell(
fg: AnsiColor,
underline: Option<AnsiColor>,
combining: &[char],
) -> Self {
let row = combining.first().copied().and_then(diacritic_to_index);
let col = combining.get(1).copied().and_then(diacritic_to_index);
let image_id_high = combining
.get(2)
.copied()
.and_then(diacritic_to_index)
.and_then(|i| if i <= 255 { Some(i as u8) } else { None });
Self {
image_id_low: color_to_id(fg),
image_id_high,
placement_id: underline.map(color_to_id).unwrap_or(0),
row,
col,
width: 1,
}
}
pub fn can_append(&self, other: &IncompletePlacement) -> bool {
self.image_id_low == other.image_id_low
&& self.placement_id == other.placement_id
&& (other.row.is_none() || other.row == self.row)
&& match other.col {
None => true,
Some(c) => self.col.map(|sc| c == sc + self.width).unwrap_or(false),
}
&& (other.image_id_high.is_none()
|| other.image_id_high == self.image_id_high)
}
pub fn append(&mut self) {
self.width += 1;
}
pub fn complete(&self) -> PlaceholderRun {
PlaceholderRun {
image_id: ((self.image_id_high.unwrap_or(0) as u32) << 24)
| (self.image_id_low & 0x00FF_FFFF),
placement_id: self.placement_id,
row: self.row.unwrap_or(0),
col: self.col.unwrap_or(0),
width: self.width,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlaceholderRun {
pub image_id: u32,
pub placement_id: u32,
pub row: u32,
pub col: u32,
pub width: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RunGeometry {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub source_rect: [f32; 4],
}
#[allow(clippy::too_many_arguments)]
pub fn compute_run_geometry(
run: &PlaceholderRun,
placement_cols: u32,
placement_rows: u32,
image_width_px: u32,
image_height_px: u32,
cell_width: f32,
cell_height: f32,
origin_x: f32,
origin_y: f32,
screen_line: usize,
start_screen_col: usize,
) -> Option<RunGeometry> {
let img_w = image_width_px as f32;
let img_h = image_height_px as f32;
if img_w <= 0.0 || img_h <= 0.0 {
return None;
}
let p_cols_px = placement_cols.max(1) as f32 * cell_width;
let p_rows_px = placement_rows.max(1) as f32 * cell_height;
let scale = (p_cols_px / img_w).min(p_rows_px / img_h);
let fit_w = img_w * scale;
let fit_h = img_h * scale;
let pad_x = (p_cols_px - fit_w) * 0.5;
let pad_y = (p_rows_px - fit_h) * 0.5;
let run_box_x = run.col as f32 * cell_width;
let run_box_y = run.row as f32 * cell_height;
let run_box_w = run.width as f32 * cell_width;
let run_box_h = cell_height;
let img_box_x0 = pad_x;
let img_box_y0 = pad_y;
let img_box_x1 = pad_x + fit_w;
let img_box_y1 = pad_y + fit_h;
let vis_x0 = run_box_x.max(img_box_x0);
let vis_y0 = run_box_y.max(img_box_y0);
let vis_x1 = (run_box_x + run_box_w).min(img_box_x1);
let vis_y1 = (run_box_y + run_box_h).min(img_box_y1);
if vis_x1 <= vis_x0 || vis_y1 <= vis_y0 {
return None;
}
let src_u0 = (vis_x0 - img_box_x0) / fit_w;
let src_v0 = (vis_y0 - img_box_y0) / fit_h;
let src_u1 = (vis_x1 - img_box_x0) / fit_w;
let src_v1 = (vis_y1 - img_box_y0) / fit_h;
let intra_x = vis_x0 - run_box_x;
let intra_y = vis_y0 - run_box_y;
Some(RunGeometry {
x: origin_x + start_screen_col as f32 * cell_width + intra_x,
y: origin_y + screen_line as f32 * cell_height + intra_y,
width: vis_x1 - vis_x0,
height: vis_y1 - vis_y0,
source_rect: [src_u0, src_v0, src_u1, src_v1],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diacritic_conversion() {
assert_eq!(index_to_diacritic(0), Some('\u{0305}'));
assert_eq!(diacritic_to_index('\u{0305}'), Some(0));
let last_idx = (DIACRITICS.len() - 1) as u32;
assert_eq!(index_to_diacritic(last_idx), Some('\u{1D244}'));
assert_eq!(diacritic_to_index('\u{1D244}'), Some(last_idx));
}
#[test]
fn test_rgb_id_conversion() {
let rgb = ColorRgb {
r: 0x12,
g: 0x34,
b: 0x56,
};
let id = rgb_to_id(rgb);
assert_eq!(id, 0x123456);
assert_eq!(id_to_rgb(id), rgb);
}
#[test]
fn test_encode_placeholder() {
let s = encode_placeholder(0, 0, None);
assert!(s.starts_with('\u{10EEEE}'));
assert_eq!(s.chars().count(), 3);
let s = encode_placeholder(0, 0, Some(1));
assert_eq!(s.chars().count(), 4); }
#[test]
fn test_encode_decode_roundtrip() {
let encoded = encode_placeholder(5, 10, None);
let decoded = decode_placeholder(&encoded).unwrap();
assert_eq!(decoded, (5, 10, None));
let encoded = encode_placeholder(5, 10, Some(42));
let decoded = decode_placeholder(&encoded).unwrap();
assert_eq!(decoded, (5, 10, Some(42)));
}
use crate::config::colors::NamedColor;
#[test]
fn from_cell_indexed_fg_two_diacritics() {
let combining = [DIACRITICS[3], DIACRITICS[7]]; let p = IncompletePlacement::from_cell(AnsiColor::Indexed(42), None, &combining);
assert_eq!(p.image_id_low, 42);
assert_eq!(p.image_id_high, None);
assert_eq!(p.placement_id, 0);
assert_eq!(p.row, Some(3));
assert_eq!(p.col, Some(7));
let run = p.complete();
assert_eq!(run.image_id, 42);
assert_eq!(run.row, 3);
assert_eq!(run.col, 7);
assert_eq!(run.width, 1);
}
#[test]
fn from_cell_rgb_fg_three_diacritics() {
let rgb = ColorRgb {
r: 0xAB,
g: 0xCD,
b: 0xEF,
};
let combining = [DIACRITICS[0], DIACRITICS[1], DIACRITICS[2]];
let p = IncompletePlacement::from_cell(AnsiColor::Spec(rgb), None, &combining);
assert_eq!(p.image_id_low, 0x00AB_CDEF);
assert_eq!(p.image_id_high, Some(2));
assert_eq!(p.row, Some(0));
assert_eq!(p.col, Some(1));
let run = p.complete();
assert_eq!(run.image_id, 0x0200_0000 | 0x00AB_CDEF);
}
#[test]
fn from_cell_with_placement_id_underline() {
let fg_rgb = ColorRgb { r: 1, g: 2, b: 3 };
let ul_rgb = ColorRgb { r: 0, g: 0, b: 99 };
let combining = [DIACRITICS[0], DIACRITICS[0]];
let p = IncompletePlacement::from_cell(
AnsiColor::Spec(fg_rgb),
Some(AnsiColor::Spec(ul_rgb)),
&combining,
);
assert_eq!(p.image_id_low, 0x0001_0203);
assert_eq!(p.placement_id, 99);
}
#[test]
fn from_cell_missing_diacritics_yields_none_fields() {
let p = IncompletePlacement::from_cell(AnsiColor::Indexed(1), None, &[]);
assert_eq!(p.row, None);
assert_eq!(p.col, None);
assert_eq!(p.image_id_high, None);
assert_eq!(p.image_id_low, 1);
let p =
IncompletePlacement::from_cell(AnsiColor::Indexed(1), None, &[DIACRITICS[5]]);
assert_eq!(p.row, Some(5));
assert_eq!(p.col, None);
let run = p.complete();
assert_eq!(run.row, 5);
assert_eq!(run.col, 0);
}
#[test]
fn from_cell_named_fg_yields_zero_id() {
let combining = [DIACRITICS[0], DIACRITICS[0]];
let p = IncompletePlacement::from_cell(
AnsiColor::Named(NamedColor::Foreground),
None,
&combining,
);
assert_eq!(p.image_id_low, 0);
}
fn p(row: Option<u32>, col: Option<u32>) -> IncompletePlacement {
IncompletePlacement {
image_id_low: 7,
image_id_high: None,
placement_id: 0,
row,
col,
width: 1,
}
}
#[test]
fn can_append_inherits_row_and_col() {
let mut a = p(Some(0), Some(0));
let b = p(None, None);
assert!(a.can_append(&b));
a.append();
assert_eq!(a.width, 2);
}
#[test]
fn can_append_explicit_sequential_col() {
let a = p(Some(0), Some(0));
let b = p(Some(0), Some(1));
assert!(a.can_append(&b));
}
#[test]
fn can_append_inherit_row_explicit_col() {
let a = p(Some(0), Some(0));
let b = p(None, Some(1));
assert!(a.can_append(&b));
}
#[test]
fn cannot_append_col_jump() {
let a = p(Some(0), Some(0));
let b = p(Some(0), Some(2));
assert!(!a.can_append(&b));
}
#[test]
fn cannot_append_different_row() {
let a = p(Some(0), Some(0));
let b = p(Some(1), Some(1));
assert!(!a.can_append(&b));
}
#[test]
fn cannot_append_different_image_id() {
let mut a = p(Some(0), Some(0));
a.image_id_low = 1;
let mut b = p(Some(0), Some(1));
b.image_id_low = 2;
assert!(!a.can_append(&b));
}
#[test]
fn cannot_append_different_image_id_high() {
let mut a = p(Some(0), Some(0));
a.image_id_high = Some(5);
let mut b = p(Some(0), Some(1));
b.image_id_high = Some(6);
assert!(!a.can_append(&b));
}
#[test]
fn can_append_inherits_image_id_high() {
let mut a = p(Some(0), Some(0));
a.image_id_high = Some(5);
let b = p(Some(0), Some(1)); assert!(a.can_append(&b));
}
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "expected ~{b}, got {a}");
}
fn run(row: u32, col: u32, width: u32) -> PlaceholderRun {
PlaceholderRun {
image_id: 1,
placement_id: 0,
row,
col,
width,
}
}
#[test]
fn geom_image_matches_grid_aspect_no_padding() {
let g = compute_run_geometry(
&run(0, 0, 3),
10,
5,
100,
50,
10.0,
10.0,
0.0,
0.0,
0,
0,
)
.expect("visible");
approx(g.x, 0.0);
approx(g.y, 0.0);
approx(g.width, 30.0);
approx(g.height, 10.0);
approx(g.source_rect[0], 0.0);
approx(g.source_rect[1], 0.0);
approx(g.source_rect[2], 0.30);
approx(g.source_rect[3], 0.20);
}
#[test]
fn geom_image_taller_than_grid_centers_horizontally() {
let none = compute_run_geometry(
&run(0, 0, 2),
10,
10,
50,
100,
10.0,
10.0,
0.0,
0.0,
0,
0,
);
assert!(none.is_none(), "left-padding run should be culled");
let g = compute_run_geometry(
&run(0, 3, 1),
10,
10,
50,
100,
10.0,
10.0,
0.0,
0.0,
0,
3,
)
.expect("visible");
approx(g.x, 30.0);
approx(g.y, 0.0);
approx(g.width, 10.0);
approx(g.height, 10.0);
approx(g.source_rect[0], 0.10);
approx(g.source_rect[1], 0.0);
approx(g.source_rect[2], 0.30);
approx(g.source_rect[3], 0.10);
}
#[test]
fn geom_image_wider_than_grid_centers_vertically() {
let none = compute_run_geometry(
&run(0, 0, 10),
10,
10,
200,
50,
10.0,
10.0,
0.0,
0.0,
0,
0,
);
assert!(none.is_none());
let g = compute_run_geometry(
&run(4, 0, 10),
10,
10,
200,
50,
10.0,
10.0,
0.0,
0.0,
4,
0,
)
.expect("visible");
approx(g.x, 0.0);
approx(g.y, 40.0);
approx(g.width, 100.0);
approx(g.height, 10.0);
approx(g.source_rect[0], 0.0);
approx(g.source_rect[1], 0.10);
approx(g.source_rect[2], 1.0);
approx(g.source_rect[3], 0.50);
}
#[test]
fn geom_partial_visibility_scrolled_off_top() {
let g = compute_run_geometry(
&run(2, 0, 10),
10,
10,
100,
100,
10.0,
10.0,
0.0,
0.0,
0, 0, )
.expect("visible");
approx(g.x, 0.0);
approx(g.y, 0.0); approx(g.width, 100.0);
approx(g.height, 10.0);
approx(g.source_rect[1], 0.20);
approx(g.source_rect[3], 0.30);
}
#[test]
fn geom_origin_offset_applies_to_screen_pos_only() {
let g = compute_run_geometry(
&run(0, 0, 3),
10,
5,
100,
50,
10.0,
10.0,
100.0,
50.0,
0,
0,
)
.expect("visible");
approx(g.x, 100.0);
approx(g.y, 50.0);
approx(g.source_rect[0], 0.0);
approx(g.source_rect[2], 0.30);
}
#[test]
fn geom_screen_line_and_start_col_offset_screen_pos() {
let g = compute_run_geometry(
&run(0, 0, 2),
10,
5,
100,
50,
10.0,
10.0,
0.0,
0.0,
7,
5,
)
.expect("visible");
approx(g.x, 50.0);
approx(g.y, 70.0);
}
#[test]
fn geom_returns_none_when_image_zero_sized() {
let none =
compute_run_geometry(&run(0, 0, 1), 10, 5, 0, 50, 10.0, 10.0, 0.0, 0.0, 0, 0);
assert!(none.is_none());
}
#[test]
fn run_of_three_cells_with_only_first_diacritics() {
let mut run = IncompletePlacement::from_cell(
AnsiColor::Indexed(7),
None,
&[DIACRITICS[0], DIACRITICS[0]], );
for _ in 0..2 {
let next = IncompletePlacement::from_cell(AnsiColor::Indexed(7), None, &[]);
assert!(run.can_append(&next));
run.append();
}
let r = run.complete();
assert_eq!(r.row, 0);
assert_eq!(r.col, 0);
assert_eq!(r.width, 3);
}
}