"Let's build a browser engine!" を写経したもの
$ cargo build
$ ./target/debug/rust-toy-browser-engine --html <path_to_html> --css <path_to_css>
# Examples:
$ ./target/debug/rust-toy-browser-engine --html examples/test.html --css examples/test.css
$ ./target/debug/rust-toy-browser-engine --html examples/perf-rainbow.html --css examples/perf-rainbow.css
で output.png
に結果が出力されます。
fn consume_char()
- 1 文字進める
- Rust の文字列は UTF-8 のバイト列なので、次の文字列に進めるために単純に 1 バイト進めるだけだとマルチバイト文字列に対応できない
- ->
char_indices()
というメソッドを使うといい
// Return the current character, and advance self.pos to the next character.
fn consume_char(&mut self) -> char {
let mut iter = self.input[self.pos..].char_indices();
let (_, cur_char) = iter.next().unwrap();
let (next_pos, _) = iter.next().unwrap_or((1, ' '));
self.pos += next_pos;
return cur_char;
}
- simple selector のみ
- combinators で結合されたセレクタのチェインは対象外
- Specificity 詳細度
- スタイルのコンフリクト時、どのスタイルを override するかを決める方法
- class より id
-
DOM と CSS ルールを入力として、各ノードに適用する CSS プロパティの値を決定する処理
- Style Tree と呼ぶ <- CSSOM と同じかな?
-
Selector Matching
- simple selector しかサポートしてないので簡単
- simple selector がある要素にマッチするかどうかは、その要素だけを見ればいいので
-
🤔
fn matches_simple_selector()
if selector.tag_name.iter().any(|name| elem.tag_name != *name)
というようにイテレータが登場する理由がわからない。 tag_name
って1個のタグ名じゃないの?
- Building the Style Tree
- DOM Tree を走査し各要素にマッチする rule を検索する
- 複数見つかった場合は詳細度(specificity)の一番高いものを選ぶ
- 今回実装した CSS パーサーはセレクタを詳細度の高い順に保持している(css >
parse_selectors()
参照)ので最初の1個を選べばよい
- The Cascade
- web ページのオーナーが提供するスタイルシート:author style sheets
- それに加えて、ブラウザが提供するデフォルトのスタイル:user agent style shetts
- ユーザーがカスタムスタイルを追加する user style sheets
- 3 つの "origins" のうちどれが優先されるかをカスケードという
- 6 つのレベルがある
- このブラウザではカスケードは未実装。なので
<head>
タグとかも自分で明示的に消す必要がある - カスケードの実装はまあまあ簡単で、各ルールの origin をトラックし、declaration を詳細度に加え origin と importance でソートすればよい
- Computed Values
- Inheritance
- Style Attributes
- Part 4 で作成した Style tree を input にし、Layout tree を作る
- content area: コンテンツが描画される矩形領域(rectangular section)
- width, height, ページ内での position を持つ
- content area の周りに padding, borders, margins
- layout tree: box の集まり(collection)
- box は dimensions と子の box を持つ
- box は Block, Inline, Anonymous のいずれかのノード
- style tree を走査しながら、display の値を見て layout tree に box を追加していく
- 今回は display: none なら追加しないという実装
- block boxes のレイアウトを実装する
- headings や paragraphs など、垂直方向に stack する
- 実装するのは normal flow のみ:float や absolute、fixed positioning はスコープ外
- ある block のレイアウトは、それを含む block (containing block) の dimensions に依存する
- normal flow における block boxes の場合、単に親
- root はブラウザのウィンドウサイズ (または "viewport")
- (前 Part の復習) block の width は親に依存し、height は子に依存する
- ...ということは、widths の計算には木構造をトップダウンに走査し、heights を計算するにはボトムアップで走査する必要がある
- 子が親の width を変えることはできないので、自身の width が親の width にフィットしているかを確認する必要がある
- CSS の spec では constraints のセットで表現されている
- 計算アルゴリズム
- 要素の(margin, padding も含めた)トータルの width を計算する ->
total
- トータルの width が親(containing_block)の width を超えていた場合、
- margin_left または margin_right が auto なら 0 をセットする
- underflow = containing_block.content.width - total;
- 要素の(margin, padding も含めた)トータルの width を計算する ->
- width の計算よりはシンプル
- 要素の margin, border, padding の幅を計算し、親(containing_block)の content の x 座標から margin, border, padding の left 部分だけずらした位置が content の x 座標
- y 座標も同様だが、垂直方向にスタックする挙動にするためには、要素を追加するたびに ↓ の
containing_block.content.height
が更新されてないといけない
d.content.y = containing_block.content.height + containing_block.content.y +
d.margin.top + d.border.top + d.padding.top;
- Positioning の最後で触れた
containing_block.content.height
の更新をやっているのがlayout_block_children()
- margin collapsing (?) は未実装
- [Rust] ↓ のところで containing_block がムーブしてるって怒られないのはなぜ?と思ったら Copy トレイト実装してるからだった
fn layout_block(&mut self, containing_block: Dimensions) {
// Child width can depend on parent width, so we need to calculate
// this box's width before laying out its children.
self.calculate_block_width(containing_block);
// Determine where the box is located within its container.
self.calculate_block_position(containing_block);
- "rasterization" (日本語だとラスタライズかな)と呼ばれる処理
- ここでは矩形描画のみ
- 前 Part で作成した layout tree から、はじめに display list と呼ばれるものを作る
- display list は "円を描け" "テキスト文字を描け" のような描画に関する命令のリスト
- なぜ直接描画せず display list に一度変換するのか?
- 後々の命令で完全にカバーされる要素を検索し、取り除いて無駄な描画を排除できる
- 一部の要素しか変更されていないことを知っている場合、display list を変更したり再利用できる
- 同じ display list から異なるアウトプット(たとえばスクリーン用にピクセル、プリンタ用にベクタ)を生成できる
- display list を構築するには、基本的には LayoutBox を順に走査しながら background -> border -> content というように描画すればよい
- デフォルトでは HTML 要素は登場する順にスタックされるので、2 つの要素にオーバーラップがあれば、後者が上に描画される
z-index
がサポートされていればまた違った実装になる (display list を stacking order 順にソートする必要がある)
- [Rust]
clamp
は 1.50.0 で実装された paing_item()
は不透明な色 (opaque colors) しか対応してない。opacity
やrgba()
をサポートする場合は色をブレンドする必要がある
- main.rs を実装する
- pdf は別途実装の必要がある。スキップ
image
(https://docs.rs/image/0.23.13/image) は v0.14.0 と最新版ではインターフェースちょっと変わってるImageRgba8
はimage::DynamicImage::ImageRgba8
からsave()
に渡すのはファイルではなくパス。フォーマットも省略できる