diff --git a/Cargo.toml b/Cargo.toml index 1778c00..c948423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ path = "src/main.rs" [dependencies] getopts = "0.2.3" +image = "0.14" diff --git a/README.md b/README.md index 4f55868..961de4b 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,4 @@ A browser engine written in Rust. I heard that’s what the cool kids do these days. -Current status: can read HTML/CSS, convert to internal representation, and back -to (unformatted) HTML/CSS. +It can currently only render a tiny subset of HTML and CSS to PNG. diff --git a/examples/test.html b/examples/test.html new file mode 100644 index 0000000..a73a95d --- /dev/null +++ b/examples/test.html @@ -0,0 +1,30 @@ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/src/css.rs b/src/css.rs index 97e1815..a4c7fc2 100644 --- a/src/css.rs +++ b/src/css.rs @@ -1,17 +1,20 @@ use std; +#[derive(Clone)] pub struct SimpleSelector { pub tag_name: Option, pub id: Option, pub class: Vec, } +#[derive(Clone)] struct ChainSelector { tag_name: Option>, id: Option>, class: Vec>, } +#[derive(Clone)] pub enum Selector { Simple(SimpleSelector), //Chain(ChainSelector), @@ -43,7 +46,7 @@ impl std::fmt::Display for Selector { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub enum Unit { Px, Em, @@ -66,15 +69,15 @@ impl std::fmt::Display for Unit { } } -#[derive(Clone)] +#[derive(Clone, Copy, PartialEq)] pub struct Color { - r: u8, - g: u8, - b: u8, - a: u8, + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub enum Value { Keyword(String), Length(f32, Unit), @@ -101,6 +104,7 @@ impl std::fmt::Display for Value { } } +#[derive(Clone)] pub struct Declaration { pub name: String, pub value: Value, @@ -112,6 +116,7 @@ impl std::fmt::Display for Declaration { } } +#[derive(Clone)] pub struct Rule { pub selectors: Vec, pub declarations: Vec, @@ -136,6 +141,7 @@ impl std::fmt::Display for Rule { } } +#[derive(Clone)] pub struct Stylesheet { pub rules: Vec, } diff --git a/src/display.rs b/src/display.rs new file mode 100644 index 0000000..21c5be4 --- /dev/null +++ b/src/display.rs @@ -0,0 +1,133 @@ +use css; +use layout; + +enum Command { + SolidR(css::Color, layout::Rect), + // Text(String, layout::Rect), + // TODO: more commands here +} + +type DisplayList = Vec; + +fn build_display_list(root: &layout::LayoutBox) -> DisplayList { + let mut list = Vec::new(); + render_layout_box(&mut list, root); + return list; +} + +fn render_layout_box(list: &mut DisplayList, lbox: &layout::LayoutBox) { + render_background(list, lbox); + render_borders(list, lbox); + + for child in &lbox.children { + render_layout_box(list, child); + } +} + +fn render_background(list: &mut DisplayList, lbox: &layout::LayoutBox) { + get_color(lbox, "background").map(|color| + list.push(Command::SolidR(color, lbox.dimensions.border_box()))); +} + +fn get_color(lbox: &layout::LayoutBox, name: &str) -> Option { + match lbox.btype { + layout::BoxType::Block(style) | layout::BoxType::Inline(style) => + match style.value(name) { + Some(css::Value::ColorValue(color)) => Some(color), + _ => None + }, + layout::BoxType::Anonymous => None + } +} + +fn render_borders(list: &mut DisplayList, lbox: &layout::LayoutBox) { + let color = match get_color(lbox, "border-color") { + Some(color) => color, + _ => return + }; + + let d = &lbox.dimensions; + let border_box = d.border_box(); + + list.push(Command::SolidR(color,layout::Rect { + x: border_box.x, + y: border_box.y, + width: d.border.left, + height: border_box.height, + })); + + list.push(Command::SolidR(color,layout::Rect { + x: border_box.x + border_box.width - d.border.right, + y: border_box.y, + width: d.border.right, + height: border_box.height, + })); + + list.push(Command::SolidR(color,layout::Rect { + x: border_box.x, + y: border_box.y, + width: border_box.width, + height: d.border.top, + })); + + list.push(Command::SolidR(color,layout::Rect { + x: border_box.x, + y: border_box.y + border_box.height - d.border.bottom, + width: border_box.width, + height: d.border.bottom, + })); +} + +pub struct Canvas { + pub pixels: Vec, + pub width: usize, + pub height: usize, +} + +impl Canvas { + fn new(width: usize, height: usize) -> Canvas { + let white = css::Color { r: 255, g: 255, b: 255, a: 255 }; + return Canvas { + pixels: vec![white; width * height], + width: width, + height: height, + } + } + + fn paint_item(&mut self, item: &Command) { + match item { + &Command::SolidR(color, rect) => { + let x0 = rect.x.clamp(0.0, self.width as f32) as usize; + let y0 = rect.y.clamp(0.0, self.height as f32) as usize; + let x1 = (rect.x + rect.width).clamp(0.0, self.width as f32) as usize; + let y1 = (rect.y + rect.height).clamp(0.0, self.height as f32) as usize; + + for y in y0 .. y1 { + for x in x0 .. x1 { + // TODO: alpha compositing with existing pixel + self.pixels[x + y * self.width] = color; + } + } + } + } + } +} + +pub fn paint(root: &layout::LayoutBox, bounds: layout::Rect) -> Canvas { + let display_list = build_display_list(root); + let mut canvas = Canvas::new(bounds.width as usize, bounds.height as usize); + for item in display_list { + canvas.paint_item(&item); + } + return canvas; +} + +trait Clamp { + fn clamp(self, lower: Self, upper: Self) -> Self; +} + +impl Clamp for f32 { + fn clamp(self, lower: f32, upper: f32) -> f32 { + self.max(lower).min(upper) + } +} diff --git a/src/dom.rs b/src/dom.rs index 24c8e5b..12aa6e2 100644 --- a/src/dom.rs +++ b/src/dom.rs @@ -3,10 +3,12 @@ use std; use css; +#[derive(Clone)] pub struct Attr { attrs: HashMap, } +#[derive(Clone)] pub struct EData { pub name: String, pub attr: Attr, @@ -25,11 +27,13 @@ impl EData { } } +#[derive(Clone)] pub struct SData { attr: Attr, content: css::Stylesheet, } +#[derive(Clone)] pub enum NType { Text(String), Comment(String), @@ -37,6 +41,7 @@ pub enum NType { Stylesheet(SData) } +#[derive(Clone)] pub struct Node { pub children: Vec, pub ntype: NType, @@ -101,3 +106,39 @@ impl std::fmt::Display for Attr { return Result::Ok(()) } } + + +pub fn find_node(name: String, node: &Node) -> Option { + match node.ntype { + NType::Text(_) | NType::Comment(_) => None, + NType::Element(ref d) => { + if d.name == name { + return Some(node.clone()); + } + for ref child in &node.children { + if let Some(n) = find_node(name.clone(), child) { + return Some(n); + } + } + return None; + }, + NType::Stylesheet(_) => + if name == "style" { + return Some(node.clone()); + } else { + return None; + } + } +} + +pub fn find_style(node: &Node) -> Option { + match find_node("style".to_string(), node) { + Some(n) => { + match n.ntype { + NType::Stylesheet(d) => Some(d.content), + _ => None + } + } + _ => None + } +} diff --git a/src/layout.rs b/src/layout.rs index d84939e..d3ef421 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,8 +1,12 @@ -struct Rect { - x: f32, - y: f32, - width: f32, - height: f32, +use css; +use styling; + +#[derive(Clone, Copy, Default)] +pub struct Rect { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, } impl Rect { @@ -16,46 +20,48 @@ impl Rect { } } -struct Edge { - left: f32, - right: f32, - top: f32, - bottom: f32, +#[derive(Clone, Copy, Default)] +pub struct Edge { + pub left: f32, + pub right: f32, + pub top: f32, + pub bottom: f32, } -impl Dimensions { - fn padding_box(self) -> Rect { +#[derive(Clone, Copy, Default)] +pub struct Dim { + pub content: Rect, + + pub padding: Edge, + pub border: Edge, + pub margin: Edge, +} + +impl Dim { + pub fn padding_box(self) -> Rect { self.content.expanded_by(self.padding) } - fn border_box(self) -> Rect { + pub fn border_box(self) -> Rect { self.padding_box().expanded_by(self.border) } - fn margin_box(self) -> Rect { + pub fn margin_box(self) -> Rect { self.border_box().expanded_by(self.margin) } } -struct Dim { - content: Rect, - - padding: Edge, - border: Edge, - margin: Edge, -} - -enum BoxType<'a> { - Block(&'a StyledNode<'a>), - Inline(&'a StyledNode<'a>), +pub enum BoxType<'a> { + Block(&'a styling::Node<'a>), + Inline(&'a styling::Node<'a>), Anonymous } -struct LayoutBox<'a> { - dimensions: Dim, - btype: BoxType<'a>, - children: Vec>, +pub struct LayoutBox<'a> { + pub dimensions: Dim, + pub btype: BoxType<'a>, + pub children: Vec>, } -impl LayoutBox { +impl<'a> LayoutBox<'a> { fn new(btype: BoxType) -> LayoutBox { LayoutBox { btype: btype, @@ -64,13 +70,20 @@ impl LayoutBox { } } - fn get_inline_container(&mut self) -> &mut LayoutBox { + fn get_style_node(&self) -> &'a styling::Node<'a> { match self.btype { - Inline(_) | Anonymous => self, - Block(_) => { + BoxType::Block(node) | BoxType::Inline(node) => node, + BoxType::Anonymous => panic!("Anonymous block box has no style node") + } + } + + fn get_inline_container(&mut self) -> &mut LayoutBox<'a> { + match self.btype { + BoxType::Inline(_) | BoxType::Anonymous => self, + BoxType::Block(_) => { match self.children.last() { - Some(&LayoutBox { btype: Anonymous,..}) => {} - _ => self.children.push(LayoutBox::new(Anonymous)) + Some(&LayoutBox { btype: BoxType::Anonymous,..}) => {} + _ => self.children.push(LayoutBox::new(BoxType::Anonymous)) } self.children.last_mut().unwrap() } @@ -79,9 +92,9 @@ impl LayoutBox { fn layout(&mut self, containing: Dim) { match self.btype { - Block(_) => self.layout_block(containing), - Inline(_) => {} // TODO - Anonymous => {} // TODO + BoxType::Block(_) => self.layout_block(containing), + BoxType::Inline(_) => {} // TODO + BoxType::Anonymous => {} // TODO } } @@ -98,10 +111,10 @@ impl LayoutBox { fn calculate_block_width(&mut self, containing: Dim) { let style = self.get_style_node(); - let auto = Keyword("auto".to_string()); + let auto = css::Value::Keyword("auto".to_string()); let mut width = style.value("width").unwrap_or(auto.clone()); - let zero = Length(0.0, Px); + let zero = css::Value::Length(0.0, css::Unit::Px); let mut margin_left = style.lookup("margin-left", "margin", &zero); let mut margin_right = style.lookup("margin-right", "margin", &zero); @@ -112,16 +125,17 @@ impl LayoutBox { let padding_left = style.lookup("padding-left", "padding", &zero); let padding_right = style.lookup("padding-right", "padding", &zero); - let total = [&margin_left, &margin_right, &border_left, &border_right, - &padding_left, &padding_right, &width - ].iter().map(|v| v.to_px()).sum(); + let total = sum([&margin_left, &margin_right, &border_left, + &border_right, &padding_left, &padding_right, + &width + ].iter().map(|v| v.to_px())); if width != auto && total > containing.content.width { if margin_left == auto { - margin_left = Length(0.0, Px); + margin_left = css::Value::Length(0.0, css::Unit::Px); } if margin_right == auto { - margin_right = Length(0.0, Px); + margin_right = css::Value::Length(0.0, css::Unit::Px); } } @@ -129,27 +143,27 @@ impl LayoutBox { match (width == auto, margin_left == auto, margin_right == auto) { (false, false, false) => { - margin_right = Length(margin_right.to_px() + underflow, Px); + margin_right = css::Value::Length(margin_right.to_px() + underflow, css::Unit::Px); } - (false, false, true) => { margin_right = Length(underflow, Px); } - (false, true, false) => { margin_left = Length(underflow, Px); } + (false, false, true) => { margin_right = css::Value::Length(underflow, css::Unit::Px); } + (false, true, false) => { margin_left = css::Value::Length(underflow, css::Unit::Px); } (true, _, _) => { - if margin_left == auto { margin_left = Length(0.0, Px); } - if margin_right == auto { margin_right = Length(0.0, Px); } + if margin_left == auto { margin_left = css::Value::Length(0.0, css::Unit::Px); } + if margin_right == auto { margin_right = css::Value::Length(0.0, css::Unit::Px); } if underflow >= 0.0 { - width = Length(underflow, Px); + width = css::Value::Length(underflow, css::Unit::Px); } else { - width = Length(0.0, Px); - margin_right = Length(margin_right.to_px() + underflow, Px); + width = css::Value::Length(0.0, css::Unit::Px); + margin_right = css::Value::Length(margin_right.to_px() + underflow, css::Unit::Px); } } (false, true, true) => { - margin_left = Length(underflow / 2.0, Px); - margin_right = Length(underflow / 2.0, Px); + margin_left = css::Value::Length(underflow / 2.0, css::Unit::Px); + margin_right = css::Value::Length(underflow / 2.0, css::Unit::Px); } } @@ -170,7 +184,7 @@ impl LayoutBox { let style = self.get_style_node(); let d = &mut self.dimensions; - let zero = Length(0.0, Px); + let zero = css::Value::Length(0.0, css::Unit::Px); d.margin.top = style.lookup("margin-top", "margin", &zero).to_px(); d.margin.bottom = style.lookup("margin-bottom", "margin", &zero).to_px(); @@ -188,6 +202,14 @@ impl LayoutBox { d.margin.top + d.border.top + d.padding.top; } + fn calculate_block_height(&mut self) { + // If the height is set to an explicit length, use that exact length. + // Otherwise, just keep the value set by `layout_block_children`. + if let Some(css::Value::Length(h, css::Unit::Px)) = self.get_style_node().value("height") { + self.dimensions.content.height = h; + } + } + fn layout_block_children(&mut self) { let d = &mut self.dimensions; for child in &mut self.children { @@ -197,19 +219,32 @@ impl LayoutBox { } } -fn build_layout_tree<'a>(style_node: &'a StyledNode<'a>) -> LayoutBox<'a> { +pub fn layout_tree<'a>(node: &'a styling::Node<'a>, mut containing: Dim) -> LayoutBox<'a> { + containing.content.height = 0.0; + + let mut root_box = build_layout_tree(node); + root_box.layout(containing); + root_box +} + + +fn build_layout_tree<'a>(style_node: &'a styling::Node<'a>) -> LayoutBox<'a> { let mut root = LayoutBox::new(match style_node.display() { - css::Display::Block => Block(style_node), - css::Display::Inline => Inline(style_node), - css::Display::DisplayNone => panic!("Root node has display: none.") + styling::Display::Block => BoxType::Block(style_node), + styling::Display::Inline => BoxType::Inline(style_node), + styling::Display::None => panic!("Root node has display: none.") }); for child in &style_node.children { match child.display() { - css::Display::Block => root.children.push(build_layout_tree(child)), - css::Display::Inline => root.get_inline_container().children.push(build_layout_tree(child)), - css::Display::DisplayNone => {} + styling::Display::Block => root.children.push(build_layout_tree(child)), + styling::Display::Inline => root.get_inline_container().children.push(build_layout_tree(child)), + styling::Display::None => {} } } return root; } + +fn sum(iter: I) -> f32 where I: Iterator { + iter.fold(0., |a, b| a + b) +} diff --git a/src/main.rs b/src/main.rs index b9a0ad5..8311d87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ extern crate getopts; +extern crate image; use std::io::Read; use std::fs::File; pub mod css; +pub mod display; pub mod dom; pub mod html; +pub mod layout; pub mod styling; fn read_source(filename: String) -> String { @@ -17,14 +20,40 @@ fn read_source(filename: String) -> String { fn main() { let mut opts = getopts::Options::new(); opts.optopt("h", "html", "HTML document", "FILENAME"); + opts.optopt("o", "out", "PNG output", "FILENAME"); let matches = opts.parse(std::env::args().skip(1)).unwrap(); let str_arg = |flag: &str, default: &str| -> String { matches.opt_str(flag).unwrap_or(default.to_string()) }; + let initial_block = layout::Dim { + content: layout::Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 }, + padding: Default::default(), + border: Default::default(), + margin: Default::default(), + }; + let html = read_source(str_arg("h", "examples/test.html")); let node = html::parse(html); + let style = dom::find_style(&node).unwrap(); + let styled_node = styling::style_tree(&node, &style); - println!("{}", node) + let layout = layout::layout_tree(&styled_node, initial_block); + let canvas = display::paint(&layout, initial_block.content); + + let filename = str_arg("o", "out.png"); + let mut file = File::create(&filename).unwrap(); + + let (w, h) = (canvas.width as u32, canvas.height as u32); + let img = image::ImageBuffer::from_fn(w, h, move |x, y| { + let color = canvas.pixels[(y * w + x) as usize]; + image::Pixel::from_channels(color.r, color.g, color.b, color.a) + }); + + let result = image::ImageRgba8(img).save(&mut file, image::PNG); + match result { + Ok(_) => println!("Saved output as {}", filename), + Err(_) => println!("Error saving output as {}", filename) + } } diff --git a/src/styling.rs b/src/styling.rs index e122b49..2aeca71 100644 --- a/src/styling.rs +++ b/src/styling.rs @@ -8,21 +8,21 @@ type Properties = HashMap; pub struct Node<'a> { node: & 'a dom::Node, values: Properties, - children: Vec>, + pub children: Vec>, } -enum Display { +pub enum Display { Inline, Block, None, } impl<'a> Node<'a> { - fn value(&self, name: &str) -> Option { + pub fn value(&self, name: &str) -> Option { self.values.get(name).map(|v| v.clone()) } - fn display(&self) -> Display { + pub fn display(&self) -> Display { match self.value("display") { Some(css::Value::Keyword(s)) => match &*s { "block" => Display::Block, @@ -32,6 +32,11 @@ impl<'a> Node<'a> { _ => Display::Inline } } + + pub fn lookup(&self, name: &str, fallback: &str, default: &css::Value) -> css::Value { + self.value(name).unwrap_or_else(|| self.value(fallback) + .unwrap_or_else(|| default.clone())) + } } fn matches(elem: &dom::EData, selector: &css::Selector) -> bool {