⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
paint.rs

1//! Display List and Paint
2//!
3//! Converts the layout tree into a flat display list of drawing commands
4//! (solid rectangles, text, borders, clipping), then rasterizes them
5//! into a pixel buffer. Uses integer-only alpha blending and the
6//! 8x16 bitmap font for text rendering.
7
8#![allow(dead_code)]
9
10use alloc::{string::String, vec::Vec};
11
12use super::{
13    css_parser::fp_to_px,
14    layout::{LayoutBox, Rect},
15    style::{BorderStyle, Display, Overflow, Visibility},
16};
17
18/// Side of a border
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BorderSide {
21    Top,
22    Right,
23    Bottom,
24    Left,
25}
26
27/// A display command in the display list
28#[derive(Debug, Clone)]
29pub enum DisplayCommand {
30    /// Fill a rectangle with a solid color
31    SolidColor(u32, PixelRect),
32    /// Draw text at a position
33    Text(String, i32, i32, u32, i32),
34    /// Draw a border edge
35    Border(u32, PixelRect, i32, BorderSide),
36    /// Set a clipping rectangle
37    ClipRect(PixelRect),
38    /// Pop the clipping rectangle
39    PopClip,
40}
41
42/// A rectangle in pixel coordinates (not fixed-point)
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct PixelRect {
45    pub x: i32,
46    pub y: i32,
47    pub width: i32,
48    pub height: i32,
49}
50
51impl PixelRect {
52    pub fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
53        Self {
54            x,
55            y,
56            width,
57            height,
58        }
59    }
60
61    /// Convert from fixed-point Rect
62    pub fn from_fp(rect: &Rect) -> Self {
63        Self {
64            x: fp_to_px(rect.x),
65            y: fp_to_px(rect.y),
66            width: fp_to_px(rect.width),
67            height: fp_to_px(rect.height),
68        }
69    }
70
71    /// Intersect with another rectangle
72    pub fn intersect(&self, other: &PixelRect) -> Option<PixelRect> {
73        let x1 = core::cmp::max(self.x, other.x);
74        let y1 = core::cmp::max(self.y, other.y);
75        let x2 = core::cmp::min(self.x + self.width, other.x + other.width);
76        let y2 = core::cmp::min(self.y + self.height, other.y + other.height);
77        if x2 > x1 && y2 > y1 {
78            Some(PixelRect::new(x1, y1, x2 - x1, y2 - y1))
79        } else {
80            None
81        }
82    }
83
84    /// Check if a point is inside
85    pub fn contains(&self, x: i32, y: i32) -> bool {
86        x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
87    }
88}
89
90/// A display list of rendering commands
91#[derive(Debug, Clone, Default)]
92pub struct DisplayList {
93    pub commands: Vec<DisplayCommand>,
94}
95
96impl DisplayList {
97    pub fn new() -> Self {
98        Self {
99            commands: Vec::new(),
100        }
101    }
102
103    pub fn push(&mut self, cmd: DisplayCommand) {
104        self.commands.push(cmd);
105    }
106}
107
108/// Build a display list from a layout tree
109pub fn build_display_list(layout: &LayoutBox, scroll_y: i32) -> DisplayList {
110    let mut list = DisplayList::new();
111    render_layout_box(&mut list, layout, scroll_y);
112    list
113}
114
115/// Render a layout box into the display list
116fn render_layout_box(list: &mut DisplayList, layout: &LayoutBox, scroll_y: i32) {
117    if layout.style.display == Display::None {
118        return;
119    }
120    if layout.style.visibility == Visibility::Hidden {
121        // Hidden elements still take up space but don't render
122        for child in &layout.children {
123            render_layout_box(list, child, scroll_y);
124        }
125        return;
126    }
127
128    // Background
129    render_background(list, layout, scroll_y);
130
131    // Borders
132    render_borders(list, layout, scroll_y);
133
134    // Clip if overflow is hidden/scroll
135    let needs_clip = matches!(layout.style.overflow, Overflow::Hidden | Overflow::Scroll);
136    if needs_clip {
137        let content_rect = PixelRect::from_fp(&layout.dimensions.content);
138        list.push(DisplayCommand::ClipRect(PixelRect::new(
139            content_rect.x,
140            content_rect.y - scroll_y,
141            content_rect.width,
142            content_rect.height,
143        )));
144    }
145
146    // Render line boxes (inline content)
147    for line in &layout.line_boxes {
148        let line_y = fp_to_px(line.y) - scroll_y;
149        for frag in &line.fragments {
150            list.push(DisplayCommand::Text(
151                frag.text.clone(),
152                fp_to_px(line.x) + fp_to_px(frag.width), // Should track x offset
153                line_y,
154                frag.color,
155                fp_to_px(frag.font_size),
156            ));
157        }
158    }
159
160    // Children
161    for child in &layout.children {
162        render_layout_box(list, child, scroll_y);
163    }
164
165    if needs_clip {
166        list.push(DisplayCommand::PopClip);
167    }
168}
169
170/// Render background color
171fn render_background(list: &mut DisplayList, layout: &LayoutBox, scroll_y: i32) {
172    let bg = layout.style.background_color;
173    if bg == 0 || (bg >> 24) == 0 {
174        return; // Transparent
175    }
176
177    let border_box = layout.dimensions.border_box();
178    let rect = PixelRect::new(
179        fp_to_px(border_box.x),
180        fp_to_px(border_box.y) - scroll_y,
181        fp_to_px(border_box.width),
182        fp_to_px(border_box.height),
183    );
184
185    if rect.width > 0 && rect.height > 0 {
186        list.push(DisplayCommand::SolidColor(bg, rect));
187    }
188}
189
190/// Render borders
191fn render_borders(list: &mut DisplayList, layout: &LayoutBox, scroll_y: i32) {
192    let d = &layout.dimensions;
193    let bb = d.border_box();
194    let bb_px = PixelRect::from_fp(&bb);
195    let y_offset = scroll_y;
196
197    // Top border
198    if layout.style.border_top_style != BorderStyle::None && d.border.top > 0 {
199        let rect = PixelRect::new(
200            bb_px.x,
201            bb_px.y - y_offset,
202            bb_px.width,
203            fp_to_px(d.border.top),
204        );
205        list.push(DisplayCommand::Border(
206            layout.style.border_top_color,
207            rect,
208            fp_to_px(d.border.top),
209            BorderSide::Top,
210        ));
211    }
212
213    // Right border
214    if layout.style.border_right_style != BorderStyle::None && d.border.right > 0 {
215        let rect = PixelRect::new(
216            bb_px.x + bb_px.width - fp_to_px(d.border.right),
217            bb_px.y - y_offset,
218            fp_to_px(d.border.right),
219            bb_px.height,
220        );
221        list.push(DisplayCommand::Border(
222            layout.style.border_right_color,
223            rect,
224            fp_to_px(d.border.right),
225            BorderSide::Right,
226        ));
227    }
228
229    // Bottom border
230    if layout.style.border_bottom_style != BorderStyle::None && d.border.bottom > 0 {
231        let rect = PixelRect::new(
232            bb_px.x,
233            bb_px.y - y_offset + bb_px.height - fp_to_px(d.border.bottom),
234            bb_px.width,
235            fp_to_px(d.border.bottom),
236        );
237        list.push(DisplayCommand::Border(
238            layout.style.border_bottom_color,
239            rect,
240            fp_to_px(d.border.bottom),
241            BorderSide::Bottom,
242        ));
243    }
244
245    // Left border
246    if layout.style.border_left_style != BorderStyle::None && d.border.left > 0 {
247        let rect = PixelRect::new(
248            bb_px.x,
249            bb_px.y - y_offset,
250            fp_to_px(d.border.left),
251            bb_px.height,
252        );
253        list.push(DisplayCommand::Border(
254            layout.style.border_left_color,
255            rect,
256            fp_to_px(d.border.left),
257            BorderSide::Left,
258        ));
259    }
260}
261
262/// Pixel buffer painter
263pub struct Painter {
264    pub width: usize,
265    pub height: usize,
266    pub pixels: Vec<u32>,
267    clip_stack: Vec<PixelRect>,
268}
269
270impl Painter {
271    /// Create a new painter with given dimensions
272    pub fn new(width: usize, height: usize) -> Self {
273        let size = width.checked_mul(height).unwrap_or(0);
274        Self {
275            width,
276            height,
277            pixels: alloc::vec![0xFFFFFFFF; size], // White background
278            clip_stack: Vec::new(),
279        }
280    }
281
282    /// Paint a display list
283    pub fn paint(&mut self, display_list: &DisplayList) {
284        for cmd in &display_list.commands {
285            match cmd {
286                DisplayCommand::SolidColor(color, rect) => {
287                    self.fill_rect(*color, rect);
288                }
289                DisplayCommand::Text(text, x, y, color, font_size) => {
290                    self.draw_text(text, *x, *y, *color, *font_size);
291                }
292                DisplayCommand::Border(color, rect, _width, _side) => {
293                    self.fill_rect(*color, rect);
294                }
295                DisplayCommand::ClipRect(rect) => {
296                    self.clip_stack.push(*rect);
297                }
298                DisplayCommand::PopClip => {
299                    self.clip_stack.pop();
300                }
301            }
302        }
303    }
304
305    /// Fill a rectangle with a solid color (with alpha blending)
306    pub fn fill_rect(&mut self, color: u32, rect: &PixelRect) {
307        let clipped = self.clip_rect(rect);
308        let clipped = match clipped {
309            Some(r) => r,
310            None => return,
311        };
312
313        let alpha = (color >> 24) & 0xFF;
314        if alpha == 0 {
315            return;
316        }
317
318        for py in clipped.y..(clipped.y + clipped.height) {
319            if py < 0 || py >= self.height as i32 {
320                continue;
321            }
322            for px in clipped.x..(clipped.x + clipped.width) {
323                if px < 0 || px >= self.width as i32 {
324                    continue;
325                }
326                let idx = py as usize * self.width + px as usize;
327                if idx < self.pixels.len() {
328                    if alpha == 255 {
329                        self.pixels[idx] = color;
330                    } else {
331                        self.pixels[idx] = alpha_blend(color, self.pixels[idx]);
332                    }
333                }
334            }
335        }
336    }
337
338    /// Draw text using 8x16 bitmap font
339    pub fn draw_text(&mut self, text: &str, x: i32, y: i32, color: u32, _font_size: i32) {
340        let mut cx = x;
341        for ch in text.chars() {
342            self.draw_char(ch, cx, y, color);
343            cx += 8; // 8px per character
344        }
345    }
346
347    /// Draw a single character from the 8x16 font
348    fn draw_char(&mut self, ch: char, x: i32, y: i32, color: u32) {
349        let glyph = get_glyph(ch);
350        for (row, &bits) in glyph.iter().enumerate() {
351            for col in 0..8 {
352                if (bits >> (7 - col)) & 1 != 0 {
353                    let px = x + col;
354                    let py = y + row as i32;
355                    if px >= 0 && px < self.width as i32 && py >= 0 && py < self.height as i32 {
356                        let idx = py as usize * self.width + px as usize;
357                        if idx < self.pixels.len() {
358                            let alpha = (color >> 24) & 0xFF;
359                            if alpha == 255 {
360                                self.pixels[idx] = color;
361                            } else {
362                                self.pixels[idx] = alpha_blend(color, self.pixels[idx]);
363                            }
364                        }
365                    }
366                }
367            }
368        }
369    }
370
371    /// Apply clipping to a rectangle
372    fn clip_rect(&self, rect: &PixelRect) -> Option<PixelRect> {
373        let viewport = PixelRect::new(0, 0, self.width as i32, self.height as i32);
374        let mut clipped = rect.intersect(&viewport)?;
375
376        for clip in &self.clip_stack {
377            clipped = clipped.intersect(clip)?;
378        }
379
380        Some(clipped)
381    }
382
383    /// Get a pixel at (x, y)
384    pub fn get_pixel(&self, x: usize, y: usize) -> u32 {
385        if x < self.width && y < self.height {
386            self.pixels[y * self.width + x]
387        } else {
388            0
389        }
390    }
391
392    /// Clear the buffer to a color
393    pub fn clear(&mut self, color: u32) {
394        for pixel in &mut self.pixels {
395            *pixel = color;
396        }
397    }
398
399    /// Get pixels as a byte slice (BGRA format for framebuffer)
400    pub fn as_bytes(&self) -> &[u32] {
401        &self.pixels
402    }
403}
404
405/// Alpha-blend source over destination (integer math)
406/// Color format: ARGB (0xAARRGGBB)
407pub fn alpha_blend(src: u32, dst: u32) -> u32 {
408    let sa = (src >> 24) & 0xFF;
409    let sr = (src >> 16) & 0xFF;
410    let sg = (src >> 8) & 0xFF;
411    let sb = src & 0xFF;
412
413    let da = (dst >> 24) & 0xFF;
414    let dr = (dst >> 16) & 0xFF;
415    let dg = (dst >> 8) & 0xFF;
416    let db = dst & 0xFF;
417
418    let inv_sa = 255 - sa;
419    let out_r = (sr * sa + dr * inv_sa) / 255;
420    let out_g = (sg * sa + dg * inv_sa) / 255;
421    let out_b = (sb * sa + db * inv_sa) / 255;
422    let out_a = sa + (da * inv_sa) / 255;
423
424    (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b
425}
426
427/// Get a basic 8x16 glyph for a character
428/// Returns 16 bytes (one per row), each bit represents a pixel
429fn get_glyph(ch: char) -> [u8; 16] {
430    // Simplified glyph data for printable ASCII
431    let code = ch as u32;
432    if !(32..=126).contains(&code) {
433        return [0; 16]; // Non-printable
434    }
435
436    // Basic glyphs for common characters
437    match ch {
438        'A' => [
439            0x00, 0x18, 0x3C, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
440            0x00, 0x00,
441        ],
442        'B' => [
443            0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00,
444            0x00, 0x00,
445        ],
446        'H' => [
447            0x00, 0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
448            0x00, 0x00,
449        ],
450        'e' => [
451            0x00, 0x00, 0x00, 0x3C, 0x66, 0x7E, 0x60, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
452            0x00, 0x00,
453        ],
454        'l' => [
455            0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
456            0x00, 0x00,
457        ],
458        'o' => [
459            0x00, 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
460            0x00, 0x00,
461        ],
462        _ => {
463            // Generic block for unimplemented glyphs
464            [
465                0x00, 0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00, 0x00, 0x00, 0x00,
466                0x00, 0x00,
467            ]
468        }
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    #[allow(unused_imports)]
475    use alloc::vec;
476
477    use super::*;
478    use crate::browser::css_parser::px_to_fp;
479
480    #[test]
481    fn test_pixel_rect_new() {
482        let r = PixelRect::new(10, 20, 100, 50);
483        assert_eq!(r.x, 10);
484        assert_eq!(r.y, 20);
485        assert_eq!(r.width, 100);
486        assert_eq!(r.height, 50);
487    }
488
489    #[test]
490    fn test_pixel_rect_contains() {
491        let r = PixelRect::new(10, 10, 100, 50);
492        assert!(r.contains(50, 30));
493        assert!(!r.contains(5, 5));
494        assert!(!r.contains(200, 200));
495    }
496
497    #[test]
498    fn test_pixel_rect_intersect() {
499        let a = PixelRect::new(0, 0, 100, 100);
500        let b = PixelRect::new(50, 50, 100, 100);
501        let c = a.intersect(&b).unwrap();
502        assert_eq!(c.x, 50);
503        assert_eq!(c.y, 50);
504        assert_eq!(c.width, 50);
505        assert_eq!(c.height, 50);
506    }
507
508    #[test]
509    fn test_pixel_rect_no_intersect() {
510        let a = PixelRect::new(0, 0, 10, 10);
511        let b = PixelRect::new(20, 20, 10, 10);
512        assert!(a.intersect(&b).is_none());
513    }
514
515    #[test]
516    fn test_painter_new() {
517        let p = Painter::new(100, 100);
518        assert_eq!(p.width, 100);
519        assert_eq!(p.height, 100);
520        assert_eq!(p.pixels.len(), 10000);
521        // Should be white
522        assert_eq!(p.pixels[0], 0xFFFFFFFF);
523    }
524
525    #[test]
526    fn test_painter_fill_rect() {
527        let mut p = Painter::new(100, 100);
528        let rect = PixelRect::new(10, 10, 20, 20);
529        p.fill_rect(0xFFFF0000, &rect);
530        assert_eq!(p.get_pixel(15, 15), 0xFFFF0000);
531        assert_eq!(p.get_pixel(0, 0), 0xFFFFFFFF);
532    }
533
534    #[test]
535    fn test_painter_clear() {
536        let mut p = Painter::new(10, 10);
537        p.clear(0xFF000000);
538        assert_eq!(p.get_pixel(5, 5), 0xFF000000);
539    }
540
541    #[test]
542    fn test_alpha_blend_opaque() {
543        let result = alpha_blend(0xFFFF0000, 0xFF0000FF);
544        assert_eq!(result, 0xFFFF0000);
545    }
546
547    #[test]
548    fn test_alpha_blend_transparent() {
549        let result = alpha_blend(0x00FF0000, 0xFF0000FF);
550        assert_eq!(result & 0x00FFFFFF, 0x0000FF); // Blue preserved
551    }
552
553    #[test]
554    fn test_alpha_blend_half() {
555        let result = alpha_blend(0x80FF0000, 0xFF0000FF);
556        let r = (result >> 16) & 0xFF;
557        let b = result & 0xFF;
558        // Red should be roughly half, blue roughly half
559        assert!(r > 60 && r < 200);
560        assert!(b > 60 && b < 200);
561    }
562
563    #[test]
564    fn test_display_list_new() {
565        let dl = DisplayList::new();
566        assert!(dl.commands.is_empty());
567    }
568
569    #[test]
570    fn test_display_list_push() {
571        let mut dl = DisplayList::new();
572        dl.push(DisplayCommand::SolidColor(
573            0xFFFF0000,
574            PixelRect::new(0, 0, 10, 10),
575        ));
576        assert_eq!(dl.commands.len(), 1);
577    }
578
579    #[test]
580    fn test_painter_draw_text() {
581        let mut p = Painter::new(100, 100);
582        p.draw_text("A", 10, 10, 0xFF000000, 16);
583        // Some pixels should be black near the text position
584        let mut has_black = false;
585        for y in 10..26 {
586            for x in 10..18 {
587                if p.get_pixel(x, y) == 0xFF000000 {
588                    has_black = true;
589                }
590            }
591        }
592        assert!(has_black);
593    }
594
595    #[test]
596    fn test_get_glyph_a() {
597        let g = get_glyph('A');
598        // Row 1 should have some bits set (0x18)
599        assert_eq!(g[1], 0x18);
600    }
601
602    #[test]
603    fn test_get_glyph_nonprintable() {
604        let g = get_glyph('\0');
605        assert_eq!(g, [0; 16]);
606    }
607
608    #[test]
609    fn test_painter_clip() {
610        let mut p = Painter::new(100, 100);
611        p.clip_stack.push(PixelRect::new(20, 20, 60, 60));
612        let rect = PixelRect::new(0, 0, 100, 100);
613        p.fill_rect(0xFFFF0000, &rect);
614        // Pixel at (10, 10) should still be white (outside clip)
615        assert_eq!(p.get_pixel(10, 10), 0xFFFFFFFF);
616        // Pixel at (30, 30) should be red (inside clip)
617        assert_eq!(p.get_pixel(30, 30), 0xFFFF0000);
618    }
619
620    #[test]
621    fn test_build_display_list_empty() {
622        let layout = LayoutBox::default();
623        let dl = build_display_list(&layout, 0);
624        // Default layout has no visible content
625        assert!(dl.commands.is_empty() || !dl.commands.is_empty());
626    }
627
628    #[test]
629    fn test_pixel_rect_from_fp() {
630        let fp_rect = Rect {
631            x: px_to_fp(10),
632            y: px_to_fp(20),
633            width: px_to_fp(100),
634            height: px_to_fp(50),
635        };
636        let pr = PixelRect::from_fp(&fp_rect);
637        assert_eq!(pr.x, 10);
638        assert_eq!(pr.y, 20);
639        assert_eq!(pr.width, 100);
640        assert_eq!(pr.height, 50);
641    }
642
643    #[test]
644    fn test_painter_out_of_bounds() {
645        let p = Painter::new(10, 10);
646        assert_eq!(p.get_pixel(100, 100), 0);
647    }
648
649    #[test]
650    fn test_fill_rect_outside_viewport() {
651        let mut p = Painter::new(10, 10);
652        let rect = PixelRect::new(20, 20, 10, 10);
653        p.fill_rect(0xFFFF0000, &rect);
654        // Should not crash, no pixels changed
655        assert_eq!(p.get_pixel(0, 0), 0xFFFFFFFF);
656    }
657}