forked from NotAShelf/beer
render: draw underline styles, strike, overline, and dim
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I0cf6a44446240a59c2fc8c6735afaf1d6a6a6964
This commit is contained in:
parent
6b3c8dc059
commit
2afb4875be
3 changed files with 141 additions and 24 deletions
24
src/grid.rs
24
src/grid.rs
|
|
@ -20,13 +20,13 @@ impl Flags {
|
|||
pub const BOLD: Self = Self(1 << 0);
|
||||
pub const DIM: Self = Self(1 << 1);
|
||||
pub const ITALIC: Self = Self(1 << 2);
|
||||
pub const UNDERLINE: Self = Self(1 << 3);
|
||||
pub const BLINK: Self = Self(1 << 4);
|
||||
pub const REVERSE: Self = Self(1 << 5);
|
||||
pub const HIDDEN: Self = Self(1 << 6);
|
||||
pub const STRIKE: Self = Self(1 << 7);
|
||||
/// Trailing column of a double-width glyph; holds no character of its own.
|
||||
pub const WIDE_CONT: Self = Self(1 << 8);
|
||||
pub const OVERLINE: Self = Self(1 << 9);
|
||||
|
||||
pub const fn empty() -> Self {
|
||||
Self(0)
|
||||
|
|
@ -49,6 +49,18 @@ impl Flags {
|
|||
}
|
||||
}
|
||||
|
||||
/// Underline style (SGR 4 / 4:x / 21).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum Underline {
|
||||
#[default]
|
||||
None,
|
||||
Single,
|
||||
Double,
|
||||
Curly,
|
||||
Dotted,
|
||||
Dashed,
|
||||
}
|
||||
|
||||
/// One grid cell: a character plus its rendering style.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Cell {
|
||||
|
|
@ -56,6 +68,9 @@ pub struct Cell {
|
|||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub flags: Flags,
|
||||
pub underline: Underline,
|
||||
/// Underline colour; `Default` means "follow the foreground".
|
||||
pub underline_color: Color,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
|
|
@ -65,6 +80,8 @@ impl Default for Cell {
|
|||
fg: Color::Default,
|
||||
bg: Color::Default,
|
||||
flags: Flags::empty(),
|
||||
underline: Underline::None,
|
||||
underline_color: Color::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -259,11 +276,10 @@ impl Grid {
|
|||
}
|
||||
|
||||
fn pen_blank(&self) -> Cell {
|
||||
// A space carrying only the current background (back-colour erase).
|
||||
Cell {
|
||||
c: ' ',
|
||||
fg: Color::Default,
|
||||
bg: self.pen.bg,
|
||||
flags: Flags::empty(),
|
||||
..Cell::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//! neighbouring cell's background fill.
|
||||
|
||||
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
|
||||
use crate::grid::{Color, Flags, Grid};
|
||||
use crate::grid::{Cell, Color, Flags, Grid, Underline};
|
||||
|
||||
/// Foreground/background used for `Color::Default`.
|
||||
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
|
||||
|
|
@ -54,6 +54,20 @@ impl Canvas<'_> {
|
|||
self.pixels[i + 3] = 0xff;
|
||||
}
|
||||
|
||||
/// Set a single opaque pixel.
|
||||
fn put(&mut self, x: i32, y: i32, c: Rgb) {
|
||||
if let Some(i) = self.index(x, y) {
|
||||
self.pixels[i] = c.2;
|
||||
self.pixels[i + 1] = c.1;
|
||||
self.pixels[i + 2] = c.0;
|
||||
self.pixels[i + 3] = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
fn hline(&mut self, x0: i32, y: i32, w: u32, c: Rgb) {
|
||||
self.fill_rect(x0, y, w, 1, c);
|
||||
}
|
||||
|
||||
/// Composite one pre-multiplied BGRA source pixel over the destination.
|
||||
fn over(&mut self, x: i32, y: i32, src: &[u8]) {
|
||||
let Some(i) = self.index(x, y) else { return };
|
||||
|
|
@ -104,17 +118,27 @@ impl Renderer {
|
|||
for y in 0..grid.rows() {
|
||||
for x in 0..grid.cols() {
|
||||
let cell = grid.cell(x, y);
|
||||
if cell.flags.contains(Flags::WIDE_CONT) || cell.c == ' ' {
|
||||
if cell.flags.contains(Flags::WIDE_CONT) {
|
||||
continue;
|
||||
}
|
||||
let (fg, _) = cell_colors(cell, (x, y) == cursor);
|
||||
let style = Style {
|
||||
bold: cell.flags.contains(Flags::BOLD),
|
||||
italic: cell.flags.contains(Flags::ITALIC),
|
||||
};
|
||||
let origin_x = x as i32 * m.width as i32;
|
||||
let baseline = y as i32 * m.height as i32 + m.ascent as i32;
|
||||
self.draw_glyph(&mut canvas, cell.c, style, origin_x, baseline, fg);
|
||||
let cell_top = y as i32 * m.height as i32;
|
||||
if cell.c != ' ' {
|
||||
let style = Style {
|
||||
bold: cell.flags.contains(Flags::BOLD),
|
||||
italic: cell.flags.contains(Flags::ITALIC),
|
||||
};
|
||||
self.draw_glyph(
|
||||
&mut canvas,
|
||||
cell.c,
|
||||
style,
|
||||
origin_x,
|
||||
cell_top + m.ascent as i32,
|
||||
fg,
|
||||
);
|
||||
}
|
||||
draw_decorations(&mut canvas, cell, origin_x, cell_top, m, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,20 +189,69 @@ impl Renderer {
|
|||
}
|
||||
|
||||
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
|
||||
/// bold-as-bright for the foreground, and hidden.
|
||||
fn cell_colors(cell: &crate::grid::Cell, cursor: bool) -> (Rgb, Rgb) {
|
||||
/// bold-as-bright, dim, and hidden.
|
||||
fn cell_colors(cell: &Cell, cursor: bool) -> (Rgb, Rgb) {
|
||||
let bold = cell.flags.contains(Flags::BOLD);
|
||||
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
|
||||
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
|
||||
if cell.flags.contains(Flags::REVERSE) ^ cursor {
|
||||
std::mem::swap(&mut fg, &mut bg);
|
||||
}
|
||||
if cell.flags.contains(Flags::DIM) {
|
||||
fg = blend_rgb(fg, bg);
|
||||
}
|
||||
if cell.flags.contains(Flags::HIDDEN) {
|
||||
fg = bg;
|
||||
}
|
||||
(fg, bg)
|
||||
}
|
||||
|
||||
/// Mix `c` two-thirds of the way from `toward`, used for the dim attribute.
|
||||
fn blend_rgb(c: Rgb, toward: Rgb) -> Rgb {
|
||||
let mix = |a: u8, b: u8| ((u32::from(a) * 2 + u32::from(b)) / 3) as u8;
|
||||
Rgb(mix(c.0, toward.0), mix(c.1, toward.1), mix(c.2, toward.2))
|
||||
}
|
||||
|
||||
/// Draw underline, strikethrough, and overline for one cell.
|
||||
fn draw_decorations(canvas: &mut Canvas, cell: &Cell, x0: i32, top: i32, m: CellMetrics, fg: Rgb) {
|
||||
let w = m.width;
|
||||
let baseline = top + m.ascent as i32;
|
||||
let uy = (baseline + 1).min(top + m.height as i32 - 1);
|
||||
let uc = resolve(cell.underline_color, fg, false);
|
||||
match cell.underline {
|
||||
Underline::None => {}
|
||||
Underline::Single => canvas.hline(x0, uy, w, uc),
|
||||
Underline::Double => {
|
||||
canvas.hline(x0, uy, w, uc);
|
||||
canvas.hline(x0, (uy - 2).max(top), w, uc);
|
||||
}
|
||||
Underline::Curly => {
|
||||
for dx in 0..w as i32 {
|
||||
let wobble = if (dx / 2) % 2 == 0 { 0 } else { 1 };
|
||||
canvas.put(x0 + dx, uy - wobble, uc);
|
||||
}
|
||||
}
|
||||
Underline::Dotted => {
|
||||
for dx in (0..w as i32).step_by(2) {
|
||||
canvas.put(x0 + dx, uy, uc);
|
||||
}
|
||||
}
|
||||
Underline::Dashed => {
|
||||
for dx in 0..w as i32 {
|
||||
if (dx / 3) % 2 == 0 {
|
||||
canvas.put(x0 + dx, uy, uc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cell.flags.contains(Flags::OVERLINE) {
|
||||
canvas.hline(x0, top, w, fg);
|
||||
}
|
||||
if cell.flags.contains(Flags::STRIKE) {
|
||||
canvas.hline(x0, top + m.ascent as i32 * 2 / 3, w, fg);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(color: Color, default: Rgb, bold: bool) -> Rgb {
|
||||
match color {
|
||||
Color::Default => default,
|
||||
|
|
|
|||
48
src/vt.rs
48
src/vt.rs
|
|
@ -4,7 +4,7 @@ use std::io::Write as _;
|
|||
|
||||
use vte::{Params, Perform};
|
||||
|
||||
use crate::grid::{Color, Flags, Grid};
|
||||
use crate::grid::{Color, Flags, Grid, Underline};
|
||||
|
||||
/// G0/G1 character set designation.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
|
|
@ -26,6 +26,18 @@ fn set_reset(on: bool) -> u8 {
|
|||
if on { 1 } else { 2 }
|
||||
}
|
||||
|
||||
/// Map an SGR 4 param (`4` or `4:x`) to an underline style.
|
||||
fn underline_from(param: &[u16]) -> Underline {
|
||||
match param.get(1).copied().unwrap_or(1) {
|
||||
0 => Underline::None,
|
||||
2 => Underline::Double,
|
||||
3 => Underline::Curly,
|
||||
4 => Underline::Dotted,
|
||||
5 => Underline::Dashed,
|
||||
_ => Underline::Single,
|
||||
}
|
||||
}
|
||||
|
||||
/// The terminal model: a grid plus the escape-sequence state around it.
|
||||
#[derive(Debug)]
|
||||
pub struct Term {
|
||||
|
|
@ -120,14 +132,15 @@ impl Term {
|
|||
1 => pen.flags.insert(Flags::BOLD),
|
||||
2 => pen.flags.insert(Flags::DIM),
|
||||
3 => pen.flags.insert(Flags::ITALIC),
|
||||
4 => pen.flags.insert(Flags::UNDERLINE),
|
||||
4 => pen.underline = underline_from(p),
|
||||
5 | 6 => pen.flags.insert(Flags::BLINK),
|
||||
7 => pen.flags.insert(Flags::REVERSE),
|
||||
8 => pen.flags.insert(Flags::HIDDEN),
|
||||
9 => pen.flags.insert(Flags::STRIKE),
|
||||
21 | 22 => pen.flags.remove(Flags::BOLD.union(Flags::DIM)),
|
||||
21 => pen.underline = Underline::Double,
|
||||
22 => pen.flags.remove(Flags::BOLD.union(Flags::DIM)),
|
||||
23 => pen.flags.remove(Flags::ITALIC),
|
||||
24 => pen.flags.remove(Flags::UNDERLINE),
|
||||
24 => pen.underline = Underline::None,
|
||||
25 => pen.flags.remove(Flags::BLINK),
|
||||
27 => pen.flags.remove(Flags::REVERSE),
|
||||
28 => pen.flags.remove(Flags::HIDDEN),
|
||||
|
|
@ -136,20 +149,22 @@ impl Term {
|
|||
39 => pen.fg = Color::Default,
|
||||
40..=47 => pen.bg = Color::Indexed((code - 40) as u8),
|
||||
49 => pen.bg = Color::Default,
|
||||
53 => pen.flags.insert(Flags::OVERLINE),
|
||||
55 => pen.flags.remove(Flags::OVERLINE),
|
||||
90..=97 => pen.fg = Color::Indexed((code - 90 + 8) as u8),
|
||||
100..=107 => pen.bg = Color::Indexed((code - 100 + 8) as u8),
|
||||
38 | 48 => {
|
||||
38 | 48 | 58 => {
|
||||
let (color, consumed) = ext_color(&items, i);
|
||||
if let Some(color) = color {
|
||||
if code == 38 {
|
||||
pen.fg = color;
|
||||
} else {
|
||||
pen.bg = color;
|
||||
match code {
|
||||
38 => pen.fg = color,
|
||||
48 => pen.bg = color,
|
||||
_ => pen.underline_color = color,
|
||||
}
|
||||
}
|
||||
step = consumed;
|
||||
}
|
||||
// 58/59 (underline colour) and others: no storage yet.
|
||||
59 => pen.underline_color = Color::Default,
|
||||
_ => {}
|
||||
}
|
||||
i += step;
|
||||
|
|
@ -501,6 +516,19 @@ mod tests {
|
|||
assert_eq!(t.take_response(), b"\x1b[?9999;0$y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sgr_underline_styles_and_lines() {
|
||||
let mut t = Term::new(20, 1);
|
||||
feed(&mut t, b"\x1b[4:3;58;5;1;53mX");
|
||||
let cell = t.grid().cell(0, 0);
|
||||
assert_eq!(cell.underline, Underline::Curly);
|
||||
assert_eq!(cell.underline_color, Color::Indexed(1));
|
||||
assert!(cell.flags.contains(Flags::OVERLINE));
|
||||
// 4:0 turns the underline back off.
|
||||
feed(&mut t, b"\x1b[4:0mY");
|
||||
assert_eq!(t.grid().cell(1, 0).underline, Underline::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_stack_push_pop() {
|
||||
let mut t = Term::new(20, 4);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue