term: switch to truetype fonts

This commit is contained in:
Mark Poliakov 2025-03-01 01:20:51 +02:00
parent 8c4bdcbe64
commit c4c8b8acc6
7 changed files with 170 additions and 34 deletions

31
userspace/Cargo.lock generated
View File

@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser",
"owned_ttf_parser 0.25.0",
]
[[package]]
@ -1818,13 +1818,22 @@ dependencies = [
"libredox",
]
[[package]]
name = "owned_ttf_parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb"
dependencies = [
"ttf-parser 0.15.2",
]
[[package]]
name = "owned_ttf_parser"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
dependencies = [
"ttf-parser",
"ttf-parser 0.25.1",
]
[[package]]
@ -2312,6 +2321,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rusttype"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser 0.15.2",
]
[[package]]
name = "rustversion"
version = "1.0.19"
@ -2665,6 +2684,7 @@ dependencies = [
"logsink",
"pci-ids",
"rand 0.9.0-alpha.1",
"regex",
"runtime",
"serde",
"serde_json",
@ -2697,6 +2717,7 @@ dependencies = [
"libpsf",
"log",
"logsink",
"rusttype",
"thiserror",
]
@ -2834,6 +2855,12 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
[[package]]
name = "ttf-parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "ttf-parser"
version = "0.25.1"

View File

@ -12,3 +12,5 @@ logsink.workspace = true
log.workspace = true
thiserror.workspace = true
clap.workspace = true
rusttype = "0.9.3"

View File

@ -14,9 +14,9 @@ pub enum Color {
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(C)]
pub struct DisplayColor {
r: u8,
g: u8,
b: u8,
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Clone, Copy, Debug)]

View File

@ -4,4 +4,6 @@ pub enum Error {
ApplicationError(#[from] libcolors::error::Error),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Font load error")]
FontLoadError,
}

View File

@ -0,0 +1,70 @@
use std::{fs, path::Path};
use crate::error::Error;
pub trait Font {
fn layout(&self) -> &FontLayout;
fn map_glyph<F: FnMut(usize, usize, f32)>(&mut self, ch: char, mapper: F);
}
pub struct TrueTypeFont<'a> {
inner: rusttype::Font<'a>,
scale: rusttype::Scale,
layout: FontLayout,
ascent: f32,
}
impl TrueTypeFont<'static> {
pub fn load<P: AsRef<Path>>(path: P, pixel_height: usize) -> Result<Self, Error> {
let font_data = fs::read(path)?;
let inner = rusttype::Font::try_from_vec(font_data).ok_or(Error::FontLoadError)?;
let unscaled_v_metrics = inner.v_metrics(rusttype::Scale::uniform(1.0f32));
let scale =
(pixel_height as f32) / (unscaled_v_metrics.ascent - unscaled_v_metrics.descent);
let scale = rusttype::Scale::uniform(scale);
let v_metrics = inner.v_metrics(scale);
let a_h_metrics = inner.glyph('a').scaled(scale).h_metrics();
let layout = FontLayout {
width: a_h_metrics.advance_width.ceil() as usize,
height: (v_metrics.ascent - v_metrics.descent).ceil() as usize,
};
let ascent = v_metrics.ascent;
Ok(Self {
inner,
scale,
layout,
ascent,
})
}
}
impl Font for TrueTypeFont<'_> {
fn layout(&self) -> &FontLayout {
&self.layout
}
fn map_glyph<F: FnMut(usize, usize, f32)>(&mut self, ch: char, mut mapper: F) {
let glyph = self
.inner
.glyph(ch)
.scaled(self.scale)
.positioned(rusttype::Point {
x: 0.0,
y: self.ascent,
});
if let Some(bb) = glyph.pixel_bounding_box() {
glyph.draw(|x, y, v| {
let x = (x as i32 + bb.min.x).clamp(0, self.layout.width as i32);
let y = (y as i32 + bb.min.y).clamp(0, self.layout.height as i32);
mapper(x as usize, y as usize, v);
});
}
}
}
#[derive(Debug)]
pub struct FontLayout {
pub width: usize,
pub height: usize,
}

View File

