From 7ef9d1317b99f3b0693a35246f856462a9bd6ee8 Mon Sep 17 00:00:00 2001 From: andromeda Date: Sat, 17 Jan 2026 17:31:29 +0100 Subject: [PATCH] add font rendering --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/main.rs | 217 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 192 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 83a0c42..68b4a45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -430,6 +430,7 @@ version = "0.1.0" dependencies = [ "minifb", "nix 0.30.1", + "ttf-parser", "vte", "zeno", ] @@ -554,6 +555,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index d6a6f8f..5faf1c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2024" [dependencies] minifb = "0.28.0" nix = {default-features = false, features=["term","process","fs"], version="0.30.1"} +ttf-parser = "0.25.1" vte = "0.15.0" zeno = "0.3.3" diff --git a/src/main.rs b/src/main.rs index fb5f90d..df6e45a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,27 +5,99 @@ use nix::unistd::{execv, read, write}; use minifb::{Key, KeyRepeat, Window, WindowOptions}; use std::vec; +use std::collections::HashMap; use std::ffi::CString; +use std::fmt::Write; + +use ttf_parser::{Face, OutlineBuilder, Rect}; use vte::{Params, Parser, Perform}; -use zeno::{Join, Mask, Stroke}; +use zeno::{Mask, Transform}; -// window stuff -const WIDTH: usize = 640; -const HEIGHT: usize = 360; +// params +const SHELL: &str = "/home/andromeda/.nix-profile/bin/sh"; +const FONT: &str = "/home/andromeda/.nix-profile/share/fonts/truetype/Miracode.ttf"; -// yoinked from vte docs -struct Performer; -impl Perform for Performer { +struct Buffer { + buffer: Vec, + width: usize, + height: usize, +} +impl Buffer { + fn new(val: T, width: usize, height: usize) -> Self { + Buffer { + buffer: vec![val; width * height], + width: width, + height: height, + } + } + fn get(&self, col: usize, row: usize) -> T { + self.buffer.get(col + row * self.width).unwrap().clone() + } + fn set(&mut self, col: usize, row: usize, val: T) { + self.buffer[col + row * self.width] = val; + } +} + +struct Cursor { + col: usize, + row: usize, +} + +struct Model { + screenbuffer: Buffer, + buffer: Buffer>, + cell: Rect, + cursor: Cursor, +} +impl Model { + // `cell` is the bbox of a cell + // width and height are measured in cells + fn new(cell: Rect, width: usize, height: usize, scale: f32) -> Self { + Model { + screenbuffer: Buffer::new( + 0, + (cell.width() as f32 * width as f32 * scale) as usize, + (cell.height() as f32 * height as f32 * scale) as usize + ), + buffer: Buffer::new( + None, + width, + height + ), + cell: cell, + cursor: Cursor { + col: 0, + row: 0, + }, + } + } + + // returns scale + fn scale(&self) -> f32 { + self.screenbuffer.height as f32 / (self.cell.height() as f32 * self.buffer.height as f32) + } +} +impl Perform for Model { + // draw a character to the screen and update states fn print(&mut self, c: char) { + self.buffer.set(self.cursor.col, self.cursor.row, Some(c)); + self.cursor.col += 1; println!("[print] {:?}", c); } + // execute a C0 or C1 control function fn execute(&mut self, byte: u8) { + match byte { + 0x0D => self.cursor.col = 0, + 0x0A => self.cursor.row = self.cursor.row + 1, + _ => (), + } println!("[execute] {:02x}", byte); } + // invoked when a final character arrives in first part of device control string fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) { println!( "[hook] params={:?}, intermediates={:?}, ignore={:?}, char={:?}", @@ -33,18 +105,23 @@ impl Perform for Performer { ); } + // pass bytes as part of a device control string to the handle chosen by hook + // C0 controls are also passed to this handler fn put(&mut self, byte: u8) { println!("[put] {:02x}", byte); } + // called when a device control string is terminated fn unhook(&mut self) { println!("[unhook]"); } + // dispatch an operating system command fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { println!("[osc_dispatch] params={:?} bell_terminated={}", params, bell_terminated); } + // a final character has arrived for a csi sequence fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) { println!( "[csi_dispatch] params={:#?}, intermediates={:?}, ignore={:?}, char={:?}", @@ -52,6 +129,7 @@ impl Perform for Performer { ); } + // the final character of an escape sequence has arrived fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { println!( "[esc_dispatch] intermediates={:?}, ignore={:?}, byte={:02x}", @@ -60,52 +138,103 @@ impl Perform for Performer { } } +// yoinked from tty_parser docs +struct Builder(String); +impl OutlineBuilder for Builder { + fn move_to(&mut self, x: f32, y: f32) { + write!(&mut self.0, "M {} {} ", x, y).unwrap(); + } + + fn line_to(&mut self, x: f32, y: f32) { + write!(&mut self.0, "L {} {} ", x, y).unwrap(); + } + + fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { + write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap(); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap(); + } + + fn close(&mut self) { + write!(&mut self.0, "Z ").unwrap(); + } +} + fn main() { - let mut buffer: Vec = vec![0; WIDTH * HEIGHT]; + // initialize the font + let font_data = std::fs::read(FONT).unwrap(); + let face = Face::parse(&font_data, 0).unwrap(); + let font = generate_font(&face); + + let mut model = Model::new( + Rect { + x_min: 0, + y_min: 0, + x_max: face.height(), + y_max: face.height(), + }, + 80, + 24, + 0.008, + ); + + // initialize window and its buffer let mut window = Window::new( "rust-term", - WIDTH, - HEIGHT, + model.screenbuffer.width, + model.screenbuffer.height, WindowOptions::default(), ).unwrap(); - window.set_target_fps(60); - let pty = spawn_pty("/home/andromeda/.nix-profile/bin/sh").unwrap(); - println!("{:?}", &pty); - let pty_flags = fcntl(&pty, F_GETFL).unwrap(); - fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(pty_flags) | OFlag::O_NONBLOCK)).unwrap(); + let pty = spawn_pty(&SHELL).unwrap(); + fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(fcntl(&pty, F_GETFL).unwrap()) | OFlag::O_NONBLOCK)).unwrap(); let mut statemachine = Parser::new(); - let mut performer = Performer; + let mut buf = [0u8; 2048]; - while window.is_open() && !window.is_key_down(Key::Escape) { + // mask to render into + let mut mask = vec![0u8; model.screenbuffer.width * model.screenbuffer.height]; - let mask = Mask::new("M 8,56 32,8 56,56 Z") - .size(WIDTH.try_into().unwrap(), HEIGHT.try_into().unwrap()) - .style(Stroke::new(8.0).join(Join::Round)) - .render() - .0; + // render each char into mask + for row in 0..model.buffer.height { + for col in 0..model.buffer.width { + if let Some(c) = model.buffer.get(col, row) { + Mask::new(font.get(&c).map_or("", |v| v)) // get svg + .transform(Some( + Transform::scale(model.scale(), -model.scale()) // scale it + .then_translate( + col as f32 * model.cell.width() as f32 * model.scale(), + // shift right by the cell width * the scale + (1 + row) as f32 * model.scale() * model.cell.height() as f32, + ) + )) + .size(model.screenbuffer.width as u32, model.screenbuffer.height as u32) + .render_into(&mut mask, None); } + }; + }; - // windowing stuff - for (p, a) in buffer.iter_mut().zip(mask.iter()) { - let a0 = *a as u32; - *p = a0 << 24 | 0x00FF0000; - *p = - if a0 > 0 - { a0 << 16 | a0 << 8 | a0 } - else { 0x00000000 }; + // render in white/grayscale to screen + for (p, m) in model.screenbuffer.buffer.iter_mut().zip(mask.iter()) { + let m0 = *m as u32; + *p = m0 << 16 | m0 << 8 | m0; } - let keys = window.get_keys_pressed(KeyRepeat::No); - window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap(); + + // update screen with buffer + window.update_with_buffer(&model.screenbuffer.buffer, model.screenbuffer.width, model.screenbuffer.height).unwrap(); + // other stuff match read(&pty, &mut buf) { Ok(0) => (), - Ok(n) => statemachine.advance(&mut performer, &buf[..n]), + Ok(n) => statemachine.advance(&mut model, &buf[..n]), Err(_e) => (), }; + + let keys = window.get_keys_pressed(KeyRepeat::No); if !keys.is_empty() { let bytes: Vec = keys.iter() // TODO apply modifiers @@ -283,3 +412,25 @@ fn key_to_u8(key: Key, shift: bool, ctrl: bool) -> u8 { return base_shift_ctrl; } +// creats a mapping from a `char` to its svg spec for a selection of characters +fn generate_font(face: &Face) -> HashMap { + let chars = + vec![ + '\'', '`', '\\', ',', '=', '[', '-', '.', ']', ';', '/', + ')', '!', '@', '#', '$', '%', '^', '&', '*', '(', '"', '~', '|', '<', '+', '{', '_', '>', '}', ':', '?' + ] + .into_iter() + .chain('a'..='z') + .chain('A'..='Z') + .chain('0'..='9'); + + let mut hm = HashMap::new(); + + for c in chars { + let mut builder = Builder(String::new()); + face.outline_glyph(face.glyph_index(c).unwrap(), &mut builder).unwrap(); + hm.entry(c).insert_entry(builder.0); + } + + hm +}