H
horizon-browser
↓ 0 downloadsHorizon: 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-browserCode / 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));
}
}