@ -15,6 +15,7 @@ use std::{
rt::io::device,
},
},
path::PathBuf,
process::{Child, Command, ExitCode, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
@ -22,29 +23,32 @@ use std::{
},
};
use clap::Parser;
use error::Error;
use libpsf::PcScreenFont;
use font::{Font, TrueTypeFont};
use libcolors::{
application::{
window::{EventOutcome, Window},
Application,
},
event::KeyModifiers, input::Key,
event::KeyModifiers,
input::Key,
};
use state::{Cursor, State};
pub mod attr;
pub mod error;
pub mod font;
pub mod state;
struct DrawState {
struct DrawState<F: Font> {
width: usize,
force_redraw: bool,
focus_changed: bool,
focused: bool,
old_cursor: Cursor,
font: PcScreenFont<'static>,
font: F,
}
pub struct Terminal<'a> {
@ -59,24 +63,24 @@ pub struct Terminal<'a> {
shell: Child,
}
impl DrawState {
pub fn new(font: PcScreenFont<'static>, width: usize) -> Self {
impl<F: Font> DrawState<F> {
pub fn new(font: F, width: usize) -> Self {
Self {
width,
font,
force_redraw: true,
focus_changed: false,
focused: true,
old_cursor: Cursor { row: 0, col: 0 },
font,
}
}
pub fn draw(&mut self, dt: &mut [u32], state: &mut State) {
let default_fg = state.default_attributes.fg.to_display(false).to_u32();
let default_bg = state.default_attributes.bg.to_display(false).to_u32();
let fw = self.font.width() as usize;
let fh = self.font.height() as usize;
let font_layout = self.font.layout();
let fw = font_layout.width;
let fh = font_layout.height;
let cursor_dirty = self.old_cursor != state.cursor;
@ -95,7 +99,7 @@ impl DrawState {
for (j, cell) in row.cells().enumerate() {
let bg = cell.attrs.bg.to_display(false).to_u32();
let fg = cell.attrs.fg.to_display(cell.attrs.bright).to_u32();
let fg = cell.attrs.fg.to_display(cell.attrs.bright);
let cx = j * fw;
@ -109,9 +113,14 @@ impl DrawState {
continue;
}
let c = cell.char as u32;
self.font.map_glyph_pixels(c, |x, y, set| {
let color = if set { fg } else { bg };
let c = cell.char as char;
self.font.map_glyph(c, |x, y, v| {
let v = (v * 2.0).min(1.0);
let r = fg.r as f32 * v;
let g = fg.g as f32 * v;
let b = fg.b as f32 * v;
let color = (b as u32) | ((g as u32) << 8) | ((r as u32) << 16) | 0xFF000000;
dt[(cy + y) * self.width + cx + x] = color;
});
}
@ -145,7 +154,7 @@ impl DrawState {
}
impl Terminal<'_> {
pub fn new(font: PcScreenFont<'static>) -> Result<Self, Error> {
pub fn new<F: Font + 'static>(font: F) -> Result<Self, Error> {
let mut app = Application::new()?;
let mut window = Window::new(&app)?;
let mut poll = PollChannel::new()?;
@ -153,8 +162,10 @@ impl Terminal<'_> {
let width = window.width() as usize;
let height = window.height() as usize;
let rows = height / font.height() as usize;
let columns = width / font.width() as usize;
let font_layout = font.layout();
let rows = height / font_layout.height;
let columns = width / font_layout.width;
let termios = TerminalOptions::default();
let (pty_master, pty_slave) = create_pty(Some(termios), TerminalSize { rows, columns })?;
@ -175,8 +186,9 @@ impl Terminal<'_> {
let width = width as usize;
let height = height as usize;
let rows = height / ds.font.height() as usize;
let columns = width / ds.font.width() as usize;
let font_layout = ds.font.layout();
let rows = height / font_layout.height;
let columns = width / font_layout.width;
let term_size = TerminalSize { rows, columns };
let mut buffer = [0; 64];
@ -187,10 +199,7 @@ impl Terminal<'_> {
)
.unwrap();
s.resize(
width / ds.font.width() as usize,
height / ds.font.height() as usize,
);
s.resize(width / font_layout.width, height / font_layout.height);
ds.width = width;
ds.force_redraw = true;
@ -361,10 +370,36 @@ impl Terminal<'_> {
static ABORT: AtomicBool = AtomicBool::new(false);
fn main() -> ExitCode {
logsink::setup_logging(false);
let font = PcScreenFont::default();
let term = Terminal::new(font).unwrap();
term.run()
#[derive(Debug, Parser)]
struct Args {
#[clap(
long,
help = "TTF font to use",
default_value = "/etc/fonts/fixed-regular.ttf"
)]
regular_font: PathBuf,
#[clap(
long,
help = "Font height in pixels (only for TTF fonts)",
default_value_t = 16
)]
font_size: usize,
}
fn run(args: &Args) -> Result<ExitCode, Error> {
let font = TrueTypeFont::load(&args.regular_font, args.font_size)?;
let term = Terminal::new(font)?;
Ok(term.run())
}
fn main() -> ExitCode {
let args = Args::parse();
logsink::setup_logging(false);
match run(&args) {
Ok(code) => code,
Err(error) => {
log::error!("{error}");
ExitCode::FAILURE
}
}
}