├── .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("") {
40 | break;
41 | }
42 | nodes.push(self.parse_node());
43 | }
44 | nodes
45 | }
46 |
47 | /// Parse a single node.
48 | fn parse_node(&mut self) -> 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("");
69 | self.expect(&tag_name);
70 | 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 |
--------------------------------------------------------------------------------