all: can render basic things

This commit is contained in:
2017-08-28 19:22:08 +02:00
parent 3e91f8ed5b
commit 646714f0ab
9 changed files with 356 additions and 77 deletions

View File

@@ -9,3 +9,4 @@ path = "src/main.rs"
[dependencies]
getopts = "0.2.3"
image = "0.14"

View File

@@ -3,5 +3,4 @@
A browser engine written in Rust. I heard thats 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.

30
examples/test.html Normal file
View File

@@ -0,0 +1,30 @@
<html>
<head>
<style>
* { display: block; padding: 12px; }
.a { background: #ff0000; }
.b { background: #ffa500; }
.c { background: #ffff00; }
.d { background: #008000; }
.e { background: #0000ff; }
.f { background: #4b0082; }
.g { background: #800080; border-color: #808080; border-width: 3px; }
</style>
</head>
<body>
<div class="a">
<div class="b">
<div class="c">
<div class="d">
<div class="e">
<div class="f">
<div class="g">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,17 +1,20 @@
use std;
#[derive(Clone)]
pub struct SimpleSelector {
pub tag_name: Option<String>,
pub id: Option<String>,
pub class: Vec<String>,
}
#[derive(Clone)]
struct ChainSelector {
tag_name: Option<Vec<String>>,
id: Option<Vec<String>>,
class: Vec<Vec<String>>,
}
#[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<Selector>,
pub declarations: Vec<Declaration>,
@@ -136,6 +141,7 @@ impl std::fmt::Display for Rule {
}
}
#[derive(Clone)]
pub struct Stylesheet {
pub rules: Vec<Rule>,
}

133
src/display.rs Normal file
View File

@@ -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<Command>;
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<css::Color> {
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<css::Color>,
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)
}
}

View File

@@ -3,10 +3,12 @@ use std;
use css;
#[derive(Clone)]
pub struct Attr {
attrs: HashMap<String, String>,
}
#[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<Node>,
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<Node> {
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<css::Stylesheet> {
match find_node("style".to_string(), node) {
Some(n) => {
match n.ntype {
NType::Stylesheet(d) => Some(d.content),
_ => None
}
}
_ => None
}
}

View File

@@ -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<LayoutBox<'a>>,
pub struct LayoutBox<'a> {
pub dimensions: Dim,
pub btype: BoxType<'a>,
pub children: Vec<LayoutBox<'a>>,
}
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<I>(iter: I) -> f32 where I: Iterator<Item=f32> {
iter.fold(0., |a, b| a + b)
}

View File

@@ -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)
}
}

View File

@@ -8,21 +8,21 @@ type Properties = HashMap<String, css::Value>;
pub struct Node<'a> {
node: & 'a dom::Node,
values: Properties,
children: Vec<Node<'a>>,
pub children: Vec<Node<'a>>,
}
enum Display {
pub enum Display {
Inline,
Block,
None,
}
impl<'a> Node<'a> {
fn value(&self, name: &str) -> Option<css::Value> {
pub fn value(&self, name: &str) -> Option<css::Value> {
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 {