524 lines
16 KiB
Rust
524 lines
16 KiB
Rust
// Copyright (c) 2022 Marceline Cramer
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use std::collections::VecDeque;
|
|
use std::time::Duration;
|
|
|
|
use alacritty_terminal::ansi::{Color, NamedColor};
|
|
use alacritty_terminal::term::cell::{Cell, Flags as CellFlags};
|
|
use alacritty_terminal::term::color::{Colors, Rgb};
|
|
use alacritty_terminal::Term;
|
|
use fnv::FnvHashMap as HashMap;
|
|
use softbuffer::GraphicsContext;
|
|
use winit::window::Window;
|
|
|
|
use crate::config::{ColorConfig, Config, FontConfig};
|
|
|
|
use super::TermListener;
|
|
|
|
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub struct Point(pub usize, pub usize);
|
|
|
|
impl Point {
|
|
pub const ZERO: Self = Self(0, 0);
|
|
pub const ONE: Self = Self(1, 1);
|
|
}
|
|
|
|
impl std::ops::Add for Point {
|
|
type Output = Self;
|
|
|
|
fn add(self, rhs: Self) -> Self {
|
|
Self(self.0 + rhs.0, self.1 + rhs.1)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Sub for Point {
|
|
type Output = Self;
|
|
|
|
fn sub(self, rhs: Self) -> Self {
|
|
Self(self.0 - rhs.0, self.1 - rhs.1)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Mul for Point {
|
|
type Output = Self;
|
|
|
|
fn mul(self, rhs: Self) -> Self {
|
|
Self(self.0 * rhs.0, self.1 * rhs.1)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Canvas {
|
|
pub width: usize,
|
|
pub height: usize,
|
|
pub buffer: Box<[u32]>,
|
|
}
|
|
|
|
impl Canvas {
|
|
#[inline]
|
|
pub fn new(color: u32, width: usize, height: usize) -> Self {
|
|
Self {
|
|
width,
|
|
height,
|
|
buffer: vec![color; width * height].into_boxed_slice(),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn clip(&self, xy: Point) -> Point {
|
|
Point(xy.0.min(self.width - 1), xy.1.min(self.height - 1))
|
|
}
|
|
|
|
#[inline]
|
|
pub fn draw_rect(&mut self, color: u32, tl: Point, br: Point) {
|
|
if tl.0 >= self.width || tl.1 >= self.height {
|
|
return;
|
|
}
|
|
|
|
let br = self.clip(br);
|
|
|
|
unsafe {
|
|
self.draw_rect_unchecked(color, tl, br);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub unsafe fn draw_rect_unchecked(&mut self, color: u32, tl: Point, br: Point) {
|
|
let row = tl.1 * self.width;
|
|
let mut dst_start = row + tl.0;
|
|
let mut dst_end = row + br.0;
|
|
|
|
for _ in tl.1..br.1 {
|
|
let dst = dst_start..dst_end;
|
|
self.buffer.get_unchecked_mut(dst).fill(color);
|
|
dst_start += self.width;
|
|
dst_end += self.width;
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn blit(&mut self, xy: Point, src: &Canvas) {
|
|
let size = Point(src.width, src.height);
|
|
self.blit_from_slice(xy, &src.buffer, size);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn blit_from_slice(&mut self, xy: Point, src_buffer: &[u32], src_size: Point) {
|
|
if xy.0 < self.width && xy.1 < self.height {
|
|
let cw = (self.width - xy.0 - 1).min(src_size.0);
|
|
let ch = (self.height - xy.1 - 1).min(src_size.1);
|
|
|
|
let dst_row = xy.1 * self.width;
|
|
let mut dst_start = dst_row + xy.0;
|
|
let mut dst_end = dst_start + cw;
|
|
|
|
let mut src_start = 0;
|
|
let mut src_end = cw;
|
|
|
|
for _ in 0..ch {
|
|
unsafe {
|
|
let dst_buf = self.buffer.get_unchecked_mut(dst_start..dst_end);
|
|
let src_buf = src_buffer.get_unchecked(src_start..src_end);
|
|
dst_buf.copy_from_slice(src_buf);
|
|
dst_start += self.width;
|
|
dst_end += self.width;
|
|
src_start += src_size.0;
|
|
src_end += src_size.0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub struct GlyphInfo {
|
|
c: char,
|
|
fg: u32,
|
|
bg: u32,
|
|
flags: CellFlags,
|
|
}
|
|
|
|
pub struct GlyphCache {
|
|
renderer: BitmapGlyphRenderer,
|
|
glyphs: HashMap<GlyphInfo, Box<[u32]>>,
|
|
}
|
|
|
|
impl GlyphCache {
|
|
pub fn new(font_config: &FontConfig) -> Self {
|
|
Self {
|
|
renderer: BitmapGlyphRenderer::new(font_config),
|
|
glyphs: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn cell_info(&self, graphics: &Graphics, cell: &Cell) -> GlyphInfo {
|
|
GlyphInfo {
|
|
c: cell.c,
|
|
fg: graphics.color_to_u32(&cell.fg),
|
|
bg: graphics.color_to_u32(&cell.bg),
|
|
flags: cell.flags,
|
|
}
|
|
}
|
|
|
|
pub fn lookup(&mut self, glyph: &GlyphInfo) -> &[u32] {
|
|
self.glyphs
|
|
.entry(glyph.clone())
|
|
.or_insert_with(|| self.renderer.draw(glyph).into_boxed_slice())
|
|
}
|
|
}
|
|
|
|
pub struct BitmapGlyphRenderer {
|
|
normal_font: bdf::Font,
|
|
bold_font: bdf::Font,
|
|
cell_width: usize,
|
|
cell_height: usize,
|
|
}
|
|
|
|
impl BitmapGlyphRenderer {
|
|
pub fn new(font_config: &FontConfig) -> Self {
|
|
let normal_font = bdf::open(&font_config.normal).unwrap();
|
|
let bold_font = bdf::open(&font_config.bold).unwrap();
|
|
|
|
if normal_font.bounds() != bold_font.bounds() {
|
|
panic!("Normal and bold font bounds do not match!");
|
|
}
|
|
|
|
let cell_width = normal_font.bounds().width as usize;
|
|
let cell_height = normal_font.bounds().height as usize;
|
|
|
|
Self {
|
|
normal_font,
|
|
bold_font,
|
|
cell_width,
|
|
cell_height,
|
|
}
|
|
}
|
|
|
|
pub fn draw(&self, glyph: &GlyphInfo) -> Vec<u32> {
|
|
// eprintln!("rendering glyph: {:?}", glyph);
|
|
|
|
let mut buf = Vec::with_capacity(self.cell_width * self.cell_height);
|
|
|
|
let font = if glyph.flags.contains(CellFlags::BOLD) {
|
|
&self.bold_font
|
|
} else {
|
|
&self.normal_font
|
|
};
|
|
|
|
if let Some(bitmap) = font.glyphs().get(&glyph.c) {
|
|
let (bg, fg) = if glyph.flags.contains(CellFlags::INVERSE) {
|
|
(glyph.fg, glyph.bg)
|
|
} else {
|
|
(glyph.bg, glyph.fg)
|
|
};
|
|
|
|
for y in 0..self.cell_height {
|
|
if y == self.cell_height >> 1 && glyph.flags.contains(CellFlags::STRIKEOUT)
|
|
|| y == self.cell_height - 1 && glyph.flags.contains(CellFlags::UNDERLINE)
|
|
{
|
|
buf.resize(buf.len() + self.cell_width, fg);
|
|
} else {
|
|
for x in 0..self.cell_width {
|
|
let color = if bitmap.get(x as u32, y as u32) {
|
|
fg
|
|
} else {
|
|
bg
|
|
};
|
|
|
|
buf.push(color);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("character bitmap miss: {:?}", glyph.c);
|
|
buf.resize(buf.capacity(), 0x00ff00ff);
|
|
}
|
|
|
|
buf
|
|
}
|
|
}
|
|
|
|
pub struct Graphics {
|
|
pub canvas: Canvas,
|
|
pub cell_width: usize,
|
|
pub cell_height: usize,
|
|
pub pad_x: usize,
|
|
pub pad_y: usize,
|
|
pub colors: Colors,
|
|
glyph_cache: GlyphCache,
|
|
cell_cache: HashMap<Point, GlyphInfo>,
|
|
redraw_profiler: Profiler,
|
|
present_profiler: Profiler,
|
|
}
|
|
|
|
impl Graphics {
|
|
pub fn new(config: &Config) -> Self {
|
|
let mut colors = Colors::default();
|
|
Self::load_colors(&config.colors, &mut colors);
|
|
|
|
let bg = &config.colors.background;
|
|
let bg = ((bg.r as u32) << 16) | ((bg.g as u32) << 8) | (bg.b as u32);
|
|
let canvas = Canvas::new(bg, 2000, 2000);
|
|
|
|
let glyph_cache = GlyphCache::new(&config.fonts);
|
|
let cell_cache = Default::default();
|
|
|
|
Self {
|
|
canvas,
|
|
cell_width: glyph_cache.renderer.cell_width,
|
|
cell_height: glyph_cache.renderer.cell_height,
|
|
pad_x: config.draw.pad_x as usize,
|
|
pad_y: config.draw.pad_y as usize,
|
|
glyph_cache,
|
|
cell_cache,
|
|
colors,
|
|
redraw_profiler: Profiler::new("redraw", 300),
|
|
present_profiler: Profiler::new("present", 300),
|
|
}
|
|
}
|
|
|
|
pub fn load_colors(config: &ColorConfig, colors: &mut Colors) {
|
|
use NamedColor::*;
|
|
|
|
let rgb = |value: hex_color::HexColor| {
|
|
Some(Rgb {
|
|
r: value.r,
|
|
g: value.g,
|
|
b: value.b,
|
|
})
|
|
};
|
|
|
|
let mut set = |color: NamedColor, value: hex_color::HexColor| {
|
|
colors[color] = rgb(value);
|
|
};
|
|
|
|
set(Background, config.background);
|
|
set(Foreground, config.foreground);
|
|
set(Black, config.black);
|
|
set(Red, config.red);
|
|
set(Green, config.green);
|
|
set(Yellow, config.yellow);
|
|
set(Blue, config.blue);
|
|
set(Magenta, config.magenta);
|
|
set(Cyan, config.cyan);
|
|
set(White, config.white);
|
|
set(BrightBlack, config.bright_black);
|
|
set(BrightRed, config.bright_red);
|
|
set(BrightGreen, config.bright_green);
|
|
set(BrightYellow, config.bright_yellow);
|
|
set(BrightBlue, config.bright_blue);
|
|
set(BrightMagenta, config.bright_magenta);
|
|
set(BrightCyan, config.bright_cyan);
|
|
set(BrightWhite, config.bright_white);
|
|
|
|
colors[256] = rgb(config.foreground);
|
|
colors[257] = rgb(config.background);
|
|
}
|
|
|
|
pub fn get_color_idx(&self, index: usize) -> Rgb {
|
|
if let Some(color) = self.colors[index] {
|
|
color
|
|
} else if index > 255 {
|
|
eprintln!("color miss and out-of-bounds at {index}");
|
|
|
|
Rgb {
|
|
r: 0xff,
|
|
g: 0x00,
|
|
b: 0xff,
|
|
}
|
|
} else if let Some(gray) = index.checked_sub(232) {
|
|
let value = gray as u8 * 10 + 8;
|
|
Rgb {
|
|
r: value,
|
|
g: value,
|
|
b: value,
|
|
}
|
|
} else if let Some(cube_idx) = index.checked_sub(16) {
|
|
let cube_idx = cube_idx as u8;
|
|
let r = cube_idx / 36;
|
|
let g = (cube_idx / 6) % 6;
|
|
let b = cube_idx % 6;
|
|
|
|
let c = |c| {
|
|
if c == 0 {
|
|
0
|
|
} else {
|
|
c * 40 + 55
|
|
}
|
|
};
|
|
|
|
Rgb {
|
|
r: c(r),
|
|
g: c(g),
|
|
b: c(b),
|
|
}
|
|
} else {
|
|
eprintln!("color index miss at {index}");
|
|
|
|
Rgb {
|
|
r: 0xff,
|
|
g: 0x00,
|
|
b: 0xff,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_color(&self, color: &Color) -> Rgb {
|
|
match color {
|
|
Color::Named(name) => self.colors[*name].unwrap(),
|
|
Color::Spec(rgb) => *rgb,
|
|
Color::Indexed(index) => self.get_color_idx(*index as usize),
|
|
}
|
|
}
|
|
|
|
pub fn color_to_u32(&self, color: &Color) -> u32 {
|
|
let rgb = self.get_color(color);
|
|
((rgb.r as u32) << 16) | ((rgb.g as u32) << 8) | (rgb.b as u32)
|
|
}
|
|
|
|
pub fn resize(&mut self, width: usize, height: usize) {
|
|
let bg = self.color_to_u32(&Color::Named(NamedColor::Background));
|
|
self.canvas = Canvas::new(bg, width, height);
|
|
self.cell_cache.clear();
|
|
}
|
|
|
|
pub fn redraw(&mut self, term: &Term<TermListener>, context: &mut GraphicsContext<Window>) {
|
|
let start = std::time::Instant::now();
|
|
|
|
let (width, height) = {
|
|
let size = context.window().inner_size();
|
|
(size.width as usize, size.height as usize)
|
|
};
|
|
|
|
if width != self.canvas.width || height != self.canvas.height {
|
|
self.resize(width, height);
|
|
}
|
|
|
|
let cursor_color = Color::Named(NamedColor::Foreground);
|
|
let cursor_color = self.color_to_u32(&cursor_color);
|
|
let cell_size = Point(self.cell_width, self.cell_height);
|
|
let content = term.renderable_content();
|
|
|
|
let pad_offset = Point(self.pad_x, self.pad_y);
|
|
let inner_bounds = Point(width, height) - pad_offset - pad_offset;
|
|
|
|
for cell in content.display_iter.into_iter() {
|
|
let term_col = cell.point.column.0 as usize;
|
|
let term_row = cell.point.line.0 as usize;
|
|
let term_coords = Point(term_col, term_row);
|
|
|
|
if (term_row + 1) * self.cell_height >= inner_bounds.1
|
|
|| (term_col + 1) * self.cell_width >= inner_bounds.0
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if cell.flags.contains(CellFlags::HIDDEN) {
|
|
continue;
|
|
}
|
|
|
|
let glyph_info = self.glyph_cache.cell_info(self, &cell);
|
|
|
|
if self.cell_cache.get(&term_coords) == Some(&glyph_info) {
|
|
continue;
|
|
} else {
|
|
self.cell_cache.insert(term_coords, glyph_info.clone());
|
|
}
|
|
|
|
let xy = term_coords * cell_size + pad_offset;
|
|
let glyph = self.glyph_cache.lookup(&glyph_info);
|
|
self.canvas.blit_from_slice(xy, glyph, cell_size);
|
|
}
|
|
|
|
let term_col = content.cursor.point.column.0 as usize;
|
|
let term_row = content.cursor.point.line.0 as usize;
|
|
let term_coords = Point(term_col, term_row);
|
|
|
|
if (term_row + 1) * self.cell_height < inner_bounds.1
|
|
&& (term_col + 1) * self.cell_width < inner_bounds.0
|
|
{
|
|
let mut px_row = term_row * width * self.cell_height + term_col * self.cell_width;
|
|
|
|
use alacritty_terminal::ansi::CursorShape;
|
|
match content.cursor.shape {
|
|
CursorShape::Block => {
|
|
let tl = term_coords * cell_size + pad_offset;
|
|
let br = tl + cell_size;
|
|
self.canvas.draw_rect(cursor_color, tl, br);
|
|
}
|
|
CursorShape::Beam => {
|
|
let tl = term_coords * cell_size + pad_offset;
|
|
let br = tl + Point(2, cell_size.1);
|
|
self.canvas.draw_rect(cursor_color, tl, br);
|
|
}
|
|
CursorShape::Underline => {
|
|
let mut tl = term_coords * cell_size + pad_offset;
|
|
let br = tl + cell_size;
|
|
tl.1 = br.1 - 2;
|
|
self.canvas.draw_rect(cursor_color, tl, br);
|
|
}
|
|
CursorShape::HollowBlock => {
|
|
// TODO Canvas::draw_hollow_rect()
|
|
let buffer = &mut self.canvas.buffer;
|
|
|
|
for x in 0..self.cell_width {
|
|
buffer[px_row + x as usize] = cursor_color;
|
|
}
|
|
|
|
for _y in 0..(self.cell_height - 1) {
|
|
buffer[px_row] = cursor_color;
|
|
buffer[px_row + self.cell_width - 1] = cursor_color;
|
|
px_row += width;
|
|
}
|
|
|
|
for x in 0..self.cell_width {
|
|
buffer[px_row + x as usize] = cursor_color;
|
|
}
|
|
}
|
|
CursorShape::Hidden => {}
|
|
}
|
|
|
|
if content.cursor.shape != CursorShape::Hidden {
|
|
self.cell_cache.remove(&term_coords);
|
|
}
|
|
}
|
|
|
|
self.redraw_profiler.push(start.elapsed());
|
|
|
|
let present_start = std::time::Instant::now();
|
|
context.set_buffer(&self.canvas.buffer, width as u16, height as u16);
|
|
self.present_profiler.push(present_start.elapsed());
|
|
}
|
|
}
|
|
|
|
pub struct Profiler {
|
|
name: String,
|
|
limit: usize,
|
|
sum: Duration,
|
|
ring: VecDeque<Duration>,
|
|
}
|
|
|
|
impl Profiler {
|
|
pub fn new(name: &str, limit: usize) -> Self {
|
|
Self {
|
|
name: name.to_string(),
|
|
limit,
|
|
sum: Duration::ZERO,
|
|
ring: VecDeque::with_capacity(limit),
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, time: Duration) {
|
|
if self.ring.len() == self.limit {
|
|
self.sum -= self.ring.pop_front().unwrap();
|
|
}
|
|
|
|
self.sum += time;
|
|
self.ring.push_back(time);
|
|
|
|
let average = self.sum.div_f32(self.ring.len() as f32);
|
|
log::debug!("{} time: {:?}", self.name, average);
|
|
}
|
|
}
|