From 0597684ebd204c815c146a79a271ce62857de0d1 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sun, 28 Feb 2021 09:43:26 -0800 Subject: [PATCH] Render custom block glyphs As explained in the docs included in this commit, ideally this wouldn't be needed, but due to a long-standing hinting bug in freetype it seems most expedient to just render our own block glyphs, so that's what this does! refs: #433 --- config/src/lib.rs | 3 + docs/changelog.md | 1 + docs/config/lua/config/custom_block_glyphs.md | 13 + test-data/blocks.py | 12 + wezterm-gui/src/glyphcache.rs | 234 ++++++++++++++++++ wezterm-gui/src/termwindow/render.rs | 66 ++++- 6 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 docs/config/lua/config/custom_block_glyphs.md create mode 100755 test-data/blocks.py diff --git a/config/src/lib.rs b/config/src/lib.rs index 8b46f21cc..fe20f64a3 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -876,6 +876,9 @@ pub struct Config { #[serde(default = "default_true")] pub prefer_egl: bool, + #[serde(default = "default_true")] + pub custom_block_glyphs: bool, + /// Controls the amount of padding to use around the terminal cell area #[serde(default)] pub window_padding: WindowPadding, diff --git a/docs/changelog.md b/docs/changelog.md index 760917ee7..62e453e59 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -36,6 +36,7 @@ brief notes about them may accumulate here. * New: added `--config name=value` options to `wezterm`, `wezterm-gui` and `wezterm-mux-server`. The `--front-end`, `--font-locator`, `--font-rasterizer` and `--font-shaper` CLI options have been removed in favor of this new mechanism. * New: [window:set_config_overrides](config/lua/window/set_config_overrides.md) method that can be used to override GUI related configuration options on a per-window basis. Click through to see examples of dynamically toggling ligatures and window opacity. [#469](https://github.com/wez/wezterm/issues/469) [#329](https://github.com/wez/wezterm/issues/329) * Fixed: line-based mouse selection (default: triple click) now extends forwards to include wrapped lines. [#466](https://github.com/wez/wezterm/issues/466) +* New: introduced [custom_block_glyphs](config/lua/config/custom_block_glyphs.md) option to ensure that block glyphs don't have gaps. [#433](https://github.com/wez/wezterm/issues/433) ### 20210203-095643-70a364eb diff --git a/docs/config/lua/config/custom_block_glyphs.md b/docs/config/lua/config/custom_block_glyphs.md new file mode 100644 index 000000000..4a1c15c68 --- /dev/null +++ b/docs/config/lua/config/custom_block_glyphs.md @@ -0,0 +1,13 @@ +## `custom_block_glyphs = true` + +*Since: nightly builds only* + +When set to `true` (the default), WezTerm will compute its own idea of what the +[U2580](https://www.unicode.org/charts/PDF/U2580.pdf) unicode block elements +range, instead of using glyphs resolved from a font. + +Ideally this option wouldn't exist, but it is present to work around a [hinting issue in freetype](https://gitlab.freedesktop.org/freetype/freetype/-/issues/761). + +You can set this to `false` to use the block characters provided by your font selection. + + diff --git a/test-data/blocks.py b/test-data/blocks.py new file mode 100755 index 000000000..5338aa88a --- /dev/null +++ b/test-data/blocks.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 + +# Print out the unicde block range + +s = "" +for b in range(0x2580, 0x25A0): + s += chr(b) + " " + +print(s) +print(s) +print(s) + diff --git a/wezterm-gui/src/glyphcache.rs b/wezterm-gui/src/glyphcache.rs index 61c420cfb..441384a23 100644 --- a/wezterm-gui/src/glyphcache.rs +++ b/wezterm-gui/src/glyphcache.rs @@ -10,6 +10,7 @@ use anyhow::{anyhow, Context}; use config::{configuration, AllowSquareGlyphOverflow, TextStyle}; use euclid::num::Zero; use std::collections::HashMap; +use std::ops::Range; use std::rc::Rc; use std::sync::Arc; use termwiz::image::ImageData; @@ -123,12 +124,104 @@ struct LineKey { overline: bool, } +bitflags::bitflags! { + pub struct Quadrant: u8{ + const UPPER_LEFT = 1<<1; + const UPPER_RIGHT = 1<<2; + const LOWER_LEFT = 1<<3; + const LOWER_RIGHT = 1<<4; + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum BlockAlpha { + /// 100% + Full, + /// 75% + Dark, + /// 50% + Medium, + /// 25% + Light, +} + +/// Represents a Block Element glyph, decoded from +/// +/// +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum BlockKey { + /// Number of 1/8ths in the upper half + Upper(u8), + /// Number of 1/8ths in the lower half + Lower(u8), + /// Number of 1/8ths in the left half + Left(u8), + /// Number of 1/8ths in the right half + Right(u8), + /// Full block with alpha level + Full(BlockAlpha), + /// A combination of quadrants + Quadrants(Quadrant), +} + +impl BlockKey { + pub fn from_char(c: char) -> Option { + let c = c as u32; + Some(match c { + // Upper half block + 0x2580 => Self::Upper(4), + // Lower 1..7 eighths + 0x2581..=0x2587 => Self::Lower((c - 0x2580) as u8), + 0x2588 => Self::Full(BlockAlpha::Full), + // Left 7..1 eighths + 0x2589..=0x258f => Self::Left((0x2590 - c) as u8), + // Right half + 0x2590 => Self::Right(4), + 0x2591 => Self::Full(BlockAlpha::Light), + 0x2592 => Self::Full(BlockAlpha::Medium), + 0x2593 => Self::Full(BlockAlpha::Dark), + 0x2594 => Self::Upper(1), + 0x2595 => Self::Right(1), + 0x2596 => Self::Quadrants(Quadrant::LOWER_LEFT), + 0x2597 => Self::Quadrants(Quadrant::LOWER_RIGHT), + 0x2598 => Self::Quadrants(Quadrant::UPPER_LEFT), + 0x2599 => { + Self::Quadrants(Quadrant::UPPER_LEFT | Quadrant::LOWER_LEFT | Quadrant::LOWER_RIGHT) + } + 0x259a => Self::Quadrants(Quadrant::UPPER_LEFT | Quadrant::LOWER_RIGHT), + 0x259b => { + Self::Quadrants(Quadrant::UPPER_LEFT | Quadrant::UPPER_RIGHT | Quadrant::LOWER_LEFT) + } + 0x259c => Self::Quadrants( + Quadrant::UPPER_LEFT | Quadrant::UPPER_RIGHT | Quadrant::LOWER_RIGHT, + ), + 0x259d => Self::Quadrants(Quadrant::UPPER_RIGHT), + 0x259e => Self::Quadrants(Quadrant::UPPER_RIGHT | Quadrant::LOWER_LEFT), + 0x259f => Self::Quadrants( + Quadrant::UPPER_RIGHT | Quadrant::LOWER_LEFT | Quadrant::LOWER_RIGHT, + ), + _ => return None, + }) + } + + pub fn from_cell(cell: &termwiz::cell::Cell) -> Option { + let mut chars = cell.str().chars(); + let first_char = chars.next()?; + if chars.next().is_some() { + None + } else { + Self::from_char(first_char) + } + } +} + pub struct GlyphCache { glyph_cache: HashMap>>, pub atlas: Atlas, fonts: Rc, image_cache: HashMap>, line_glyphs: HashMap>, + block_glyphs: HashMap>, metrics: RenderMetrics, } @@ -149,6 +242,7 @@ impl GlyphCache { atlas, metrics: metrics.clone(), line_glyphs: HashMap::new(), + block_glyphs: HashMap::new(), }) } } @@ -176,6 +270,7 @@ impl GlyphCache { atlas, metrics: metrics.clone(), line_glyphs: HashMap::new(), + block_glyphs: HashMap::new(), }) } @@ -184,6 +279,7 @@ impl GlyphCache { self.image_cache.clear(); self.glyph_cache.clear(); self.line_glyphs.clear(); + self.block_glyphs.clear(); } } @@ -349,6 +445,144 @@ impl GlyphCache { Ok(sprite) } + fn block_sprite(&mut self, block: BlockKey) -> anyhow::Result> { + let mut buffer = Image::new( + self.metrics.cell_size.width as usize, + self.metrics.cell_size.height as usize, + ); + let black = ::window::color::Color::rgba(0, 0, 0, 0); + let white = ::window::color::Color::rgb(0xff, 0xff, 0xff); + + let cell_rect = Rect::new(Point::new(0, 0), self.metrics.cell_size); + + let y_eighth = (self.metrics.cell_size.height / 8) as usize; + let x_eighth = (self.metrics.cell_size.width / 8) as usize; + + buffer.clear_rect(cell_rect, black); + + let draw_horizontal = |buffer: &mut Image, y: usize| { + buffer.draw_line( + Point::new(cell_rect.origin.x, cell_rect.origin.y + y as isize), + Point::new( + cell_rect.origin.x + self.metrics.cell_size.width, + cell_rect.origin.y + y as isize, + ), + white, + Operator::Source, + ); + }; + + let draw_vertical = |buffer: &mut Image, x: usize| { + buffer.draw_line( + Point::new(cell_rect.origin.x + x as isize, cell_rect.origin.y), + Point::new( + cell_rect.origin.x + x as isize, + cell_rect.origin.y + self.metrics.cell_size.height, + ), + white, + Operator::Source, + ); + }; + + let draw_quad = |buffer: &mut Image, x: Range, y: Range| { + for y in y { + buffer.draw_line( + Point::new( + cell_rect.origin.x + x.start as isize, + cell_rect.origin.y + y as isize, + ), + Point::new( + cell_rect.origin.x + x.end as isize, + cell_rect.origin.y + y as isize, + ), + white, + Operator::Source, + ); + } + }; + + match block { + BlockKey::Upper(num) => { + for n in 0..usize::from(num) { + for a in 0..y_eighth { + draw_horizontal(&mut buffer, (n * y_eighth) + a); + } + } + } + BlockKey::Lower(num) => { + for n in 0..usize::from(num) { + let y = (self.metrics.cell_size.height - 1) as usize - (n * y_eighth); + for a in 0..y_eighth { + draw_horizontal(&mut buffer, y + a); + } + } + } + BlockKey::Left(num) => { + for n in 0..usize::from(num) { + for a in 0..x_eighth { + draw_vertical(&mut buffer, (n * x_eighth) + a); + } + } + } + BlockKey::Right(num) => { + for n in 0..usize::from(num) { + let x = (self.metrics.cell_size.width - 1) as usize - (n * x_eighth); + for a in 0..x_eighth { + draw_vertical(&mut buffer, x + a); + } + } + } + BlockKey::Full(alpha) => { + let alpha = match alpha { + BlockAlpha::Full => 0xff, + BlockAlpha::Dark => (0.75 * 255.) as u8, + BlockAlpha::Medium => (0.5 * 255.) as u8, + BlockAlpha::Light => (0.25 * 255.) as u8, + }; + let fill = ::window::color::Color::with_linear_rgba_u8(alpha, alpha, alpha, alpha); + + buffer.clear_rect(cell_rect, fill); + } + BlockKey::Quadrants(quads) => { + if quads.contains(Quadrant::UPPER_LEFT) { + draw_quad(&mut buffer, 0..(x_eighth * 4), 0..(y_eighth * 4)); + } + if quads.contains(Quadrant::UPPER_RIGHT) { + draw_quad( + &mut buffer, + (x_eighth * 4)..(x_eighth * 8), + 0..(y_eighth * 4), + ); + } + if quads.contains(Quadrant::LOWER_LEFT) { + draw_quad( + &mut buffer, + 0..(x_eighth * 4), + (y_eighth * 4)..(y_eighth * 8), + ); + } + if quads.contains(Quadrant::LOWER_RIGHT) { + draw_quad( + &mut buffer, + (x_eighth * 4)..(x_eighth * 8), + (y_eighth * 4)..(y_eighth * 8), + ); + } + } + } + + let sprite = self.atlas.allocate(&buffer)?; + self.block_glyphs.insert(block, sprite.clone()); + Ok(sprite) + } + + pub fn cached_block(&mut self, block: BlockKey) -> anyhow::Result> { + if let Some(s) = self.block_glyphs.get(&block) { + return Ok(s.clone()); + } + self.block_sprite(block) + } + fn line_sprite(&mut self, key: LineKey) -> anyhow::Result> { let mut buffer = Image::new( self.metrics.cell_size.width as usize, diff --git a/wezterm-gui/src/termwindow/render.rs b/wezterm-gui/src/termwindow/render.rs index 957fb69cf..bd6dea6dc 100644 --- a/wezterm-gui/src/termwindow/render.rs +++ b/wezterm-gui/src/termwindow/render.rs @@ -1,5 +1,5 @@ use crate::glium::texture::SrgbTexture2d; -use crate::glyphcache::{CachedGlyph, GlyphCache}; +use crate::glyphcache::{BlockKey, CachedGlyph, GlyphCache}; use crate::shapecache::*; use crate::termwindow::{BorrowedShapeCacheKey, MappedQuads, RenderState, ScrollHit, ShapedInfo}; use ::window::bitmaps::atlas::OutOfTextureSpace; @@ -779,6 +779,25 @@ impl super::TermWindow { continue; } + if self.config.custom_block_glyphs { + if let Some(block) = BlockKey::from_cell(¶ms.line.cells()[cell_idx]) { + self.populate_block_quad( + block, + gl_state, + quads, + cell_idx, + ¶ms, + hsv, + cursor_shape, + glyph_color, + underline_color, + bg_color, + white_space, + )?; + continue; + } + } + let texture = glyph .texture .as_ref() @@ -889,6 +908,51 @@ impl super::TermWindow { Ok(()) } + pub fn populate_block_quad( + &self, + block: BlockKey, + gl_state: &RenderState, + quads: &mut MappedQuads, + cell_idx: usize, + params: &RenderScreenLineOpenGLParams, + hsv: Option, + cursor_shape: Option, + glyph_color: Color, + underline_color: Color, + bg_color: Color, + white_space: TextureRect, + ) -> anyhow::Result<()> { + let sprite = gl_state + .glyph_cache + .borrow_mut() + .cached_block(block)? + .texture_coords(); + + let mut quad = + match quads.cell(cell_idx + params.pos.left, params.line_idx + params.pos.top) { + Ok(quad) => quad, + Err(_) => return Ok(()), + }; + + quad.set_hsv(hsv); + quad.set_fg_color(glyph_color); + quad.set_underline_color(underline_color); + quad.set_bg_color(bg_color); + quad.set_texture(sprite); + quad.set_texture_adjust(0., 0., 0., 0.); + quad.set_underline(white_space); + quad.set_has_color(false); + quad.set_cursor( + gl_state + .util_sprites + .cursor_sprite(cursor_shape) + .texture_coords(), + ); + quad.set_cursor_color(params.cursor_border_color); + + Ok(()) + } + /// Render iTerm2 style image attributes pub fn populate_image_quad( &self,