nx_paint_text.nx source
↩ module page · 259 lines · 9967 B
1// nx_paint_text.nx -- rasterize an ASCII text string into a
2// Framebuffer using nx_font_bitmap_5x7. Phase 4 sixth primitive of
3// NISHI_BROWSER_ROADMAP. First text-rendering primitive of the
4// bits-up browser substrate.
5//
6// Algorithm:
7// For each char in the text string, lookup its 7 row bit patterns
8// from nx_font_bitmap_5x7, then for each row scan the 5 columns
9// and set a pixel for each "on" bit.
10//
11// Cell stride: 6 columns per character (5 glyph + 1 inter-char
12// space). Vertical advance: 0 (single line; nx_paint_text_lines
13// queued for wrapping).
14//
15// Pixels are written via direct framebuffer access; bounds-checked
16// per pixel (pixels off-screen are silently skipped). For
17// sub-pixel-aware AA glyph edges, use nx_paint_text_aa (queued).
18//
19// What it does NOT handle yet (Phase 4b queued, explicit in header):
20// - line wrapping (caller's responsibility for now)
21// - vertical line advance for multi-line text
22// - Unicode beyond ASCII (multi-byte UTF-8 decode)
23// - kerning pairs / proportional spacing (current is fixed cell)
24// - bold / italic / underline emphasis
25// - text-align / text-decoration CSS support
26// - anti-aliased glyph edges (current 1-bit-per-pixel)
27// - scale != 1x (would need block-fill per pixel; Phase 4c)
28//
29// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
30// gap list is EXACT in this header.
31//
32// genealogy_id: classical_bitmap_font_rasterizer +
33// substrate_browser_phase_4_text
34// lineage_id: nishi_browser_paint_text_v1
35
36import "nx_syscalls.nx"
37import "nx_css_color_decode.nx"
38import "nx_paint_solid_rect.nx"
39import "nx_font_bitmap_5x7.nx"
40
41const NX_PAINT_TEXT_CELL_W: i64 = 6 // 5 glyph + 1 inter-char space
42const NX_PAINT_TEXT_CELL_H: i64 = 7
43
44// ---- helpers ----
45
46// Write one foreground pixel at (px, py) in the framebuffer, if in
47// bounds. Returns 1 if the pixel was written, 0 otherwise.
48func _paint_text_pixel(fb: *Framebuffer, px: i64, py: i64,
49 r: i64, g: i64, b: i64, a: i64) -> i64 {
50 if px < 0 { return 0 }
51 if py < 0 { return 0 }
52 if px >= fb.width { return 0 }
53 if py >= fb.height { return 0 }
54 let off: i64 = (py * fb.width + px) * 4
55 fb.pixels[off + 0] = r as u8
56 fb.pixels[off + 1] = g as u8
57 fb.pixels[off + 2] = b as u8
58 fb.pixels[off + 3] = a as u8
59 return 1
60}
61
62// Rasterize one glyph at (cell_x, cell_y). Returns the count of
63// pixels actually set (post-clipping).
64func _paint_text_glyph(fb: *Framebuffer,
65 cell_x: i64, cell_y: i64,
66 ascii_char: i64,
67 r: i64, g: i64, b: i64, a: i64) -> i64 {
68 var written: i64 = 0
69 var row: i64 = 0
70 while row < 7 {
71 let bits: i64 = nx_font_5x7_glyph_row(ascii_char, row)
72 if bits < 0 {
73 // Unsupported char -- skip the whole glyph cell.
74 row = 7
75 } else {
76 var col: i64 = 0
77 while col < 5 {
78 // Leftmost pixel is bit 4 (0x10), so shift by (4 - col).
79 let shift: i64 = 4 - col
80 let bit: i64 = (bits >> shift) & 1
81 if bit == 1 {
82 written = written + _paint_text_pixel(fb,
83 cell_x + col, cell_y + row, r, g, b, a)
84 }
85 col = col + 1
86 }
87 row = row + 1
88 }
89 }
90 return written
91}
92
93// ---- public API ----
94
95// Paint `text` (text_len bytes of ASCII) starting at (x, y) using
96// the 5x7 bitmap font in the given color. Returns total pixels set.
97//
98// Characters not supported by nx_font_5x7_glyph_row consume their
99// cell width but emit no pixels (so layout is preserved).
100func nx_paint_text(fb: *Framebuffer,
101 x: i64, y: i64,
102 text: *u8, text_len: i64,
103 color: *CssColor) -> i64 {
104 if text_len <= 0 { return 0 }
105
106 let r: i64 = color.r & 255
107 let g: i64 = color.g & 255
108 let b: i64 = color.b & 255
109 let a: i64 = color.a & 255
110 if a == 0 { return 0 } // fully transparent foreground
111
112 var total: i64 = 0
113 var i: i64 = 0
114 var line_idx: i64 = 0
115 var col_idx: i64 = 0
116 while i < text_len {
117 let c: i64 = (text[i] as i64) & 255
118 if c == 10 { // '\n' newline
119 line_idx = line_idx + 1
120 col_idx = 0
121 } else {
122 let cell_x: i64 = x + col_idx * NX_PAINT_TEXT_CELL_W
123 let cell_y: i64 = y + line_idx * NX_PAINT_TEXT_CELL_H
124 total = total + _paint_text_glyph(fb, cell_x, cell_y, c, r, g, b, a)
125 col_idx = col_idx + 1
126 }
127 i = i + 1
128 }
129 return total
130}
131
132// Paint `text` at integer `scale` (1 = same as nx_paint_text). Each glyph pixel becomes a
133// scale x scale block; cell stride = 6*scale. Used for CSS font-size hierarchy (big headings) with
134// the 5x7 bitmap font -- integer scaling keeps glyphs crisp (no resample blur). Single line only
135// (newline ignored). Returns pixels set.
136func nx_paint_text_scaled(fb: *Framebuffer,
137 x: i64, y: i64,
138 text: *u8, text_len: i64,
139 color: *CssColor, scale: i64) -> i64 {
140 if scale <= 1 { return nx_paint_text(fb, x, y, text, text_len, color) }
141 if text_len <= 0 { return 0 }
142 let r: i64 = color.r & 255
143 let g: i64 = color.g & 255
144 let b: i64 = color.b & 255
145 let a: i64 = color.a & 255
146 if a == 0 { return 0 }
147 let cw: i64 = NX_PAINT_TEXT_CELL_W * scale
148 var total: i64 = 0
149 var i: i64 = 0
150 var col_idx: i64 = 0
151 while i < text_len {
152 let c: i64 = (text[i] as i64) & 255
153 if c == 10 {
154 col_idx = col_idx + 1 // single line: treat newline as a blank cell
155 } else {
156 let cell_x: i64 = x + col_idx * cw
157 var row: i64 = 0
158 while row < 7 {
159 let bits: i64 = nx_font_5x7_glyph_row(c, row)
160 if bits < 0 { row = 7 }
161 else {
162 var col: i64 = 0
163 while col < 5 {
164 let shift: i64 = 4 - col
165 let bit: i64 = (bits >> shift) & 1
166 if bit == 1 {
167 var dy: i64 = 0
168 while dy < scale {
169 var dx: i64 = 0
170 while dx < scale {
171 total = total + _paint_text_pixel(fb,
172 cell_x + col * scale + dx, y + row * scale + dy, r, g, b, a)
173 dx = dx + 1
174 }
175 dy = dy + 1
176 }
177 }
178 col = col + 1
179 }
180 row = row + 1
181 }
182 }
183 col_idx = col_idx + 1
184 }
185 i = i + 1
186 }
187 return total
188}
189
190// Paint `text` with integer `scale` + faux BOLD (thicken each set pixel by one column to the right) +
191// faux ITALIC (per-row rightward slant: top rows lean further right). bold=0/italic=0 + scale matches
192// nx_paint_text_scaled exactly. Used for CSS font-weight:bold / font-style:italic via the cascade --
193// the 5x7 bitmap has no real bold/italic faces, so we synthesize them (legible at 1x; crisp scaled).
194func nx_paint_text_styled(fb: *Framebuffer,
195 x: i64, y: i64,
196 text: *u8, text_len: i64,
197 color: *CssColor, scale: i64,
198 bold: i64, italic: i64) -> i64 {
199 if text_len <= 0 { return 0 }
200 var sc: i64 = scale
201 if sc < 1 { sc = 1 }
202 let r: i64 = color.r & 255
203 let g: i64 = color.g & 255
204 let b: i64 = color.b & 255
205 let a: i64 = color.a & 255
206 if a == 0 { return 0 }
207 let cw: i64 = NX_PAINT_TEXT_CELL_W * sc
208 var total: i64 = 0
209 var i: i64 = 0
210 var col_idx: i64 = 0
211 while i < text_len {
212 let c: i64 = (text[i] as i64) & 255
213 if c == 10 {
214 col_idx = col_idx + 1
215 } else {
216 let cell_x: i64 = x + col_idx * cw
217 var row: i64 = 0
218 while row < 7 {
219 let bits: i64 = nx_font_5x7_glyph_row(c, row)
220 if bits < 0 { row = 7 }
221 else {
222 var slant: i64 = 0
223 if italic == 1 { slant = ((6 - row) * sc) / 3 } // top rows lean right (~2*sc max)
224 var col: i64 = 0
225 while col < 5 {
226 let shift: i64 = 4 - col
227 let bit: i64 = (bits >> shift) & 1
228 if bit == 1 {
229 let bx: i64 = cell_x + col * sc + slant
230 let by: i64 = y + row * sc
231 var dy: i64 = 0
232 while dy < sc {
233 var dx: i64 = 0
234 while dx < sc {
235 total = total + _paint_text_pixel(fb, bx + dx, by + dy, r, g, b, a)
236 if bold == 1 { total = total + _paint_text_pixel(fb, bx + dx + 1, by + dy, r, g, b, a) }
237 dx = dx + 1
238 }
239 dy = dy + 1
240 }
241 }
242 col = col + 1
243 }
244 row = row + 1
245 }
246 }
247 col_idx = col_idx + 1
248 }
249 i = i + 1
250 }
251 return total
252}
253
254// Convenience: report the rendered cell-width of a string in pixels.
255// Useful for text-align centering when nx_layout grows it.
256func nx_paint_text_measure(text_len: i64) -> i64 {
257 if text_len <= 0 { return 0 }
258 return text_len * NX_PAINT_TEXT_CELL_W
259}