use std::{collections::HashMap, error::Error, rc::Rc};
use fontdue::{FontSettings, Metrics};
use rustybuzz::{Direction, GlyphBuffer, Language, Script, ShapePlan, UnicodeBuffer};
use font_loader::system_fonts::{self, FontPropertyBuilder};
use crate::{
RcLine, Ui,
json_ui::{self, Config, InfoShowStyle, MenuShowStyle},
};
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, Rc<[Run]>)>;
#[derive(Debug)]
struct Run {
fg: [u8; 4],
bg: Option<[u8; 4]>,
font: FontIndex,
glyph_buffer: GlyphBuffer,
font_size: u32,
upe: i32,
}
impl Run {
fn length(&self) -> usize {
self.glyph_buffer
.glyph_positions()
.iter()
.map(|i| i.x_advance * self.font_size as i32 / self.upe)
.sum::<i32>() as usize
}
}
#[derive(Default)]
pub struct Shaper {
fonts: Vec<(Vec<u8>, rustybuzz::Face<'static>)>,
plans: Plans,
}
fn shape(plans: &mut Plans, index: FontIndex, font: &rustybuzz::Face, text: &str) -> GlyphBuffer {
let mut ubuf = UnicodeBuffer::new();
ubuf.push_str(text);
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)
}
impl Shaper {
fn make_runs(&mut self, line: RcLine, config: &Config, default_fg: [u8; 4]) -> Rc<[Run]> {
let font_size = config.font_size;
let mut res = Vec::new();
let mut font = 0;
let slice: &[json_ui::Atom] = &line;
for atom in slice {
let mut text = String::new();
for c in atom.contents.chars() {
let Some(curr_font) = (if self.fonts[font].1.unicode_ranges().contains_char(c) {
Some(font)
} else {
self.fonts
.iter()
.position(|i| i.1.unicode_ranges().contains_char(c))
}) else {
text.push('⋄');
continue;
};
if font != curr_font {
let rb_font = &self.fonts[font].1;
let glyph_buffer = shape(&mut self.plans, font, rb_font, &text);
let run = Run {
fg: atom.face.fg.to_rgba(config).unwrap_or(default_fg),
bg: atom.face.bg.to_rgba(config),
font_size,
font,
glyph_buffer,
upe: rb_font.units_per_em(),
};
text.clear();
res.push(run);
font = curr_font;
}
if c == '\n' {
text.push(' ');
} else {
text.push(c);
}
}
let rb_font = &self.fonts[font].1;
let glyph_buffer = shape(&mut self.plans, font, rb_font, &text);
let run = Run {
fg: atom.face.fg.to_rgba(config).unwrap_or(default_fg),
bg: atom.face.bg.to_rgba(config),
font_size,
font,
glyph_buffer,
upe: rb_font.units_per_em(),
};
res.push(run);
}
res.into()
}
}
#[derive(Debug, 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: u32,
) -> &(Metrics, Vec<u8>) {
self.cache
.entry((font, glyph))
.or_insert_with(|| self.fonts[font].rasterize_indexed(glyph, font_size as f32))
}
}
pub struct Target<'a> {
pub size: (usize, usize),
pub buf: &'a mut [u32],
}
impl<'a> Target<'a> {
pub fn new(buf: &'a mut [u32], width: usize, height: usize) -> Self {
Self {
buf,
size: (width, height),
}
}
#[inline]
fn draw_rect(&mut self, x: usize, y: usize, w: usize, h: usize, color: [u8; 4]) {
for i in y..(y + h) {
for j in x..(x + w) {
if j >= self.size.0 || i >= self.size.1 {
break;
}
let bidx = j + i * self.size.0;
self.buf[bidx] = u32::from_be_bytes(color);
}
}
}
}
pub fn load_fonts(
fonts: &[String],
shaper: &mut Shaper,
rasterizer: &mut Rasterizer,
) -> Result<(), Box<dyn Error>> {
shaper.fonts.clear();
rasterizer.fonts.clear();
rasterizer.cache.clear();
for font in fonts {
let property = if font == "monospace" {
FontPropertyBuilder::new().monospace().build()
} else {
FontPropertyBuilder::new().family(font).build()
};
let (data, _) = system_fonts::get(&property).expect("Failed to load font");
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(())
}
pub fn line_height(font_size: f32) -> f32 {
font_size * 1.5
}
fn rasterize_segment(
buf: &mut Target,
pos: &mut (usize, usize),
run: &Run,
rasterizer: &mut Rasterizer,
) {
let Run {
upe,
glyph_buffer,
font,
font_size,
..
} = run;
let line_height = line_height(*font_size as f32) as usize;
if let Some(bg) = run.bg {
buf.draw_rect(
pos.0,
pos.1 - line_height + 4,
run.length(),
line_height,
bg,
);
}
let fs = *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) = rasterizer.rasterize_glyph(*font, ginfo.glyph_id as u16, *font_size);
if gpos.x_offset < 0 {
continue;
}
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 as i32 + x_off) as usize + column;
let y = (pos.1 as i32 + y_off) as usize + row;
let bidx = x + y * buf.size.0;
let sidx = column + row * metrics.width;
if bidx >= buf.buf.len() {
break;
}
if x > buf.size.0 {
break;
}
let mut pixel = u32::to_be_bytes(buf.buf[bidx]);
for i in 0..4 {
let m = data[sidx] as u32;
let d = pixel[i] as u32;
let s = run.fg[i] as u32;
pixel[i] = ((d * (255 - m) + s * m) / 255) as u8;
}
buf.buf[bidx] = u32::from_be_bytes(pixel);
}
}
pos.0 += (gpos.x_advance * fs / upe) as usize;
pos.1 += (gpos.y_advance * fs / upe) as usize;
}
}
pub fn render(
buf: &mut Target,
shaper: &mut Shaper,
rs: &mut Rasterizer,
ui: &Ui,
config: &Config,
) -> Result<(), Box<dyn Error>> {
let default_bg = ui.default_face.bg.to_rgba(config).unwrap_or(config.black.0);
let default_fg = ui.default_face.fg.to_rgba(config).unwrap_or(config.white.0);
buf.draw_rect(0, 0, buf.size.0, buf.size.1, default_bg);
let line_height = line_height(config.font_size as f32) as usize;
let mut pos = (0, line_height);
for line in &ui.lines {
for run in shaper.make_runs(line.clone(), config, default_fg).iter() {
rasterize_segment(buf, &mut pos, run, rs);
}
pos.0 = 0;
pos.1 += line_height;
}
let status_padding: usize = 10;
pos.1 = buf.size.1 - status_padding;
pos.0 = 0;
if let Some(bg) = ui.line_face.bg.to_rgba(config) {
buf.draw_rect(0, pos.1 - line_height + 4, buf.size.0, line_height, bg);
}
let line_fg = ui.line_face.fg.to_rgba(config).unwrap_or(default_fg);
for run in shaper
.make_runs(ui.prompt_line.clone(), config, line_fg)
.iter()
{
rasterize_segment(buf, &mut pos, run, rs);
}
let mode_fg = ui.line_face.fg.to_rgba(config).unwrap_or(default_fg);
let mode_line = shaper.make_runs(ui.mode_line.clone(), config, mode_fg);
let text_width: i32 = mode_line.iter().map(|run| run.length() as i32).sum();
pos.0 = buf.size.0.saturating_sub(text_width as usize);
for run in mode_line.iter() {
rasterize_segment(buf, &mut pos, run, rs);
}
let menu_padding_x: usize = 2;
let menu_padding_y: usize = 2;
let mut menu_space: usize = 0;
for menu in &ui.menus {
let menu_bg = menu.menu_face.bg.to_rgba(config).unwrap_or(default_bg);
let menu_fg = menu.menu_face.fg.to_rgba(config).unwrap_or(default_fg);
let selected_fg = menu.selected_face.fg.to_rgba(config).unwrap_or(default_fg);
let selected_bg = menu.selected_face.bg.to_rgba(config).unwrap_or(default_bg);
let lines = menu.content.iter().enumerate().map(|(i, line)| {
if menu.selected == i {
(
i,
selected_bg,
shaper.make_runs(line.clone(), config, selected_fg),
)
} else {
(i, menu_bg, shaper.make_runs(line.clone(), config, menu_fg))
}
});
match menu.style {
MenuShowStyle::Prompt => {
let width = config.font_size * 15;
let num_width = buf.size.0 / width as usize;
let num_height = usize::min(menu.content.len() / num_width, 10);
let width = buf.size.0 / num_width;
menu_space = line_height * num_height + menu_padding_y * 2;
for (i, bg, line) in lines {
let x = i % num_width;
let y = i / num_width;
if y >= num_height {
break;
}
pos.0 = x * width + menu_padding_x;
pos.1 = (buf.size.1)
.saturating_sub(line_height * (y + 1) + menu_padding_y + status_padding);
buf.draw_rect(pos.0, pos.1 - line_height + 4, width, line_height, bg);
for run in line.iter() {
rasterize_segment(buf, &mut pos, run, rs);
}
}
}
_ => {}
}
}
let info_padding_x: usize = 5;
let info_padding_y: usize = 5;
for info in &ui.infos {
let info_fg = info.face.fg.to_rgba(config).unwrap_or(default_fg);
let lines: Vec<_> = info
.content
.iter()
.map(|line| shaper.make_runs(line.clone(), config, info_fg))
.collect();
let width = lines
.iter()
.map(|run| run.iter().map(|run| run.length() as i32).sum::<i32>())
.max()
.unwrap_or(0) as usize
+ info_padding_x * 2;
let height = line_height * info.content.len() + info_padding_y * 2;
let info_bg = info.face.bg.to_rgba(config).unwrap_or(default_bg);
match info.style {
InfoShowStyle::Prompt => {
pos.0 = buf.size.0.saturating_sub(width);
pos.1 = buf
.size
.1
.saturating_sub(height + line_height + status_padding + menu_space);
}
InfoShowStyle::Modal => {
pos.0 = (buf.size.0.saturating_sub(width)) / 2;
pos.1 = (buf.size.1.saturating_sub(height)) / 2;
}
_ => {}
}
buf.draw_rect(pos.0, pos.1, width, height, info_bg);
let start = pos.0 + info_padding_x;
pos.1 += info_padding_y;
for runs in lines {
pos.0 = start;
pos.1 += line_height;
for run in runs.iter() {
rasterize_segment(buf, &mut pos, run, rs);
}
}
}
Ok(())
}