Skip to content

Commit fd4fda9

Browse files
ryanbreenclaude
andcommitted
feat(graphics): Phase 4 - text rendering support
Add text rendering capabilities to the graphics stack: - font.rs: Font abstraction wrapping noto-sans-mono-bitmap - FontSize and Weight enums - Font struct with metrics() and glyph() methods - FontMetrics for char_width, char_height, spacing - Glyph wrapper with pixels() iterator for rendering - primitives.rs: Text drawing functions - TextStyle struct with foreground, background, font - blend_colors() for anti-aliased text rendering - draw_char(), draw_text() for character/string rendering - text_width(), text_height(), text_line_height() for measurement - Comprehensive unit tests for text rendering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2532624 commit fd4fda9

File tree

3 files changed

+586
-0
lines changed

3 files changed

+586
-0
lines changed

kernel/src/graphics/font.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! Font rendering types and utilities.
2+
//!
3+
//! Provides bitmap font support using the noto-sans-mono-bitmap crate.
4+
//! This module abstracts over the underlying font library to provide
5+
//! a clean API for text rendering in the graphics stack.
6+
7+
// This is a public API module - functions are intentionally available for external use
8+
#![allow(dead_code)]
9+
10+
use noto_sans_mono_bitmap::{
11+
get_raster, get_raster_width, FontWeight, RasterHeight, RasterizedChar,
12+
};
13+
14+
/// Available font sizes.
15+
/// Currently only Size16 is enabled in Cargo.toml features.
16+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17+
pub enum FontSize {
18+
/// 16 pixel height (default, currently enabled)
19+
#[default]
20+
Size16,
21+
}
22+
23+
impl FontSize {
24+
/// Convert to the underlying RasterHeight.
25+
fn to_raster_height(self) -> RasterHeight {
26+
match self {
27+
FontSize::Size16 => RasterHeight::Size16,
28+
}
29+
}
30+
31+
/// Get the pixel height for this font size.
32+
pub fn height(self) -> usize {
33+
match self {
34+
FontSize::Size16 => 16,
35+
}
36+
}
37+
}
38+
39+
/// Font weight options.
40+
/// Currently only Regular is enabled in Cargo.toml features.
41+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42+
pub enum Weight {
43+
/// Regular weight (default, currently enabled)
44+
#[default]
45+
Regular,
46+
}
47+
48+
impl Weight {
49+
/// Convert to the underlying FontWeight.
50+
fn to_font_weight(self) -> FontWeight {
51+
match self {
52+
Weight::Regular => FontWeight::Regular,
53+
}
54+
}
55+
}
56+
57+
/// Font configuration combining size and weight.
58+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
59+
pub struct Font {
60+
/// Font size
61+
pub size: FontSize,
62+
/// Font weight
63+
pub weight: Weight,
64+
}
65+
66+
impl Font {
67+
/// Create a new font with the specified size and weight.
68+
pub const fn new(size: FontSize, weight: Weight) -> Self {
69+
Self { size, weight }
70+
}
71+
72+
/// Get the default font (16px Regular).
73+
pub const fn default_font() -> Self {
74+
Self {
75+
size: FontSize::Size16,
76+
weight: Weight::Regular,
77+
}
78+
}
79+
80+
/// Get metrics for this font configuration.
81+
pub fn metrics(&self) -> FontMetrics {
82+
let char_width = get_raster_width(
83+
self.weight.to_font_weight(),
84+
self.size.to_raster_height(),
85+
);
86+
FontMetrics {
87+
char_width,
88+
char_height: self.size.height(),
89+
line_spacing: 2,
90+
letter_spacing: 0,
91+
}
92+
}
93+
94+
/// Get the glyph for a character, or None if not available.
95+
pub fn glyph(&self, c: char) -> Option<Glyph> {
96+
get_raster(c, self.weight.to_font_weight(), self.size.to_raster_height())
97+
.map(|rc| Glyph::from_rasterized(rc))
98+
}
99+
100+
/// Get the replacement glyph ('?') for unknown characters.
101+
pub fn replacement_glyph(&self) -> Glyph {
102+
self.glyph('?').expect("Font should have '?' character")
103+
}
104+
105+
/// Get the glyph for a character, using replacement if not found.
106+
pub fn glyph_or_replacement(&self, c: char) -> Glyph {
107+
self.glyph(c).unwrap_or_else(|| self.replacement_glyph())
108+
}
109+
}
110+
111+
/// Metrics for a font configuration.
112+
#[derive(Debug, Clone, Copy)]
113+
pub struct FontMetrics {
114+
/// Width of each character in pixels
115+
pub char_width: usize,
116+
/// Height of each character in pixels
117+
pub char_height: usize,
118+
/// Additional vertical space between lines
119+
pub line_spacing: usize,
120+
/// Additional horizontal space between characters
121+
pub letter_spacing: usize,
122+
}
123+
124+
impl FontMetrics {
125+
/// Get the total line height (char height + line spacing).
126+
pub fn line_height(&self) -> usize {
127+
self.char_height + self.line_spacing
128+
}
129+
130+
/// Get the total character advance (char width + letter spacing).
131+
pub fn char_advance(&self) -> usize {
132+
self.char_width + self.letter_spacing
133+
}
134+
}
135+
136+
/// Character glyph data - a wrapper around RasterizedChar.
137+
pub struct Glyph {
138+
/// The rasterized character data
139+
rasterized: RasterizedChar,
140+
}
141+
142+
impl Glyph {
143+
/// Create a Glyph from a RasterizedChar.
144+
fn from_rasterized(rc: RasterizedChar) -> Self {
145+
Self { rasterized: rc }
146+
}
147+
148+
/// Get the width of this glyph in pixels.
149+
pub fn width(&self) -> usize {
150+
self.rasterized.width()
151+
}
152+
153+
/// Get the height of this glyph in pixels.
154+
pub fn height(&self) -> usize {
155+
self.rasterized.height()
156+
}
157+
158+
/// Get the raster data as rows of intensity values (0-255).
159+
/// Each row is a slice of bytes, one per pixel column.
160+
pub fn raster(&self) -> &[[u8; 8]] {
161+
// noto-sans-mono-bitmap returns fixed-width arrays
162+
self.rasterized.raster()
163+
}
164+
165+
/// Iterate over the glyph pixels with coordinates and intensity.
166+
/// Yields (x, y, intensity) for each pixel.
167+
pub fn pixels(&self) -> impl Iterator<Item = (usize, usize, u8)> + '_ {
168+
let width = self.width();
169+
self.raster().iter().enumerate().flat_map(move |(y, row)| {
170+
row.iter()
171+
.take(width)
172+
.enumerate()
173+
.map(move |(x, &intensity)| (x, y, intensity))
174+
})
175+
}
176+
}
177+
178+
#[cfg(test)]
179+
mod tests {
180+
use super::*;
181+
182+
#[test]
183+
fn default_font_has_expected_metrics() {
184+
let font = Font::default_font();
185+
let metrics = font.metrics();
186+
assert_eq!(metrics.char_height, 16);
187+
assert!(metrics.char_width > 0);
188+
}
189+
190+
#[test]
191+
fn can_get_glyph_for_ascii() {
192+
let font = Font::default_font();
193+
let glyph = font.glyph('A');
194+
assert!(glyph.is_some());
195+
let g = glyph.unwrap();
196+
assert!(g.width() > 0);
197+
assert_eq!(g.height(), 16);
198+
}
199+
200+
#[test]
201+
fn replacement_glyph_exists() {
202+
let font = Font::default_font();
203+
let glyph = font.replacement_glyph();
204+
assert!(glyph.width() > 0);
205+
}
206+
207+
#[test]
208+
fn glyph_pixels_iterator_yields_data() {
209+
let font = Font::default_font();
210+
let glyph = font.glyph('X').unwrap();
211+
let pixels: Vec<_> = glyph.pixels().collect();
212+
assert!(!pixels.is_empty());
213+
// X should have some non-zero intensity pixels
214+
assert!(pixels.iter().any(|(_, _, intensity)| *intensity > 0));
215+
}
216+
}

kernel/src/graphics/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
//! Provides framebuffer abstractions used by the kernel graphics stack.
44
55
pub mod double_buffer;
6+
pub mod font;
67
pub mod primitives;
78

89
pub use double_buffer::DoubleBufferedFrameBuffer;
10+
pub use font::{Font, FontMetrics, FontSize, Glyph, Weight};
11+
pub use primitives::TextStyle;

0 commit comments

Comments
 (0)