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 BOLD: Self = Self(1 << 0);
|
||||||
pub const DIM: Self = Self(1 << 1);
|
pub const DIM: Self = Self(1 << 1);
|
||||||
pub const ITALIC: Self = Self(1 << 2);
|
pub const ITALIC: Self = Self(1 << 2);
|
||||||
pub const UNDERLINE: Self = Self(1 << 3);
|
|
||||||
pub const BLINK: Self = Self(1 << 4);
|
pub const BLINK: Self = Self(1 << 4);
|
||||||
pub const REVERSE: Self = Self(1 << 5);
|
pub const REVERSE: Self = Self(1 << 5);
|
||||||
pub const HIDDEN: Self = Self(1 << 6);
|
pub const HIDDEN: Self = Self(1 << 6);
|
||||||
pub const STRIKE: Self = Self(1 << 7);
|
pub const STRIKE: Self = Self(1 << 7);
|
||||||
/// Trailing column of a double-width glyph; holds no character of its own.
|
/// Trailing column of a double-width glyph; holds no character of its own.
|
||||||
pub const WIDE_CONT: Self = Self(1 << 8);
|
pub const WIDE_CONT: Self = Self(1 << 8);
|
||||||
|
pub const OVERLINE: Self = Self(1 << 9);
|
||||||
|
|
||||||
pub const fn empty() -> Self {
|
pub const fn empty() -> Self {
|
||||||
Self(0)
|
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.
|
/// One grid cell: a character plus its rendering style.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct Cell {
|
pub struct Cell {
|
||||||
|
|
@ -56,6 +68,9 @@ pub struct Cell {
|
||||||
pub fg: Color,
|
pub fg: Color,
|
||||||
pub bg: Color,
|
pub bg: Color,
|
||||||
pub flags: Flags,
|
pub flags: Flags,
|
||||||
|
pub underline: Underline,
|
||||||
|
/// Underline colour; `Default` means "follow the foreground".
|
||||||
|
pub underline_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Cell {
|
impl Default for Cell {
|
||||||
|
|
@ -65,6 +80,8 @@ impl Default for Cell {
|
||||||
fg: Color::Default,
|
fg: Color::Default,
|
||||||
bg: Color::Default,
|
bg: Color::Default,
|
||||||
flags: Flags::empty(),
|
flags: Flags::empty(),
|
||||||
|
underline: Underline::None,
|
||||||
|
underline_color: Color::Default,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,11 +276,10 @@ impl Grid {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pen_blank(&self) -> Cell {
|
fn pen_blank(&self) -> Cell {
|
||||||
|
// A space carrying only the current background (back-colour erase).
|
||||||
Cell {
|
Cell {
|
||||||
c: ' ',
|
|
||||||
fg: Color::Default,
|
|
||||||
bg: self.pen.bg,
|
bg: self.pen.bg,
|
||||||
flags: Flags::empty(),
|
..Cell::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
//! neighbouring cell's background fill.
|
//! neighbouring cell's background fill.
|
||||||
|
|
||||||
use crate::font::{CellMetrics, Fonts, GlyphData, Style};
|
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`.
|
/// Foreground/background used for `Color::Default`.
|
||||||
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
|
const DEFAULT_FG: Rgb = Rgb(0xc5, 0xc8, 0xc6);
|
||||||
|
|
@ -54,6 +54,20 @@ impl Canvas<'_> {
|
||||||
self.pixels[i + 3] = 0xff;
|
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.
|
/// Composite one pre-multiplied BGRA source pixel over the destination.
|
||||||
fn over(&mut self, x: i32, y: i32, src: &[u8]) {
|
fn over(&mut self, x: i32, y: i32, src: &[u8]) {
|
||||||
let Some(i) = self.index(x, y) else { return };
|
let Some(i) = self.index(x, y) else { return };
|
||||||
|
|
@ -104,17 +118,27 @@ impl Renderer {
|
||||||
for y in 0..grid.rows() {
|
for y in 0..grid.rows() {
|
||||||
for x in 0..grid.cols() {
|
for x in 0..grid.cols() {
|
||||||
let cell = grid.cell(x, y);
|
let cell = grid.cell(x, y);
|
||||||
if cell.flags.contains(Flags::WIDE_CONT) || cell.c == ' ' {
|
if cell.flags.contains(Flags::WIDE_CONT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (fg, _) = cell_colors(cell, (x, y) == cursor);
|
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 origin_x = x as i32 * m.width as i32;
|
||||||
let baseline = y as i32 * m.height as i32 + m.ascent as i32;
|
let cell_top = y as i32 * m.height as i32;
|
||||||
self.draw_glyph(&mut canvas, cell.c, style, origin_x, baseline, fg);
|
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,
|
/// Resolve a cell's (foreground, background) RGB, applying reverse video,
|
||||||
/// bold-as-bright for the foreground, and hidden.
|
/// bold-as-bright, dim, and hidden.
|
||||||
fn cell_colors(cell: &crate::grid::Cell, cursor: bool) -> (Rgb, Rgb) {
|
fn cell_colors(cell: &Cell, cursor: bool) -> (Rgb, Rgb) {
|
||||||
let bold = cell.flags.contains(Flags::BOLD);
|
let bold = cell.flags.contains(Flags::BOLD);
|
||||||
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
|
let mut fg = resolve(cell.fg, DEFAULT_FG, bold);
|
||||||
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
|
let mut bg = resolve(cell.bg, DEFAULT_BG, false);
|
||||||
if cell.flags.contains(Flags::REVERSE) ^ cursor {
|
if cell.flags.contains(Flags::REVERSE) ^ cursor {
|
||||||
std::mem::swap(&mut fg, &mut bg);
|
std::mem::swap(&mut fg, &mut bg);
|
||||||
}
|
}
|
||||||
|
if cell.flags.contains(Flags::DIM) {
|
||||||
|
fg = blend_rgb(fg, bg);
|
||||||
|
}
|
||||||
if cell.flags.contains(Flags::HIDDEN) {
|
if cell.flags.contains(Flags::HIDDEN) {
|
||||||
fg = bg;
|
fg = bg;
|
||||||
}
|
}
|
||||||
(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 {
|
fn resolve(color: Color, default: Rgb, bold: bool) -> Rgb {
|
||||||
match color {
|
match color {
|
||||||
Color::Default => default,
|
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 vte::{Params, Perform};
|
||||||
|
|
||||||
use crate::grid::{Color, Flags, Grid};
|
use crate::grid::{Color, Flags, Grid, Underline};
|
||||||
|
|
||||||
/// G0/G1 character set designation.
|
/// G0/G1 character set designation.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
|
@ -26,6 +26,18 @@ fn set_reset(on: bool) -> u8 {
|
||||||
if on { 1 } else { 2 }
|
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.
|
/// The terminal model: a grid plus the escape-sequence state around it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Term {
|
pub struct Term {
|
||||||
|
|
@ -120,14 +132,15 @@ impl Term {
|
||||||
1 => pen.flags.insert(Flags::BOLD),
|
1 => pen.flags.insert(Flags::BOLD),
|
||||||
2 => pen.flags.insert(Flags::DIM),
|
2 => pen.flags.insert(Flags::DIM),
|
||||||
3 => pen.flags.insert(Flags::ITALIC),
|
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),
|
5 | 6 => pen.flags.insert(Flags::BLINK),
|
||||||
7 => pen.flags.insert(Flags::REVERSE),
|
7 => pen.flags.insert(Flags::REVERSE),
|
||||||
8 => pen.flags.insert(Flags::HIDDEN),
|
8 => pen.flags.insert(Flags::HIDDEN),
|
||||||
9 => pen.flags.insert(Flags::STRIKE),
|
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),
|
23 => pen.flags.remove(Flags::ITALIC),
|
||||||
24 => pen.flags.remove(Flags::UNDERLINE),
|
24 => pen.underline = Underline::None,
|
||||||
25 => pen.flags.remove(Flags::BLINK),
|
25 => pen.flags.remove(Flags::BLINK),
|
||||||
27 => pen.flags.remove(Flags::REVERSE),
|
27 => pen.flags.remove(Flags::REVERSE),
|
||||||
28 => pen.flags.remove(Flags::HIDDEN),
|
28 => pen.flags.remove(Flags::HIDDEN),
|
||||||
|
|
@ -136,20 +149,22 @@ impl Term {
|
||||||
39 => pen.fg = Color::Default,
|
39 => pen.fg = Color::Default,
|
||||||
40..=47 => pen.bg = Color::Indexed((code - 40) as u8),
|
40..=47 => pen.bg = Color::Indexed((code - 40) as u8),
|
||||||
49 => pen.bg = Color::Default,
|
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),
|
90..=97 => pen.fg = Color::Indexed((code - 90 + 8) as u8),
|
||||||
100..=107 => pen.bg = Color::Indexed((code - 100 + 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);
|
let (color, consumed) = ext_color(&items, i);
|
||||||
if let Some(color) = color {
|
if let Some(color) = color {
|
||||||
if code == 38 {
|
match code {
|
||||||
pen.fg = color;
|
38 => pen.fg = color,
|
||||||
} else {
|
48 => pen.bg = color,
|
||||||
pen.bg = color;
|
_ => pen.underline_color = color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step = consumed;
|
step = consumed;
|
||||||
}
|
}
|
||||||
// 58/59 (underline colour) and others: no storage yet.
|
59 => pen.underline_color = Color::Default,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
i += step;
|
i += step;
|
||||||
|
|
@ -501,6 +516,19 @@ mod tests {
|
||||||
assert_eq!(t.take_response(), b"\x1b[?9999;0$y");
|
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]
|
#[test]
|
||||||
fn title_stack_push_pop() {
|
fn title_stack_push_pop() {
|
||||||
let mut t = Term::new(20, 4);
|
let mut t = Term::new(20, 4);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue