灵符lingfu
H

horizon-browser

↓ 0 downloads

Horizon: a real box-model HTML/CSS layout engine with a Canvas 2D surface, no_std + alloc, zero dependencies -- LingOS's next-generation browser engine, sibling to bring-browser

Keywords browser, html, css, layout, canvas, no_std, lingos
lingfu install horizon-browser
LicenseMIT
AuthorsLing Lin <taellinglin@gmail.com>
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (9)

src/lib.rs
//! horizon: a real box-model HTML/CSS layout engine with a `<canvas>` 2D
//! surface, `no_std` + `alloc`, zero dependencies -- LingOS's second web
//! engine, sibling to `bring-browser` (which stays the `links`/`lynx`-style
//! text-flow engine). See the crate README for exactly what's in scope.
//!
//! Unlike `bring`, which is fully allocation-free (fixed line buffers) so a
//! `Page` can sit in a `static`, `horizon`'s `Page` owns `Vec`/`String`
//! (a DOM tree and a dynamic display list don't fit fixed buffers). That's
//! a deliberate scope decision, not an oversight: `alloc` is already
//! available everywhere in `ling-kernel` (its own global allocator), so
//! this costs nothing extra there, and `Page::new()` is a `const fn` of
//! empty `Vec`s -- no heap touched until the first `layout()` call.
//!
//! The engine never touches a screen. Like `bring`, it hands a laid-out
//! `Page` -- a flat `Vec<DisplayItem>` in paint order, in pixels -- to a
//! thin embedder that walks the list with its own primitives (LingOS's
//! `framebuffer` + `font8x8`, or anything else).

#![cfg_attr(not(test), no_std)]

extern crate alloc;

mod css;
mod dom;
mod layout;

pub mod canvas;

pub use canvas::CanvasSurface;
pub use css::{Align, Display, Style};
pub use dom::Tag;

use alloc::string::String;
use alloc::vec::Vec;

use dom::Dom;
use layout::Metrics;

/// One item in a page's paint-ordered display list, in pixels.
#[derive(Clone, Debug)]
pub enum DisplayItem {
    Rect { x: u32, y: u32, w: u32, h: u32, color: u32 },
    /// A run of same-style text. `color: None` means "no CSS color set --
    /// pick a sensible default" (the embedder's job, same as `bring`'s
    /// `NO_COLOR` sentinel: it knows its own theme, this engine doesn't).
    Text { x: u32, y: u32, text: String, color: Option<u32>, bold: bool, heading: bool, link: Option<u16> },
    /// An `<img>`: the embedder fetches/decodes `Page::images[image]` and
    /// blits it into this box (or shows its alt text on failure).
    Image { x: u32, y: u32, w: u32, h: u32, image: usize },
    /// A `<canvas>`: `Page::canvases[canvas]` is a real pixel surface the
    /// embedder (or any native code holding `&mut Page`) can paint into;
    /// the embedder blits its current contents into this box each frame.
    Canvas { x: u32, y: u32, w: u32, h: u32, canvas: usize },
}

pub struct Link {
    pub href: String,
}

pub struct ImgRef {
    pub src: String,
}

/// The laid-out page: a display list plus the tables it references.
/// Embedder-owned; cheap to replace wholesale each navigation (`layout()`
/// just reassigns each field, dropping the previous page's allocations).
pub struct Page {
    pub items: Vec<DisplayItem>,
    pub links: Vec<Link>,
    pub images: Vec<ImgRef>,
    pub canvases: Vec<CanvasSurface>,
    canvas_ids: Vec<String>,
    /// Document title (from `<title>`), best-effort.
    pub title: String,
    /// Total laid-out content height in pixels, for the embedder's scroll clamp.
    pub height: u32,
    /// Page background color (from a `body` CSS rule), if set.
    pub background: Option<u32>,
}

