본문으로 건너뛰기

HTML+CSS Embed Renderer

Renders HTML+CSS to a Skia Picture for opaque embedding on the canvas (HTMLEmbedNode).

Source: crates/grida-canvas/src/htmlcss/


Architecture

Three-phase pipeline inspired by Chromium's Style → Layout → Paint separation. All Skia object construction is deferred to phase 3 because Stylo's global DOM slot corrupts Skia objects built during borrow_data() traversal.

Phase 1: Collect (collect.rs)     Phase 2: Layout (layout.rs)     Phase 3: Paint (paint.rs)
┌──────────────────────┐ tree ┌──────────────────────┐ tree ┌──────────────────────┐
│ csscascade (Stylo) │ ──────► │ Taffy (block/flex/ │ ────► │ Skia PictureRecorder │
│ ComputedValues → │ Styled │ grid layout) │ Layout │ Canvas draw ops │
│ StyledElement tree │ Element │ + MeasureFunc for │ Box │ Paragraph, Paint, │
│ (plain Rust, no Skia)│ │ text (Skia Para) │ │ RRect, Shader │
└──────────────────────┘ └──────────────────────┘ └──────────────────────┘

Module structure:

FilePurpose
mod.rsPublic API: render(), measure_content_height()
types.rsCSS-specific enums (Display, Position, Overflow, FlexDirection, ...)
style.rsStyledElement IR, reusing cg primitives where aligned
collect.rsStylo DOM → StyledElement tree (no Skia objects)
layout.rsStyledElement → Taffy → LayoutBox tree (positioned)
paint.rsLayoutBox → Skia Picture (backgrounds, borders, text, gradients)

CG type reuse

Types from cg::prelude reused where they 100% align with CSS semantics:

cg typeCSS property
CGColorcolor, background-color, border-color
EdgeInsetspadding (resolved px)
BlendModemix-blend-mode
TextAligntext-align
FontWeightfont-weight
TextTransformtext-transform
TextDecorationStyletext-decoration-style

CSS Property Support

Status key: ✅ supported | ⚠️ partial | ❌ not yet

Display & Layout

CSS PropertyStatusNotes
display: blockVia Taffy Display::Block with margin collapsing
display: inlineMerged into parent's Paragraph as InlineRunItem
display: noneSubtree skipped
display: flexVia Taffy — direction, wrap, align, justify, gap
display: gridVia Taffy Display::Grid
display: list-itemMarker text generated (bullet/number)
display: table⚠️Falls back to block flow (no column grid)
display: inline-block⚠️Treated as inline

Box Model

CSS PropertyStatusNotes
width, heightpx and auto
min-width, max-widthVia Taffy
padding (all sides)px values
margin (all sides)px, auto; collapsing via Taffy block flow
border-width/color/styleAll sides; solid/dashed/dotted
border-radiusPer-corner elliptical (separate rx/ry)
box-sizingVia Taffy

Background

CSS PropertyStatusNotes
background-colorSolid color with border-radius
linear-gradient()All directions + angles, multi-stop
radial-gradient()Circle/ellipse
conic-gradient()Sweep gradient
Multi-layer backgroundsStacked gradient + solid layers
background-image: url()

Text & Font

CSS PropertyStatusNotes
colorInherited
font-sizeComputed px
font-weight100–900
font-style (italic)
font-familyGeneric families mapped to platform names
line-heightnormal, number, length
letter-spacing, word-spacing
text-alignleft, right, center, justify
text-transformuppercase, lowercase, capitalize
text-decorationunderline, line-through, overline (bitfield — simultaneous)
white-spacenormal, pre, pre-wrap, pre-line, nowrap

Inline Elements

FeatureStatusNotes
<strong>, <em>, <b>, <i>Bold/italic via font properties
<u>, <ins>Underline decoration
<s>, <del>, <strike>Line-through decoration
<small>Smaller font size
<code>, <kbd>, <mark>Background, border, border-radius, padding via InlineBoxDecoration
Inline box padding as layout spaceSkia placeholders at OpenBox/CloseBox boundaries
Text wrapping with inline boxesTaffy MeasureFunc with Skia Paragraph

Lists

FeatureStatusNotes
<ul> with disc/circle/squareMarker text prepended to list item content
<ol> with decimal numberingAuto-incrementing counter
lower-alpha, upper-alpha
lower-roman, upper-romanStylo servo-mode limitation (servo/stylo#349)
list-style-type: none
Nested listsIndependent counters per list

Visual Effects

CSS PropertyStatusNotes
opacityVia canvas save_layer
visibilityhidden/collapse skips painting
overflowhidden/clip via canvas clip_rect
box-shadow (outer)blur, spread, offset, border-radius
mix-blend-modeAll CSS blend modes

Positioning

CSS PropertyStatusNotes
position: staticDefault
position: relativeVia Taffy
position: absoluteVia Taffy
z-index⚠️Stored but not used for paint order

Not Yet Supported

CategoryProperties
Background imagesbackground-image: url(), background-position, background-size
Transformtransform, transform-origin
Box shadow insetbox-shadow: inset
Table layoutdisplay: table-row, table-cell (proper grid)
Floatfloat, clear
Filterfilter, backdrop-filter
Clip/Maskclip-path, mask
Outlineoutline
Texttext-indent, text-overflow, vertical-align (sub/super)

Key Design Decisions

Subpixel layout

Taffy rounding is disabled (taffy.disable_rounding()) to match Chromium's subpixel layout precision. Text intrinsic width is ceiled to prevent subpixel-induced wrapping.

Inline box model

Follows Chromium's kOpenTag/kCloseTag model. InlineRunItem::OpenBox and CloseBox inject Skia placeholders that consume inline space matching padding + border width. Decoration rects are painted using Paragraph::get_rects_for_range().

Root margin stripping

<html> and <body> margins are zeroed in the collector since the embed container provides its own bounds. Author padding is preserved.

Whitespace collapsing

Inter-element whitespace (newlines/spaces between block elements) is detected and dropped during inline group flushing to prevent empty 24px-tall blocks.