add font rendering
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -430,6 +430,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"minifb",
|
"minifb",
|
||||||
"nix 0.30.1",
|
"nix 0.30.1",
|
||||||
|
"ttf-parser",
|
||||||
"vte",
|
"vte",
|
||||||
"zeno",
|
"zeno",
|
||||||
]
|
]
|
||||||
@@ -554,6 +555,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ttf-parser"
|
||||||
|
version = "0.25.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
minifb = "0.28.0"
|
minifb = "0.28.0"
|
||||||
nix = {default-features = false, features=["term","process","fs"], version="0.30.1"}
|
nix = {default-features = false, features=["term","process","fs"], version="0.30.1"}
|
||||||
|
ttf-parser = "0.25.1"
|
||||||
vte = "0.15.0"
|
vte = "0.15.0"
|
||||||
zeno = "0.3.3"
|
zeno = "0.3.3"
|
||||||
|
|||||||
217
src/main.rs
217
src/main.rs
@@ -5,27 +5,99 @@ use nix::unistd::{execv, read, write};
|
|||||||
use minifb::{Key, KeyRepeat, Window, WindowOptions};
|
use minifb::{Key, KeyRepeat, Window, WindowOptions};
|
||||||
|
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use ttf_parser::{Face, OutlineBuilder, Rect};
|
||||||
|
|
||||||
use vte::{Params, Parser, Perform};
|
use vte::{Params, Parser, Perform};
|
||||||
|
|
||||||
use zeno::{Join, Mask, Stroke};
|
use zeno::{Mask, Transform};
|
||||||
|
|
||||||
// window stuff
|
// params
|
||||||
const WIDTH: usize = 640;
|
const SHELL: &str = "/home/andromeda/.nix-profile/bin/sh";
|
||||||
const HEIGHT: usize = 360;
|
const FONT: &str = "/home/andromeda/.nix-profile/share/fonts/truetype/Miracode.ttf";
|
||||||
|
|
||||||
// yoinked from vte docs
|
struct Buffer<T: std::clone::Clone> {
|
||||||
struct Performer;
|
buffer: Vec<T>,
|
||||||
impl Perform for Performer {
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
impl<T: Clone> Buffer<T> {
|
||||||
|
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<u32>,
|
||||||
|
buffer: Buffer<Option<char>>,
|
||||||
|
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) {
|
fn print(&mut self, c: char) {
|
||||||
|
self.buffer.set(self.cursor.col, self.cursor.row, Some(c));
|
||||||
|
self.cursor.col += 1;
|
||||||
println!("[print] {:?}", c);
|
println!("[print] {:?}", c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute a C0 or C1 control function
|
||||||
fn execute(&mut self, byte: u8) {
|
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);
|
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) {
|
fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
|
||||||
println!(
|
println!(
|
||||||
"[hook] params={:?}, intermediates={:?}, ignore={:?}, char={:?}",
|
"[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) {
|
fn put(&mut self, byte: u8) {
|
||||||
println!("[put] {:02x}", byte);
|
println!("[put] {:02x}", byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// called when a device control string is terminated
|
||||||
fn unhook(&mut self) {
|
fn unhook(&mut self) {
|
||||||
println!("[unhook]");
|
println!("[unhook]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dispatch an operating system command
|
||||||
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
|
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
|
||||||
println!("[osc_dispatch] params={:?} bell_terminated={}", params, bell_terminated);
|
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) {
|
fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) {
|
||||||
println!(
|
println!(
|
||||||
"[csi_dispatch] params={:#?}, intermediates={:?}, ignore={:?}, char={:?}",
|
"[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) {
|
fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
|
||||||
println!(
|
println!(
|
||||||
"[esc_dispatch] intermediates={:?}, ignore={:?}, byte={:02x}",
|
"[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() {
|
fn main() {
|
||||||
let mut buffer: Vec<u32> = 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(
|
let mut window = Window::new(
|
||||||
"rust-term",
|
"rust-term",
|
||||||
WIDTH,
|
model.screenbuffer.width,
|
||||||
HEIGHT,
|
model.screenbuffer.height,
|
||||||
WindowOptions::default(),
|
WindowOptions::default(),
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
window.set_target_fps(60);
|
window.set_target_fps(60);
|
||||||
|
|
||||||
let pty = spawn_pty("/home/andromeda/.nix-profile/bin/sh").unwrap();
|
let pty = spawn_pty(&SHELL).unwrap();
|
||||||
println!("{:?}", &pty);
|
fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(fcntl(&pty, F_GETFL).unwrap()) | OFlag::O_NONBLOCK)).unwrap();
|
||||||
let pty_flags = fcntl(&pty, F_GETFL).unwrap();
|
|
||||||
fcntl(&pty, F_SETFL(OFlag::from_bits_truncate(pty_flags) | OFlag::O_NONBLOCK)).unwrap();
|
|
||||||
|
|
||||||
let mut statemachine = Parser::new();
|
let mut statemachine = Parser::new();
|
||||||
let mut performer = Performer;
|
|
||||||
let mut buf = [0u8; 2048];
|
let mut buf = [0u8; 2048];
|
||||||
|
|
||||||
while window.is_open() && !window.is_key_down(Key::Escape) {
|
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")
|
// render each char into mask
|
||||||
.size(WIDTH.try_into().unwrap(), HEIGHT.try_into().unwrap())
|
for row in 0..model.buffer.height {
|
||||||
.style(Stroke::new(8.0).join(Join::Round))
|
for col in 0..model.buffer.width {
|
||||||
.render()
|
if let Some(c) = model.buffer.get(col, row) {
|
||||||
.0;
|
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
|
// render in white/grayscale to screen
|
||||||
for (p, a) in buffer.iter_mut().zip(mask.iter()) {
|
for (p, m) in model.screenbuffer.buffer.iter_mut().zip(mask.iter()) {
|
||||||
let a0 = *a as u32;
|
let m0 = *m as u32;
|
||||||
*p = a0 << 24 | 0x00FF0000;
|
*p = m0 << 16 | m0 << 8 | m0;
|
||||||
*p =
|
|
||||||
if a0 > 0
|
|
||||||
{ a0 << 16 | a0 << 8 | a0 }
|
|
||||||
else { 0x00000000 };
|
|
||||||
}
|
}
|
||||||
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
|
// other stuff
|
||||||
match read(&pty, &mut buf) {
|
match read(&pty, &mut buf) {
|
||||||
Ok(0) => (),
|
Ok(0) => (),
|
||||||
Ok(n) => statemachine.advance(&mut performer, &buf[..n]),
|
Ok(n) => statemachine.advance(&mut model, &buf[..n]),
|
||||||
Err(_e) => (),
|
Err(_e) => (),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let keys = window.get_keys_pressed(KeyRepeat::No);
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
let bytes: Vec<u8> = keys.iter()
|
let bytes: Vec<u8> = keys.iter()
|
||||||
// TODO apply modifiers
|
// TODO apply modifiers
|
||||||
@@ -283,3 +412,25 @@ fn key_to_u8(key: Key, shift: bool, ctrl: bool) -> u8 {
|
|||||||
return base_shift_ctrl;
|
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<char, String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user