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 {