T07: box drawing, block elements and powerline shapes painted as geometry, after Ghostty's sprite rules
This commit is contained in:
7 files changed
+738
-17
No files matched your search
@@ -0,0 +1,373 @@
|
||||
/// One step of a path, in device pixels from the cell's top-left.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Step {
|
||||
Move(f32, f32),
|
||||
Line(f32, f32),
|
||||
/// A cubic curve: two control points, then the end.
|
||||
Curve([f32; 6]),
|
||||
}
|
||||
|
||||
/// A piece of a drawn character, in device pixels from the cell's top-left.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Shape {
|
||||
/// A rectangle filled at `alpha` of the text color.
|
||||
Rect {
|
||||
x: u32,
|
||||
y: u32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
alpha: f32,
|
||||
},
|
||||
/// A closed outline, filled.
|
||||
Fill(Vec<Step>),
|
||||
/// An open path, stroked this wide with butt ends.
|
||||
Stroke(Vec<Step>, f32),
|
||||
}
|
||||
|
||||
/// Whether the terminal draws `ch` as shapes instead of through the font.
|
||||
pub fn drawn(ch: char) -> bool {
|
||||
matches!(ch, '\u{2500}'..='\u{259F}' | '\u{E0B0}'..='\u{E0B7}')
|
||||
}
|
||||
|
||||
const NONE: u8 = 0;
|
||||
const LIGHT: u8 = 1;
|
||||
const HEAVY: u8 = 2;
|
||||
const DOUBLE: u8 = 3;
|
||||
|
||||
/// Each line character's arms, up, right, down, left, from U+2500.
|
||||
#[rustfmt::skip]
|
||||
const LINES: [[u8; 4]; 128] = [
|
||||
[0, 1, 0, 1], [0, 2, 0, 2], [1, 0, 1, 0], [2, 0, 2, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
|
||||
[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 1, 1, 0], [0, 2, 1, 0], [0, 1, 2, 0], [0, 2, 2, 0],
|
||||
[0, 0, 1, 1], [0, 0, 1, 2], [0, 0, 2, 1], [0, 0, 2, 2], [1, 1, 0, 0], [1, 2, 0, 0], [2, 1, 0, 0], [2, 2, 0, 0],
|
||||
[1, 0, 0, 1], [1, 0, 0, 2], [2, 0, 0, 1], [2, 0, 0, 2], [1, 1, 1, 0], [1, 2, 1, 0], [2, 1, 1, 0], [1, 1, 2, 0],
|
||||
[2, 1, 2, 0], [2, 2, 1, 0], [1, 2, 2, 0], [2, 2, 2, 0], [1, 0, 1, 1], [1, 0, 1, 2], [2, 0, 1, 1], [1, 0, 2, 1],
|
||||
[2, 0, 2, 1], [2, 0, 1, 2], [1, 0, 2, 2], [2, 0, 2, 2], [0, 1, 1, 1], [0, 1, 1, 2], [0, 2, 1, 1], [0, 2, 1, 2],
|
||||
[0, 1, 2, 1], [0, 1, 2, 2], [0, 2, 2, 1], [0, 2, 2, 2], [1, 1, 0, 1], [1, 1, 0, 2], [1, 2, 0, 1], [1, 2, 0, 2],
|
||||
[2, 1, 0, 1], [2, 1, 0, 2], [2, 2, 0, 1], [2, 2, 0, 2], [1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 1, 1], [1, 2, 1, 2],
|
||||
[2, 1, 1, 1], [1, 1, 2, 1], [2, 1, 2, 1], [2, 1, 1, 2], [2, 2, 1, 1], [1, 1, 2, 2], [1, 2, 2, 1], [2, 2, 1, 2],
|
||||
[1, 2, 2, 2], [2, 1, 2, 2], [2, 2, 2, 1], [2, 2, 2, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
|
||||
[0, 3, 0, 3], [3, 0, 3, 0], [0, 3, 1, 0], [0, 1, 3, 0], [0, 3, 3, 0], [0, 0, 1, 3], [0, 0, 3, 1], [0, 0, 3, 3],
|
||||
[1, 3, 0, 0], [3, 1, 0, 0], [3, 3, 0, 0], [1, 0, 0, 3], [3, 0, 0, 1], [3, 0, 0, 3], [1, 3, 1, 0], [3, 1, 3, 0],
|
||||
[3, 3, 3, 0], [1, 0, 1, 3], [3, 0, 3, 1], [3, 0, 3, 3], [0, 3, 1, 3], [0, 1, 3, 1], [0, 3, 3, 3], [1, 3, 0, 3],
|
||||
[3, 1, 0, 1], [3, 3, 0, 3], [1, 3, 1, 3], [3, 1, 3, 1], [3, 3, 3, 3], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
|
||||
[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0],
|
||||
[0, 0, 0, 2], [2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 2, 0, 1], [1, 0, 2, 0], [0, 1, 0, 2], [2, 0, 1, 0],
|
||||
];
|
||||
|
||||
/// Quadrant blocks from U+2596: top-left, top-right, bottom-left, bottom-right bits.
|
||||
const QUADRANTS: [u8; 10] = [
|
||||
0b0010, 0b0001, 0b1000, 0b1011, 0b1001, 0b1110, 0b1101, 0b0100, 0b0110, 0b0111,
|
||||
];
|
||||
|
||||
/// The shapes of a `drawn` char in a `w` by `h` cell whose light lines are `t` thick.
|
||||
pub fn shapes(ch: char, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let code = ch as u32;
|
||||
match code {
|
||||
0x2504..=0x250B | 0x254C..=0x254F => dashes(code, w, h, t),
|
||||
0x256D..=0x2570 => arc(code, w, h, t),
|
||||
0x2571..=0x2573 => diagonals(code, w, h, t),
|
||||
0x2500..=0x257F => lines(LINES[(code - 0x2500) as usize], w, h, t),
|
||||
0x2580..=0x259F => blocks(code, w, h),
|
||||
0xE0B0..=0xE0B7 => powerline(code, w, h, t),
|
||||
_ => panic!("U+{code:04X} is not a drawn character"),
|
||||
}
|
||||
}
|
||||
|
||||
fn rect(x0: u32, y0: u32, x1: u32, y1: u32) -> Shape {
|
||||
Shape::Rect {
|
||||
x: x0,
|
||||
y: y0,
|
||||
w: x1.saturating_sub(x0),
|
||||
h: y1.saturating_sub(y0),
|
||||
alpha: 1.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an arm ends at the center, by the arms crossing it: `[heavy, double, light, short]`.
|
||||
fn stop((a, b): (u8, u8), straight: bool, [heavy, double, light, short]: [u32; 4]) -> u32 {
|
||||
if a == HEAVY || b == HEAVY {
|
||||
heavy
|
||||
} else if a != b || straight {
|
||||
if a == DOUBLE || b == DOUBLE {
|
||||
double
|
||||
} else {
|
||||
light
|
||||
}
|
||||
} else if a == NONE && b == NONE {
|
||||
light
|
||||
} else {
|
||||
short
|
||||
}
|
||||
}
|
||||
|
||||
/// Arms from the center to the edges, joined as Ghostty joins them.
|
||||
fn lines([up, right, down, left]: [u8; 4], w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let heavy = 2 * t;
|
||||
let h_light_top = h.saturating_sub(t) / 2;
|
||||
let h_light_bottom = h_light_top + t;
|
||||
let h_heavy_top = h.saturating_sub(heavy) / 2;
|
||||
let h_heavy_bottom = h_heavy_top + heavy;
|
||||
let h_double_top = h_light_top.saturating_sub(t);
|
||||
let h_double_bottom = h_light_bottom + t;
|
||||
let v_light_left = w.saturating_sub(t) / 2;
|
||||
let v_light_right = v_light_left + t;
|
||||
let v_heavy_left = w.saturating_sub(heavy) / 2;
|
||||
let v_heavy_right = v_heavy_left + heavy;
|
||||
let v_double_left = v_light_left.saturating_sub(t);
|
||||
let v_double_right = v_light_right + t;
|
||||
let up_bottom = stop(
|
||||
(left, right),
|
||||
down == up,
|
||||
[h_heavy_bottom, h_double_bottom, h_light_bottom, h_light_top],
|
||||
);
|
||||
let down_top = stop(
|
||||
(left, right),
|
||||
up == down,
|
||||
[h_heavy_top, h_double_top, h_light_top, h_light_bottom],
|
||||
);
|
||||
let left_right = stop(
|
||||
(up, down),
|
||||
left == right,
|
||||
[v_heavy_right, v_double_right, v_light_right, v_light_left],
|
||||
);
|
||||
let right_left = stop(
|
||||
(up, down),
|
||||
right == left,
|
||||
[v_heavy_left, v_double_left, v_light_left, v_light_right],
|
||||
);
|
||||
let (top_double, bottom_double) = (up == DOUBLE, down == DOUBLE);
|
||||
let mut out = Vec::new();
|
||||
match up {
|
||||
LIGHT => out.push(rect(v_light_left, 0, v_light_right, up_bottom)),
|
||||
HEAVY => out.push(rect(v_heavy_left, 0, v_heavy_right, up_bottom)),
|
||||
DOUBLE => {
|
||||
let stop = |side| {
|
||||
if side == DOUBLE {
|
||||
h_light_top
|
||||
} else {
|
||||
up_bottom
|
||||
}
|
||||
};
|
||||
out.push(rect(v_double_left, 0, v_light_left, stop(left)));
|
||||
out.push(rect(v_light_right, 0, v_double_right, stop(right)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match right {
|
||||
LIGHT => out.push(rect(right_left, h_light_top, w, h_light_bottom)),
|
||||
HEAVY => out.push(rect(right_left, h_heavy_top, w, h_heavy_bottom)),
|
||||
DOUBLE => {
|
||||
let start = |double| if double { v_light_right } else { right_left };
|
||||
out.push(rect(start(top_double), h_double_top, w, h_light_top));
|
||||
out.push(rect(
|
||||
start(bottom_double),
|
||||
h_light_bottom,
|
||||
w,
|
||||
h_double_bottom,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match down {
|
||||
LIGHT => out.push(rect(v_light_left, down_top, v_light_right, h)),
|
||||
HEAVY => out.push(rect(v_heavy_left, down_top, v_heavy_right, h)),
|
||||
DOUBLE => {
|
||||
let start = |side| {
|
||||
if side == DOUBLE {
|
||||
h_light_bottom
|
||||
} else {
|
||||
down_top
|
||||
}
|
||||
};
|
||||
out.push(rect(v_double_left, start(left), v_light_left, h));
|
||||
out.push(rect(v_light_right, start(right), v_double_right, h));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match left {
|
||||
LIGHT => out.push(rect(0, h_light_top, left_right, h_light_bottom)),
|
||||
HEAVY => out.push(rect(0, h_heavy_top, left_right, h_heavy_bottom)),
|
||||
DOUBLE => {
|
||||
let end = |double| if double { v_light_left } else { left_right };
|
||||
out.push(rect(0, h_double_top, end(top_double), h_light_top));
|
||||
out.push(rect(0, h_light_bottom, end(bottom_double), h_double_bottom));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Dashes that tile across cells; a cell too small for them takes a solid light line.
|
||||
fn dashes(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let wide = t.max(4);
|
||||
let (count, thick, gap, vertical) = match code {
|
||||
0x2504 => (3, t, wide, false),
|
||||
0x2505 => (3, 2 * t, wide, false),
|
||||
0x2506 => (3, t, wide, true),
|
||||
0x2507 => (3, 2 * t, wide, true),
|
||||
0x2508 => (4, t, wide, false),
|
||||
0x2509 => (4, 2 * t, wide, false),
|
||||
0x250A => (4, t, wide, true),
|
||||
0x250B => (4, 2 * t, wide, true),
|
||||
0x254C => (2, t, t, false),
|
||||
0x254D => (2, 2 * t, 2 * t, false),
|
||||
0x254E => (2, t, 2 * t, true),
|
||||
_ => (2, 2 * t, 2 * t, true),
|
||||
};
|
||||
let (along, across) = if vertical { (h, w) } else { (w, h) };
|
||||
if along < 2 * count {
|
||||
let solid = if vertical {
|
||||
[LIGHT, 0, LIGHT, 0]
|
||||
} else {
|
||||
[0, LIGHT, 0, LIGHT]
|
||||
};
|
||||
return lines(solid, w, h, t);
|
||||
}
|
||||
let gap = gap.min(along / (2 * count));
|
||||
let total = along - count * gap;
|
||||
let (dash, mut extra) = (total / count, total % count);
|
||||
let side = across.saturating_sub(thick) / 2;
|
||||
let mut at = if vertical { 0 } else { gap / 2 };
|
||||
(0..count)
|
||||
.map(|_| {
|
||||
let end = at + dash + u32::from(extra > 0);
|
||||
extra = extra.saturating_sub(1);
|
||||
let piece = if vertical {
|
||||
rect(side, at, side + thick, end)
|
||||
} else {
|
||||
rect(at, side, end, side + thick)
|
||||
};
|
||||
at = end + gap;
|
||||
piece
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A rounded corner from the light lines' centers to two edges.
|
||||
fn arc(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf, tf) = (w as f32, h as f32, t as f32);
|
||||
let cx = (w.saturating_sub(t) / 2) as f32 + tf / 2.;
|
||||
let cy = (h.saturating_sub(t) / 2) as f32 + tf / 2.;
|
||||
let r = wf.min(hf) / 2.;
|
||||
let s = 0.25;
|
||||
let [dy, dx]: [f32; 2] = [[1., 1.], [1., -1.], [-1., -1.], [-1., 1.]][(code - 0x256D) as usize];
|
||||
let (edge_x, edge_y) = (if dx > 0. { wf } else { 0. }, if dy > 0. { hf } else { 0. });
|
||||
let path = vec![
|
||||
Step::Move(cx, edge_y),
|
||||
Step::Line(cx, cy + dy * r),
|
||||
Step::Curve([cx, cy + dy * s * r, cx + dx * s * r, cy, cx + dx * r, cy]),
|
||||
Step::Line(edge_x, cy),
|
||||
];
|
||||
vec![Shape::Stroke(path, tf)]
|
||||
}
|
||||
|
||||
/// Corner-to-corner lines, run half a pixel past so neighbors join once clipped.
|
||||
fn diagonals(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf) = (w as f32, h as f32);
|
||||
let (sx, sy) = ((wf / hf).min(1.) / 2., (hf / wf).min(1.) / 2.);
|
||||
let line = |from: (f32, f32), to: (f32, f32)| {
|
||||
Shape::Stroke(
|
||||
vec![Step::Move(from.0, from.1), Step::Line(to.0, to.1)],
|
||||
t as f32,
|
||||
)
|
||||
};
|
||||
let rising = line((wf + sx, -sy), (-sx, hf + sy));
|
||||
let falling = line((-sx, -sy), (wf + sx, hf + sy));
|
||||
match code {
|
||||
0x2571 => vec![rising],
|
||||
0x2572 => vec![falling],
|
||||
_ => vec![rising, falling],
|
||||
}
|
||||
}
|
||||
|
||||
/// Block elements: eighths, halves, shades and quadrants.
|
||||
fn blocks(code: u32, w: u32, h: u32) -> Vec<Shape> {
|
||||
let part = |share: f32, side: u32| (side as f32 * share).round() as u32;
|
||||
let eighths = |n: u32| n as f32 / 8.;
|
||||
let shade = |alpha| Shape::Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w,
|
||||
h,
|
||||
alpha,
|
||||
};
|
||||
match code {
|
||||
0x2580 => vec![rect(0, 0, w, part(0.5, h))],
|
||||
0x2581..=0x2587 => vec![rect(0, h - part(eighths(code - 0x2580), h), w, h)],
|
||||
0x2588 => vec![rect(0, 0, w, h)],
|
||||
0x2589..=0x258F => vec![rect(0, 0, part(eighths(0x2590 - code), w), h)],
|
||||
0x2590 => vec![rect(w - part(0.5, w), 0, w, h)],
|
||||
0x2591..=0x2593 => vec![shade((code - 0x2590) as f32 / 4.)],
|
||||
0x2594 => vec![rect(0, 0, w, part(0.125, h))],
|
||||
0x2595 => vec![rect(w - part(0.125, w), 0, w, h)],
|
||||
_ => {
|
||||
let bits = QUADRANTS[(code - 0x2596) as usize];
|
||||
let (mx, my) = (part(0.5, w), part(0.5, h));
|
||||
let (nx, ny) = (w - mx, h - my);
|
||||
[
|
||||
rect(0, 0, mx, my),
|
||||
rect(nx, 0, w, my),
|
||||
rect(0, ny, mx, h),
|
||||
rect(nx, ny, w, h),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| bits & (0b1000 >> index) != 0)
|
||||
.map(|(_, piece)| piece)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Powerline's arrows and half circles, filling the cell's height.
|
||||
fn powerline(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
|
||||
let (wf, hf, tf) = (w as f32, h as f32, t as f32);
|
||||
let steps = match code {
|
||||
0xE0B0..=0xE0B3 => vec![
|
||||
Step::Move(0., 0.),
|
||||
Step::Line(wf, hf / 2.),
|
||||
Step::Line(0., hf),
|
||||
],
|
||||
0xE0B4 | 0xE0B6 => half_circle(wf, hf, 0.),
|
||||
_ => half_circle(wf, hf, tf / 2.),
|
||||
};
|
||||
let steps = if matches!(code, 0xE0B2 | 0xE0B3 | 0xE0B6 | 0xE0B7) {
|
||||
mirror(steps, wf)
|
||||
} else {
|
||||
steps
|
||||
};
|
||||
if matches!(code, 0xE0B0 | 0xE0B2 | 0xE0B4 | 0xE0B6) {
|
||||
vec![Shape::Fill(steps)]
|
||||
} else {
|
||||
vec![Shape::Stroke(steps, tf)]
|
||||
}
|
||||
}
|
||||
|
||||
/// The right half of a circle against the left edge, inset `by` so a stroke stays inside.
|
||||
fn half_circle(w: f32, h: f32, by: f32) -> Vec<Step> {
|
||||
let c = (2f32.sqrt() - 1.) * 4. / 3.;
|
||||
let r = w.min(h / 2.) - by;
|
||||
let (top, bottom) = (by, h - by);
|
||||
vec![
|
||||
Step::Move(0., top),
|
||||
Step::Curve([r * c, top, r, top + r - r * c, r, top + r]),
|
||||
Step::Line(r, bottom - r),
|
||||
Step::Curve([r, bottom - r + r * c, r * c, bottom, 0., bottom]),
|
||||
]
|
||||
}
|
||||
|
||||
/// The steps flipped left to right within a cell `w` wide.
|
||||
fn mirror(steps: Vec<Step>, w: f32) -> Vec<Step> {
|
||||
steps
|
||||
.into_iter()
|
||||
.map(|step| match step {
|
||||
Step::Move(x, y) => Step::Move(w - x, y),
|
||||
Step::Line(x, y) => Step::Line(w - x, y),
|
||||
Step::Curve([ax, ay, bx, by, x, y]) => Step::Curve([w - ax, ay, w - bx, by, w - x, y]),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,229 @@
|
||||
use super::*;
|
||||
|
||||
/// Cells as fonts give them: even and odd sides, thin and thick lines.
|
||||
const CELLS: [(u32, u32, u32); 4] = [(16, 32, 2), (9, 19, 1), (20, 41, 3), (7, 15, 1)];
|
||||
|
||||
fn arms(ch: char) -> [u8; 4] {
|
||||
LINES[(ch as u32 - 0x2500) as usize]
|
||||
}
|
||||
|
||||
fn every_drawn() -> impl Iterator<Item = char> {
|
||||
('\u{2500}'..='\u{259F}').chain('\u{E0B0}'..='\u{E0B7}')
|
||||
}
|
||||
|
||||
/// The spans across an edge of the rects touching it; sides go up, right, down, left.
|
||||
fn edge(shapes: &[Shape], side: usize, cell_w: u32, cell_h: u32) -> Vec<(u32, u32)> {
|
||||
let mut spans: Vec<(u32, u32)> = shapes
|
||||
.iter()
|
||||
.filter_map(|shape| match *shape {
|
||||
Shape::Rect { x, y, w, h, .. } => {
|
||||
let touches = [y == 0, x + w == cell_w, y + h == cell_h, x == 0][side];
|
||||
let span = if side.is_multiple_of(2) {
|
||||
(x, x + w)
|
||||
} else {
|
||||
(y, y + h)
|
||||
};
|
||||
touches.then_some(span)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
spans.sort();
|
||||
spans
|
||||
}
|
||||
|
||||
fn points(steps: &[Step]) -> Vec<(f32, f32)> {
|
||||
steps
|
||||
.iter()
|
||||
.flat_map(|step| match *step {
|
||||
Step::Move(x, y) | Step::Line(x, y) => vec![(x, y)],
|
||||
Step::Curve([ax, ay, bx, by, x, y]) => vec![(ax, ay), (bx, by), (x, y)],
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_table_reads_as_unicode_names_say() {
|
||||
assert_eq!(arms('─'), [0, 1, 0, 1]);
|
||||
assert_eq!(arms('┃'), [2, 0, 2, 0]);
|
||||
assert_eq!(arms('┍'), [0, 2, 1, 0], "down light and right heavy");
|
||||
assert_eq!(arms('╒'), [0, 3, 1, 0], "down single and right double");
|
||||
assert_eq!(arms('╬'), [3, 3, 3, 3]);
|
||||
assert_eq!(arms('╽'), [1, 0, 2, 0], "light up and heavy down");
|
||||
assert_eq!(arms('╾'), [0, 1, 0, 2], "heavy left and light right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_drawn_char_draws_inside_its_cell() {
|
||||
for (w, h, t) in CELLS {
|
||||
for ch in every_drawn() {
|
||||
let shapes = shapes(ch, w, h, t);
|
||||
assert!(!shapes.is_empty(), "{ch} draws nothing");
|
||||
for shape in &shapes {
|
||||
match shape {
|
||||
Shape::Rect {
|
||||
x, y, w: rw, h: rh, ..
|
||||
} => {
|
||||
assert!(*rw > 0 && *rh > 0, "{ch} has an empty rect at {w}x{h}");
|
||||
assert!(x + rw <= w && y + rh <= h, "{ch} leaves {w}x{h}");
|
||||
}
|
||||
Shape::Fill(steps) | Shape::Stroke(steps, _) => {
|
||||
let reach = t as f32;
|
||||
for (px, py) in points(steps) {
|
||||
let inside = (-reach..=w as f32 + reach).contains(&px)
|
||||
&& (-reach..=h as f32 + reach).contains(&py);
|
||||
assert!(inside, "{ch} reaches ({px}, {py}) in {w}x{h}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn straight_lines_sit_centered_at_their_weight() {
|
||||
let (w, h, t) = (16, 32, 2);
|
||||
let across = |ch: char, side: usize| edge(&shapes(ch, w, h, t), side, w, h);
|
||||
assert_eq!(across('─', 1), [(15, 17)]);
|
||||
assert_eq!(across('━', 1), [(14, 18)]);
|
||||
assert_eq!(across('═', 1), [(13, 15), (17, 19)]);
|
||||
assert_eq!(across('│', 2), [(7, 9)]);
|
||||
assert_eq!(across('┃', 2), [(6, 10)]);
|
||||
assert_eq!(across('║', 2), [(5, 7), (9, 11)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_arm_meets_the_straight_line_of_its_weight() {
|
||||
for (w, h, t) in CELLS {
|
||||
let across = [['│', '┃', '║'], ['─', '━', '═']];
|
||||
for code in 0x2500..=0x257F_u32 {
|
||||
let ch = char::from_u32(code).expect("a char");
|
||||
let weights = arms(ch);
|
||||
if weights == [0; 4] {
|
||||
continue;
|
||||
}
|
||||
let shapes = shapes(ch, w, h, t);
|
||||
for (side, weight) in weights.into_iter().enumerate() {
|
||||
let found = edge(&shapes, side, w, h);
|
||||
if weight == 0 {
|
||||
assert!(found.is_empty(), "{ch} touches side {side} with no arm");
|
||||
continue;
|
||||
}
|
||||
let straight = across[side % 2][usize::from(weight) - 1];
|
||||
let want = edge(&super::shapes(straight, w, h, t), side, w, h);
|
||||
assert_eq!(
|
||||
found, want,
|
||||
"{ch}'s side {side} misses {straight} in {w}x{h}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashes_tile_with_one_gap_between_cells() {
|
||||
for (w, h, t) in CELLS {
|
||||
for (dashes, count) in [("┄┅┆┇", 3), ("┈┉┊┋", 4), ("╌╍╎╏", 2)] {
|
||||
for ch in dashes.chars() {
|
||||
let vertical = "┆┇┊┋╎╏".contains(ch);
|
||||
let side = if vertical { h } else { w };
|
||||
let drawn = shapes(ch, w, h, t);
|
||||
if side < 2 * count {
|
||||
let solid = if vertical { '│' } else { '─' };
|
||||
assert_eq!(drawn, shapes(solid, w, h, t), "{ch} at {w}x{h} is solid");
|
||||
continue;
|
||||
}
|
||||
let mut runs: Vec<(u32, u32)> = drawn
|
||||
.iter()
|
||||
.map(|shape| match *shape {
|
||||
Shape::Rect { y, h: rh, .. } if vertical => (y, y + rh),
|
||||
Shape::Rect { x, w: rw, .. } => (x, x + rw),
|
||||
_ => panic!("{ch} is drawn with rects"),
|
||||
})
|
||||
.collect();
|
||||
runs.sort();
|
||||
assert_eq!(runs.len() as u32, count, "{ch} at {w}x{h}");
|
||||
let gap = runs[1].0 - runs[0].1;
|
||||
assert!(gap > 0, "{ch} has no gap in {w}x{h}");
|
||||
for pair in runs.windows(2) {
|
||||
assert_eq!(pair[1].0 - pair[0].1, gap, "{ch} gaps unevenly");
|
||||
}
|
||||
let lead = if vertical { 0 } else { gap / 2 };
|
||||
assert_eq!(runs[0].0, lead, "{ch} starts off center at {w}x{h}");
|
||||
let margins = runs[0].0 + side - runs[runs.len() - 1].1;
|
||||
assert_eq!(
|
||||
margins, gap,
|
||||
"{ch} breaks the rhythm across cells at {w}x{h}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halves_cover_the_cell_and_shades_wash_it() {
|
||||
for (w, h, t) in CELLS {
|
||||
let span = |ch: char| match shapes(ch, w, h, t)[0] {
|
||||
Shape::Rect {
|
||||
x, y, w: rw, h: rh, ..
|
||||
} => (x, y, x + rw, y + rh),
|
||||
_ => panic!("{ch} is a rect"),
|
||||
};
|
||||
let (upper, lower) = (span('▀'), span('▄'));
|
||||
assert_eq!((upper.1, lower.3), (0, h));
|
||||
assert!(upper.3 >= lower.1, "a gap between ▀ and ▄ at {w}x{h}");
|
||||
let (left, right) = (span('▌'), span('▐'));
|
||||
assert_eq!((left.0, right.2), (0, w));
|
||||
assert!(left.2 >= right.0, "a gap between ▌ and ▐ at {w}x{h}");
|
||||
for (ch, alpha) in [('░', 0.25), ('▒', 0.5), ('▓', 0.75)] {
|
||||
let want = Shape::Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w,
|
||||
h,
|
||||
alpha,
|
||||
};
|
||||
assert_eq!(shapes(ch, w, h, t), [want]);
|
||||
}
|
||||
assert_eq!(shapes('▚', w, h, t).len(), 2);
|
||||
assert_eq!(shapes('▟', w, h, t).len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powerline_fills_the_cell_and_mirrors() {
|
||||
let (w, h) = (16., 32.);
|
||||
let right = vec![Step::Move(0., 0.), Step::Line(w, h / 2.), Step::Line(0., h)];
|
||||
let left = vec![Step::Move(w, 0.), Step::Line(0., h / 2.), Step::Line(w, h)];
|
||||
assert_eq!(shapes('\u{E0B0}', 16, 32, 2), [Shape::Fill(right.clone())]);
|
||||
assert_eq!(shapes('\u{E0B2}', 16, 32, 2), [Shape::Fill(left)]);
|
||||
assert_eq!(shapes('\u{E0B1}', 16, 32, 2), [Shape::Stroke(right, 2.)]);
|
||||
let Shape::Fill(round) = &shapes('\u{E0B6}', 16, 32, 2)[0] else {
|
||||
panic!("a filled half circle");
|
||||
};
|
||||
assert_eq!(round.first(), Some(&Step::Move(16., 0.)));
|
||||
let Shape::Stroke(outline, 2.) = &shapes('\u{E0B5}', 16, 32, 2)[0] else {
|
||||
panic!("a stroked half circle");
|
||||
};
|
||||
assert_eq!(
|
||||
outline.first(),
|
||||
Some(&Step::Move(0., 1.)),
|
||||
"inset by half a line"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arcs_end_on_the_light_lines_centers() {
|
||||
let (w, h, t) = (16, 32, 2);
|
||||
let light = (w - t) / 2;
|
||||
let Shape::Stroke(path, width) = &shapes('╭', w, h, t)[0] else {
|
||||
panic!("╭ is a stroke");
|
||||
};
|
||||
assert_eq!(*width, t as f32);
|
||||
assert_eq!(path.first(), Some(&Step::Move(light as f32 + 1., h as f32)));
|
||||
assert_eq!(
|
||||
path.last(),
|
||||
Some(&Step::Line(w as f32, ((h - t) / 2) as f32 + 1.))
|
||||
);
|
||||
}
|
||||
+34
-3
@@ -6,7 +6,10 @@ use alacritty_terminal::{
|
||||
};
|
||||
use gpui::Hsla;
|
||||
|
||||
use super::ink::{DIM, Ink};
|
||||
use super::{
|
||||
boxes,
|
||||
ink::{DIM, Ink},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Underline {
|
||||
@@ -39,6 +42,8 @@ pub struct Row {
|
||||
pub runs: Vec<(usize, Style)>,
|
||||
/// Cells with combining marks, painted alone at their column.
|
||||
pub clusters: Vec<(usize, String, Style)>,
|
||||
/// Cells the terminal draws as shapes: column, char, style.
|
||||
pub boxes: Vec<(usize, char, Style)>,
|
||||
}
|
||||
|
||||
/// The cursor where it shows: its cell, shape, and what the cell holds.
|
||||
@@ -70,6 +75,9 @@ impl Frame {
|
||||
for (column, cluster, _) in &row.clusters {
|
||||
cells[*column] = cluster.clone();
|
||||
}
|
||||
for (column, ch, _) in &row.boxes {
|
||||
cells[*column] = ch.to_string();
|
||||
}
|
||||
cells.concat().trim_end().to_string()
|
||||
})
|
||||
.collect();
|
||||
@@ -101,7 +109,8 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
|
||||
.chain(marks.iter().copied())
|
||||
.collect::<String>()
|
||||
});
|
||||
let shown = if spacer || hidden || cluster.is_some() {
|
||||
let drawn = !hidden && cluster.is_none() && boxes::drawn(cell.c);
|
||||
let shown = if spacer || hidden || drawn || cluster.is_some() {
|
||||
' '
|
||||
} else {
|
||||
cell.c
|
||||
@@ -113,13 +122,18 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
|
||||
column: point.column.0,
|
||||
shape: content.cursor.shape,
|
||||
wide: cell.flags.contains(Flags::WIDE_CHAR),
|
||||
text: cluster.clone().unwrap_or_else(|| shown.to_string()),
|
||||
text: cluster
|
||||
.clone()
|
||||
.unwrap_or_else(|| if drawn { cell.c } else { shown }.to_string()),
|
||||
style: look,
|
||||
});
|
||||
}
|
||||
if let Some(cluster) = cluster {
|
||||
rows[row].clusters.push((point.column.0, cluster, look));
|
||||
}
|
||||
if drawn {
|
||||
rows[row].boxes.push((point.column.0, cell.c, look));
|
||||
}
|
||||
}
|
||||
Frame { rows, cursor }
|
||||
}
|
||||
@@ -256,6 +270,23 @@ mod tests {
|
||||
assert_eq!(hidden.cursor, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawn_characters_leave_the_font() {
|
||||
let (frame, ink) = shown("a\u{2500}\u{E0B0}\x1b[1D".as_bytes());
|
||||
assert_eq!(&frame.rows[0].text[..3], "a ");
|
||||
let drawn: Vec<_> = frame.rows[0]
|
||||
.boxes
|
||||
.iter()
|
||||
.map(|(at, ch, _)| (*at, *ch))
|
||||
.collect();
|
||||
assert_eq!(drawn, [(1, '\u{2500}'), (2, '\u{E0B0}')]);
|
||||
assert_eq!(frame.rows[0].boxes[0].2.fg, ink.fg);
|
||||
assert_eq!(frame.text(), "a\u{2500}\u{E0B0}");
|
||||
assert_eq!(frame.cursor.expect("a cursor").text, "\u{E0B0}");
|
||||
let (hidden, _) = shown(b"\x1b[8m\xe2\x94\x80");
|
||||
assert!(hidden.rows[0].boxes.is_empty(), "hidden lines draw nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_cells_show_nothing_but_their_ground() {
|
||||
let (frame, _) = shown("\x1b[8;4;9;41mSe\u{301}\x1b[0m\x1b[2D".as_bytes());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod boxes;
|
||||
pub mod frame;
|
||||
pub mod ink;
|
||||
mod paint;
|
||||
|
||||
+78
-13
@@ -2,12 +2,13 @@ use std::rc::Rc;
|
||||
|
||||
use alacritty_terminal::vte::ansi::CursorShape;
|
||||
use gpui::{
|
||||
Bounds, Entity, Font, FontId, FontStyle, FontWeight, Hsla, IntoElement, Pixels, Point, Size,
|
||||
StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window, canvas, fill, outline, point, px,
|
||||
size,
|
||||
Bounds, ContentMask, Entity, Font, FontId, FontStyle, FontWeight, Hsla, IntoElement,
|
||||
PathBuilder, Pixels, Point, Size, StrikethroughStyle, Styled, TextRun, UnderlineStyle, Window,
|
||||
canvas, fill, outline, point, px, size,
|
||||
};
|
||||
|
||||
use super::{
|
||||
boxes::{self, Shape, Step},
|
||||
frame::{Cursor, Frame, Style, Underline},
|
||||
view::TerminalView,
|
||||
};
|
||||
@@ -54,12 +55,15 @@ pub fn grid(
|
||||
bounds,
|
||||
frame.rows.first().map(|row| row.text.trim_end())
|
||||
);
|
||||
let scale = window.scale_factor();
|
||||
let snap = |length: Pixels| px((f32::from(length) * scale).round() / scale);
|
||||
let origin = point(snap(bounds.origin.x), snap(bounds.origin.y));
|
||||
for (index, row) in frame.rows.iter().enumerate() {
|
||||
let top = bounds.origin + point(px(0.), cell.height * index as f32);
|
||||
let top = origin + point(px(0.), cell.height * index as f32);
|
||||
paint_row(row, top, &look, window);
|
||||
}
|
||||
if let Some((cursor, shape)) = &cursor {
|
||||
paint_cursor(cursor, *shape, bounds.origin, &look, window);
|
||||
paint_cursor(cursor, *shape, origin, &look, window);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -121,6 +125,9 @@ fn paint_row(row: &super::frame::Row, top: Point<Pixels>, look: &Look, window: &
|
||||
for (column, cluster, style) in &row.clusters {
|
||||
text(cluster, style.fg, style, at(*column), look, window);
|
||||
}
|
||||
for (column, ch, style) in &row.boxes {
|
||||
paint_drawn(*ch, style.fg, at(*column), look, window);
|
||||
}
|
||||
for (column, columns, style) in spans {
|
||||
let width = cell.width * columns as f32;
|
||||
decorate(at(column), width, &style, look, window);
|
||||
@@ -224,6 +231,57 @@ fn upward(y: Pixels) -> Pixels {
|
||||
y
|
||||
}
|
||||
|
||||
/// Paints `ch` as shapes in `color`, clipped to the cell at `corner`, a device pixel corner.
|
||||
fn paint_drawn(ch: char, color: Hsla, corner: Point<Pixels>, look: &Look, window: &mut Window) {
|
||||
let scale = window.scale_factor();
|
||||
let device = |length: Pixels| (f32::from(length) * scale).round() as u32;
|
||||
let (w, h, t) = (
|
||||
device(look.cell.width),
|
||||
device(look.cell.height),
|
||||
device(look.rule).max(1),
|
||||
);
|
||||
let at = |x: f32, y: f32| corner + point(px(x / scale), px(y / scale));
|
||||
let mask = ContentMask {
|
||||
bounds: Bounds::new(corner, look.cell),
|
||||
};
|
||||
window.with_content_mask(Some(mask), |window| {
|
||||
for piece in boxes::shapes(ch, w, h, t) {
|
||||
let (builder, steps, close) = match piece {
|
||||
Shape::Rect { x, y, w, h, alpha } => {
|
||||
let length = |device: u32| px(device as f32 / scale);
|
||||
let bounds = Bounds::new(at(x as f32, y as f32), size(length(w), length(h)));
|
||||
window.paint_quad(fill(bounds, color.opacity(alpha)));
|
||||
continue;
|
||||
}
|
||||
Shape::Fill(steps) => (PathBuilder::fill(), steps, true),
|
||||
Shape::Stroke(steps, width) => {
|
||||
(PathBuilder::stroke(px(width / scale)), steps, false)
|
||||
}
|
||||
};
|
||||
let mut builder = builder;
|
||||
for step in steps {
|
||||
match step {
|
||||
Step::Move(x, y) => builder.move_to(at(x, y)),
|
||||
Step::Line(x, y) => builder.line_to(at(x, y)),
|
||||
Step::Curve([ax, ay, bx, by, x, y]) => {
|
||||
builder.cubic_bezier_to(at(x, y), at(ax, ay), at(bx, by))
|
||||
}
|
||||
}
|
||||
}
|
||||
if close {
|
||||
builder.close();
|
||||
}
|
||||
match builder.build() {
|
||||
Ok(path) => window.paint_path(path, color),
|
||||
Err(error) => log::error!(
|
||||
"terminal: U+{:04X} failed to tessellate: {error:?}",
|
||||
ch as u32
|
||||
),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Underlines and strikethrough under `width` from `origin`, a cell's top-left.
|
||||
fn decorate(origin: Point<Pixels>, width: Pixels, style: &Style, look: &Look, window: &mut Window) {
|
||||
let rule = look.rule;
|
||||
@@ -297,14 +355,20 @@ fn paint_cursor(
|
||||
match shape {
|
||||
CursorShape::Block => {
|
||||
window.paint_quad(fill(whole, look.cursor));
|
||||
text(
|
||||
&cursor.text,
|
||||
look.cursor_text,
|
||||
&cursor.style,
|
||||
corner,
|
||||
look,
|
||||
window,
|
||||
);
|
||||
let mut chars = cursor.text.chars();
|
||||
match (chars.next(), chars.next()) {
|
||||
(Some(ch), None) if boxes::drawn(ch) => {
|
||||
paint_drawn(ch, look.cursor_text, corner, look, window)
|
||||
}
|
||||
_ => text(
|
||||
&cursor.text,
|
||||
look.cursor_text,
|
||||
&cursor.style,
|
||||
corner,
|
||||
look,
|
||||
window,
|
||||
),
|
||||
}
|
||||
}
|
||||
CursorShape::HollowBlock => {
|
||||
window.paint_quad(outline(whole, look.cursor, gpui::BorderStyle::Solid));
|
||||
@@ -349,6 +413,7 @@ mod tests {
|
||||
text: "中 ab".into(),
|
||||
runs: vec![(4, style), (2, bold)],
|
||||
clusters: Vec::new(),
|
||||
boxes: Vec::new(),
|
||||
};
|
||||
let spans: Vec<_> = spans(&row).into_iter().map(|(at, n, _)| (at, n)).collect();
|
||||
assert_eq!(spans, [(0, 2), (2, 2)]);
|
||||
|
||||
Reference in new issue
Block a user