├── .gitignore ├── examples ├── test.html ├── perf-rainbow.css └── test.css ├── Cargo.toml ├── LICENSE ├── src ├── dom.rs ├── main.rs ├── painting.rs ├── style.rs ├── html.rs ├── pdf.rs ├── css.rs └── layout.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | target 3 | output.png 4 | output.pdf 5 | -------------------------------------------------------------------------------- /examples/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Test 4 | 5 |
6 |

7 | Hello, world! 8 |

9 |

10 | Goodbye! 11 |

12 |
13 | 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "robinson" 3 | version = "0.0.1" 4 | authors = ["Matt Brubeck "] 5 | edition = "2021" 6 | 7 | [[bin]] 8 | name = "robinson" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | getopts = "0.2.21" 13 | image = { version = "0.25", default-features = false, features = ["png"] } 14 | -------------------------------------------------------------------------------- /examples/perf-rainbow.css: -------------------------------------------------------------------------------- 1 | head { 2 | display: none; 3 | } 4 | * { 5 | display: block; 6 | padding: 12px; 7 | } 8 | 9 | div.a { background: #ff0000; } 10 | div.b { background: #ffa500; } 11 | div.c { background: #ffff00; } 12 | div.d { background: #008000; } 13 | div.e { background: #0000ff; } 14 | div.f { background: #4b0082; } 15 | div.g { background: #800080; } 16 | -------------------------------------------------------------------------------- /examples/test.css: -------------------------------------------------------------------------------- 1 | * { 2 | display: block; 3 | } 4 | 5 | span { 6 | display: inline; 7 | } 8 | 9 | html { 10 | width: 600px; 11 | padding: 10px; 12 | border-width: 1px; 13 | margin: auto; 14 | background: #ffffff; 15 | } 16 | 17 | head { 18 | display: none; 19 | } 20 | 21 | .outer { 22 | background: #00ccff; 23 | border-color: #666666; 24 | border-width: 2px; 25 | margin: 50px; 26 | padding: 50px; 27 | } 28 | 29 | .inner { 30 | border-color: #cc0000; 31 | border-width: 4px; 32 | height: 100px; 33 | margin-bottom: 20px; 34 | width: 500px; 35 | } 36 | 37 | .inner#bye { 38 | background: #ffff00; 39 | } 40 | 41 | span#name { 42 | background: red; 43 | color: white; 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Matt Brubeck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/dom.rs: -------------------------------------------------------------------------------- 1 | //! Basic DOM data structures. 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | 5 | pub type AttrMap = HashMap; 6 | 7 | #[derive(Debug)] 8 | pub struct Node { 9 | // data common to all nodes: 10 | pub children: Vec, 11 | 12 | // data specific to each node type: 13 | pub node_type: NodeType, 14 | } 15 | 16 | #[derive(Debug)] 17 | pub enum NodeType { 18 | Element(ElementData), 19 | Text(String), 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct ElementData { 24 | pub tag_name: String, 25 | pub attrs: AttrMap, 26 | } 27 | 28 | // Constructor functions for convenience: 29 | 30 | pub fn text(data: String) -> Node { 31 | Node { children: vec![], node_type: NodeType::Text(data) } 32 | } 33 | 34 | pub fn elem(tag_name: String, attrs: AttrMap, children: Vec) -> Node { 35 | Node { 36 | children, 37 | node_type: NodeType::Element(ElementData { tag_name, attrs }) 38 | } 39 | } 40 | 41 | // Element methods 42 | 43 | impl ElementData { 44 | pub fn id(&self) -> Option<&String> { 45 | self.attrs.get("id") 46 | } 47 | 48 | pub fn classes(&self) -> HashSet<&str> { 49 | match self.attrs.get("class") { 50 | Some(classlist) => classlist.split(' ').collect(), 51 | None => HashSet::new() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Robinson 2 | ======== 3 | 4 | A toy web rendering engine written in the Rust language, by Matt Brubeck 5 | (mbrubeck@limpet.net). 6 | 7 | I'm writing this code purely for educational purposes. My goal is to create an 8 | incomplete but extremely simple engine as a way to learn more about basic 9 | implementation techniques, *without* worrying about complications like: 10 | 11 | * Real-world usability 12 | * Standards compliance 13 | * Performance and efficiency 14 | * Interoperability 15 | 16 | These are all important goals, but there are other projects working on them. 17 | By ignoring them completely, this project can focus on being as simple and 18 | easy-to-understand as possible. 19 | 20 | Why create a simple—but useless—toy rendering engine? Mostly because I 21 | personally want to learn how to do it. If I succeed, I also hope that other 22 | people can learn from my code by reading or modifying it, or learn from my 23 | experience as they set out to build their own toy browser engines. 24 | 25 | For more details see [Let's build a browser engine!][blog], a series of 26 | how-to articles based on this project. 27 | 28 | [blog]: http://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html 29 | 30 | Status 31 | ------ 32 | 33 | Currently implemented: 34 | 35 | * Parse a small subset of HTML and build a DOM tree. 36 | * Parse a small subset of CSS. 37 | * Perform selector matching to apply styles to elements. 38 | * Basic block layout. 39 | 40 | Coming soon, I hope: 41 | 42 | * Inline layout. 43 | * Paint text and boxes. 44 | * Load resources from network or filesystem. 45 | 46 | Instructions 47 | ------------ 48 | 49 | 1. [Install Rust 1.0 beta or newer.](http://www.rust-lang.org/install.html) 50 | 51 | 2. Clone the robinson source code from https://github.com/mbrubeck/robinson 52 | 53 | 3. Run `cargo build` to build robinson, and `cargo run` to run it. 54 | 55 | To build and run with optimizations enabled, use `cargo build --release` and 56 | `cargo run --release`. 57 | 58 | By default, robinson will load test.html and test.css from the `examples` 59 | directory. You can use the `--html` and `--css` arguments to the robinson 60 | executable to change the input files: 61 | 62 | ./target/debug/robinson --html examples/test.html --css examples/test.css 63 | 64 | The rendered page will be saved to a file named `output.png`. To change the 65 | output filename, use the `-o` option. To switch to PDF output, use add 66 | `--format pdf`. 67 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate getopts; 2 | extern crate image; 3 | 4 | use std::default::Default; 5 | use std::io::{Read, BufWriter}; 6 | use std::fs::File; 7 | 8 | pub mod css; 9 | pub mod dom; 10 | pub mod html; 11 | pub mod layout; 12 | pub mod style; 13 | pub mod painting; 14 | pub mod pdf; 15 | 16 | fn main() { 17 | // Parse command-line options: 18 | let mut opts = getopts::Options::new(); 19 | opts.optopt("h", "html", "HTML document", "FILENAME"); 20 | opts.optopt("c", "css", "CSS stylesheet", "FILENAME"); 21 | opts.optopt("o", "output", "Output file", "FILENAME"); 22 | opts.optopt("f", "format", "Output file format", "png | pdf"); 23 | 24 | let matches = opts.parse(std::env::args().skip(1)).unwrap(); 25 | let str_arg = |flag: &str, default: &str| -> String { 26 | matches.opt_str(flag).unwrap_or(default.to_string()) 27 | }; 28 | 29 | // Choose a format: 30 | let png = match &str_arg("f", "png")[..] { 31 | "png" => true, 32 | "pdf" => false, 33 | x => panic!("Unknown output format: {}", x), 34 | }; 35 | 36 | // Read input files: 37 | let html = read_source(str_arg("h", "examples/test.html")); 38 | let css = read_source(str_arg("c", "examples/test.css")); 39 | 40 | // Since we don't have an actual window, hard-code the "viewport" size. 41 | let mut viewport: layout::Dimensions = Default::default(); 42 | viewport.content.width = 800.0; 43 | viewport.content.height = 600.0; 44 | 45 | // Parsing and rendering: 46 | let root_node = html::parse(html); 47 | let stylesheet = css::parse(css); 48 | let style_root = style::style_tree(&root_node, &stylesheet); 49 | let layout_root = layout::layout_tree(&style_root, viewport); 50 | 51 | // Create the output file: 52 | let filename = str_arg("o", if png { "output.png" } else { "output.pdf" }); 53 | let mut file = BufWriter::new(File::create(&filename).unwrap()); 54 | 55 | // Write to the file: 56 | let ok = if png { 57 | let canvas = painting::paint(&layout_root, viewport.content); 58 | let (w, h) = (canvas.width as u32, canvas.height as u32); 59 | let img = image::ImageBuffer::from_fn(w, h, move |x, y| { 60 | let color = canvas.pixels[(y * w + x) as usize]; 61 | image::Rgba([color.r, color.g, color.b, color.a]) 62 | }); 63 | image::DynamicImage::ImageRgba8(img).write_to(&mut file, image::ImageFormat::Png).is_ok() 64 | } else { 65 | pdf::render(&layout_root, viewport.content, &mut file).is_ok() 66 | }; 67 | if ok { 68 | println!("Saved output as {}", filename) 69 | } else { 70 | println!("Error saving output as {}", filename) 71 | } 72 | } 73 | 74 | fn read_source(filename: String) -> String { 75 | let mut str = String::new(); 76 | File::open(filename).unwrap().read_to_string(&mut str).unwrap(); 77 | str 78 | } 79 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "autocfg" 13 | version = "1.3.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 16 | 17 | [[package]] 18 | name = "bitflags" 19 | version = "1.3.2" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 22 | 23 | [[package]] 24 | name = "bytemuck" 25 | version = "1.16.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" 28 | 29 | [[package]] 30 | name = "byteorder" 31 | version = "1.5.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 34 | 35 | [[package]] 36 | name = "cfg-if" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 40 | 41 | [[package]] 42 | name = "crc32fast" 43 | version = "1.4.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 46 | dependencies = [ 47 | "cfg-if", 48 | ] 49 | 50 | [[package]] 51 | name = "fdeflate" 52 | version = "0.3.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" 55 | dependencies = [ 56 | "simd-adler32", 57 | ] 58 | 59 | [[package]] 60 | name = "flate2" 61 | version = "1.0.30" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 64 | dependencies = [ 65 | "crc32fast", 66 | "miniz_oxide", 67 | ] 68 | 69 | [[package]] 70 | name = "getopts" 71 | version = "0.2.21" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 74 | dependencies = [ 75 | "unicode-width", 76 | ] 77 | 78 | [[package]] 79 | name = "image" 80 | version = "0.25.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" 83 | dependencies = [ 84 | "bytemuck", 85 | "byteorder", 86 | "num-traits", 87 | "png", 88 | ] 89 | 90 | [[package]] 91 | name = "miniz_oxide" 92 | version = "0.7.4" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 95 | dependencies = [ 96 | "adler", 97 | "simd-adler32", 98 | ] 99 | 100 | [[package]] 101 | name = "num-traits" 102 | version = "0.2.19" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 105 | dependencies = [ 106 | "autocfg", 107 | ] 108 | 109 | [[package]] 110 | name = "png" 111 | version = "0.17.13" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" 114 | dependencies = [ 115 | "bitflags", 116 | "crc32fast", 117 | "fdeflate", 118 | "flate2", 119 | "miniz_oxide", 120 | ] 121 | 122 | [[package]] 123 | name = "robinson" 124 | version = "0.0.1" 125 | dependencies = [ 126 | "getopts", 127 | "image", 128 | ] 129 | 130 | [[package]] 131 | name = "simd-adler32" 132 | version = "0.3.7" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 135 | 136 | [[package]] 137 | name = "unicode-width" 138 | version = "0.1.13" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 141 | -------------------------------------------------------------------------------- /src/painting.rs: -------------------------------------------------------------------------------- 1 | use crate::layout::{AnonymousBlock, BlockNode, InlineNode, LayoutBox, Rect}; 2 | use crate::css::{Value, Color}; 3 | 4 | pub struct Canvas { 5 | pub pixels: Vec, 6 | pub width: usize, 7 | pub height: usize, 8 | } 9 | 10 | /// Paint a tree of LayoutBoxes to an array of pixels. 11 | pub fn paint(layout_root: &LayoutBox, bounds: Rect) -> Canvas { 12 | let display_list = build_display_list(layout_root); 13 | let mut canvas = Canvas::new(bounds.width as usize, bounds.height as usize); 14 | for item in display_list { 15 | canvas.paint_item(&item); 16 | } 17 | canvas 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum DisplayCommand { 22 | SolidColor(Color, Rect), 23 | } 24 | 25 | pub type DisplayList = Vec; 26 | 27 | pub fn build_display_list(layout_root: &LayoutBox) -> DisplayList { 28 | let mut list = Vec::new(); 29 | render_layout_box(&mut list, layout_root); 30 | list 31 | } 32 | 33 | fn render_layout_box(list: &mut DisplayList, layout_box: &LayoutBox) { 34 | render_background(list, layout_box); 35 | render_borders(list, layout_box); 36 | for child in &layout_box.children { 37 | render_layout_box(list, child); 38 | } 39 | } 40 | 41 | fn render_background(list: &mut DisplayList, layout_box: &LayoutBox) { 42 | if let Some(color) = get_color(layout_box, "background") { 43 | list.push(DisplayCommand::SolidColor(color, layout_box.dimensions.border_box())); 44 | } 45 | } 46 | 47 | fn render_borders(list: &mut DisplayList, layout_box: &LayoutBox) { 48 | let color = match get_color(layout_box, "border-color") { 49 | Some(color) => color, 50 | _ => return 51 | }; 52 | 53 | let d = &layout_box.dimensions; 54 | let border_box = d.border_box(); 55 | 56 | // Left border 57 | list.push(DisplayCommand::SolidColor(color, Rect { 58 | x: border_box.x, 59 | y: border_box.y, 60 | width: d.border.left, 61 | height: border_box.height, 62 | })); 63 | 64 | // Right border 65 | list.push(DisplayCommand::SolidColor(color, Rect { 66 | x: border_box.x + border_box.width - d.border.right, 67 | y: border_box.y, 68 | width: d.border.right, 69 | height: border_box.height, 70 | })); 71 | 72 | // Top border 73 | list.push(DisplayCommand::SolidColor(color, Rect { 74 | x: border_box.x, 75 | y: border_box.y, 76 | width: border_box.width, 77 | height: d.border.top, 78 | })); 79 | 80 | // Bottom border 81 | list.push(DisplayCommand::SolidColor(color, Rect { 82 | x: border_box.x, 83 | y: border_box.y + border_box.height - d.border.bottom, 84 | width: border_box.width, 85 | height: d.border.bottom, 86 | })); 87 | } 88 | 89 | /// Return the specified color for CSS property `name`, or None if no color was specified. 90 | fn get_color(layout_box: &LayoutBox, name: &str) -> Option { 91 | match layout_box.box_type { 92 | BlockNode(style) | InlineNode(style) => match style.value(name) { 93 | Some(Value::ColorValue(color)) => Some(color), 94 | _ => None 95 | }, 96 | AnonymousBlock => None 97 | } 98 | } 99 | 100 | impl Canvas { 101 | /// Create a blank canvas 102 | fn new(width: usize, height: usize) -> Canvas { 103 | let white = Color { r: 255, g: 255, b: 255, a: 255 }; 104 | Canvas { 105 | pixels: vec![white; width * height], 106 | width, 107 | height, 108 | } 109 | } 110 | 111 | fn paint_item(&mut self, item: &DisplayCommand) { 112 | match *item { 113 | DisplayCommand::SolidColor(color, rect) => { 114 | // Clip the rectangle to the canvas boundaries. 115 | let x0 = rect.x.clamp(0.0, self.width as f32) as usize; 116 | let y0 = rect.y.clamp(0.0, self.height as f32) as usize; 117 | let x1 = (rect.x + rect.width).clamp(0.0, self.width as f32) as usize; 118 | let y1 = (rect.y + rect.height).clamp(0.0, self.height as f32) as usize; 119 | 120 | for y in y0 .. y1 { 121 | for x in x0 .. x1 { 122 | // TODO: alpha compositing with existing pixel 123 | self.pixels[y * self.width + x] = color; 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | //! Code for applying CSS styles to the DOM. 2 | //! 3 | //! This is not very interesting at the moment. It will get much more 4 | //! complicated if I add support for compound selectors. 5 | 6 | use crate::dom::{Node, NodeType, ElementData}; 7 | use crate::css::{Stylesheet, Rule, Selector, SimpleSelector, Value, Specificity}; 8 | use std::collections::HashMap; 9 | 10 | /// Map from CSS property names to values. 11 | pub type PropertyMap = HashMap; 12 | 13 | /// A node with associated style data. 14 | pub struct StyledNode<'a> { 15 | pub node: &'a Node, 16 | pub specified_values: PropertyMap, 17 | pub children: Vec>, 18 | } 19 | 20 | #[derive(PartialEq)] 21 | pub enum Display { 22 | Inline, 23 | Block, 24 | None, 25 | } 26 | 27 | impl<'a> StyledNode<'a> { 28 | /// Return the specified value of a property if it exists, otherwise `None`. 29 | pub fn value(&self, name: &str) -> Option { 30 | self.specified_values.get(name).cloned() 31 | } 32 | 33 | /// Return the specified value of property `name`, or property `fallback_name` if that doesn't 34 | /// exist, or value `default` if neither does. 35 | pub fn lookup(&self, name: &str, fallback_name: &str, default: &Value) -> Value { 36 | self.value(name).unwrap_or_else(|| self.value(fallback_name) 37 | .unwrap_or_else(|| default.clone())) 38 | } 39 | 40 | /// The value of the `display` property (defaults to inline). 41 | pub fn display(&self) -> Display { 42 | match self.value("display") { 43 | Some(Value::Keyword(s)) => match &*s { 44 | "block" => Display::Block, 45 | "none" => Display::None, 46 | _ => Display::Inline 47 | }, 48 | _ => Display::Inline 49 | } 50 | } 51 | } 52 | 53 | /// Apply a stylesheet to an entire DOM tree, returning a StyledNode tree. 54 | /// 55 | /// This finds only the specified values at the moment. Eventually it should be extended to find the 56 | /// computed values too, including inherited values. 57 | pub fn style_tree<'a>(root: &'a Node, stylesheet: &'a Stylesheet) -> StyledNode<'a> { 58 | StyledNode { 59 | node: root, 60 | specified_values: match root.node_type { 61 | NodeType::Element(ref elem) => specified_values(elem, stylesheet), 62 | NodeType::Text(_) => HashMap::new() 63 | }, 64 | children: root.children.iter().map(|child| style_tree(child, stylesheet)).collect(), 65 | } 66 | } 67 | 68 | /// Apply styles to a single element, returning the specified styles. 69 | /// 70 | /// To do: Allow multiple UA/author/user stylesheets, and implement the cascade. 71 | fn specified_values(elem: &ElementData, stylesheet: &Stylesheet) -> PropertyMap { 72 | let mut values = HashMap::new(); 73 | let mut rules = matching_rules(elem, stylesheet); 74 | 75 | // Go through the rules from lowest to highest specificity. 76 | rules.sort_by(|&(a, _), &(b, _)| a.cmp(&b)); 77 | for (_, rule) in rules { 78 | for declaration in &rule.declarations { 79 | values.insert(declaration.name.clone(), declaration.value.clone()); 80 | } 81 | } 82 | values 83 | } 84 | 85 | /// A single CSS rule and the specificity of its most specific matching selector. 86 | type MatchedRule<'a> = (Specificity, &'a Rule); 87 | 88 | /// Find all CSS rules that match the given element. 89 | fn matching_rules<'a>(elem: &ElementData, stylesheet: &'a Stylesheet) -> Vec> { 90 | // For now, we just do a linear scan of all the rules. For large 91 | // documents, it would be more efficient to store the rules in hash tables 92 | // based on tag name, id, class, etc. 93 | stylesheet.rules.iter().filter_map(|rule| match_rule(elem, rule)).collect() 94 | } 95 | 96 | /// If `rule` matches `elem`, return a `MatchedRule`. Otherwise return `None`. 97 | fn match_rule<'a>(elem: &ElementData, rule: &'a Rule) -> Option> { 98 | // Find the first (most specific) matching selector. 99 | rule.selectors 100 | .iter().find(|selector| matches(elem, selector)) 101 | .map(|selector| (selector.specificity(), rule)) 102 | } 103 | 104 | /// Selector matching: 105 | fn matches(elem: &ElementData, selector: &Selector) -> bool { 106 | match selector { 107 | Selector::Simple(s) => matches_simple_selector(elem, s) 108 | } 109 | } 110 | 111 | fn matches_simple_selector(elem: &ElementData, selector: &SimpleSelector) -> bool { 112 | // Check type selector 113 | if selector.tag_name.iter().any(|name| elem.tag_name != *name) { 114 | return false; 115 | } 116 | 117 | // Check ID selector 118 | if selector.id.iter().any(|id| elem.id() != Some(id)) { 119 | return false; 120 | } 121 | 122 | // Check class selectors 123 | if selector.class.iter().any(|class| !elem.classes().contains(class.as_str())) { 124 | return false; 125 | } 126 | 127 | // We didn't find any non-matching selector components. 128 | true 129 | } 130 | -------------------------------------------------------------------------------- /src/html.rs: -------------------------------------------------------------------------------- 1 | //! A simple parser for a tiny subset of HTML. 2 | //! 3 | //! Can parse basic opening and closing tags, and text nodes. 4 | //! 5 | //! Not yet supported: 6 | //! 7 | //! * Comments 8 | //! * Doctypes and processing instructions 9 | //! * Self-closing tags 10 | //! * Non-well-formed markup 11 | //! * Character entities 12 | 13 | use crate::dom; 14 | use std::collections::HashMap; 15 | 16 | /// Parse an HTML document and return the root element. 17 | pub fn parse(source: String) -> dom::Node { 18 | let mut nodes = Parser { pos: 0, input: source }.parse_nodes(); 19 | 20 | // If the document contains a root element, just return it. Otherwise, create one. 21 | if nodes.len() == 1 { 22 | nodes.remove(0) 23 | } else { 24 | dom::elem("html".to_string(), HashMap::new(), nodes) 25 | } 26 | } 27 | 28 | struct Parser { 29 | pos: usize, 30 | input: String, 31 | } 32 | 33 | impl Parser { 34 | /// Parse a sequence of sibling nodes. 35 | fn parse_nodes(&mut self) -> Vec { 36 | let mut nodes = vec!(); 37 | loop { 38 | self.consume_whitespace(); 39 | if self.eof() || self.starts_with(" dom::Node { 49 | if self.starts_with("<") { 50 | self.parse_element() 51 | } else { 52 | self.parse_text() 53 | } 54 | } 55 | 56 | /// Parse a single element, including its open tag, contents, and closing tag. 57 | fn parse_element(&mut self) -> dom::Node { 58 | // Opening tag. 59 | self.expect("<"); 60 | let tag_name = self.parse_name(); 61 | let attrs = self.parse_attributes(); 62 | self.expect(">"); 63 | 64 | // Contents. 65 | let children = self.parse_nodes(); 66 | 67 | // Closing tag. 68 | self.expect(""); 71 | 72 | dom::elem(tag_name, attrs, children) 73 | } 74 | 75 | /// Parse a tag or attribute name. 76 | fn parse_name(&mut self) -> String { 77 | self.consume_while(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9')) 78 | } 79 | 80 | /// Parse a list of name="value" pairs, separated by whitespace. 81 | fn parse_attributes(&mut self) -> dom::AttrMap { 82 | let mut attributes = HashMap::new(); 83 | loop { 84 | self.consume_whitespace(); 85 | if self.next_char() == '>' { 86 | break; 87 | } 88 | let (name, value) = self.parse_attr(); 89 | attributes.insert(name, value); 90 | } 91 | attributes 92 | } 93 | 94 | /// Parse a single name="value" pair. 95 | fn parse_attr(&mut self) -> (String, String) { 96 | let name = self.parse_name(); 97 | self.expect("="); 98 | let value = self.parse_attr_value(); 99 | (name, value) 100 | } 101 | 102 | /// Parse a quoted value. 103 | fn parse_attr_value(&mut self) -> String { 104 | let open_quote = self.consume_char(); 105 | assert!(open_quote == '"' || open_quote == '\''); 106 | let value = self.consume_while(|c| c != open_quote); 107 | let close_quote = self.consume_char(); 108 | assert_eq!(open_quote, close_quote); 109 | value 110 | } 111 | 112 | /// Parse a text node. 113 | fn parse_text(&mut self) -> dom::Node { 114 | dom::text(self.consume_while(|c| c != '<')) 115 | } 116 | 117 | /// Consume and discard zero or more whitespace characters. 118 | fn consume_whitespace(&mut self) { 119 | self.consume_while(char::is_whitespace); 120 | } 121 | 122 | /// Consume characters until `test` returns false. 123 | fn consume_while(&mut self, test: impl Fn(char) -> bool) -> String { 124 | let mut result = String::new(); 125 | while !self.eof() && test(self.next_char()) { 126 | result.push(self.consume_char()); 127 | } 128 | result 129 | } 130 | 131 | /// Return the current character, and advance self.pos to the next character. 132 | fn consume_char(&mut self) -> char { 133 | let c = self.next_char(); 134 | self.pos += c.len_utf8(); 135 | c 136 | } 137 | 138 | /// Read the current character without consuming it. 139 | fn next_char(&self) -> char { 140 | self.input[self.pos..].chars().next().unwrap() 141 | } 142 | 143 | /// Does the current input start with the given string? 144 | fn starts_with(&self, s: &str) -> bool { 145 | self.input[self.pos ..].starts_with(s) 146 | } 147 | 148 | /// If the exact string `s` is found at the current position, consume it. 149 | /// Otherwise, panic. 150 | fn expect(&mut self, s: &str) { 151 | if self.starts_with(s) { 152 | self.pos += s.len(); 153 | } else { 154 | panic!("Expected {:?} at byte {} but it was not found", s, self.pos); 155 | } 156 | } 157 | 158 | /// Return true if all input is consumed. 159 | fn eof(&self) -> bool { 160 | self.pos >= self.input.len() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/pdf.rs: -------------------------------------------------------------------------------- 1 | use crate::layout::{LayoutBox, Rect}; 2 | use crate::painting::{build_display_list, DisplayCommand}; 3 | use std::io::{self, Seek, Write}; 4 | 5 | fn px_to_pt(value: f32) -> f32 { 6 | // 96px = 1in = 72pt 7 | // value * 1px = value * 96px / 96 = value * 72pt / 96 = (value * 0.75) * 1pt 8 | value * 0.75 9 | } 10 | 11 | pub fn render( 12 | layout_root: &LayoutBox, 13 | bounds: Rect, 14 | file: &mut W, 15 | ) -> io::Result<()> { 16 | let display_list = build_display_list(layout_root); 17 | let mut pdf = Pdf::new(file)?; 18 | // We map CSS pt to Poscript points (which is the default length unit in PDF). 19 | pdf.render_page(px_to_pt(bounds.width), px_to_pt(bounds.height), |output| { 20 | for item in display_list { 21 | render_item(&item, output)?; 22 | } 23 | Ok(()) 24 | })?; 25 | pdf.finish() 26 | } 27 | 28 | fn render_item(item: &DisplayCommand, output: &mut W) -> io::Result<()> { 29 | match *item { 30 | DisplayCommand::SolidColor(color, rect) => { 31 | writeln!( 32 | output, 33 | "{} {} {} sc {} {} {} {} re f", 34 | // FIMXE: alpha transparency 35 | color.r, 36 | color.g, 37 | color.b, 38 | rect.x, 39 | rect.y, 40 | rect.width, 41 | rect.height 42 | ) 43 | } 44 | } 45 | } 46 | 47 | struct Pdf<'a, W: 'a + Write + Seek> { 48 | output: &'a mut W, 49 | object_offsets: Vec, 50 | page_objects_ids: Vec, 51 | } 52 | 53 | const ROOT_OBJECT_ID: usize = 1; 54 | const PAGES_OBJECT_ID: usize = 2; 55 | 56 | impl<'a, W: Write + Seek> Pdf<'a, W> { 57 | fn new(output: &'a mut W) -> io::Result> { 58 | // FIXME: Find out the lowest version that contains the features we’re using. 59 | output.write_all(b"%PDF-1.7\n%\xB5\xED\xAE\xFB\n")?; 60 | Ok(Pdf { 61 | output, 62 | // Object ID 0 is special in PDF. 63 | // We reserve IDs 1 and 2 for the catalog and page tree. 64 | object_offsets: vec![-1, -1, -1], 65 | page_objects_ids: vec![], 66 | }) 67 | } 68 | 69 | /// Return the current read/write position in the output file. 70 | fn tell(&mut self) -> io::Result { 71 | self.output.stream_position() 72 | } 73 | 74 | fn render_page(&mut self, width: f32, height: f32, render_contents: F) -> io::Result<()> 75 | where 76 | F: FnOnce(&mut W) -> io::Result<()>, 77 | { 78 | let (contents_object_id, content_length) = 79 | self.write_new_object(move |contents_object_id, pdf| { 80 | // Guess the ID of the next object. (We’ll assert it below.) 81 | writeln!(pdf.output, "<< /Length {} 0 R", contents_object_id + 1)?; 82 | writeln!(pdf.output, ">>")?; 83 | writeln!(pdf.output, "stream")?; 84 | 85 | let start = pdf.tell()?; 86 | writeln!(pdf.output, "/DeviceRGB cs /DeviceRGB CS")?; 87 | writeln!(pdf.output, "0.75 0 0 -0.75 0 {} cm", height)?; 88 | render_contents(pdf.output)?; 89 | let end = pdf.tell()?; 90 | 91 | writeln!(pdf.output, "endstream")?; 92 | Ok((contents_object_id, end - start)) 93 | })?; 94 | self.write_new_object(|length_object_id, pdf| { 95 | assert_eq!(length_object_id, contents_object_id + 1); 96 | writeln!(pdf.output, "{}", content_length) 97 | })?; 98 | let page_object_id = self.write_new_object(|page_object_id, pdf| { 99 | writeln!(pdf.output, "<< /Type /Page")?; 100 | writeln!(pdf.output, " /Parent {} 0 R", PAGES_OBJECT_ID)?; 101 | writeln!(pdf.output, " /Resources << >>")?; 102 | writeln!(pdf.output, " /MediaBox [ 0 0 {} {} ]", width, height)?; 103 | writeln!(pdf.output, " /Contents {} 0 R", contents_object_id)?; 104 | writeln!(pdf.output, ">>")?; 105 | Ok(page_object_id) 106 | })?; 107 | self.page_objects_ids.push(page_object_id); 108 | Ok(()) 109 | } 110 | 111 | fn write_new_object(&mut self, write_content: F) -> io::Result 112 | where 113 | F: FnOnce(usize, &mut Pdf) -> io::Result, 114 | { 115 | let id = self.object_offsets.len(); 116 | // `as i64` here would only overflow for PDF files bigger than 2**63 bytes 117 | let offset = self.tell()? as i64; 118 | self.object_offsets.push(offset); 119 | self._write_object(id, move |pdf| write_content(id, pdf)) 120 | } 121 | 122 | fn write_object_with_id(&mut self, id: usize, write_content: F) -> io::Result 123 | where 124 | F: FnOnce(&mut Pdf) -> io::Result, 125 | { 126 | assert_eq!(self.object_offsets[id], -1); 127 | // `as i64` here would only overflow for PDF files bigger than 2**63 bytes 128 | let offset = self.tell()? as i64; 129 | self.object_offsets[id] = offset; 130 | self._write_object(id, write_content) 131 | } 132 | 133 | fn _write_object(&mut self, id: usize, write_content: F) -> io::Result 134 | where 135 | F: FnOnce(&mut Pdf) -> io::Result, 136 | { 137 | writeln!(self.output, "{} 0 obj", id)?; 138 | let result = write_content(self)?; 139 | writeln!(self.output, "endobj")?; 140 | Ok(result) 141 | } 142 | 143 | fn finish(mut self) -> io::Result<()> { 144 | self._finish() 145 | } 146 | 147 | fn _finish(&mut self) -> io::Result<()> { 148 | self.write_object_with_id(PAGES_OBJECT_ID, |pdf| { 149 | writeln!(pdf.output, "<< /Type /Pages")?; 150 | writeln!(pdf.output, " /Count {}", pdf.page_objects_ids.len())?; 151 | write!(pdf.output, " /Kids [ ")?; 152 | for &page_object_id in &pdf.page_objects_ids { 153 | write!(pdf.output, "{} 0 R ", page_object_id)?; 154 | } 155 | writeln!(pdf.output, "]")?; 156 | writeln!(pdf.output, ">>")?; 157 | Ok(()) 158 | })?; 159 | self.write_object_with_id(ROOT_OBJECT_ID, |pdf| { 160 | writeln!(pdf.output, "<< /Type /Catalog")?; 161 | writeln!(pdf.output, " /Pages {} 0 R", PAGES_OBJECT_ID)?; 162 | writeln!(pdf.output, ">>")?; 163 | Ok(()) 164 | })?; 165 | let startxref = self.tell(); 166 | writeln!(self.output, "xref")?; 167 | writeln!(self.output, "0 {}", self.object_offsets.len())?; 168 | // Object 0 is special 169 | writeln!(self.output, "0000000000 65535 f ")?; 170 | // Use [1..] to skip object 0 in self.object_offsets. 171 | for &offset in &self.object_offsets[1..] { 172 | assert!(offset >= 0); 173 | writeln!(self.output, "{:010} 00000 n ", offset)?; 174 | } 175 | writeln!(self.output, "trailer")?; 176 | writeln!(self.output, "<< /Size {}", self.object_offsets.len())?; 177 | writeln!(self.output, " /Root {} 0 R", ROOT_OBJECT_ID)?; 178 | writeln!(self.output, ">>")?; 179 | writeln!(self.output, "startxref")?; 180 | writeln!(self.output, "{:?}", startxref)?; 181 | writeln!(self.output, "%%EOF")?; 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/css.rs: -------------------------------------------------------------------------------- 1 | //! A simple parser for a tiny subset of CSS. 2 | //! 3 | //! To support more CSS syntax, it would probably be easiest to replace this 4 | //! hand-rolled parser with one based on a library or parser generator. 5 | 6 | // Data structures: 7 | 8 | #[derive(Debug)] 9 | pub struct Stylesheet { 10 | pub rules: Vec, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct Rule { 15 | pub selectors: Vec, 16 | pub declarations: Vec, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum Selector { 21 | Simple(SimpleSelector), 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct SimpleSelector { 26 | pub tag_name: Option, 27 | pub id: Option, 28 | pub class: Vec, 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct Declaration { 33 | pub name: String, 34 | pub value: Value, 35 | } 36 | 37 | #[derive(Debug, Clone, PartialEq)] 38 | pub enum Value { 39 | Keyword(String), 40 | Length(f32, Unit), 41 | ColorValue(Color), 42 | } 43 | 44 | #[derive(Debug, Clone, PartialEq)] 45 | pub enum Unit { 46 | Px, 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq, Default)] 50 | pub struct Color { 51 | pub r: u8, 52 | pub g: u8, 53 | pub b: u8, 54 | pub a: u8, 55 | } 56 | 57 | impl Copy for Color {} 58 | 59 | pub type Specificity = (usize, usize, usize); 60 | 61 | impl Selector { 62 | pub fn specificity(&self) -> Specificity { 63 | // http://www.w3.org/TR/selectors/#specificity 64 | let Selector::Simple(ref simple) = *self; 65 | let a = simple.id.iter().count(); 66 | let b = simple.class.len(); 67 | let c = simple.tag_name.iter().count(); 68 | (a, b, c) 69 | } 70 | } 71 | 72 | impl Value { 73 | /// Return the size of a length in px, or zero for non-lengths. 74 | pub fn to_px(&self) -> f32 { 75 | match *self { 76 | Value::Length(f, Unit::Px) => f, 77 | _ => 0.0 78 | } 79 | } 80 | } 81 | 82 | /// Parse a whole CSS stylesheet. 83 | pub fn parse(source: String) -> Stylesheet { 84 | let mut parser = Parser { pos: 0, input: source }; 85 | Stylesheet { rules: parser.parse_rules() } 86 | } 87 | 88 | struct Parser { 89 | pos: usize, 90 | input: String, 91 | } 92 | 93 | impl Parser { 94 | /// Parse a list of rule sets, separated by optional whitespace. 95 | fn parse_rules(&mut self) -> Vec { 96 | let mut rules = Vec::new(); 97 | loop { 98 | self.consume_whitespace(); 99 | if self.eof() { break } 100 | rules.push(self.parse_rule()); 101 | } 102 | rules 103 | } 104 | 105 | /// Parse a rule set: ` { }`. 106 | fn parse_rule(&mut self) -> Rule { 107 | Rule { 108 | selectors: self.parse_selectors(), 109 | declarations: self.parse_declarations(), 110 | } 111 | } 112 | 113 | /// Parse a comma-separated list of selectors. 114 | fn parse_selectors(&mut self) -> Vec { 115 | let mut selectors = Vec::new(); 116 | loop { 117 | selectors.push(Selector::Simple(self.parse_simple_selector())); 118 | self.consume_whitespace(); 119 | match self.next_char() { 120 | ',' => { self.consume_char(); self.consume_whitespace(); } 121 | '{' => break, 122 | c => panic!("Unexpected character {} in selector list", c) 123 | } 124 | } 125 | // Return selectors with highest specificity first, for use in matching. 126 | selectors.sort_by_key(|s| s.specificity()); 127 | selectors 128 | } 129 | 130 | /// Parse one simple selector, e.g.: `type#id.class1.class2.class3` 131 | fn parse_simple_selector(&mut self) -> SimpleSelector { 132 | let mut selector = SimpleSelector { tag_name: None, id: None, class: Vec::new() }; 133 | while !self.eof() { 134 | match self.next_char() { 135 | '#' => { 136 | self.consume_char(); 137 | selector.id = Some(self.parse_identifier()); 138 | } 139 | '.' => { 140 | self.consume_char(); 141 | selector.class.push(self.parse_identifier()); 142 | } 143 | '*' => { 144 | // universal selector 145 | self.consume_char(); 146 | } 147 | c if valid_identifier_char(c) => { 148 | selector.tag_name = Some(self.parse_identifier()); 149 | } 150 | _ => break 151 | } 152 | } 153 | selector 154 | } 155 | 156 | /// Parse a list of declarations enclosed in `{ ... }`. 157 | fn parse_declarations(&mut self) -> Vec { 158 | self.expect_char('{'); 159 | let mut declarations = Vec::new(); 160 | loop { 161 | self.consume_whitespace(); 162 | if self.next_char() == '}' { 163 | self.consume_char(); 164 | break; 165 | } 166 | declarations.push(self.parse_declaration()); 167 | } 168 | declarations 169 | } 170 | 171 | /// Parse one `: ;` declaration. 172 | fn parse_declaration(&mut self) -> Declaration { 173 | let name = self.parse_identifier(); 174 | self.consume_whitespace(); 175 | self.expect_char(':'); 176 | self.consume_whitespace(); 177 | let value = self.parse_value(); 178 | self.consume_whitespace(); 179 | self.expect_char(';'); 180 | 181 | Declaration { name, value } 182 | } 183 | 184 | // Methods for parsing values: 185 | 186 | fn parse_value(&mut self) -> Value { 187 | match self.next_char() { 188 | '0'..='9' => self.parse_length(), 189 | '#' => self.parse_color(), 190 | _ => Value::Keyword(self.parse_identifier()) 191 | } 192 | } 193 | 194 | fn parse_length(&mut self) -> Value { 195 | Value::Length(self.parse_float(), self.parse_unit()) 196 | } 197 | 198 | fn parse_float(&mut self) -> f32 { 199 | self.consume_while(|c| matches!(c, '0'..='9' | '.')).parse().unwrap() 200 | } 201 | 202 | fn parse_unit(&mut self) -> Unit { 203 | match &*self.parse_identifier().to_ascii_lowercase() { 204 | "px" => Unit::Px, 205 | _ => panic!("unrecognized unit") 206 | } 207 | } 208 | 209 | fn parse_color(&mut self) -> Value { 210 | self.expect_char('#'); 211 | Value::ColorValue(Color { 212 | r: self.parse_hex_pair(), 213 | g: self.parse_hex_pair(), 214 | b: self.parse_hex_pair(), 215 | a: 255 }) 216 | } 217 | 218 | /// Parse two hexadecimal digits. 219 | fn parse_hex_pair(&mut self) -> u8 { 220 | let s = &self.input[self.pos .. self.pos + 2]; 221 | self.pos += 2; 222 | u8::from_str_radix(s, 16).unwrap() 223 | } 224 | 225 | /// Parse a property name or keyword. 226 | fn parse_identifier(&mut self) -> String { 227 | self.consume_while(valid_identifier_char) 228 | } 229 | 230 | /// Consume and discard zero or more whitespace characters. 231 | fn consume_whitespace(&mut self) { 232 | self.consume_while(char::is_whitespace); 233 | } 234 | 235 | /// Consume characters until `test` returns false. 236 | fn consume_while(&mut self, test: impl Fn(char) -> bool) -> String { 237 | let mut result = String::new(); 238 | while !self.eof() && test(self.next_char()) { 239 | result.push(self.consume_char()); 240 | } 241 | result 242 | } 243 | 244 | /// Return the current character, and advance self.pos to the next character. 245 | fn consume_char(&mut self) -> char { 246 | let c = self.next_char(); 247 | self.pos += c.len_utf8(); 248 | c 249 | } 250 | 251 | /// If the exact string `s` is found at the current position, consume it. 252 | /// Otherwise, panic. 253 | fn expect_char(&mut self, c: char) { 254 | if self.consume_char() != c { 255 | panic!("Expected {:?} at byte {} but it was not found", c, self.pos); 256 | } 257 | } 258 | 259 | /// Read the current character without consuming it. 260 | fn next_char(&self) -> char { 261 | self.input[self.pos..].chars().next().unwrap() 262 | } 263 | 264 | /// Return true if all input is consumed. 265 | fn eof(&self) -> bool { 266 | self.pos >= self.input.len() 267 | } 268 | } 269 | 270 | fn valid_identifier_char(c: char) -> bool { 271 | // TODO: Include U+00A0 and higher. 272 | matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_') 273 | } 274 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | //! Basic CSS block layout. 2 | 3 | use crate::style::{StyledNode, Display}; 4 | use crate::css::{Value::{Keyword, Length}, Unit::Px}; 5 | use std::default::Default; 6 | 7 | pub use self::BoxType::{AnonymousBlock, InlineNode, BlockNode}; 8 | 9 | // CSS box model. All sizes are in px. 10 | 11 | #[derive(Clone, Copy, Default, Debug)] 12 | pub struct Rect { 13 | pub x: f32, 14 | pub y: f32, 15 | pub width: f32, 16 | pub height: f32, 17 | } 18 | 19 | #[derive(Clone, Copy, Default, Debug)] 20 | pub struct Dimensions { 21 | /// Position of the content area relative to the document origin: 22 | pub content: Rect, 23 | // Surrounding edges: 24 | pub padding: EdgeSizes, 25 | pub border: EdgeSizes, 26 | pub margin: EdgeSizes, 27 | } 28 | 29 | #[derive(Clone, Copy, Default, Debug)] 30 | pub struct EdgeSizes { 31 | pub left: f32, 32 | pub right: f32, 33 | pub top: f32, 34 | pub bottom: f32, 35 | } 36 | 37 | /// A node in the layout tree. 38 | pub struct LayoutBox<'a> { 39 | pub dimensions: Dimensions, 40 | pub box_type: BoxType<'a>, 41 | pub children: Vec>, 42 | } 43 | 44 | pub enum BoxType<'a> { 45 | BlockNode(&'a StyledNode<'a>), 46 | InlineNode(&'a StyledNode<'a>), 47 | AnonymousBlock, 48 | } 49 | 50 | impl<'a> LayoutBox<'a> { 51 | fn new(box_type: BoxType) -> LayoutBox { 52 | LayoutBox { 53 | box_type, 54 | dimensions: Default::default(), // initially set all fields to 0.0 55 | children: Vec::new(), 56 | } 57 | } 58 | 59 | fn get_style_node(&self) -> &'a StyledNode<'a> { 60 | match self.box_type { 61 | BlockNode(node) | InlineNode(node) => node, 62 | AnonymousBlock => panic!("Anonymous block box has no style node") 63 | } 64 | } 65 | } 66 | 67 | /// Transform a style tree into a layout tree. 68 | pub fn layout_tree<'a>(node: &'a StyledNode<'a>, mut containing_block: Dimensions) -> LayoutBox<'a> { 69 | // The layout algorithm expects the container height to start at 0. 70 | // TODO: Save the initial containing block height, for calculating percent heights. 71 | containing_block.content.height = 0.0; 72 | 73 | let mut root_box = build_layout_tree(node); 74 | root_box.layout(containing_block); 75 | root_box 76 | } 77 | 78 | /// Build the tree of LayoutBoxes, but don't perform any layout calculations yet. 79 | fn build_layout_tree<'a>(style_node: &'a StyledNode<'a>) -> LayoutBox<'a> { 80 | // Create the root box. 81 | let mut root = LayoutBox::new(match style_node.display() { 82 | Display::Block => BlockNode(style_node), 83 | Display::Inline => InlineNode(style_node), 84 | Display::None => panic!("Root node has display: none.") 85 | }); 86 | 87 | // Create the descendant boxes. 88 | for child in &style_node.children { 89 | match child.display() { 90 | Display::Block => root.children.push(build_layout_tree(child)), 91 | Display::Inline => root.get_inline_container().children.push(build_layout_tree(child)), 92 | Display::None => {} // Don't lay out nodes with `display: none;` 93 | } 94 | } 95 | root 96 | } 97 | 98 | impl LayoutBox<'_> { 99 | /// Lay out a box and its descendants. 100 | fn layout(&mut self, containing_block: Dimensions) { 101 | match self.box_type { 102 | BlockNode(_) => self.layout_block(containing_block), 103 | InlineNode(_) | AnonymousBlock => {} // TODO 104 | } 105 | } 106 | 107 | /// Lay out a block-level element and its descendants. 108 | fn layout_block(&mut self, containing_block: Dimensions) { 109 | // Child width can depend on parent width, so we need to calculate this box's width before 110 | // laying out its children. 111 | self.calculate_block_width(containing_block); 112 | 113 | // Determine where the box is located within its container. 114 | self.calculate_block_position(containing_block); 115 | 116 | // Recursively lay out the children of this box. 117 | self.layout_block_children(); 118 | 119 | // Parent height can depend on child height, so `calculate_height` must be called after the 120 | // children are laid out. 121 | self.calculate_block_height(); 122 | } 123 | 124 | /// Calculate the width of a block-level non-replaced element in normal flow. 125 | /// 126 | /// http://www.w3.org/TR/CSS2/visudet.html#blockwidth 127 | /// 128 | /// Sets the horizontal margin/padding/border dimensions, and the `width`. 129 | fn calculate_block_width(&mut self, containing_block: Dimensions) { 130 | let style = self.get_style_node(); 131 | 132 | // `width` has initial value `auto`. 133 | let auto = Keyword("auto".to_string()); 134 | let mut width = style.value("width").unwrap_or(auto.clone()); 135 | 136 | // margin, border, and padding have initial value 0. 137 | let zero = Length(0.0, Px); 138 | 139 | let mut margin_left = style.lookup("margin-left", "margin", &zero); 140 | let mut margin_right = style.lookup("margin-right", "margin", &zero); 141 | 142 | let border_left = style.lookup("border-left-width", "border-width", &zero); 143 | let border_right = style.lookup("border-right-width", "border-width", &zero); 144 | 145 | let padding_left = style.lookup("padding-left", "padding", &zero); 146 | let padding_right = style.lookup("padding-right", "padding", &zero); 147 | 148 | let total = sum([&margin_left, &margin_right, &border_left, &border_right, 149 | &padding_left, &padding_right, &width].iter().map(|v| v.to_px())); 150 | 151 | // If width is not auto and the total is wider than the container, treat auto margins as 0. 152 | if width != auto && total > containing_block.content.width { 153 | if margin_left == auto { 154 | margin_left = Length(0.0, Px); 155 | } 156 | if margin_right == auto { 157 | margin_right = Length(0.0, Px); 158 | } 159 | } 160 | 161 | // Adjust used values so that the above sum equals `containing_block.width`. 162 | // Each arm of the `match` should increase the total width by exactly `underflow`, 163 | // and afterward all values should be absolute lengths in px. 164 | let underflow = containing_block.content.width - total; 165 | 166 | match (width == auto, margin_left == auto, margin_right == auto) { 167 | // If the values are overconstrained, calculate margin_right. 168 | (false, false, false) => { 169 | margin_right = Length(margin_right.to_px() + underflow, Px); 170 | } 171 | 172 | // If exactly one size is auto, its used value follows from the equality. 173 | (false, false, true) => { margin_right = Length(underflow, Px); } 174 | (false, true, false) => { margin_left = Length(underflow, Px); } 175 | 176 | // If width is set to auto, any other auto values become 0. 177 | (true, _, _) => { 178 | if margin_left == auto { margin_left = Length(0.0, Px); } 179 | if margin_right == auto { margin_right = Length(0.0, Px); } 180 | 181 | if underflow >= 0.0 { 182 | // Expand width to fill the underflow. 183 | width = Length(underflow, Px); 184 | } else { 185 | // Width can't be negative. Adjust the right margin instead. 186 | width = Length(0.0, Px); 187 | margin_right = Length(margin_right.to_px() + underflow, Px); 188 | } 189 | } 190 | 191 | // If margin-left and margin-right are both auto, their used values are equal. 192 | (false, true, true) => { 193 | margin_left = Length(underflow / 2.0, Px); 194 | margin_right = Length(underflow / 2.0, Px); 195 | } 196 | } 197 | 198 | let d = &mut self.dimensions; 199 | d.content.width = width.to_px(); 200 | 201 | d.padding.left = padding_left.to_px(); 202 | d.padding.right = padding_right.to_px(); 203 | 204 | d.border.left = border_left.to_px(); 205 | d.border.right = border_right.to_px(); 206 | 207 | d.margin.left = margin_left.to_px(); 208 | d.margin.right = margin_right.to_px(); 209 | } 210 | 211 | /// Finish calculating the block's edge sizes, and position it within its containing block. 212 | /// 213 | /// http://www.w3.org/TR/CSS2/visudet.html#normal-block 214 | /// 215 | /// Sets the vertical margin/padding/border dimensions, and the `x`, `y` values. 216 | fn calculate_block_position(&mut self, containing_block: Dimensions) { 217 | let style = self.get_style_node(); 218 | let d = &mut self.dimensions; 219 | 220 | // margin, border, and padding have initial value 0. 221 | let zero = Length(0.0, Px); 222 | 223 | // If margin-top or margin-bottom is `auto`, the used value is zero. 224 | d.margin.top = style.lookup("margin-top", "margin", &zero).to_px(); 225 | d.margin.bottom = style.lookup("margin-bottom", "margin", &zero).to_px(); 226 | 227 | d.border.top = style.lookup("border-top-width", "border-width", &zero).to_px(); 228 | d.border.bottom = style.lookup("border-bottom-width", "border-width", &zero).to_px(); 229 | 230 | d.padding.top = style.lookup("padding-top", "padding", &zero).to_px(); 231 | d.padding.bottom = style.lookup("padding-bottom", "padding", &zero).to_px(); 232 | 233 | d.content.x = containing_block.content.x + 234 | d.margin.left + d.border.left + d.padding.left; 235 | 236 | // Position the box below all the previous boxes in the container. 237 | d.content.y = containing_block.content.height + containing_block.content.y + 238 | d.margin.top + d.border.top + d.padding.top; 239 | } 240 | 241 | /// Lay out the block's children within its content area. 242 | /// 243 | /// Sets `self.dimensions.height` to the total content height. 244 | fn layout_block_children(&mut self) { 245 | for child in &mut self.children { 246 | child.layout(self.dimensions); 247 | // Increment the height so each child is laid out below the previous one. 248 | self.dimensions.content.height += child.dimensions.margin_box().height; 249 | } 250 | } 251 | 252 | /// Height of a block-level non-replaced element in normal flow with overflow visible. 253 | fn calculate_block_height(&mut self) { 254 | // If the height is set to an explicit length, use that exact length. 255 | // Otherwise, just keep the value set by `layout_block_children`. 256 | if let Some(Length(h, Px)) = self.get_style_node().value("height") { 257 | self.dimensions.content.height = h; 258 | } 259 | } 260 | 261 | /// Where a new inline child should go. 262 | fn get_inline_container(&mut self) -> &mut Self { 263 | match self.box_type { 264 | InlineNode(_) | AnonymousBlock => self, 265 | BlockNode(_) => { 266 | // If we've just generated an anonymous block box, keep using it. 267 | // Otherwise, create a new one. 268 | match self.children.last() { 269 | Some(&LayoutBox { box_type: AnonymousBlock,..}) => {} 270 | _ => self.children.push(LayoutBox::new(AnonymousBlock)) 271 | } 272 | self.children.last_mut().unwrap() 273 | } 274 | } 275 | } 276 | } 277 | 278 | impl Rect { 279 | pub fn expanded_by(self, edge: EdgeSizes) -> Rect { 280 | Rect { 281 | x: self.x - edge.left, 282 | y: self.y - edge.top, 283 | width: self.width + edge.left + edge.right, 284 | height: self.height + edge.top + edge.bottom, 285 | } 286 | } 287 | } 288 | 289 | impl Dimensions { 290 | /// The area covered by the content area plus its padding. 291 | pub fn padding_box(self) -> Rect { 292 | self.content.expanded_by(self.padding) 293 | } 294 | /// The area covered by the content area plus padding and borders. 295 | pub fn border_box(self) -> Rect { 296 | self.padding_box().expanded_by(self.border) 297 | } 298 | /// The area covered by the content area plus padding, borders, and margin. 299 | pub fn margin_box(self) -> Rect { 300 | self.border_box().expanded_by(self.margin) 301 | } 302 | } 303 | 304 | fn sum(iter: I) -> f32 where I: Iterator { 305 | iter.fold(0., |a, b| a + b) 306 | } 307 | --------------------------------------------------------------------------------