use std::{collections::HashMap, error::Error};
use fontdue::{FontSettings, Metrics};
use pixels::{Pixels, SurfaceTexture};
use rustybuzz::{Direction, GlyphBuffer, Language, Script, ShapePlan, UnicodeBuffer};
use winit::{dpi::PhysicalSize, event_loop::ActiveEventLoop, window::Window};
use crate::{
HEIGHT, RcLine, Ui, WIDTH,
json_ui::{self, BuiltinColor, Color},
};
type GlyphIndex = u16;
type FontIndex = usize;
type Plans = HashMap<(FontIndex, Direction, Script, Option<Language>), ShapePlan>;
type GlyphCache = HashMap<(FontIndex, GlyphIndex), (Metrics, Vec<u8>)>;
type ShapeCache = HashMap<RcLine, (bool, Vec<(json_ui::Face, FontIndex, i32, GlyphBuffer)>)>;
#[derive(Default)]
pub struct Shaper {
pub fonts: Vec<(Vec<u8>, rustybuzz::Face<'static>)>,
pub cache: ShapeCache,
pub plans: Plans,
}
impl Shaper {
fn shape(
plans: &mut Plans,
index: FontIndex,
font: &rustybuzz::Face,
mut ubuf: UnicodeBuffer,
) -> GlyphBuffer {
ubuf.guess_segment_properties();
let direction = match ubuf.direction() {
Direction::Invalid => Direction::LeftToRight,
dir => dir,
};
let script = ubuf.script();
let language = ubuf.language();
let plan = plans
.entry((index, direction, script, language.clone()))
.or_insert_with(|| {
ShapePlan::new(font, direction, Some(script), language.as_ref(), &[])
});
rustybuzz::shape_with_plan(font, plan, ubuf)
}
fn make_runs(&mut self, line: RcLine) -> &[(json_ui::Face, FontIndex, i32, GlyphBuffer)] {
let res = self.cache.entry(line.clone()).or_insert_with(|| {
let mut res = Vec::new();
let mut curr_font = 0;
let slice: &[json_ui::Atom] = &line;
for atom in slice {
let mut start = 0;
for (i, c) in atom.contents.char_indices() {
let font = self
.fonts
.iter()
.position(|i| i.1.unicode_ranges().contains_char(c))
.unwrap_or(0);
if curr_font != font || c == '\n' {
let mut ubuf = UnicodeBuffer::new();
ubuf.push_str(&atom.contents[start..i]);
let rb_font = &self.fonts[curr_font].1;
let glyph_buffer = Self::shape(&mut self.plans, curr_font, rb_font, ubuf);
res.push((
atom.face.clone(),
curr_font,
rb_font.units_per_em(),
glyph_buffer,
));
curr_font = font;
start = i;
}
if c == '\n' {
start += 1;
}
}
let mut ubuf = UnicodeBuffer::new();
ubuf.push_str(&atom.contents[start..]);
let rb_font = &self.fonts[curr_font].1;
let glyph_buffer = Self::shape(&mut self.plans, curr_font, rb_font, ubuf);
res.push((
atom.face.clone(),
curr_font,
rb_font.units_per_em(),
glyph_buffer,
));
}
(true, res)
});
res.0 = true;
&res.1
}
fn clear_cache(&mut self) {
self.cache.retain(|_, v| {
let oldv = v.0;
v.0 = false;
oldv
})
}
}
#[derive(Default)]
pub struct Rasterizer {
pub fonts: Vec<fontdue::Font>,
pub cache: GlyphCache,
}
impl Rasterizer {
fn rasterize_glyph(
&mut self,
font: FontIndex,
glyph: GlyphIndex,
font_size: f32,
) -> &(Metrics, Vec<u8>) {
self.cache
.entry((font, glyph))
.or_insert_with(|| self.fonts[font].rasterize_indexed(glyph, font_size))
}
#[allow(clippy::too_many_arguments)]
fn rasterize_segment(
&mut self,
buf: &mut [u8],
size: (u32, u32),
font: FontIndex,
glyph_buffer: &GlyphBuffer,
upe: i32,
font_size: f32,
face: &json_ui::Face,
default_face: &json_ui::Face,
pos: &mut (i32, i32),
) {
let fs = font_size as i32;
let line_height = font_size as i32;
for i in 0..glyph_buffer.len() {
let ginfo = glyph_buffer.glyph_infos()[i];
let gpos = glyph_buffer.glyph_positions()[i];
let (metrics, data) = self.rasterize_glyph(font, ginfo.glyph_id as u16, font_size);
if face.bg != Color::BuiltinColor(BuiltinColor::Default) {
for row in -4..line_height {
for column in 0..(gpos.x_advance * fs / upe) {
let x = pos.0 + column;
let y = pos.1 - row;
let bidx = ((x + y * size.0 as i32) * 4) as usize;
let bg = face.bg.to_rgba([0, 0, 0, 255]);
buf[bidx..(4 + bidx)].copy_from_slice(&bg);
}
}
}
let x_off = gpos.x_offset * fs / upe + metrics.xmin;
let y_off = gpos.y_offset * fs / upe - metrics.ymin - metrics.height as i32;
for row in 0..metrics.height {
for column in 0..metrics.width {
let x = pos.0 + x_off + column as i32;
let y = pos.1 + y_off + row as i32;
let bidx = ((x + y * size.0 as i32) * 4) as usize;
let sidx = column + row * metrics.width;
if bidx >= buf.len() {
break;
}
if x > size.0 as i32 {
break;
}
let fg = &face
.fg
.to_rgba(default_face.fg.to_rgba([255, 255, 255, 255]));
for i in 0..3 {
let m = data[sidx] as u32;
let d = buf[bidx + i] as u32;
let s = fg[i] as u32;
let res = (d * (255 - m) + s * m) / 255;
buf[bidx + i] = res as u8;
}
buf[bidx + 3] = 255;
}
}
pos.0 += gpos.x_advance * fs / upe;
pos.1 += gpos.y_advance * fs / upe;
}
}
}
pub struct Renderer {
#[allow(unused)]
pub window: Window,
pub pixels: Pixels<'static>,
pub size: (u32, u32),
shaper: Shaper,
rasterizer: Rasterizer,
font_size: f32,
}
impl Renderer {
pub fn new(event_loop: &ActiveEventLoop) -> Result<Self, Box<dyn Error>> {
let window = event_loop.create_window(
Window::default_attributes()
.with_inner_size(PhysicalSize::new(WIDTH as f64, HEIGHT as f64))
.with_title("Kakoune Client"),
)?;
let st = SurfaceTexture::new(
WIDTH,
HEIGHT,
unsafe { &*(&window as *const Window) },
);
let pixels = Pixels::new(WIDTH, HEIGHT, st)?;
let mut fontdb = fontdb::Database::new();
fontdb.load_system_fonts();
let mut shaper = Shaper::default();
let mut rasterizer = Rasterizer::default();
for font in ["Iosevka", "Manjari"] {
let id = fontdb
.query(&fontdb::Query {
families: &[fontdb::Family::Name(font)],
..Default::default()
})
.ok_or("Unknown font")?;
fontdb
.with_face_data(id, |data, _| {
let data = data.to_vec();
let rb_face = rustybuzz::Face::from_slice(
unsafe { &*(data.as_slice() as *const [u8]) },
0,
)
.ok_or("Failed to load rustybuzz font")?;
let fd_face =
fontdue::Font::from_bytes(data.as_slice(), FontSettings::default())?;
shaper.fonts.push((data, rb_face));
rasterizer.fonts.push(fd_face);
Ok(())
})
.ok_or("Failed to load font")
.flatten()?;
}
Ok(Self {
window,
pixels,
size: (WIDTH, HEIGHT),
shaper,
rasterizer,
font_size: 16.0,
})
}
pub fn line_height(&self) -> u32 {
(self.font_size * 1.5) as u32
}
pub fn render(&mut self, ui: &Ui) -> Result<(), Box<dyn Error>> {
let bg = ui.default_face.bg.to_rgba([0, 0, 0, 255]);
for c in self.pixels.frame_mut().chunks_exact_mut(4) {
c.copy_from_slice(&bg);
}
let line_height = self.line_height() as i32;
let mut pos = (0, line_height);
for line in &ui.lines {
for (face, font, upe, glyph_buffer) in self.shaper.make_runs(line.clone()) {
self.rasterizer.rasterize_segment(
self.pixels.frame_mut(),
self.size,
*font,
glyph_buffer,
*upe,
self.font_size,
face,
&ui.default_face,
&mut pos,
);
}
pos.0 = 0;
pos.1 += line_height;
}
let line_height = self.font_size as i32;
let line_bg = ui.line_face.bg.to_rgba([0, 0, 0, 255]);
pos.1 = self.size.1 as i32 - line_height;
pos.0 = 0;
let start = (pos.0 as usize + (pos.1 - line_height) as usize * self.size.0 as usize) * 4;
let end = (pos.0 as usize + (pos.1 + 4) as usize * self.size.0 as usize) * 4;
for c in self.pixels.frame_mut()[start..=end].chunks_exact_mut(4) {
c.copy_from_slice(&line_bg);
}
for (face, font, upe, glyph_buffer) in self.shaper.make_runs(ui.prompt_line.clone()) {
self.rasterizer.rasterize_segment(
self.pixels.frame_mut(),
self.size,
*font,
glyph_buffer,
*upe,
self.font_size,
face,
&ui.line_face,
&mut pos,
);
}
let mode_line = self.shaper.make_runs(ui.mode_line.clone());
let text_width: i32 = mode_line
.iter()
.map(|(_, _, upe, glyph_buffer)| {
glyph_buffer
.glyph_positions()
.iter()
.map(|gpos| gpos.x_advance * self.font_size as i32 / upe)
.sum::<i32>()
})
.sum();
pos.0 = self.size.0 as i32 - text_width;
for (face, font, upe, glyph_buffer) in mode_line {
self.rasterizer.rasterize_segment(
self.pixels.frame_mut(),
self.size,
*font,
glyph_buffer,
*upe,
self.font_size,
face,
&ui.line_face,
&mut pos,
);
}
self.shaper.clear_cache();
self.pixels.render().unwrap();
Ok(())
}
}