1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-27 12:23:46 +03:00
wezterm/wezterm-gui/src/glyphcache.rs
Wez Furlong 91cd2e22e4 prefer local/specific config for a couple more cases
Avoid using `configuration()` when there may be a more specific
config with overrides that we can resolve.
2021-04-16 09:04:51 -07:00

1012 lines
35 KiB
Rust

use super::utilsprites::RenderMetrics;
use ::window::bitmaps::atlas::OutOfTextureSpace;
use ::window::bitmaps::atlas::{Atlas, Sprite};
#[cfg(test)]
use ::window::bitmaps::ImageTexture;
use ::window::bitmaps::{BitmapImage, Image, Texture2d};
use ::window::color::{LinearRgba, SrgbaPixel};
use ::window::glium;
use ::window::glium::backend::Context as GliumContext;
use ::window::glium::texture::SrgbTexture2d;
use ::window::{Point, Rect};
use config::{AllowSquareGlyphOverflow, TextStyle};
use euclid::num::Zero;
use lru::LruCache;
use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use termwiz::image::ImageData;
use wezterm_font::units::*;
use wezterm_font::{FontConfiguration, GlyphInfo};
use wezterm_term::Underline;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GlyphKey {
pub font_idx: usize,
pub glyph_pos: u32,
pub style: TextStyle,
pub followed_by_space: bool,
}
/// We'd like to avoid allocating when resolving from the cache
/// so this is the borrowed version of GlyphKey.
/// It's a bit involved to make this work; more details can be
/// found in the excellent guide here:
/// <https://github.com/sunshowers/borrow-complex-key-example/blob/master/src/lib.rs>
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct BorrowedGlyphKey<'a> {
pub font_idx: usize,
pub glyph_pos: u32,
pub style: &'a TextStyle,
pub followed_by_space: bool,
}
impl<'a> BorrowedGlyphKey<'a> {
fn to_owned(&self) -> GlyphKey {
GlyphKey {
font_idx: self.font_idx,
glyph_pos: self.glyph_pos,
style: self.style.clone(),
followed_by_space: self.followed_by_space,
}
}
}
trait GlyphKeyTrait {
fn key<'k>(&'k self) -> BorrowedGlyphKey<'k>;
}
impl GlyphKeyTrait for GlyphKey {
fn key<'k>(&'k self) -> BorrowedGlyphKey<'k> {
BorrowedGlyphKey {
font_idx: self.font_idx,
glyph_pos: self.glyph_pos,
style: &self.style,
followed_by_space: self.followed_by_space,
}
}
}
impl<'a> GlyphKeyTrait for BorrowedGlyphKey<'a> {
fn key<'k>(&'k self) -> BorrowedGlyphKey<'k> {
*self
}
}
impl<'a> std::borrow::Borrow<dyn GlyphKeyTrait + 'a> for GlyphKey {
fn borrow(&self) -> &(dyn GlyphKeyTrait + 'a) {
self
}
}
impl<'a> PartialEq for (dyn GlyphKeyTrait + 'a) {
fn eq(&self, other: &Self) -> bool {
self.key().eq(&other.key())
}
}
impl<'a> Eq for (dyn GlyphKeyTrait + 'a) {}
impl<'a> std::hash::Hash for (dyn GlyphKeyTrait + 'a) {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.key().hash(state)
}
}
/// Caches a rendered glyph.
/// The image data may be None for whitespace glyphs.
pub struct CachedGlyph<T: Texture2d> {
pub has_color: bool,
pub x_offset: PixelLength,
pub y_offset: PixelLength,
pub bearing_x: PixelLength,
pub bearing_y: PixelLength,
pub texture: Option<Sprite<T>>,
pub scale: f64,
}
impl<T: Texture2d> std::fmt::Debug for CachedGlyph<T> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
fmt.debug_struct("CachedGlyph")
.field("has_color", &self.has_color)
.field("x_offset", &self.x_offset)
.field("y_offset", &self.y_offset)
.field("bearing_x", &self.bearing_x)
.field("bearing_y", &self.bearing_y)
.field("scale", &self.scale)
.field("texture", &self.texture)
.finish()
}
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct LineKey {
strike_through: bool,
underline: Underline,
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, Debug)]
pub enum BlockAlpha {
/// 100%
Full,
/// 75%
Dark,
/// 50%
Medium,
/// 25%
Light,
}
/// Represents a Block Element glyph, decoded from
/// <https://en.wikipedia.org/wiki/Block_Elements>
/// <https://www.unicode.org/charts/PDF/U2580.pdf>
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
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<Self> {
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<Self> {
let mut chars = cell.str().chars();
let first_char = chars.next()?;
if chars.next().is_some() {
None
} else {
Self::from_char(first_char)
}
}
}
#[derive(Debug)]
pub struct ImageFrame {
duration: Duration,
image: ::window::bitmaps::Image,
}
#[derive(Debug)]
pub enum CachedImage {
Animation(DecodedImage),
SingleFrame,
}
#[derive(Debug)]
pub struct DecodedImage {
frame_start: Instant,
current_frame: usize,
frames: Vec<ImageFrame>,
}
impl DecodedImage {
fn placeholder() -> Self {
let image = ::window::bitmaps::Image::new(1, 1);
let frame = ImageFrame {
duration: Duration::default(),
image,
};
Self {
frame_start: Instant::now(),
current_frame: 0,
frames: vec![frame],
}
}
fn with_frames(frames: Vec<image::Frame>) -> Self {
let frames = frames
.into_iter()
.map(|frame| {
let duration: Duration = frame.delay().into();
let image = image::DynamicImage::ImageRgba8(frame.into_buffer()).to_rgba8();
let (w, h) = image.dimensions();
let width = w as usize;
let height = h as usize;
let image = ::window::bitmaps::Image::from_raw(width, height, image.into_vec());
ImageFrame { duration, image }
})
.collect();
Self {
frame_start: Instant::now(),
current_frame: 0,
frames,
}
}
fn with_single(image_data: &Arc<ImageData>) -> anyhow::Result<Self> {
let image = image::load_from_memory(image_data.data())?.to_rgba8();
let (width, height) = image.dimensions();
let width = width as usize;
let height = height as usize;
let image = ::window::bitmaps::Image::from_raw(width, height, image.into_vec());
Ok(Self {
frame_start: Instant::now(),
current_frame: 0,
frames: vec![ImageFrame {
duration: Default::default(),
image,
}],
})
}
fn load(image_data: &Arc<ImageData>) -> anyhow::Result<Self> {
use image::{AnimationDecoder, ImageFormat};
let format = image::guess_format(image_data.data())?;
match format {
ImageFormat::Gif => image::gif::GifDecoder::new(image_data.data())
.and_then(|decoder| decoder.into_frames().collect_frames())
.and_then(|frames| Ok(Self::with_frames(frames)))
.or_else(|err| {
log::error!(
"Unable to parse animated gif: {:#}, trying as single frame",
err
);
Self::with_single(image_data)
}),
ImageFormat::Png => {
let decoder = image::png::PngDecoder::new(image_data.data())?;
if decoder.is_apng() {
let frames = decoder.apng().into_frames().collect_frames()?;
Ok(Self::with_frames(frames))
} else {
Self::with_single(image_data)
}
}
_ => Self::with_single(image_data),
}
}
}
pub struct GlyphCache<T: Texture2d> {
glyph_cache: HashMap<GlyphKey, Rc<CachedGlyph<T>>>,
pub atlas: Atlas<T>,
fonts: Rc<FontConfiguration>,
pub image_cache: LruCache<usize, CachedImage>,
frame_cache: HashMap<(usize, usize), Sprite<T>>,
line_glyphs: HashMap<LineKey, Sprite<T>>,
block_glyphs: HashMap<BlockKey, Sprite<T>>,
metrics: RenderMetrics,
}
#[cfg(test)]
impl GlyphCache<ImageTexture> {
pub fn new_in_memory(
fonts: &Rc<FontConfiguration>,
size: usize,
metrics: &RenderMetrics,
) -> anyhow::Result<Self> {
let surface = Rc::new(ImageTexture::new(size, size));
let atlas = Atlas::new(&surface).expect("failed to create new texture atlas");
Ok(Self {
fonts: Rc::clone(fonts),
glyph_cache: HashMap::new(),
image_cache: LruCache::new(16),
frame_cache: HashMap::new(),
atlas,
metrics: metrics.clone(),
line_glyphs: HashMap::new(),
block_glyphs: HashMap::new(),
})
}
}
impl GlyphCache<SrgbTexture2d> {
pub fn new_gl(
backend: &Rc<GliumContext>,
fonts: &Rc<FontConfiguration>,
size: usize,
metrics: &RenderMetrics,
) -> anyhow::Result<Self> {
let surface = Rc::new(SrgbTexture2d::empty_with_format(
backend,
glium::texture::SrgbFormat::U8U8U8U8,
glium::texture::MipmapsOption::NoMipmap,
size as u32,
size as u32,
)?);
let atlas = Atlas::new(&surface).expect("failed to create new texture atlas");
Ok(Self {
fonts: Rc::clone(fonts),
glyph_cache: HashMap::new(),
image_cache: LruCache::new(16),
frame_cache: HashMap::new(),
atlas,
metrics: metrics.clone(),
line_glyphs: HashMap::new(),
block_glyphs: HashMap::new(),
})
}
pub fn clear(&mut self) {
self.atlas.clear();
// self.image_cache.clear(); - relatively expensive to re-populate
self.frame_cache.clear();
self.glyph_cache.clear();
self.line_glyphs.clear();
self.block_glyphs.clear();
}
}
impl<T: Texture2d> GlyphCache<T> {
/// Resolve a glyph from the cache, rendering the glyph on-demand if
/// the cache doesn't already hold the desired glyph.
pub fn cached_glyph(
&mut self,
info: &GlyphInfo,
style: &TextStyle,
followed_by_space: bool,
) -> anyhow::Result<Rc<CachedGlyph<T>>> {
let key = BorrowedGlyphKey {
font_idx: info.font_idx,
glyph_pos: info.glyph_pos,
style,
followed_by_space,
};
if let Some(entry) = self.glyph_cache.get(&key as &dyn GlyphKeyTrait) {
return Ok(Rc::clone(entry));
}
let glyph = match self.load_glyph(info, style, followed_by_space) {
Ok(g) => g,
Err(err) => {
if err
.root_cause()
.downcast_ref::<OutOfTextureSpace>()
.is_some()
{
// Ensure that we propagate this signal to expand
// our available teexture space
return Err(err);
}
// But otherwise: don't allow glyph loading errors to propagate,
// as that will result in incomplete window painting.
// Log the error and substitute instead.
log::error!(
"load_glyph failed; using blank instead. Error: {:#}. {:?} {:?}",
err,
info,
style
);
Rc::new(CachedGlyph {
has_color: false,
texture: None,
x_offset: PixelLength::zero(),
y_offset: PixelLength::zero(),
bearing_x: PixelLength::zero(),
bearing_y: PixelLength::zero(),
scale: 1.0,
})
}
};
self.glyph_cache.insert(key.to_owned(), Rc::clone(&glyph));
Ok(glyph)
}
/// Perform the load and render of a glyph
#[allow(clippy::float_cmp)]
fn load_glyph(
&mut self,
info: &GlyphInfo,
style: &TextStyle,
followed_by_space: bool,
) -> anyhow::Result<Rc<CachedGlyph<T>>> {
let base_metrics;
let idx_metrics;
let glyph;
{
let font = self.fonts.resolve_font(style)?;
base_metrics = font.metrics();
glyph = font.rasterize_glyph(info.glyph_pos, info.font_idx)?;
idx_metrics = font.metrics_for_idx(info.font_idx)?;
}
let x_scale;
let y_scale;
if info.font_idx == 0 {
// The base font is the current font, so there's no additional metrics
// based scaling, however, we may need to scale to accomodate num_cells
x_scale = 1.0 / info.num_cells as f64;
y_scale = 1.0;
} else if let (Some(base_cap), Some(cap)) =
(base_metrics.cap_height_ratio, idx_metrics.cap_height_ratio)
{
// If we know the cap height ratio for both fonts, we can scale
// the second one to match the cap height of the first
x_scale = base_cap / cap;
y_scale = x_scale / info.num_cells as f64;
} else {
// Otherwise, we scale based on the ratio of the metrics for
// the two fonts.
// If we know the cap height ratio for the first, we can adjust
// the overall scale so that the second font isn't oversized
let base_cap = base_metrics.cap_height_ratio.unwrap_or(1.);
y_scale = base_cap * base_metrics.cell_height.get() / idx_metrics.cell_height.get();
x_scale = base_cap * base_metrics.cell_width.get()
/ (idx_metrics.cell_width.get() / info.num_cells as f64);
}
let aspect = (idx_metrics.cell_width / idx_metrics.cell_height).get();
let is_square_or_wide = aspect >= 0.9;
let allow_width_overflow = if is_square_or_wide {
match self.fonts.config().allow_square_glyphs_to_overflow_width {
AllowSquareGlyphOverflow::Never => false,
AllowSquareGlyphOverflow::Always => true,
AllowSquareGlyphOverflow::WhenFollowedBySpace => followed_by_space,
}
} else {
false
};
let scale = if !allow_width_overflow
&& y_scale * glyph.width as f64 > base_metrics.cell_width.get() * info.num_cells as f64
{
// y-scaling would make us too wide, so use the x-scale
x_scale
} else {
y_scale
};
let (cell_width, cell_height) = (base_metrics.cell_width, base_metrics.cell_height);
let glyph = if glyph.width == 0 || glyph.height == 0 {
// a whitespace glyph
CachedGlyph {
has_color: glyph.has_color,
texture: None,
x_offset: info.x_offset * scale,
y_offset: info.y_offset * scale,
bearing_x: PixelLength::zero(),
bearing_y: PixelLength::zero(),
scale,
}
} else {
let raw_im = Image::with_rgba32(
glyph.width as usize,
glyph.height as usize,
4 * glyph.width as usize,
&glyph.data,
);
let bearing_x = glyph.bearing_x * scale;
let bearing_y = glyph.bearing_y * scale;
let x_offset = info.x_offset * scale;
let y_offset = info.y_offset * scale;
let (scale, raw_im) = if scale != 1.0 {
log::trace!(
"physically scaling {:?} by {} bcos {}x{} > {:?}x{:?}. aspect={}",
info,
scale,
glyph.width,
glyph.height,
cell_width,
cell_height,
aspect,
);
(1.0, raw_im.scale_by(scale))
} else {
(scale, raw_im)
};
let tex = self.atlas.allocate(&raw_im)?;
let g = CachedGlyph {
has_color: glyph.has_color,
texture: Some(tex),
x_offset,
y_offset,
bearing_x,
bearing_y,
scale,
};
if info.font_idx != 0 {
// It's generally interesting to examine eg: emoji or ligatures
// that we might have fallen back to
log::trace!("{:?} {:?}", info, g);
}
g
};
Ok(Rc::new(glyph))
}
pub fn cached_image(
&mut self,
image_data: &Arc<ImageData>,
padding: Option<usize>,
) -> anyhow::Result<(Sprite<T>, Option<Instant>)> {
let id = image_data.id();
if let Some(cached) = self.image_cache.get_mut(&id) {
match cached {
CachedImage::SingleFrame => {
// We can simply use the frame cache to manage
// the texture space; the frame is always 0 for
// a single frame
if let Some(sprite) = self.frame_cache.get(&(id, 0)) {
return Ok((sprite.clone(), None));
}
}
CachedImage::Animation(decoded) => {
let mut next = None;
if decoded.frames.len() > 1 {
let now = Instant::now();
let mut next_due =
decoded.frame_start + decoded.frames[decoded.current_frame].duration;
if now >= next_due {
// Advance to next frame
decoded.current_frame += 1;
if decoded.current_frame >= decoded.frames.len() {
decoded.current_frame = 0;
}
decoded.frame_start = now;
next_due = decoded.frame_start
+ decoded.frames[decoded.current_frame].duration;
}
next.replace(next_due);
}
if let Some(sprite) = self.frame_cache.get(&(id, decoded.current_frame)) {
return Ok((sprite.clone(), next));
}
let sprite = self.atlas.allocate_with_padding(
&decoded.frames[decoded.current_frame].image,
padding,
)?;
self.frame_cache
.insert((id, decoded.current_frame), sprite.clone());
return Ok((
sprite,
Some(decoded.frame_start + decoded.frames[decoded.current_frame].duration),
));
}
}
}
let decoded =
DecodedImage::load(image_data).or_else(|e| -> anyhow::Result<DecodedImage> {
log::debug!("Failed to decode image: {:#}", e);
// Use a placeholder instead
Ok(DecodedImage::placeholder())
})?;
let sprite = self
.atlas
.allocate_with_padding(&decoded.frames[0].image, padding)?;
self.frame_cache.insert((id, 0), sprite.clone());
if decoded.frames.len() > 1 {
let next = Some(decoded.frame_start + decoded.frames[0].duration);
self.image_cache.put(id, CachedImage::Animation(decoded));
Ok((sprite, next))
} else {
self.image_cache.put(id, CachedImage::SingleFrame);
Ok((sprite, None))
}
}
fn block_sprite(&mut self, block: BlockKey) -> anyhow::Result<Sprite<T>> {
let mut buffer = Image::new(
self.metrics.cell_size.width as usize,
self.metrics.cell_size.height as usize,
);
let black = SrgbaPixel::rgba(0, 0, 0, 0);
let white = SrgbaPixel::rgba(0xff, 0xff, 0xff, 0xff);
let cell_rect = Rect::new(Point::new(0, 0), self.metrics.cell_size);
let y_eighth = self.metrics.cell_size.height as f32 / 8.;
let x_eighth = self.metrics.cell_size.width as f32 / 8.;
fn scale(f: f32) -> usize {
f.ceil().max(1.) 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,
);
};
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,
);
};
let draw_quad = |buffer: &mut Image, x: Range<usize>, y: Range<usize>| {
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(
// Note: draw_line uses inclusive coordinates, but our
// range is exclusive coordinates, so compensate here!
// We don't need to do this for `y` since we are already
// iterating over the correct set of `y` values in our loop.
cell_rect.origin.x + x.end.saturating_sub(1) as isize,
cell_rect.origin.y + y as isize,
),
white,
);
}
};
match block {
BlockKey::Upper(num) => {
for n in 0..usize::from(num) {
for a in 0..scale(y_eighth) {
draw_horizontal(&mut buffer, (n as f32 * y_eighth).floor() as usize + a);
}
}
}
BlockKey::Lower(num) => {
for n in 0..usize::from(num) {
let y =
(self.metrics.cell_size.height - 1) as usize - scale(n as f32 * y_eighth);
for a in 0..scale(y_eighth) {
draw_horizontal(&mut buffer, y + a);
}
}
}
BlockKey::Left(num) => {
for n in 0..usize::from(num) {
for a in 0..scale(x_eighth) {
draw_vertical(&mut buffer, (n as f32 * x_eighth).floor() as usize + a);
}
}
}
BlockKey::Right(num) => {
for n in 0..usize::from(num) {
let x =
(self.metrics.cell_size.width - 1) as usize - scale(n as f32 * x_eighth);
for a in 0..scale(x_eighth) {
draw_vertical(&mut buffer, x + a);
}
}
}
BlockKey::Full(alpha) => {
let alpha = match alpha {
BlockAlpha::Full => 1.0,
BlockAlpha::Dark => 0.75,
BlockAlpha::Medium => 0.5,
BlockAlpha::Light => 0.25,
};
let fill = LinearRgba::with_components(alpha, alpha, alpha, alpha);
buffer.clear_rect(cell_rect, fill.srgba_pixel());
}
BlockKey::Quadrants(quads) => {
let y_half = self.metrics.cell_size.height as f32 / 2.;
let x_half = self.metrics.cell_size.width as f32 / 2.;
let width = self.metrics.cell_size.width as usize;
let height = self.metrics.cell_size.height as usize;
if quads.contains(Quadrant::UPPER_LEFT) {
draw_quad(&mut buffer, 0..scale(x_half), 0..scale(y_half));
}
if quads.contains(Quadrant::UPPER_RIGHT) {
draw_quad(&mut buffer, scale(x_half)..width, 0..scale(y_half));
}
if quads.contains(Quadrant::LOWER_LEFT) {
draw_quad(&mut buffer, 0..scale(x_half), scale(y_half)..height);
}
if quads.contains(Quadrant::LOWER_RIGHT) {
draw_quad(&mut buffer, scale(x_half)..width, scale(y_half)..height);
}
}
}
/*
log::info!("{:?}", block);
buffer.log_bits();
*/
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<Sprite<T>> {
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<Sprite<T>> {
let mut buffer = Image::new(
self.metrics.cell_size.width as usize,
self.metrics.cell_size.height as usize,
);
let black = SrgbaPixel::rgba(0, 0, 0, 0);
let white = SrgbaPixel::rgba(0xff, 0xff, 0xff, 0xff);
let cell_rect = Rect::new(Point::new(0, 0), self.metrics.cell_size);
let draw_single = |buffer: &mut Image| {
for row in 0..self.metrics.underline_height {
buffer.draw_line(
Point::new(
cell_rect.origin.x,
cell_rect.origin.y + self.metrics.descender_row + row,
),
Point::new(
cell_rect.origin.x + self.metrics.cell_size.width,
cell_rect.origin.y + self.metrics.descender_row + row,
),
white,
);
}
};
let draw_dotted = |buffer: &mut Image| {
for row in 0..self.metrics.underline_height {
let y = (cell_rect.origin.y + self.metrics.descender_row + row) as usize;
if y >= self.metrics.cell_size.height as usize {
break;
}
let mut color = white;
let segment_length = (self.metrics.cell_size.width / 4) as usize;
let mut count = segment_length;
let range =
buffer.horizontal_pixel_range_mut(0, self.metrics.cell_size.width as usize, y);
for c in range.iter_mut() {
*c = color.as_srgba32();
count -= 1;
if count == 0 {
color = if color == white { black } else { white };
count = segment_length;
}
}
}
};
let draw_dashed = |buffer: &mut Image| {
for row in 0..self.metrics.underline_height {
let y = (cell_rect.origin.y + self.metrics.descender_row + row) as usize;
if y >= self.metrics.cell_size.height as usize {
break;
}
let mut color = white;
let third = (self.metrics.cell_size.width / 3) as usize + 1;
let mut count = third;
let range =
buffer.horizontal_pixel_range_mut(0, self.metrics.cell_size.width as usize, y);
for c in range.iter_mut() {
*c = color.as_srgba32();
count -= 1;
if count == 0 {
color = if color == white { black } else { white };
count = third;
}
}
}
};
let draw_curly = |buffer: &mut Image| {
let max_y = self.metrics.cell_size.height as usize - 1;
let x_factor = (2. * std::f32::consts::PI) / self.metrics.cell_size.width as f32;
// Have the wave go from the descender to the bottom of the cell
let wave_height =
self.metrics.cell_size.height - (cell_rect.origin.y + self.metrics.descender_row);
let half_height = (wave_height as f32 / 2.).max(1.);
let y =
(cell_rect.origin.y + self.metrics.descender_row) as usize - half_height as usize;
fn add(x: usize, y: usize, val: u8, max_y: usize, buffer: &mut Image) {
let y = y.min(max_y);
let pixel = buffer.pixel_mut(x, y);
let (current, _, _, _) = SrgbaPixel::with_srgba_u32(*pixel).as_rgba();
let value = current.saturating_add(val);
*pixel = SrgbaPixel::rgba(value, value, value, 0xff).as_srgba32();
}
for x in 0..self.metrics.cell_size.width as usize {
let vertical = wave_height as f32 * (x as f32 * x_factor).cos();
let v1 = vertical.floor();
let v2 = vertical.ceil();
for row in 0..self.metrics.underline_height as usize {
let value = (255. * (vertical - v1).abs()) as u8;
add(x, row + y + v1 as usize, 255 - value, max_y, buffer);
add(x, row + y + v2 as usize, value, max_y, buffer);
}
}
};
let draw_double = |buffer: &mut Image| {
let first_line = self
.metrics
.descender_row
.min(self.metrics.descender_plus_two - 2 * self.metrics.underline_height);
for row in 0..self.metrics.underline_height {
buffer.draw_line(
Point::new(cell_rect.origin.x, cell_rect.origin.y + first_line + row),
Point::new(
cell_rect.origin.x + self.metrics.cell_size.width,
cell_rect.origin.y + first_line + row,
),
white,
);
buffer.draw_line(
Point::new(
cell_rect.origin.x,
cell_rect.origin.y + self.metrics.descender_plus_two + row,
),
Point::new(
cell_rect.origin.x + self.metrics.cell_size.width,
cell_rect.origin.y + self.metrics.descender_plus_two + row,
),
white,
);
}
};
let draw_strike = |buffer: &mut Image| {
for row in 0..self.metrics.underline_height {
buffer.draw_line(
Point::new(
cell_rect.origin.x,
cell_rect.origin.y + self.metrics.strike_row + row,
),
Point::new(
cell_rect.origin.x + self.metrics.cell_size.width,
cell_rect.origin.y + self.metrics.strike_row + row,
),
white,
);
}
};
let draw_overline = |buffer: &mut Image| {
for row in 0..self.metrics.underline_height {
buffer.draw_line(
Point::new(cell_rect.origin.x, cell_rect.origin.y + row),
Point::new(
cell_rect.origin.x + self.metrics.cell_size.width,
cell_rect.origin.y + row,
),
white,
);
}
};
buffer.clear_rect(cell_rect, black);
if key.overline {
draw_overline(&mut buffer);
}
match key.underline {
Underline::None => {}
Underline::Single => draw_single(&mut buffer),
Underline::Curly => draw_curly(&mut buffer),
Underline::Dashed => draw_dashed(&mut buffer),
Underline::Dotted => draw_dotted(&mut buffer),
Underline::Double => draw_double(&mut buffer),
}
if key.strike_through {
draw_strike(&mut buffer);
}
let sprite = self.atlas.allocate(&buffer)?;
self.line_glyphs.insert(key, sprite.clone());
Ok(sprite)
}
/// Figure out what we're going to draw for the underline.
/// If the current cell is part of the current URL highlight
/// then we want to show the underline.
pub fn cached_line_sprite(
&mut self,
is_highlited_hyperlink: bool,
is_strike_through: bool,
underline: Underline,
overline: bool,
) -> anyhow::Result<Sprite<T>> {
let effective_underline = match (is_highlited_hyperlink, underline) {
(true, Underline::None) => Underline::Single,
(true, Underline::Single) => Underline::Double,
(true, _) => Underline::Single,
(false, u) => u,
};
let key = LineKey {
strike_through: is_strike_through,
overline,
underline: effective_underline,
};
if let Some(s) = self.line_glyphs.get(&key) {
return Ok(s.clone());
}
self.line_sprite(key)
}
}