impl Page {
    pub const fn new() -> Page {
        Page {
            items: Vec::new(),
            links: Vec::new(),
            images: Vec::new(),
            canvases: Vec::new(),
            canvas_ids: Vec::new(),
            title: String::new(),
            height: 0,
            background: None,
        }
    }

    /// Look up a `<canvas id="...">`'s surface by its HTML id, for native
    /// code to paint into (e.g. an embedder animating a game/chart each
    /// frame -- see the crate README's note on scripted canvas being out
    /// of scope until there's a JS engine to drive it).
    pub fn canvas_mut(&mut self, id: &str) -> Option<&mut CanvasSurface> {
        let i = self.canvas_ids.iter().position(|s| s == id)?;
        self.canvases.get_mut(i)
    }

    pub fn canvas_at_mut(&mut self, index: usize) -> Option<&mut CanvasSurface> {
        self.canvases.get_mut(index)
    }
}

impl Default for Page {
    fn default() -> Self {
        Page::new()
    }
}

/// Parse `html`, cascade its CSS, and lay it out at `viewport_w` pixels
/// wide using a monospace glyph grid of `char_w` x `line_h` pixels (the
/// embedder's real font metrics -- e.g. LingOS's `font8x8` is 8x14 with a
/// 14px row height). Fills `page` fresh; any previous content is dropped.
pub fn layout(html: &[u8], viewport_w: u32, char_w: u32, line_h: u32, page: &mut Page) {
    let dom = dom::parse(html);
    let styles = css::cascade(&dom);
    let result = layout::layout_document(&dom, &styles, viewport_w, Metrics { char_w, line_h });

    page.items = result.items;
    page.links = result.links;
    page.images = result.images;
    page.canvases = result.canvases;
    page.canvas_ids = result.canvas_ids;
    page.height = result.height;
    page.background = result.background;
    page.title = extract_title(&dom);
}

fn extract_title(dom: &Dom) -> String {
    match find_first_tag(dom, dom.root, Tag::Title) {
        Some(t) => {
            let mut s = String::new();
            collect_text(dom, t, &mut s);
            s
        },
        None => String::new(),
    }
}

fn find_first_tag(dom: &Dom, idx: usize, tag: Tag) -> Option<usize> {
    if dom.nodes[idx].tag == tag {
        return Some(idx);
    }
    for &c in &dom.nodes[idx].children {
        if let Some(f) = find_first_tag(dom, c, tag) {
            return Some(f);
        }
    }
    None
}

fn collect_text(dom: &Dom, idx: usize, out: &mut String) {
    let node = &dom.nodes[idx];
    if node.tag == Tag::Text {
        out.push_str(&node.text);
    }
    for &c in &node.children {
        collect_text(dom, c, out);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn end_to_end_layout_produces_title_and_items() {
        let mut page = Page::new();
        layout(b"<html><head><title>Hi</title></head><body><h1>Hello</h1><p>World</p></body></html>", 640, 8, 14, &mut page);
        assert_eq!(page.title, "Hi");
        assert!(!page.items.is_empty());
        assert!(page.height > 0);
    }

    #[test]
    fn repeated_layout_calls_replace_the_page() {
        let mut page = Page::new();
        layout(b"<body><p>first page with several words</p></body>", 640, 8, 14, &mut page);
        let first_count = page.items.len();
        layout(b"<body><p>x</p></body>", 640, 8, 14, &mut page);
        assert!(page.items.len() <= first_count);
        assert_eq!(page.canvases.len(), 0);
    }

    #[test]
    fn canvas_element_is_paintable_by_id_after_layout() {
        let mut page = Page::new();
        layout(b"<body><canvas id=\"c\" width=\"20\" height=\"20\"></canvas></body>", 640, 8, 14, &mut page);
        let surf = page.canvas_mut("c").expect("canvas c should exist");
        surf.fill_rect(0, 0, 20, 20, 0x00ff00);
        assert_eq!(page.canvases[0].get_pixel(5, 5), Some(0x00ff00));
    }
}

← Back to index