T07: one coverage per pixel (overlaps cut, strokes as polygons, the cross cut around its first band); a drawn base keeps its geometry under marks
CI / platforms (windows-latest) (push) Failing after 7s
CI / macos (push) Failing after 8m53s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-14 13:57:47 -04:00
1 parent 387bfa1c8b
commit 2804aa6117
6 files changed
+506 -318

No files matched your search

+94 -162
View File
@@ -1,13 +1,9 @@
/// 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]),
}
mod outline;
/// A piece of a drawn character, in device pixels from the cell's top-left.
/// A point in device pixels from the cell's top-left.
pub type Point = (f32, f32);
/// A piece of a drawn character in device pixels from the cell's top-left; pieces never overlap.
#[derive(Clone, Debug, PartialEq)]
pub enum Shape {
/// A rectangle filled at `alpha` of the text color.
@@ -18,10 +14,8 @@ pub enum Shape {
h: u32,
alpha: f32,
},
/// A closed outline, filled.
Fill(Vec<Step>),
/// An open path, stroked this wide with butt ends.
Stroke(Vec<Step>, f32),
/// A simple polygon, filled.
Fill(Vec<Point>),
}
/// Whether the terminal draws `ch` as shapes instead of through the font.
@@ -60,28 +54,65 @@ const QUADRANTS: [u8; 10] = [
0b0010, 0b0001, 0b1000, 0b1011, 0b1001, 0b1110, 0b1101, 0b0100, 0b0110, 0b0111,
];
/// A box by its corners: left, top, right, bottom.
type Corners = [u32; 4];
/// 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),
0x2504..=0x250B | 0x254C..=0x254F => rects(dashes(code, w, h, t), 1.),
0x256D..=0x2570 => outline::arc(code, w, h, t),
0x2571..=0x2573 => outline::diagonals(code, w, h, t),
0x2500..=0x257F => rects(lines(LINES[(code - 0x2500) as usize], w, h, t), 1.),
0x2591..=0x2593 => rects(vec![[0, 0, w, h]], (code - 0x2590) as f32 / 4.),
0x2580..=0x259F => rects(blocks(code, w, h), 1.),
0xE0B0..=0xE0B7 => outline::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.,
/// Rects from boxes with their overlaps cut away, so each pixel takes the color once.
fn rects(boxes: Vec<Corners>, alpha: f32) -> Vec<Shape> {
let mut kept: Vec<Corners> = Vec::new();
for corners in boxes {
let mut pieces = vec![corners];
for seen in &kept {
pieces = pieces
.into_iter()
.flat_map(|piece| outside(piece, *seen))
.collect();
}
kept.extend(pieces);
}
kept.into_iter()
.filter(|[x0, y0, x1, y1]| x1 > x0 && y1 > y0)
.map(|[x0, y0, x1, y1]| Shape::Rect {
x: x0,
y: y0,
w: x1 - x0,
h: y1 - y0,
alpha,
})
.collect()
}
/// The parts of `piece` outside `cut`.
fn outside(piece: Corners, cut: Corners) -> Vec<Corners> {
let ([x0, y0, x1, y1], [cx0, cy0, cx1, cy1]) = (piece, cut);
if cx0 >= x1 || cx1 <= x0 || cy0 >= y1 || cy1 <= y0 {
return vec![piece];
}
let (top, bottom) = (cy0.max(y0), cy1.min(y1));
[
[x0, y0, x1, top],
[x0, bottom, x1, y1],
[x0, top, cx0.max(x0), bottom],
[cx1.min(x1), top, x1, bottom],
]
.into_iter()
.filter(|[a, b, c, d]| c > a && d > b)
.collect()
}
/// Where an arm ends at the center, by the arms crossing it: `[heavy, double, light, short]`.
@@ -102,7 +133,7 @@ fn stop((a, b): (u8, u8), straight: bool, [heavy, double, light, short]: [u32; 4
}
/// 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> {
fn lines([up, right, down, left]: [u8; 4], w: u32, h: u32, t: u32) -> Vec<Corners> {
let heavy = 2 * t;
let h_light_top = h.saturating_sub(t) / 2;
let h_light_bottom = h_light_top + t;
@@ -139,39 +170,34 @@ fn lines([up, right, down, left]: [u8; 4], w: u32, h: u32, t: u32) -> Vec<Shape>
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)),
LIGHT => out.push([v_light_left, 0, v_light_right, up_bottom]),
HEAVY => out.push([v_heavy_left, 0, v_heavy_right, up_bottom]),
DOUBLE => {
let stop = |side| {
let end = |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)));
out.push([v_double_left, 0, v_light_left, end(left)]);
out.push([v_light_right, 0, v_double_right, end(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)),
LIGHT => out.push([right_left, h_light_top, w, h_light_bottom]),
HEAVY => out.push([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,
));
out.push([start(top_double), h_double_top, w, h_light_top]);
out.push([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)),
LIGHT => out.push([v_light_left, down_top, v_light_right, h]),
HEAVY => out.push([v_heavy_left, down_top, v_heavy_right, h]),
DOUBLE => {
let start = |side| {
if side == DOUBLE {
@@ -180,18 +206,18 @@ fn lines([up, right, down, left]: [u8; 4], w: u32, h: u32, t: u32) -> Vec<Shape>
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));
out.push([v_double_left, start(left), v_light_left, h]);
out.push([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)),
LIGHT => out.push([0, h_light_top, left_right, h_light_bottom]),
HEAVY => out.push([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.push([0, h_double_top, end(top_double), h_light_top]);
out.push([0, h_light_bottom, end(bottom_double), h_double_bottom]);
}
_ => {}
}
@@ -199,7 +225,7 @@ fn lines([up, right, down, left]: [u8; 4], w: u32, h: u32, t: u32) -> Vec<Shape>
}
/// 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> {
fn dashes(code: u32, w: u32, h: u32, t: u32) -> Vec<Corners> {
let wide = t.max(4);
let (count, thick, gap, vertical) = match code {
0x2504 => (3, t, wide, false),
@@ -218,9 +244,9 @@ fn dashes(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
let (along, across) = if vertical { (h, w) } else { (w, h) };
if along < 2 * count {
let solid = if vertical {
[LIGHT, 0, LIGHT, 0]
[LIGHT, NONE, LIGHT, NONE]
} else {
[0, LIGHT, 0, LIGHT]
[NONE, LIGHT, NONE, LIGHT]
};
return lines(solid, w, h, t);
}
@@ -234,9 +260,9 @@ fn dashes(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
let end = at + dash + u32::from(extra > 0);
extra = extra.saturating_sub(1);
let piece = if vertical {
rect(side, at, side + thick, end)
[side, at, side + thick, end]
} else {
rect(at, side, end, side + thick)
[at, side, end, side + thick]
};
at = end + gap;
piece
@@ -244,130 +270,36 @@ fn dashes(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
.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> {
/// Block elements but the shades: eighths, halves and quadrants.
fn blocks(code: u32, w: u32, h: u32) -> Vec<Corners> {
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)],
0x2580 => vec![[0, 0, w, part(0.5, h)]],
0x2581..=0x2587 => vec![[0, h - part(eighths(code - 0x2580), h), w, h]],
0x2588 => vec![[0, 0, w, h]],
0x2589..=0x258F => vec![[0, 0, part(eighths(0x2590 - code), w), h]],
0x2590 => vec![[w - part(0.5, w), 0, w, h]],
0x2594 => vec![[0, 0, w, part(0.125, h)]],
0x2595 => vec![[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),
[0, 0, mx, my],
[nx, 0, w, my],
[0, ny, mx, h],
[nx, ny, w, h],
]
.into_iter()
.enumerate()
.filter(|(index, _)| bits & (0b1000 >> index) != 0)
.map(|(_, piece)| piece)
.map(|(_, corners)| corners)
.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;
+170
View File
@@ -0,0 +1,170 @@
use super::{Point, Shape};
/// Straight pieces a cubic curve is cut into.
const SEGMENTS: usize = 16;
/// A rounded corner from the light lines' centers to two edges.
pub(super) 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 (bend, end) = ((cx, cy + dy * r), (cx + dx * r, cy));
let (lead_in, lead_out) = (
(cx, if dy > 0. { hf } else { 0. }),
(if dx > 0. { wf } else { 0. }, cy),
);
let mut path = Vec::new();
if dy * (lead_in.1 - bend.1) > 0. {
path.push(lead_in);
}
path.push(bend);
path.extend(cubic(
bend,
[(cx, cy + dy * s * r), (cx + dx * s * r, cy), end],
));
if dx * (lead_out.0 - end.0) > 0. {
path.push(lead_out);
}
vec![Shape::Fill(stroke(&path, tf / 2.))]
}
/// Corner-to-corner bands, run half a pixel past so neighbors join once clipped.
pub(super) fn diagonals(code: u32, w: u32, h: u32, t: u32) -> Vec<Shape> {
let (wf, hf, half) = (w as f32, h as f32, t as f32 / 2.);
let (sx, sy) = ((wf / hf).min(1.) / 2., (hf / wf).min(1.) / 2.);
let rising = [(wf + sx, -sy), (-sx, hf + sy)];
let falling = [(-sx, -sy), (wf + sx, hf + sy)];
match code {
0x2571 => vec![Shape::Fill(stroke(&rising, half))],
0x2572 => vec![Shape::Fill(stroke(&falling, half))],
_ => {
let band = stroke(&falling, half);
let n = normal(rising[0], rising[1]);
let at = n.0 * rising[0].0 + n.1 * rising[0].1;
let before = clip(&band, (-n.0, -n.1), half - at);
let after = clip(&band, n, at + half);
[stroke(&rising, half), before, after]
.into_iter()
.filter(|polygon| polygon.len() >= 3)
.map(Shape::Fill)
.collect()
}
}
}
/// Powerline's arrows and half circles, filling the cell's height.
pub(super) 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 arrow = vec![(0., 0.), (wf, hf / 2.), (0., hf)];
let polygon = match code {
0xE0B0 | 0xE0B2 => arrow,
0xE0B1 | 0xE0B3 => stroke(&arrow, tf / 2.),
0xE0B4 | 0xE0B6 => half_circle(wf, hf, 0.),
_ => stroke(&half_circle(wf, hf, tf / 2.), tf / 2.),
};
let polygon = if matches!(code, 0xE0B2 | 0xE0B3 | 0xE0B6 | 0xE0B7) {
polygon.into_iter().map(|(x, y)| (wf - x, y)).collect()
} else {
polygon
};
vec![Shape::Fill(polygon)]
}
/// The right half of a circle against the left edge, inset `by` so a stroke stays inside; its ends run straight a pixel, as Ghostty's do.
fn half_circle(w: f32, h: f32, by: f32) -> Vec<Point> {
let c = (2f32.sqrt() - 1.) * 4. / 3.;
let r = w.min(h / 2.) - by;
let (top, bottom) = (by, h - by);
let mut path = vec![(0., top), (1., top)];
path.extend(cubic(
(1., top),
[(r * c, top), (r, top + r - r * c), (r, top + r)],
));
path.push((r, bottom - r));
path.extend(cubic(
(r, bottom - r),
[(r, bottom - r + r * c), (r * c, bottom), (1., bottom)],
));
path.push((0., bottom));
distinct(&path)
}
/// Points along a cubic after `from`, its end the last.
fn cubic(from: Point, [a, b, to]: [Point; 3]) -> impl Iterator<Item = Point> {
(1..=SEGMENTS).map(move |step| {
let t = step as f32 / SEGMENTS as f32;
let u = 1. - t;
let mix = |p: f32, q: f32, r: f32, s: f32| {
u * u * u * p + 3. * u * u * t * q + 3. * u * t * t * r + t * t * t * s
};
(mix(from.0, a.0, b.0, to.0), mix(from.1, a.1, b.1, to.1))
})
}
/// The path without points that repeat the one before.
fn distinct(path: &[Point]) -> Vec<Point> {
let mut points: Vec<Point> = Vec::with_capacity(path.len());
for &point in path {
let repeat = points
.last()
.is_some_and(|last| (point.0 - last.0).hypot(point.1 - last.1) < 1e-3);
if !repeat {
points.push(point);
}
}
points
}
/// The outline of `path` stroked `half` each side, with butt ends and mitered corners.
fn stroke(path: &[Point], half: f32) -> Vec<Point> {
let points = distinct(path);
assert!(points.len() >= 2, "a stroke needs two distinct points");
let last = points.len() - 1;
let offsets: Vec<Point> = (0..=last)
.map(|index| {
let before = normal(points[index.saturating_sub(1)], points[index.max(1)]);
let after = normal(points[index.min(last - 1)], points[(index + 1).min(last)]);
let mean = ((before.0 + after.0) / 2., (before.1 + after.1) / 2.);
let scale = half / (mean.0 * mean.0 + mean.1 * mean.1);
(mean.0 * scale, mean.1 * scale)
})
.collect();
let ahead = points
.iter()
.zip(&offsets)
.map(|(p, o)| (p.0 + o.0, p.1 + o.1));
let back = points
.iter()
.zip(&offsets)
.rev()
.map(|(p, o)| (p.0 - o.0, p.1 - o.1));
ahead.chain(back).collect()
}
/// The unit normal of the segment from `from` to `to`.
pub(super) fn normal(from: Point, to: Point) -> Point {
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let length = dx.hypot(dy);
(-dy / length, dx / length)
}
/// The part of convex `polygon` where `n · p >= at`.
fn clip(polygon: &[Point], n: Point, at: f32) -> Vec<Point> {
let side = |p: Point| n.0 * p.0 + n.1 * p.1 - at;
let mut kept = Vec::new();
for (index, &p) in polygon.iter().enumerate() {
let q = polygon[(index + 1) % polygon.len()];
let (sp, sq) = (side(p), side(q));
if sp >= 0. {
kept.push(p);
}
if (sp >= 0.) != (sq >= 0.) {
let k = sp / (sp - sq);
kept.push((p.0 + (q.0 - p.0) * k, p.1 + (q.1 - p.1) * k));
}
}
kept
}
+160 -66
View File
@@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use super::*;
/// Cells as fonts give them: even and odd sides, thin and thick lines.
@@ -11,35 +13,49 @@ 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 corners(shape: &Shape) -> Option<Corners> {
match *shape {
Shape::Rect { x, y, w, h, .. } => Some([x, y, x + w, y + h]),
Shape::Fill(_) => None,
}
}
fn pixels(boxes: &[Corners]) -> BTreeSet<(u32, u32)> {
boxes
.iter()
.flat_map(|&[x0, y0, x1, y1]| (y0..y1).flat_map(move |y| (x0..x1).map(move |x| (x, y))))
.collect()
}
/// The merged 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)
.filter_map(corners)
.filter(|&[x0, y0, x1, y1]| [y0 == 0, x1 == cell_w, y1 == cell_h, x0 == 0][side])
.map(|[x0, y0, x1, y1]| {
if side.is_multiple_of(2) {
(x0, x1)
} else {
(y0, y1)
}
_ => None,
})
.collect();
spans.sort();
spans
let mut merged: Vec<(u32, u32)> = Vec::new();
for span in spans {
match merged.last_mut() {
Some(last) if span.0 <= last.1 => last.1 = last.1.max(span.1),
_ => merged.push(span),
}
}
merged
}
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()
/// Whether segments `a`-`b` and `c`-`d` cross inside both.
fn cross(a: Point, b: Point, c: Point, d: Point) -> bool {
let turn = |p: Point, q: Point, r: Point| (q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0);
turn(c, d, a) * turn(c, d, b) < -1e-4 && turn(a, b, c) * turn(a, b, d) < -1e-4
}
#[test]
@@ -67,9 +83,10 @@ fn every_drawn_char_draws_inside_its_cell() {
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) {
Shape::Fill(polygon) => {
assert!(polygon.len() >= 3, "{ch} has a degenerate polygon");
let reach = 2. * t as f32;
for &(px, py) in polygon {
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}");
@@ -81,6 +98,90 @@ fn every_drawn_char_draws_inside_its_cell() {
}
}
#[test]
fn no_pixel_is_painted_twice() {
for (w, h, t) in CELLS {
for ch in every_drawn() {
let shapes = shapes(ch, w, h, t);
let rects: Vec<Corners> = shapes.iter().filter_map(corners).collect();
for (index, a) in rects.iter().enumerate() {
for b in &rects[index + 1..] {
let apart = a[2] <= b[0] || b[2] <= a[0] || a[3] <= b[1] || b[3] <= a[1];
assert!(apart, "{ch} overlaps {a:?} and {b:?} at {w}x{h}");
}
}
for shape in &shapes {
let Shape::Fill(polygon) = shape else {
continue;
};
let n = polygon.len();
for i in 0..n {
for j in i + 2..n {
if i == 0 && j == n - 1 {
continue;
}
let (a, b) = (polygon[i], polygon[i + 1]);
let (c, d) = (polygon[j], polygon[(j + 1) % n]);
assert!(
!cross(a, b, c, d),
"{ch}'s outline crosses itself at {w}x{h}"
);
}
}
}
}
}
}
#[test]
fn cutting_overlaps_keeps_every_pixel() {
for (w, h, t) in CELLS {
for code in 0x2500..=0x259F_u32 {
let boxes = match code {
0x2504..=0x250B | 0x254C..=0x254F => dashes(code, w, h, t),
0x256D..=0x2573 | 0x2591..=0x2593 => continue,
0x2500..=0x257F => lines(LINES[(code - 0x2500) as usize], w, h, t),
_ => blocks(code, w, h),
};
let kept: Vec<Corners> = rects(boxes.clone(), 1.)
.iter()
.filter_map(corners)
.collect();
assert_eq!(pixels(&kept), pixels(&boxes), "U+{code:04X} at {w}x{h}");
}
}
}
#[test]
fn the_cross_cuts_its_second_band_around_the_first() {
for (w, h, t) in CELLS {
let shapes = shapes('╳', w, h, t);
let [Shape::Fill(first), pieces @ ..] = shapes.as_slice() else {
panic!("╳ is polygons");
};
assert_eq!(
pieces.len(),
2,
"two pieces around the first band at {w}x{h}"
);
let n = outline::normal(first[0], first[1]);
let line = |p: Point| n.0 * p.0 + n.1 * p.1;
let centre = (line(first[0]) + line(first[3])) / 2.;
for piece in pieces {
let Shape::Fill(piece) = piece else {
panic!("pieces are polygons");
};
for &p in piece {
let off = (line(p) - centre).abs();
assert!(
off >= t as f32 / 2. - 1e-3,
"{p:?} is inside the first band at {w}x{h}"
);
}
}
}
}
#[test]
fn straight_lines_sit_centered_at_their_weight() {
let (w, h, t) = (16, 32, 2);
@@ -136,11 +237,8 @@ fn dashes_tile_with_one_gap_between_cells() {
}
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"),
})
.filter_map(corners)
.map(|[x0, y0, x1, y1]| if vertical { (y0, y1) } else { (x0, x1) })
.collect();
runs.sort();
assert_eq!(runs.len() as u32, count, "{ch} at {w}x{h}");
@@ -164,18 +262,13 @@ fn dashes_tile_with_one_gap_between_cells() {
#[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 span = |ch: char| corners(&shapes(ch, w, h, t)[0]).expect("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}");
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}");
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,
@@ -186,44 +279,45 @@ fn halves_cover_the_cell_and_shades_wash_it() {
};
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");
let fill = |ch: char| match shapes(ch, 16, 32, 2).as_slice() {
[Shape::Fill(polygon)] => polygon.clone(),
other => panic!("{ch} is one polygon, not {other:?}"),
};
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"
assert_eq!(fill('\u{E0B0}'), [(0., 0.), (16., 16.), (0., 32.)]);
assert_eq!(fill('\u{E0B2}'), [(16., 0.), (0., 16.), (16., 32.)]);
assert_eq!(fill('\u{E0B1}').len(), 6, "a mitered chevron");
assert_eq!(fill('\u{E0B6}')[0], (16., 0.));
let outline = fill('\u{E0B5}');
let low = outline.iter().map(|p| p.1).fold(f32::MAX, f32::min);
let high = outline.iter().map(|p| p.1).fold(f32::MIN, f32::max);
let right = outline.iter().map(|p| p.0).fold(f32::MIN, f32::max);
assert!(
low.abs() < 1e-3 && (high - 32.).abs() < 1e-3,
"{low}..{high}"
);
assert!(
right <= 16.01,
"the outline stays in the half circle: {right}"
);
}
#[test]
fn arcs_end_on_the_light_lines_centers() {
fn arcs_end_on_the_light_lines() {
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");
let drawn = shapes('╭', w, h, t);
let [Shape::Fill(outline)] = drawn.as_slice() else {
panic!("╭ is one polygon");
};
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.))
);
let near = |x: f32, y: f32| {
outline
.iter()
.any(|p| (p.0 - x).abs() < 0.05 && (p.1 - y).abs() < 0.05)
};
assert!(near(7., 32.) && near(9., 32.), "the bottom end spans │");
assert!(near(16., 15.) && near(16., 17.), "the right end spans ─");
}
+29 -40
View File
@@ -40,10 +40,8 @@ pub struct Style {
pub struct Row {
pub text: String,
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)>,
/// Cells painted on their own at their column: combining clusters and drawn characters.
pub alone: Vec<(usize, String, Style)>,
}
/// The cursor where it shows: its cell, shape, and what the cell holds.
@@ -72,11 +70,8 @@ impl Frame {
.iter()
.map(|row| {
let mut cells: Vec<String> = row.text.chars().map(String::from).collect();
for (column, cluster, _) in &row.clusters {
cells[*column] = cluster.clone();
}
for (column, ch, _) in &row.boxes {
cells[*column] = ch.to_string();
for (column, text, _) in &row.alone {
cells[*column] = text.clone();
}
cells.concat().trim_end().to_string()
})
@@ -100,17 +95,13 @@ pub fn frame<T: EventListener>(term: &Term<T>, ink: &Ink) -> Frame {
.flags
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER);
let hidden = cell.flags.contains(Flags::HIDDEN);
let cluster = cell
.zerowidth()
.filter(|_| !hidden)
.filter(|marks| !marks.is_empty())
.map(|marks| {
std::iter::once(cell.c)
.chain(marks.iter().copied())
.collect::<String>()
});
let drawn = !hidden && cluster.is_none() && boxes::drawn(cell.c);
let shown = if spacer || hidden || drawn || cluster.is_some() {
let marks = cell.zerowidth().filter(|marks| !marks.is_empty());
let alone = (!hidden && (marks.is_some() || boxes::drawn(cell.c))).then(|| {
std::iter::once(cell.c)
.chain(marks.into_iter().flatten().copied())
.collect::<String>()
});
let shown = if spacer || hidden || alone.is_some() {
' '
} else {
cell.c
@@ -122,17 +113,12 @@ 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(|| if drawn { cell.c } else { shown }.to_string()),
text: alone.clone().unwrap_or_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));
if let Some(alone) = alone {
rows[row].alone.push((point.column.0, alone, look));
}
}
Frame { rows, cursor }
@@ -231,7 +217,7 @@ mod tests {
let (frame, _) = shown("e\u{301}x".as_bytes());
assert_eq!(frame.text(), "e\u{301}x");
assert_eq!(&frame.rows[0].text[..2], " x");
assert_eq!(frame.rows[0].clusters[0].1, "e\u{301}");
assert_eq!(frame.rows[0].alone[0].1, "e\u{301}");
}
#[test]
@@ -271,27 +257,30 @@ mod tests {
}
#[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
fn drawn_characters_leave_the_font_with_their_marks() {
let (frame, ink) = shown("a\u{2500}\u{E0B0}\u{2500}\u{301}\x1b[2D".as_bytes());
assert_eq!(&frame.rows[0].text[..4], "a ");
let alone: Vec<_> = frame.rows[0]
.alone
.iter()
.map(|(at, ch, _)| (*at, *ch))
.map(|(at, text, _)| (*at, text.as_str()))
.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!(
alone,
[(1, "\u{2500}"), (2, "\u{E0B0}"), (3, "\u{2500}\u{301}")]
);
assert_eq!(frame.rows[0].alone[0].2.fg, ink.fg);
assert_eq!(frame.text(), "a\u{2500}\u{E0B0}\u{2500}\u{301}");
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");
assert!(hidden.rows[0].alone.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());
assert_eq!(frame.rows[0].text.trim_end(), "");
assert!(frame.rows[0].clusters.is_empty());
assert!(frame.rows[0].alone.is_empty());
let (_, style) = frame.rows[0].runs[0];
assert_eq!((style.underline, style.strike), (Underline::None, false));
assert!(style.bg.is_some(), "the ground stays");
+51 -48
View File
@@ -8,7 +8,7 @@ use gpui::{
};
use super::{
boxes::{self, Shape, Step},
boxes::{self, Shape},
frame::{Cursor, Frame, Style, Underline},
view::TerminalView,
};
@@ -122,11 +122,8 @@ fn paint_row(row: &super::frame::Row, top: Point<Pixels>, look: &Look, window: &
})
.collect();
draw(&row.text, &runs, top, Some(cell.width), 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, content, style) in &row.alone {
paint_alone(content, style.fg, style, at(*column), look, window);
}
for (column, columns, style) in spans {
let width = cell.width * columns as f32;
@@ -231,6 +228,29 @@ fn upward(y: Pixels) -> Pixels {
y
}
/// Paints a cell's text alone at `corner`: a drawn base as shapes, its marks over a no-break space.
fn paint_alone(
content: &str,
color: Hsla,
style: &Style,
corner: Point<Pixels>,
look: &Look,
window: &mut Window,
) {
let mut chars = content.chars();
match chars.next() {
Some(base) if boxes::drawn(base) => {
paint_drawn(base, color, corner, look, window);
let marks = chars.as_str();
if !marks.is_empty() {
let shown = format!("\u{A0}{marks}");
text(&shown, color, style, corner, look, window);
}
}
_ => text(content, color, style, corner, look, window),
}
}
/// 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();
@@ -240,44 +260,34 @@ fn paint_drawn(ch: char, color: Hsla, corner: Point<Pixels>, look: &Look, window
device(look.cell.height),
device(look.rule).max(1),
);
let at = |x: f32, y: f32| corner + point(px(x / scale), px(y / scale));
let at = |(x, y): (f32, 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 {
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)));
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))
Shape::Fill(points) => {
let mut builder = PathBuilder::fill();
builder.move_to(at(points[0]));
for &corner in &points[1..] {
builder.line_to(at(corner));
}
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
),
}
}
}
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
),
}
}
});
}
@@ -355,20 +365,14 @@ fn paint_cursor(
match shape {
CursorShape::Block => {
window.paint_quad(fill(whole, look.cursor));
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,
),
}
paint_alone(
&cursor.text,
look.cursor_text,
&cursor.style,
corner,
look,
window,
);
}
CursorShape::HollowBlock => {
window.paint_quad(outline(whole, look.cursor, gpui::BorderStyle::Solid));
@@ -412,8 +416,7 @@ mod tests {
let row = Row {
text: "中 ab".into(),
runs: vec![(4, style), (2, bold)],
clusters: Vec::new(),
boxes: Vec::new(),
alone: Vec::new(),
};
let spans: Vec<_> = spans(&row).into_iter().map(|(at, n, _)| (at, n)).collect();
assert_eq!(spans, [(0, 2), (2, 2)]);