use crate::style::{Sides, TextOverflow, VerticalAlign, WhiteSpace};
use crate::{Length, Props, VNode};
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::rc::Rc;
use std::sync::RwLock;
pub const DEFAULT_ROOT_FONT_SIZE: f32 = 16.0;
pub const DEFAULT_TEXT_FAMILY: &str = "system-ui";
pub const DEFAULT_WHITE_SPACE: &str = "normal";
pub const DEFAULT_POSITION: &str = "static";
pub const DEFAULT_BOX_SIZING: &str = "content-box";
pub const DEFAULT_TEXT_ALIGN: &str = "left";
pub const DEFAULT_DISPLAY_BLOCK: &str = "block";
pub const DEFAULT_FLEX_DIRECTION: &str = "row";
pub const DEFAULT_FLEX_WRAP: &str = "nowrap";
pub const DEFAULT_JUSTIFY_CONTENT: &str = "flex-start";
pub const DEFAULT_ALIGN_ITEMS: &str = "stretch";
pub const DEFAULT_TRANSFORM: &str = "none";
enum InlineRunItem<'a> {
Fragment {
path: Vec<usize>,
node: &'a VNode,
font_size: f32,
font_family: String,
align: VerticalAlign,
has_text: bool,
},
Atomic {
path: Vec<usize>,
node: &'a VNode,
font_size: f32,
font_family: String,
align: VerticalAlign,
},
}
struct InlinePiece {
item: usize,
text: String,
width: i32,
ascent: f32,
descent: f32,
is_space: bool,
atomic: bool,
strut_a: f32,
strut_d: f32,
hard_break: bool,
node: Option<LayoutNode>,
}
enum InlineToken {
Word(String),
Space(String),
Break,
}
struct InlineRunResult {
cur_y: i32,
max_y_end: i32,
}
fn inline_font_size(style: Option<&str>, inherited: f32) -> f32 {
style_lookup_str(style, "font-size")
.and_then(|v| {
v.trim()
.strip_suffix("px")
.and_then(|n| n.trim().parse::<f32>().ok())
})
.unwrap_or(inherited)
}
fn inline_font_family(style: Option<&str>, inherited: &str) -> String {
style_lookup_str(style, "font-family").unwrap_or_else(|| inherited.to_string())
}
fn inline_vertical_align(style: Option<&str>, inherited: VerticalAlign) -> VerticalAlign {
style_lookup_str(style, "vertical-align")
.and_then(|v| VerticalAlign::parse(&v))
.unwrap_or(inherited)
}
fn is_inline_run_member(node: &VNode) -> bool {
match node {
VNode::Text(t) => !t.is_empty(),
VNode::Element { .. } => {
if matches!(
explicit_display(node).as_deref(),
Some("inline-flex" | "inline-grid")
) {
return false;
}
is_inline_level_box(node) && !is_out_of_flow(node)
}
}
}
fn subtree_has_text(node: &VNode) -> bool {
match node {
VNode::Text(t) => !t.is_empty(),
VNode::Element { children, .. } => children.iter().any(subtree_has_text),
}
}
fn collect_inline_run<'a>(
node: &'a VNode,
path: &mut Vec<usize>,
inherited_size: f32,
inherited_family: &str,
inherited_align: VerticalAlign,
out: &mut Vec<InlineRunItem<'a>>,
) -> bool {
match node {
VNode::Text(t) => {
if t.is_empty() {
return false;
}
let (size, family, align) = match out.last() {
Some(InlineRunItem::Fragment {
font_size,
font_family,
align,
..
}) => (*font_size, font_family.clone(), *align),
Some(InlineRunItem::Atomic {
font_size,
font_family,
align,
..
}) => (*font_size, font_family.clone(), *align),
None => (
inherited_size,
inherited_family.to_string(),
inherited_align,
),
};
out.push(InlineRunItem::Fragment {
path: path.clone(),
node,
font_size: size,
font_family: family,
align,
has_text: true,
});
true
}
VNode::Element {
props, children, ..
} => {
if !is_inline_run_member(node) {
return false;
}
let style = props.attrs.get("style").map(String::as_str);
let size = inline_font_size(style, inherited_size);
let family = inline_font_family(style, inherited_family);
let align = inline_vertical_align(style, inherited_align);
let item = usize::from(is_atomic_inline_box(node));
let boxed = family.clone();
out.push(if item == 1 {
InlineRunItem::Atomic {
path: path.clone(),
node,
font_size: size,
font_family: boxed.clone(),
align,
}
} else {
InlineRunItem::Fragment {
path: path.clone(),
node,
font_size: size,
font_family: boxed,
align,
has_text: children.iter().any(subtree_has_text),
}
});
if item == 0 {
for (i, c) in children.iter().enumerate() {
path.push(i);
collect_inline_run(c, path, size, &family, align, out);
path.pop();
}
}
true
}
}
}
fn tokenize_inline(text: &str, preserving: bool, out: &mut Vec<InlineToken>) {
let emit_space = |space: &mut String, out: &mut Vec<InlineToken>| {
if !space.is_empty() {
let t = if preserving {
space.clone()
} else {
" ".to_string()
};
out.push(InlineToken::Space(t));
space.clear();
}
};
let mut word = String::new();
let mut space = String::new();
for ch in text.chars() {
if ch == '\n' && preserving {
emit_space(&mut space, out);
if !word.is_empty() {
out.push(InlineToken::Word(std::mem::take(&mut word)));
}
out.push(InlineToken::Break);
} else if ch.is_whitespace() {
if !word.is_empty() {
out.push(InlineToken::Word(std::mem::take(&mut word)));
}
if preserving {
space.push(ch);
} else {
emit_space(&mut space, out);
out.push(InlineToken::Space(" ".to_string()));
}
} else {
emit_space(&mut space, out);
word.push(ch);
}
}
if !word.is_empty() {
out.push(InlineToken::Word(word));
}
emit_space(&mut space, out);
}
type LayoutFn = fn(
&VNode,
i32,
i32,
i32,
i32,
i32,
i32,
ContainingBlock,
Option<usize>,
f32,
f32,
) -> LayoutNode;
struct InlineContext<'a> {
at: LayoutFn,
content_x: i32,
line_limit: i32,
font_size: f32,
text_align: &'a str,
ws: WhiteSpace,
text_overflow: TextOverflow,
scale: f32,
viewport_w: i32,
viewport_h: i32,
cb: ContainingBlock,
root_font_size: f32,
}
fn inline_leaf_node(rect: Rect, source_index: usize, children: Vec<LayoutNode>) -> LayoutNode {
LayoutNode {
rect,
z_index: 0,
display_none: false,
source_index: Some(source_index),
scroll_x: 0,
scroll_y: 0,
clip: None,
stacking_context: false,
scroll_height: 0,
max_scroll_y: 0,
scrollable: false,
children,
}
}
fn lay_out_atomic(
node: &VNode,
source_index: usize,
ctx: &InlineContext<'_>,
line_width_used: i32,
y: i32,
) -> LayoutNode {
let available = (ctx.line_limit - line_width_used).max(0);
if is_replaced_element(node) {
return lay_out_atomic_at(node, source_index, available, ctx, y).0;
}
let inset = atomic_padding_and_border(node, ctx);
let probe = available
.saturating_mul(4)
.max(4096)
.saturating_add(inset)
.min(i32::MAX / 4);
let (probe_layout, _) = lay_out_atomic_at(node, source_index, probe, ctx, y);
let max_content = max_content_width(&probe_layout);
let target = max_content.min(available).saturating_add(inset);
lay_out_atomic_at(node, source_index, target, ctx, y).0
}
fn lay_out_atomic_at(
node: &VNode,
source_index: usize,
avail_w: i32,
ctx: &InlineContext<'_>,
y: i32,
) -> (LayoutNode, i32) {
let laid = (ctx.at)(
node,
ctx.content_x,
y,
avail_w,
ctx.cb.h,
ctx.viewport_w,
ctx.viewport_h,
ctx.cb,
Some(source_index),
ctx.root_font_size,
font_size_of(node, ctx),
);
(laid, avail_w)
}
fn atomic_padding_and_border(node: &VNode, ctx: &InlineContext<'_>) -> i32 {
let VNode::Element { props, .. } = node else {
return 0;
};
let style = props.attrs.get("style").map(String::as_str);
let basis = ctx.cb.w as f32;
let (vw, vh) = (ctx.viewport_w as f32, ctx.viewport_h as f32);
let fs = font_size_of(node, ctx);
let (pl, pr, _, _) =
style_box_sides_full(style, "padding", basis, fs, ctx.root_font_size, vw, vh);
let (bl, br, _, _) = style_border_widths(style, basis, fs, ctx.root_font_size, vw, vh);
pl + pr + bl + br
}
fn font_size_of(node: &VNode, ctx: &InlineContext<'_>) -> f32 {
let VNode::Element { props, .. } = node else {
return ctx.font_size;
};
let style = props.attrs.get("style").map(String::as_str);
style_lookup_font_size(
style,
ctx.font_size,
ctx.root_font_size,
(ctx.viewport_w as f32, ctx.viewport_h as f32),
)
.unwrap_or(ctx.font_size)
}
fn atomic_baseline(node: &VNode, laid: &LayoutNode) -> (f32, f32) {
let h = laid.rect.h as f32;
let asc = atomic_baseline_offset(node, laid).clamp(0.0, h);
(asc, (h - asc).max(0.0))
}
fn atomic_baseline_offset(node: &VNode, laid: &LayoutNode) -> f32 {
if !atomic_overflow_is_visible(node) {
return laid.rect.h as f32;
}
let mut last: Option<&LayoutNode> = None;
let mut stack: Vec<&LayoutNode> = laid.children.iter().collect();
while let Some(n) = stack.pop() {
if n.children.is_empty()
&& n.rect.h > 0
&& last.is_none_or(|l: &LayoutNode| n.rect.y >= l.rect.y)
{
last = Some(n);
}
stack.extend(n.children.iter());
}
let Some(leaf) = last else {
return laid.rect.h as f32;
};
let top = (leaf.rect.y - laid.rect.y) as f32;
match last_inline_leaf_below(node, inherited_font_size(node)) {
Some(InlineLeaf::Text(fs)) => top + FontMetrics::from_font_size(fs).ascent,
Some(InlineLeaf::Atomic(child)) => top + atomic_baseline_offset(child, leaf),
None => laid.rect.h as f32,
}
}
enum InlineLeaf<'a> {
Text(f32),
Atomic(&'a VNode),
}
fn last_inline_leaf_below<'a>(node: &'a VNode, inherited: f32) -> Option<InlineLeaf<'a>> {
let VNode::Element {
props, children, ..
} = node
else {
return None;
};
let style = props.attrs.get("style").map(String::as_str);
let fs = own_font_size(style, inherited);
children.iter().rev().find_map(|c| match c {
VNode::Text(_) => Some(InlineLeaf::Text(fs)),
VNode::Element { .. } if is_out_of_flow(c) => None,
VNode::Element { .. } if is_atomic_inline_box(c) => Some(InlineLeaf::Atomic(c)),
other => last_inline_leaf_below(other, fs),
})
}
fn own_font_size(style: Option<&str>, inherited: f32) -> f32 {
style_lookup_font_size(style, inherited, inherited, (0.0, 0.0)).unwrap_or(inherited)
}
fn inherited_font_size(node: &VNode) -> f32 {
let VNode::Element { props, .. } = node else {
return DEFAULT_ROOT_FONT_SIZE;
};
let style = props.attrs.get("style").map(String::as_str);
style_lookup_font_size(
style,
DEFAULT_ROOT_FONT_SIZE,
DEFAULT_ROOT_FONT_SIZE,
(0.0, 0.0),
)
.unwrap_or(DEFAULT_ROOT_FONT_SIZE)
}
fn atomic_overflow_is_visible(node: &VNode) -> bool {
let VNode::Element { props, .. } = node else {
return true;
};
match style_lookup_str(props.attrs.get("style").map(String::as_str), "overflow") {
None => true,
Some(v) => v.trim().eq_ignore_ascii_case("visible"),
}
}
fn max_content_width(root: &LayoutNode) -> i32 {
let mut left = i32::MAX;
let mut right = i32::MIN;
let mut stack: Vec<&LayoutNode> = root.children.iter().collect();
while let Some(n) = stack.pop() {
left = left.min(n.rect.x);
right = right.max(n.rect.x + n.rect.w);
stack.extend(n.children.iter());
}
if left == i32::MAX { 0 } else { right - left }
}
struct MergedRun {
item: usize,
text: String,
x: i32,
w: i32,
ascent: f32,
descent: f32,
is_atomic: bool,
y: i32,
h: i32,
node: Option<LayoutNode>,
}
#[derive(Clone)]
enum InlineSlot {
Empty,
Leaf(usize),
Node(Vec<InlineSlot>),
}
fn build_inline_slots(merged: &[MergedRun], run: &[InlineRunItem<'_>]) -> Vec<InlineSlot> {
let mut root: Vec<InlineSlot> = Vec::new();
for (mi, m) in merged.iter().enumerate() {
let path = match &run[m.item] {
InlineRunItem::Fragment { path, .. } | InlineRunItem::Atomic { path, .. } => path,
};
let Some(&last) = path.last() else {
continue;
};
let mut cur = &mut root;
for &idx in &path[..path.len() - 1] {
if cur.len() <= idx {
cur.resize(idx + 1, InlineSlot::Empty);
}
if !matches!(cur[idx], InlineSlot::Node(_)) {
cur[idx] = InlineSlot::Node(Vec::new());
}
cur = match &mut cur[idx] {
InlineSlot::Node(v) => v,
_ => unreachable!("just replaced with a Node"),
};
}
if cur.len() <= last {
cur.resize(last + 1, InlineSlot::Empty);
}
cur[last] = InlineSlot::Leaf(mi);
}
root
}
fn translate_inline_box(node: &mut LayoutNode, dx: i32, dy: i32) {
node.rect.x += dx;
node.rect.y += dy;
if let Some(clip) = node.clip.as_mut() {
clip.x += dx;
clip.y += dy;
}
for child in node.children.iter_mut() {
translate_inline_box(child, dx, dy);
}
}
fn inline_slots_to_nodes(slots: &[InlineSlot], merged: &[MergedRun]) -> Vec<LayoutNode> {
let mut out: Vec<LayoutNode> = Vec::new();
for (idx, slot) in slots.iter().enumerate() {
match slot {
InlineSlot::Empty => {}
InlineSlot::Leaf(mi) => {
let m = &merged[*mi];
if let Some(mut node) = m.node.clone() {
let (from_x, from_y) = (node.rect.x, node.rect.y);
translate_inline_box(&mut node, m.x - from_x, m.y - from_y);
out.push(node);
continue;
}
out.push(inline_leaf_node(
Rect {
x: m.x,
y: m.y,
w: m.w,
h: m.h,
},
idx,
Vec::new(),
));
}
InlineSlot::Node(kids) => {
let kids = inline_slots_to_nodes(kids, merged);
if kids.is_empty() {
continue;
}
let x = kids.iter().map(|k| k.rect.x).min().unwrap_or(0);
let right = kids.iter().map(|k| k.rect.x + k.rect.w).max().unwrap_or(x);
let top = kids.iter().map(|k| k.rect.y).min().unwrap_or(0);
let bottom = kids
.iter()
.map(|k| k.rect.y + k.rect.h)
.max()
.unwrap_or(top);
out.push(inline_leaf_node(
Rect {
x,
y: top,
w: right - x,
h: bottom - top,
},
idx,
kids,
));
}
}
}
out
}
fn truncate_line_with_ellipsis(
pieces: &mut [InlinePiece],
line: &[usize],
run: &[InlineRunItem<'_>],
ctx: &InlineContext<'_>,
) -> Option<i32> {
let text_pieces: Vec<usize> = line
.iter()
.copied()
.filter(|&pi| {
let p = &pieces[pi];
!p.atomic && !p.hard_break && !p.text.is_empty()
})
.collect();
if text_pieces.is_empty() {
return None;
}
let font_of = |item: usize| match &run[item] {
InlineRunItem::Fragment {
font_size,
font_family,
..
}
| InlineRunItem::Atomic {
font_size,
font_family,
..
} => (*font_size, font_family.as_str()),
};
let fragments: Vec<crate::text_wrap::MeasurableFragment> = text_pieces
.iter()
.map(|&pi| {
let (font_size, font_family) = font_of(pieces[pi].item);
crate::text_wrap::MeasurableFragment {
text: pieces[pi].text.clone(),
width: pieces[pi].width as f32,
font_size,
font_family: font_family.to_string(),
}
})
.collect();
let kept = crate::text_wrap::truncate_fragments_with_ellipsis(
&fragments,
ctx.line_limit as f32,
ctx.scale,
)?;
for (fi, &pi) in text_pieces.iter().enumerate() {
let (font_size, font_family) = font_of(pieces[pi].item);
let full = pieces[pi].text.chars().count();
let take = kept[fi].min(full);
let mut text: String = pieces[pi].text.chars().take(take).collect();
if take < full {
text.push_str(crate::text_wrap::ELLIPSIS);
}
let width = crate::text_wrap::measure_text_metrics(&text, font_size, font_family, ctx.scale)
.width
.round() as i32;
pieces[pi].text = text;
pieces[pi].width = width;
if take < full {
for &after in &text_pieces[fi + 1..] {
pieces[after].text = String::new();
pieces[after].width = 0;
}
break;
}
}
Some(line.iter().map(|&pi| pieces[pi].width).sum())
}
fn flush_inline_run(
run: &mut Vec<InlineRunItem<'_>>,
ctx: &InlineContext<'_>,
laid_children: &mut Vec<LayoutNode>,
cur_y: i32,
) -> InlineRunResult {
if run.is_empty() {
return InlineRunResult {
cur_y,
max_y_end: cur_y,
};
}
let run = std::mem::take(run);
let preserving = matches!(ctx.ws, WhiteSpace::Pre | WhiteSpace::PreWrap);
let wrapping = matches!(
ctx.ws,
WhiteSpace::Normal | WhiteSpace::PreWrap | WhiteSpace::PreLine
);
let mut pieces: Vec<InlinePiece> = Vec::new();
for (ii, item) in run.iter().enumerate() {
match item {
InlineRunItem::Fragment {
node: VNode::Text(t),
font_size,
font_family,
..
} => {
let leaf_strut = FontMetrics::from_font_size(*font_size);
let mut tokens = Vec::new();
tokenize_inline(t, preserving, &mut tokens);
for token in tokens {
if let InlineToken::Break = token {
pieces.push(InlinePiece {
item: ii,
text: String::new(),
width: 0,
ascent: 0.0,
descent: 0.0,
is_space: false,
atomic: false,
strut_a: 0.0,
strut_d: 0.0,
hard_break: true,
node: None,
});
continue;
}
let (text, is_space) = match token {
InlineToken::Word(w) => (w, false),
InlineToken::Space(s) => (s, true),
InlineToken::Break => unreachable!("handled above"),
};
let m = crate::text_wrap::measure_text_metrics(
&text,
*font_size,
font_family,
ctx.scale,
);
pieces.push(InlinePiece {
item: ii,
text,
width: m.width.round() as i32,
ascent: m.ascent,
descent: m.descent,
is_space,
atomic: false,
strut_a: leaf_strut.ascent,
strut_d: leaf_strut.descent,
hard_break: false,
node: None,
});
}
}
InlineRunItem::Fragment { .. } => {
pieces.push(InlinePiece {
item: ii,
text: String::new(),
width: 0,
ascent: 0.0,
descent: 0.0,
is_space: false,
atomic: false,
strut_a: 0.0,
strut_d: 0.0,
hard_break: false,
node: None,
});
}
InlineRunItem::Atomic {
node,
path,
font_family: _,
font_size,
align: _,
} => {
let strut = FontMetrics::from_font_size(*font_size);
let Some(&source_index) = path.last() else {
continue;
};
let laid = lay_out_atomic(node, source_index, ctx, 0, cur_y);
let (ascent, descent) = atomic_baseline(node, &laid);
pieces.push(InlinePiece {
item: ii,
text: String::new(),
width: laid.rect.w,
ascent,
descent,
is_space: false,
atomic: true,
strut_a: strut.ascent,
strut_d: strut.descent,
hard_break: false,
node: Some(laid),
});
}
}
}
let mut lines: Vec<Vec<usize>> = vec![Vec::new()];
let mut line_w: Vec<i32> = vec![0];
for (pi, p) in pieces.iter().enumerate() {
if p.hard_break {
if !lines.last().is_some_and(Vec::is_empty) {
lines.push(Vec::new());
line_w.push(0);
}
continue;
}
match p.is_space && !p.atomic {
true => {
if !preserving && lines.last().is_some_and(Vec::is_empty) {
continue;
}
if wrapping && line_w.last().is_some_and(|w| w + p.width > ctx.line_limit) {
lines.push(Vec::new());
line_w.push(0);
continue;
}
if let (Some(line), Some(w)) = (lines.last_mut(), line_w.last_mut()) {
line.push(pi);
*w += p.width;
}
}
false => {
let cur_empty = lines.last().is_some_and(Vec::is_empty);
if wrapping
&& !cur_empty
&& line_w.last().is_some_and(|w| w + p.width > ctx.line_limit)
{
lines.push(Vec::new());
line_w.push(0);
}
if let (Some(line), Some(w)) = (lines.last_mut(), line_w.last_mut()) {
line.push(pi);
*w += p.width;
}
}
}
}
if lines.last().is_some_and(Vec::is_empty) && lines.len() > 1 {
lines.pop();
line_w.pop();
}
if matches!(ctx.text_overflow, TextOverflow::Ellipsis) {
let targets: Vec<usize> = if wrapping {
if lines.len() == 1 && line_w.first().is_some_and(|w| *w > ctx.line_limit) {
vec![0]
} else {
Vec::new()
}
} else {
(0..lines.len())
.filter(|li| line_w[*li] > ctx.line_limit)
.collect()
};
for li in targets {
if let Some(new_w) = truncate_line_with_ellipsis(&mut pieces, &lines[li], &run, ctx) {
line_w[li] = new_w;
}
}
}
let strut = FontMetrics::from_font_size(ctx.font_size);
let align_of = |item: usize| match &run[item] {
InlineRunItem::Fragment { align, .. } | InlineRunItem::Atomic { align, .. } => *align,
};
let mut y = cur_y;
let mut max_y_end = cur_y;
for (li, line) in lines.iter().enumerate() {
let mut max_a = strut.ascent;
let mut max_d = strut.descent;
for &pi in line {
let p = &pieces[pi];
let (ink_a, ink_d) = if p.is_space && !p.atomic {
(p.strut_a, p.strut_d)
} else {
(p.ascent.max(p.strut_a), p.descent.max(p.strut_d))
};
match align_of(p.item) {
VerticalAlign::Baseline => {
max_a = max_a.max(ink_a);
max_d = max_d.max(ink_d);
}
VerticalAlign::Middle => {
let half = strut.x_height / 2.0;
let h = ink_a + ink_d;
max_a = max_a.max(h / 2.0 - half);
max_d = max_d.max(h / 2.0 + half);
}
_ => {}
}
}
let (base_a, base_d) = (max_a, max_d);
for &pi in line {
let p = &pieces[pi];
let h = if p.atomic {
p.ascent + p.descent
} else {
p.strut_a + p.strut_d
};
match align_of(p.item) {
VerticalAlign::Top => max_d = max_d.max((h - base_a).max(0.0)),
VerticalAlign::Bottom => max_a = max_a.max((h - base_d).max(0.0)),
_ => {}
}
}
let extents = crate::text_wrap::MeasuredText {
width: 0.0,
ascent: max_a,
descent: max_d,
};
let height = crate::text_wrap::line_box_height(&extents, &strut);
let mut ink = line_w[li];
if let Some(&last_pi) = line.last()
&& pieces[last_pi].is_space
&& !pieces[last_pi].atomic
{
ink -= pieces[last_pi].width;
}
let left = match ctx.text_align {
"center" => ctx.content_x + ((ctx.line_limit - ink).max(0) / 2),
"right" => ctx.content_x + (ctx.line_limit - ink).max(0),
_ => ctx.content_x,
};
let baseline_y = y as f32 + max_a;
let x_height_half = strut.x_height / 2.0;
let mut x = left;
let mut merged: Vec<MergedRun> = Vec::new();
for &pi in line {
let p = &pieces[pi];
let box_h = p.strut_a + p.strut_d;
let (top, box_h) = if p.atomic {
let ink_h = p.ascent + p.descent;
let t = match align_of(p.item) {
VerticalAlign::Baseline => baseline_y - p.ascent,
VerticalAlign::Middle => baseline_y - (ink_h / 2.0 - x_height_half),
VerticalAlign::Top => y as f32,
VerticalAlign::Bottom => y as f32 + height as f32 - ink_h,
};
(
t.round() as i32,
p.ascent.round() as i32 + p.descent.round() as i32,
)
} else {
let t = match align_of(p.item) {
VerticalAlign::Baseline => baseline_y - p.strut_a,
VerticalAlign::Middle => baseline_y - (box_h / 2.0 - x_height_half),
VerticalAlign::Top => y as f32,
VerticalAlign::Bottom => y as f32 + height as f32 - box_h,
};
(t.round() as i32, box_h.round() as i32)
};
if p.atomic {
merged.push(MergedRun {
item: p.item,
text: String::new(),
x,
w: p.width,
ascent: p.ascent,
descent: p.descent,
is_atomic: true,
y: top,
h: box_h,
node: p.node.clone(),
});
} else if let Some(last) = merged
.last_mut()
.filter(|m| m.item == p.item && !m.is_atomic)
{
last.text.push_str(&p.text);
last.w = x + p.width - last.x;
last.ascent = last.ascent.max(p.ascent);
last.descent = last.descent.max(p.descent);
} else if !p.text.is_empty() {
merged.push(MergedRun {
item: p.item,
text: p.text.clone(),
x,
w: p.width,
ascent: p.ascent,
descent: p.descent,
is_atomic: false,
y: top,
h: box_h,
node: None,
});
}
x += p.width;
}
for (ii, item) in run.iter().enumerate() {
let InlineRunItem::Fragment { has_text, .. } = item else {
continue;
};
if *has_text || merged.iter().any(|m| m.item == ii) {
continue;
}
merged.push(MergedRun {
item: ii,
text: String::new(),
x: left,
w: 0,
ascent: 0.0,
descent: 0.0,
is_atomic: false,
y,
h: height,
node: None,
});
}
merged.sort_by_key(|m| m.item);
let slots = build_inline_slots(&merged, &run);
let mut nodes = inline_slots_to_nodes(&slots, &merged);
laid_children.append(&mut nodes);
y += height;
max_y_end = max_y_end.max(y);
}
InlineRunResult {
cur_y: y,
max_y_end,
}
}
const UNCONSTRAINED_CROSS_SIZE: f32 = i32::MAX as f32;
pub struct FontMetrics {
pub char_width: f32,
pub line_height: f32,
pub ascent: f32,
pub descent: f32,
pub x_height: f32,
}
impl FontMetrics {
pub fn heuristic_vertical(font_size_px: f32) -> (f32, f32) {
(font_size_px * 0.8, font_size_px * 0.4)
}
pub fn from_font_size(font_size_px: f32) -> Self {
let char_width = font_size_px * 0.6;
let line_height = font_size_px * 1.2;
let ascent = font_size_px * 1.069;
let descent = font_size_px * 0.293;
Self {
char_width,
line_height,
ascent,
descent,
x_height: font_size_px * 0.536,
}
}
}
pub fn root_is_viewport_filling(source_index: Option<usize>) -> bool {
source_index.is_none()
}
pub fn root_is_viewport_filling_index(index: usize) -> bool {
index == 0
}
pub fn is_viewport_filling(style: Option<&str>, is_root_index: bool) -> bool {
if is_root_index {
return true;
}
let parse = |key: &str| -> Option<Length> {
let s = style?;
for decl in s.split(';') {
let d = decl.trim();
if d.is_empty() {
continue;
}
if let Some((k, v)) = d.split_once(':')
&& k.trim() == key
{
return Length::parse(v.trim());
}
}
None
};
let width = parse("width");
let height = parse("height");
let min_h = parse("min-height");
let is_100 = |l: Length| match l {
Length::Percent(v) => (v - 100.0).abs() < 0.01,
Length::Vw(v) => (v - 100.0).abs() < 0.01,
Length::Dvw(v) => (v - 100.0).abs() < 0.01,
Length::Vh(v) => (v - 100.0).abs() < 0.01,
Length::Dvh(v) => (v - 100.0).abs() < 0.01,
_ => false,
};
let has_100pct_w =
width.map(is_100).unwrap_or(false) && matches!(width, Some(Length::Percent(_)));
let has_vw = matches!(
width,
Some(Length::Vw(v)) | Some(Length::Dvw(v)) if (v - 100.0).abs() < 0.01
);
let has_viewport_h = match height {
Some(Length::Vh(v)) | Some(Length::Dvh(v)) if (v - 100.0).abs() < 0.01 => true,
Some(Length::Percent(v)) if (v - 100.0).abs() < 0.01 => true,
_ => false,
} || match min_h {
Some(Length::Vh(v)) | Some(Length::Dvh(v)) if (v - 100.0).abs() < 0.01 => true,
Some(Length::Percent(v)) if (v - 100.0).abs() < 0.01 => true,
_ => false,
};
(has_100pct_w || has_vw) && has_viewport_h
}
pub fn collapse_margins(a: f32, b: f32) -> f32 {
if a >= 0.0 && b >= 0.0 {
a.max(b)
} else if a <= 0.0 && b <= 0.0 {
a.min(b)
} else {
a + b
}
}
#[allow(dead_code)]
fn collapse(a: f32, b: f32) -> f32 {
collapse_margins(a, b)
}
fn has_definite_cross(is_column: bool, parent_definite: bool, resolved_cross: Option<i32>) -> bool {
if is_column {
parent_definite || resolved_cross.is_some()
} else {
resolved_cross.is_some()
}
}
fn content_size_for(
declared: Option<i32>,
avail: i32,
is_border_box: bool,
padding: &Sides<i32>,
margin: &Sides<i32>,
is_viewport_filling: bool,
legacy_pair: bool,
) -> i32 {
if is_border_box {
if let Some(dw) = declared {
dw
} else {
(avail - margin.left - margin.right).max(1)
}
} else if let Some(dw) = declared {
dw + padding.left + padding.right + padding.top + padding.bottom
} else if is_viewport_filling || legacy_pair {
(avail - margin.left - margin.right).max(1)
} else {
avail
}
}
#[allow(clippy::too_many_arguments)]
fn used_max_width(
style: Option<&str>,
containing_w: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> Option<i32> {
style_lookup_len_full(
style,
"max-width",
containing_w,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
)
.filter(|v| *v >= 0)
}
fn used_min_width(
style: Option<&str>,
containing_w: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> Option<i32> {
style_lookup_len_full(
style,
"min-width",
containing_w,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
)
.filter(|v| *v >= 0)
}
fn floor_to_min_width(
rect_w: i32,
min_w: Option<i32>,
is_border_box: bool,
pl: i32,
pr: i32,
bl: i32,
br: i32,
) -> i32 {
let Some(min_w) = min_w else {
return rect_w;
};
if is_border_box {
return rect_w.max(min_w);
}
let edges = pl + pr + bl + br;
(rect_w - edges).max(min_w) + edges
}
fn cap_to_max_width(
rect_w: i32,
max_w: Option<i32>,
is_border_box: bool,
pl: i32,
pr: i32,
bl: i32,
br: i32,
) -> i32 {
let Some(max_w) = max_w else {
return rect_w;
};
if is_border_box {
return rect_w.min(max_w);
}
let edges = pl + pr + bl + br;
(rect_w - edges).min(max_w).max(0) + edges
}
#[allow(clippy::too_many_arguments)]
fn clamp_width_to_max_width(
rect_w: i32,
style: Option<&str>,
containing_w: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> i32 {
let Some(max_w) = used_max_width(
style,
containing_w,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
) else {
return rect_w;
};
let is_border_box = style_lookup_str(style, "box-sizing")
.map(|s| s.trim().eq_ignore_ascii_case("border-box"))
.unwrap_or(false);
let (pl, pr, _, _) = style_box_sides_full(
style,
"padding",
containing_w,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let (bl, br, _, _) = style_border_widths(
style,
containing_w,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
cap_to_max_width(rect_w, Some(max_w), is_border_box, pl, pr, bl, br)
}
fn clamp_to_min_max(v: f32, min: Option<f32>, max: Option<f32>) -> f32 {
let floored = min.map_or(v, |lo| v.max(lo));
max.map_or(floored, |hi| floored.min(hi))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ContainingBlock {
x: i32,
y: i32,
w: i32,
h: i32,
}
fn establishes_containing_block(position: &str) -> bool {
matches!(position, "relative" | "absolute" | "fixed")
}
fn out_of_flow_containing_block(
is_fixed: bool,
descendant_cb: ContainingBlock,
viewport_w: i32,
viewport_h: i32,
) -> ContainingBlock {
if is_fixed {
ContainingBlock {
x: 0,
y: 0,
w: viewport_w,
h: viewport_h,
}
} else {
descendant_cb
}
}
struct PendingAbsolute {
style: Option<String>,
is_fixed: bool,
static_x: i32,
static_y: i32,
node: LayoutNode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LayoutNode {
pub rect: Rect,
pub z_index: i32,
pub display_none: bool,
pub source_index: Option<usize>,
pub scroll_x: i32,
pub scroll_y: i32,
pub clip: Option<Rect>,
pub stacking_context: bool,
pub scroll_height: i32,
pub max_scroll_y: i32,
pub scrollable: bool,
pub children: Vec<LayoutNode>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScrollState {
pub offset_y: f32,
pub max_y: f32,
}
impl ScrollState {
pub fn new(max_y: f32) -> Self {
Self {
offset_y: 0.0,
max_y,
}
}
pub fn scroll_by(&mut self, delta: f32) {
self.offset_y = (self.offset_y + delta).clamp(0.0, self.max_y.max(0.0));
}
pub fn on_wheel(&mut self, delta: f32) {
self.scroll_by(delta);
}
}
pub fn is_scrollable(overflow: &str, content_h: f32, rect_h: f32) -> bool {
matches!(overflow, "auto" | "scroll") && content_h > rect_h
}
fn translate_layout_subtree(node: &mut LayoutNode, dx: i32, dy: i32) {
node.rect.x += dx;
node.rect.y += dy;
if let Some(clip) = &mut node.clip {
clip.x += dx;
clip.y += dy;
}
for child in &mut node.children {
translate_layout_subtree(child, dx, dy);
}
}
fn translate_layout_descendants(node: &mut LayoutNode, dx: i32, dy: i32) {
for child in &mut node.children {
translate_layout_subtree(child, dx, dy);
}
}
#[allow(dead_code)]
fn parse_px(s: &str) -> Option<i32> {
let t = s.trim();
if let Some(px) = t.strip_suffix("px") {
px.trim().parse().ok()
} else {
t.parse().ok()
}
}
const MAX_STYLE_TABLES: usize = 4096;
const MAX_TABLE_MEMO: usize = 4096;
#[derive(Default, Clone, Copy)]
struct FxHasher {
hash: u64,
}
const FX_SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
impl FxHasher {
#[inline]
fn add(&mut self, word: u64) {
self.hash = (self.hash.rotate_left(5) ^ word).wrapping_mul(FX_SEED);
}
}
impl std::hash::Hasher for FxHasher {
#[inline]
fn write(&mut self, bytes: &[u8]) {
let mut rest = bytes;
while let Some((chunk, tail)) = rest.split_first_chunk::<8>() {
self.add(u64::from_le_bytes(*chunk));
rest = tail;
}
if let Some((chunk, tail)) = rest.split_first_chunk::<4>() {
self.add(u32::from_le_bytes(*chunk) as u64);
rest = tail;
}
for &b in rest {
self.add(b as u64);
}
}
#[inline]
fn finish(&self) -> u64 {
self.hash
}
}
#[derive(Default, Clone, Copy)]
struct FxBuild;
impl std::hash::BuildHasher for FxBuild {
type Hasher = FxHasher;
#[inline]
fn build_hasher(&self) -> FxHasher {
FxHasher::default()
}
}
#[derive(Clone, Copy)]
struct LenCtx {
parent_size: f32,
parent_font_size: f32,
root_font_size: f32,
vw: f32,
vh: f32,
}
#[derive(PartialEq, Eq, Hash)]
struct LenFullKey {
decl: u32,
parent_size: u32,
parent_font_size: u32,
root_font_size: u32,
vw: u32,
vh: u32,
}
impl LenFullKey {
#[inline]
fn new(decl: u32, ctx: &LenCtx) -> Self {
Self {
decl,
parent_size: ctx.parent_size.to_bits(),
parent_font_size: ctx.parent_font_size.to_bits(),
root_font_size: ctx.root_font_size.to_bits(),
vw: ctx.vw.to_bits(),
vh: ctx.vh.to_bits(),
}
}
}
#[derive(PartialEq, Eq, Hash)]
struct FontSizeKey {
decl: u32,
parent_font_size: u32,
root_font_size: u32,
vw: u32,
vh: u32,
}
#[derive(PartialEq, Eq, Hash)]
struct BoxSidesKey {
base: Rc<str>,
parent_size: u32,
parent_font_size: u32,
root_font_size: u32,
vw: u32,
vh: u32,
}
impl BoxSidesKey {
#[inline]
fn new(base: &str, ctx: &LenCtx) -> Self {
Self {
base: Rc::from(base),
parent_size: ctx.parent_size.to_bits(),
parent_font_size: ctx.parent_font_size.to_bits(),
root_font_size: ctx.root_font_size.to_bits(),
vw: ctx.vw.to_bits(),
vh: ctx.vh.to_bits(),
}
}
}
#[derive(PartialEq, Eq, Hash)]
struct BorderKey {
parent_size: u32,
parent_font_size: u32,
root_font_size: u32,
vw: u32,
vh: u32,
}
impl BorderKey {
#[inline]
fn new(ctx: &LenCtx) -> Self {
Self {
parent_size: ctx.parent_size.to_bits(),
parent_font_size: ctx.parent_font_size.to_bits(),
root_font_size: ctx.root_font_size.to_bits(),
vw: ctx.vw.to_bits(),
vh: ctx.vh.to_bits(),
}
}
}
struct StyleTable {
src: String,
values: Vec<(u32, u32)>,
first: HashMap<Rc<str>, u32, FxBuild>,
last: HashMap<Rc<str>, u32, FxBuild>,
inserted: u64,
font_size: RefCell<HashMap<FontSizeKey, Option<f32>, FxBuild>>,
len_full: RefCell<HashMap<LenFullKey, Option<i32>, FxBuild>>,
box_sides: RefCell<HashMap<BoxSidesKey, (i32, i32, i32, i32), FxBuild>>,
border: RefCell<HashMap<BorderKey, (i32, i32, i32, i32), FxBuild>>,
}
impl StyleTable {
fn build(s: &str) -> StyleTable {
let mut values: Vec<(u32, u32)> = Vec::new();
let mut first: HashMap<Rc<str>, u32, FxBuild> = HashMap::default();
let mut last: HashMap<Rc<str>, u32, FxBuild> = HashMap::default();
for decl in s.split(';') {
let d = decl.trim();
if d.is_empty() {
continue;
}
let Some((k, v)) = d.split_once(':') else {
continue;
};
let (k, v) = (k.trim(), v.trim());
let start = (v.as_ptr() as usize - s.as_ptr() as usize) as u32;
values.push((start, start + v.len() as u32));
let idx = (values.len() - 1) as u32;
match first.entry(Rc::from(k)) {
Entry::Occupied(e) => {
last.insert(e.key().clone(), idx);
}
Entry::Vacant(e) => {
let key: Rc<str> = e.key().clone();
e.insert(idx);
last.insert(key, idx);
}
}
}
StyleTable {
src: s.to_string(),
values,
first,
last,
inserted: 0,
font_size: RefCell::new(HashMap::default()),
len_full: RefCell::new(HashMap::default()),
box_sides: RefCell::new(HashMap::default()),
border: RefCell::new(HashMap::default()),
}
}
#[inline]
fn value(&self, i: u32) -> &str {
let (start, end) = self.values[i as usize];
&self.src[start as usize..end as usize]
}
#[inline]
fn first(&self, key: &str) -> Option<u32> {
self.first.get(key).copied()
}
#[inline]
fn last(&self, key: &str) -> Option<u32> {
self.last.get(key).copied()
}
}
struct StyleCache {
tables: HashMap<Rc<str>, Rc<StyleTable>, FxBuild>,
seq: u64,
last: Option<(Rc<str>, Rc<StyleTable>)>,
}
thread_local! {
static STYLE_TABLES: RefCell<StyleCache> = RefCell::new(StyleCache {
tables: HashMap::default(),
seq: 0,
last: None,
});
}
fn table_for(style: &str) -> Rc<StyleTable> {
STYLE_TABLES.with(|c| {
let mut cache = c.borrow_mut();
if let Some((k, t)) = &cache.last
&& &**k == style
{
return t.clone();
}
let hit = cache.tables.get(style).cloned();
if let Some(t) = hit {
cache.last = Some((Rc::from(style), t.clone()));
return t;
}
if cache.tables.len() >= MAX_STYLE_TABLES {
let mut victims: Vec<(u64, Rc<str>)> = cache
.tables
.iter()
.map(|(k, t)| (t.inserted, k.clone()))
.collect();
victims.sort_unstable_by_key(|(seq, _)| *seq);
for (_, key) in victims.into_iter().take(MAX_STYLE_TABLES / 2) {
cache.tables.remove(&key);
}
}
let seq = cache.seq.wrapping_add(1);
cache.seq = seq;
let mut built = StyleTable::build(style);
built.inserted = seq;
let table = Rc::new(built);
cache.tables.insert(Rc::from(style), table.clone());
cache.last = Some((Rc::from(style), table.clone()));
table
})
}
fn memo_put<K, V>(cell: &RefCell<HashMap<K, V, FxBuild>>, key: K, value: V)
where
K: std::hash::Hash + Eq,
{
let mut map = cell.borrow_mut();
if map.len() >= MAX_TABLE_MEMO {
map.clear();
}
map.insert(key, value);
}
#[inline]
fn side_key<'a>(buf: &'a mut [u8; 48], base: &str, side: &str) -> &'a str {
let n = base.len() + 1 + side.len();
assert!(
n <= buf.len(),
"side_key buffer too small for {base}-{side}"
);
buf[..base.len()].copy_from_slice(base.as_bytes());
buf[base.len()] = b'-';
buf[base.len() + 1..n].copy_from_slice(side.as_bytes());
std::str::from_utf8(&buf[..n]).expect("side key is built from strs")
}
#[allow(dead_code)]
fn style_lookup(style: Option<&str>, key: &str) -> Option<i32> {
let t = table_for(style?);
parse_px(t.value(t.first(key)?))
}
#[allow(dead_code)]
fn style_lookup_len(style: Option<&str>, key: &str, base: i32) -> Option<i32> {
let t = table_for(style?);
let val = t.value(t.first(key)?);
if let Some(p) = val.strip_suffix('%')
&& let Ok(pct) = p.trim().parse::<f32>()
{
return Some(((pct / 100.0) * base as f32).round() as i32);
}
parse_px(val)
}
fn style_lookup_font_size(
style: Option<&str>,
parent_font_size: f32,
root_font_size: f32,
viewport: (f32, f32),
) -> Option<f32> {
let t = table_for(style?);
let key = FontSizeKey {
decl: t.first("font-size")?,
parent_font_size: parent_font_size.to_bits(),
root_font_size: root_font_size.to_bits(),
vw: viewport.0.to_bits(),
vh: viewport.1.to_bits(),
};
if let Some(hit) = t.font_size.borrow().get(&key).copied() {
return hit;
}
let out = parse_length_value(
t.value(key.decl),
parent_font_size,
parent_font_size,
root_font_size,
viewport,
);
memo_put(&t.font_size, key, out);
out
}
fn style_lookup_len_full(
style: Option<&str>,
key: &str,
parent_size: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> Option<i32> {
let t = table_for(style?);
len_full_in(
&t,
key,
&LenCtx {
parent_size,
parent_font_size,
root_font_size,
vw: viewport_w,
vh: viewport_h,
},
)
}
#[inline]
fn len_full_in(t: &StyleTable, key: &str, ctx: &LenCtx) -> Option<i32> {
let mkey = LenFullKey::new(t.first(key)?, ctx);
if let Some(hit) = t.len_full.borrow().get(&mkey).copied() {
return hit;
}
let out = parse_length_value(
t.value(mkey.decl),
ctx.parent_size,
ctx.parent_font_size,
ctx.root_font_size,
(ctx.vw, ctx.vh),
)
.map(|f| f.round() as i32);
memo_put(&t.len_full, mkey, out);
out
}
fn parse_length_value(
val: &str,
parent_size: f32,
parent_font_size: f32,
root_font_size: f32,
viewport: (f32, f32),
) -> Option<f32> {
let val = val.trim();
if val.eq_ignore_ascii_case("auto") {
return None;
}
if val == "0" {
return Some(0.0);
}
if let Some(p) = val.strip_suffix('%')
&& let Ok(pct) = p.trim().parse::<f32>()
{
let pct = if pct.is_finite() { pct } else { 0.0 };
return Some((pct / 100.0) * parent_size);
}
if let Some(px) = val.strip_suffix("px")
&& let Ok(v) = px.trim().parse::<f32>()
{
return Some(if v.is_finite() { v } else { 0.0 });
}
if let Some(rem) = val.strip_suffix("rem")
&& let Ok(v) = rem.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some(v * root_font_size);
}
if let Some(em) = val.strip_suffix("em")
&& let Ok(v) = em.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some(v * parent_font_size);
}
if let Some(dvw) = val.strip_suffix("dvw")
&& let Ok(v) = dvw.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some((v / 100.0) * viewport.0);
}
if let Some(dvh) = val.strip_suffix("dvh")
&& let Ok(v) = dvh.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some((v / 100.0) * viewport.1);
}
if let Some(vw) = val.strip_suffix("vw")
&& let Ok(v) = vw.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some((v / 100.0) * viewport.0);
}
if let Some(vh) = val.strip_suffix("vh")
&& let Ok(v) = vh.trim().parse::<f32>()
{
let v = if v.is_finite() { v } else { 0.0 };
return Some((v / 100.0) * viewport.1);
}
if let Ok(v) = val.parse::<f32>() {
return Some(if v.is_finite() { v } else { 0.0 });
}
None
}
fn style_lookup_str(style: Option<&str>, key: &str) -> Option<String> {
let t = table_for(style?);
Some(t.value(t.first(key)?).to_string())
}
pub const INLINE_BY_DEFAULT_TAGS: &[&str] = &[
"a", "abbr", "b", "bdi", "bdo", "cite", "code", "data", "del", "dfn", "em", "i", "img", "ins",
"kbd", "label", "mark", "meter", "output", "progress", "q", "s", "samp", "small", "span",
"strong", "sub", "sup", "time", "u", "var", "wbr",
];
fn default_display_for_tag(tag: &str) -> &'static str {
if INLINE_BY_DEFAULT_TAGS
.iter()
.any(|t| tag.eq_ignore_ascii_case(t))
{
"inline"
} else {
"block"
}
}
pub fn explicit_display(node: &VNode) -> Option<String> {
match node {
VNode::Text(_) => None,
VNode::Element { props, .. } => {
let style = props.attrs.get("style").map(String::as_str);
style_lookup_str(style, "display").map(|value| value.trim().to_ascii_lowercase())
}
}
}
pub fn is_out_of_flow(node: &VNode) -> bool {
match node {
VNode::Text(_) => false,
VNode::Element { props, .. } => {
let style = props.attrs.get("style").map(String::as_str);
style_lookup_str(style, "position")
.map(|value| value.trim().to_ascii_lowercase())
.is_some_and(|p| p == "absolute" || p == "fixed")
}
}
}
pub fn is_inline_level_box(node: &VNode) -> bool {
match node {
VNode::Text(_) => false,
VNode::Element { tag, .. } => match explicit_display(node).as_deref() {
Some("block") | Some("flex") | Some("grid") | Some("flow-root") => false,
Some("inline") | Some("inline-block") | Some("inline-flex") | Some("inline-grid") => {
true
}
Some(_) => false,
None => default_display_for_tag(tag) == "inline",
},
}
}
pub fn is_atomic_inline_box(node: &VNode) -> bool {
explicit_display(node).as_deref() == Some("inline-block") || is_replaced_element(node)
}
pub type IntrinsicSizeProbe = fn(src: &str) -> Option<(i32, i32)>;
static INTRINSIC_SIZE_PROBE: RwLock<Option<IntrinsicSizeProbe>> = RwLock::new(None);
pub fn set_intrinsic_size_probe(f: IntrinsicSizeProbe) {
if let Ok(mut g) = INTRINSIC_SIZE_PROBE.write() {
*g = Some(f);
}
}
fn intrinsic_size_of(props: &Props) -> Option<(i32, i32)> {
let src = props.attrs.get("src")?;
let (w, h) = INTRINSIC_SIZE_PROBE.read().ok().and_then(|g| *g)?(src)?;
(w > 0 && h > 0).then_some((w, h))
}
pub fn is_replaced_element(node: &VNode) -> bool {
match node {
VNode::Text(_) => false,
VNode::Element { props, .. } => props.attrs.contains_key("src"),
}
}
fn replaced_used_size(
props: &Props,
attr: &str,
intrinsic: Option<i32>,
basis: f32,
parent_font_size: f32,
root_font_size: f32,
viewport: (f32, f32),
) -> i32 {
let hinted = props.attrs.get(attr).and_then(|raw| {
is_presentational_dimension(raw)
.then(|| parse_length_value(raw, basis, parent_font_size, root_font_size, viewport))?
});
hinted
.filter(|v| *v >= 0.0)
.map(|v| v.round() as i32)
.or(intrinsic)
.unwrap_or(0)
}
fn is_presentational_dimension(raw: &str) -> bool {
let raw = raw.trim();
let number = raw
.strip_suffix("px")
.or_else(|| raw.strip_suffix('%'))
.unwrap_or(raw)
.trim();
!number.is_empty() && number.chars().all(|c| c.is_ascii_digit() || c == '.')
}
fn is_inline_formatting_participant(node: &VNode) -> bool {
match node {
VNode::Text(text) => !text.chars().all(|c| c.is_whitespace()),
VNode::Element { .. } => is_inline_level_box(node) && !is_out_of_flow(node),
}
}
fn is_formatting_participant(node: &VNode) -> bool {
match node {
VNode::Text(text) => !text.chars().all(|c| c.is_whitespace()),
VNode::Element { .. } => {
explicit_display(node).as_deref() != Some("none") && !is_out_of_flow(node)
}
}
}
fn should_drop_collapsible_whitespace(
children: &[VNode],
idx: usize,
parent_style: Option<&str>,
) -> bool {
let Some(VNode::Text(text)) = children.get(idx) else {
return false;
};
if !text.chars().all(|c| c.is_whitespace()) {
return false;
}
let white_space = style_lookup_str(parent_style, "white-space")
.map(|value| value.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_WHITE_SPACE.to_string());
if !matches!(white_space.as_str(), "normal" | "nowrap") {
return false;
}
let has_inline_before = children[..idx]
.iter()
.rev()
.find(|candidate| is_formatting_participant(candidate))
.is_some_and(is_inline_formatting_participant);
let has_inline_after = children[idx + 1..]
.iter()
.find(|candidate| is_formatting_participant(candidate))
.is_some_and(is_inline_formatting_participant);
!has_inline_before || !has_inline_after
}
fn style_lookup_i32(style: Option<&str>, key: &str) -> Option<i32> {
let t = table_for(style?);
t.value(t.first(key)?).parse::<i32>().ok()
}
#[allow(dead_code)]
fn style_box_sides(style: Option<&str>, base: &str) -> (i32, i32, i32, i32) {
let Some(t) = style.map(table_for) else {
return (0, 0, 0, 0);
};
let get = |k: &str| -> Option<i32> { parse_px(t.value(t.first(k)?)) };
let all = get(base).unwrap_or(0);
let mut buf = [0u8; 48];
let l = get(side_key(&mut buf, base, "left")).unwrap_or(all);
let r = get(side_key(&mut buf, base, "right")).unwrap_or(all);
let t_ = get(side_key(&mut buf, base, "top")).unwrap_or(all);
let b = get(side_key(&mut buf, base, "bottom")).unwrap_or(all);
(l, r, t_, b)
}
fn relative_offset_delta(
style: Option<&str>,
base_w: i32,
base_h: i32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> (i32, i32) {
let pos = style_lookup_str(style, "position").unwrap_or_else(|| DEFAULT_POSITION.to_string());
if !pos.trim().eq_ignore_ascii_case("relative") && !pos.trim().eq_ignore_ascii_case("sticky") {
return (0, 0);
}
let axis_delta = |near: &str, far: &str, base: i32| -> i32 {
if let Some(v) = style_lookup_len_full(
style,
near,
base as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
) {
v
} else if let Some(v) = style_lookup_len_full(
style,
far,
base as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
) {
-v
} else {
0
}
};
(
axis_delta("left", "right", base_w),
axis_delta("top", "bottom", base_h),
)
}
fn apply_relative_position(
style: Option<&str>,
node: &mut LayoutNode,
base_w: i32,
base_h: i32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) {
let pos = style_lookup_str(style, "position").unwrap_or_else(|| DEFAULT_POSITION.to_string());
if !pos.trim().eq_ignore_ascii_case("relative") && !pos.trim().eq_ignore_ascii_case("sticky") {
return;
}
let (dx, dy) = relative_offset_delta(
style,
base_w,
base_h,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
node.rect.x += dx;
node.rect.y += dy;
}
#[allow(clippy::too_many_arguments)]
fn apply_sticky_position(
style: Option<&str>,
node: &mut LayoutNode,
container_x: i32,
container_y: i32,
container_w: i32,
container_h: i32,
scroll_offset_x: i32,
scroll_offset_y: i32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) {
let pos = style_lookup_str(style, "position").unwrap_or_else(|| DEFAULT_POSITION.to_string());
if !pos.trim().eq_ignore_ascii_case("sticky") {
return;
}
let natural_x = node.rect.x;
let natural_y = node.rect.y;
let viewport_left = scroll_offset_x;
let viewport_top = scroll_offset_y;
let viewport_right = scroll_offset_x + container_w;
let viewport_bottom = scroll_offset_y + container_h;
let sticky_left = style_lookup_len_full(
style,
"left",
container_w as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let sticky_right = style_lookup_len_full(
style,
"right",
container_w as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let mut final_x = natural_x;
if let Some(left) = sticky_left {
let sticky_x = viewport_left + left;
final_x = final_x.max(sticky_x);
}
if let Some(right) = sticky_right {
let sticky_x = viewport_right - right - node.rect.w;
final_x = final_x.min(sticky_x);
}
let sticky_top = style_lookup_len_full(
style,
"top",
container_h as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let sticky_bottom = style_lookup_len_full(
style,
"bottom",
container_h as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let mut final_y = natural_y;
if let Some(top) = sticky_top {
let sticky_y = viewport_top + top;
final_y = final_y.max(sticky_y);
}
if let Some(bottom) = sticky_bottom {
let sticky_y = viewport_bottom - bottom - node.rect.h;
final_y = final_y.min(sticky_y);
}
final_x = final_x.max(container_x);
final_x = final_x.min(container_x + container_w - node.rect.w);
final_y = final_y.max(container_y);
final_y = final_y.min(container_y + container_h - node.rect.h);
node.rect.x = final_x;
node.rect.y = final_y;
}
#[allow(clippy::too_many_arguments)]
fn apply_absolute_position(
style: Option<&str>,
node: &mut LayoutNode,
cb: ContainingBlock,
static_pos: (i32, i32),
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) {
let left = style_lookup_len_full(
style,
"left",
cb.w as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let right = style_lookup_len_full(
style,
"right",
cb.w as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let top = style_lookup_len_full(
style,
"top",
cb.h as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let bottom = style_lookup_len_full(
style,
"bottom",
cb.h as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let declared_w = style_lookup_len_full(
style,
"width",
cb.w as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let declared_h = style_lookup_len_full(
style,
"height",
cb.h as f32,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
let (pre_x, pre_y) = (node.rect.x, node.rect.y);
if declared_w.is_none()
&& let (Some(l), Some(r)) = (left, right)
{
node.rect.w = (cb.w - l - r).max(0);
}
if declared_h.is_none()
&& let (Some(t), Some(b)) = (top, bottom)
{
node.rect.h = (cb.h - t - b).max(0);
}
if let Some(l) = left {
node.rect.x = cb.x + l;
} else if let Some(r) = right {
node.rect.x = cb.x + (cb.w - r - node.rect.w);
} else {
node.rect.x = static_pos.0;
}
if let Some(t) = top {
node.rect.y = cb.y + t;
} else if let Some(b) = bottom {
node.rect.y = cb.y + (cb.h - b - node.rect.h);
} else {
node.rect.y = static_pos.1;
}
let dx = node.rect.x - pre_x;
let dy = node.rect.y - pre_y;
if dx != 0 || dy != 0 {
if let Some(clip) = node.clip.as_mut() {
clip.x += dx;
clip.y += dy;
}
translate_layout_descendants(node, dx, dy);
}
}
fn text_dimensions(t: &str, font_size_px: f32) -> (i32, i32) {
let metrics = FontMetrics::from_font_size(font_size_px);
let len = t.chars().count() as f32;
let w = if len > 0.0 {
(len * metrics.char_width).round() as i32
} else {
0
};
let h = {
let run = crate::text_wrap::measure_text_metrics(
t,
font_size_px,
DEFAULT_TEXT_FAMILY,
crate::text_wrap::current_scale(),
);
crate::text_wrap::line_box_height(&run, &metrics)
};
(w, h)
}
#[allow(dead_code)]
fn length_to_px(len: Length, parent_size: f32, root_size: f32, viewport: (f32, f32)) -> f32 {
match len {
Length::Px(v) => v,
Length::Percent(v) => parent_size * v / 100.0,
Length::Rem(v) => v * root_size,
Length::Em(v) => v * parent_size,
Length::Vw(v) => v * viewport.0 / 100.0,
Length::Vh(v) => v * viewport.1 / 100.0,
Length::Dvw(v) => v * viewport.0 / 100.0,
Length::Dvh(v) => v * viewport.1 / 100.0,
Length::Auto => 0.0,
Length::Zero => 0.0,
}
}
fn style_box_sides_full(
style: Option<&str>,
base: &str,
parent_size: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> (i32, i32, i32, i32) {
let resolve = |val: &str| -> Option<i32> {
parse_length_value(
val,
parent_size,
parent_font_size,
root_font_size,
(viewport_w, viewport_h),
)
.map(|f| f.round() as i32)
};
let resolve_side = |val: &str| -> Option<i32> {
if base == "margin" && val.trim().eq_ignore_ascii_case("auto") {
Some(0)
} else {
resolve(val)
}
};
let Some(t) = style.map(table_for) else {
return (0, 0, 0, 0);
};
let ctx = LenCtx {
parent_size,
parent_font_size,
root_font_size,
vw: viewport_w,
vh: viewport_h,
};
let memo = BoxSidesKey::new(base, &ctx);
if let Some(hit) = t.box_sides.borrow().get(&memo).copied() {
return hit;
}
let shorthand_sides: Option<(i32, i32, i32, i32)> = (|| {
let raw = t.value(t.first(base)?);
let parts: Vec<&str> = raw.split_whitespace().collect();
match parts.len() {
0 => None,
1 => resolve_side(parts[0]).map(|v| (v, v, v, v)),
2 => {
let v = resolve_side(parts[0])?;
let h = resolve_side(parts[1])?;
Some((h, h, v, v)) }
3 => {
let top = resolve_side(parts[0])?;
let h = resolve_side(parts[1])?;
let b = resolve_side(parts[2])?;
Some((h, h, top, b))
}
4 => {
let top = resolve_side(parts[0])?;
let r = resolve_side(parts[1])?;
let b = resolve_side(parts[2])?;
let l = resolve_side(parts[3])?;
Some((l, r, top, b))
}
_ => None,
}
})();
let (sh_l, sh_r, sh_t, sh_b) = shorthand_sides.unwrap_or((0, 0, 0, 0));
let mut buf = [0u8; 48];
let out = (
len_full_in(&t, side_key(&mut buf, base, "left"), &ctx).unwrap_or(sh_l),
len_full_in(&t, side_key(&mut buf, base, "right"), &ctx).unwrap_or(sh_r),
len_full_in(&t, side_key(&mut buf, base, "top"), &ctx).unwrap_or(sh_t),
len_full_in(&t, side_key(&mut buf, base, "bottom"), &ctx).unwrap_or(sh_b),
);
memo_put(&t.box_sides, memo, out);
out
}
fn style_margin_auto_sides(style: Option<&str>) -> (bool, bool) {
let Some(s) = style else {
return (false, false);
};
let t = table_for(s);
let is_auto = |tok: &str| tok.trim().eq_ignore_ascii_case("auto");
let shorthand: Option<(bool, bool)> = match t.last("margin") {
Some(i) => {
let parts: Vec<&str> = t.value(i).split_whitespace().collect();
match parts.as_slice() {
[all] => Some((is_auto(all), is_auto(all))),
[_, h] => Some((is_auto(h), is_auto(h))),
[_, h, _] => Some((is_auto(h), is_auto(h))),
[_, right, _, left] => Some((is_auto(left), is_auto(right))),
_ => None,
}
}
None => None,
};
let long_l = t.last("margin-left").map(|i| is_auto(t.value(i)));
let long_r = t.last("margin-right").map(|i| is_auto(t.value(i)));
let (sh_l, sh_r) = shorthand.unwrap_or((false, false));
(long_l.unwrap_or(sh_l), long_r.unwrap_or(sh_r))
}
fn style_border_widths(
style: Option<&str>,
parent_size: f32,
parent_font_size: f32,
root_font_size: f32,
viewport_w: f32,
viewport_h: f32,
) -> (i32, i32, i32, i32) {
let Some(t) = style.map(table_for) else {
return (0, 0, 0, 0);
};
let ctx = LenCtx {
parent_size,
parent_font_size,
root_font_size,
vw: viewport_w,
vh: viewport_h,
};
let resolve = |val: &str| -> Option<i32> {
parse_length_value(
val,
parent_size,
parent_font_size,
root_font_size,
(viewport_w, viewport_h),
)
.map(|f| f.round() as i32)
};
let memo = BorderKey::new(&ctx);
if let Some(hit) = t.border.borrow().get(&memo).copied() {
return hit;
}
let mut default_border: Option<i32> = None;
if let Some(i) = t.first("border") {
let first = t.value(i).split_whitespace().next().unwrap_or("");
if let Some(v) = resolve(first) {
default_border = Some(v);
} else if first == "0" {
default_border = Some(0);
}
}
let (mut bl, mut br, mut bt, mut bb) =
default_border.map(|v| (v, v, v, v)).unwrap_or((0, 0, 0, 0));
if t.first("border-width").is_some() {
let (l, r, top, b) = style_box_sides_full(
style,
"border-width",
parent_size,
parent_font_size,
root_font_size,
viewport_w,
viewport_h,
);
bl = l;
br = r;
bt = top;
bb = b;
}
for (key, target) in [
("border-left-width", &mut bl),
("border-right-width", &mut br),
("border-top-width", &mut bt),
("border-bottom-width", &mut bb),
] {
if let Some(v) = len_full_in(&t, key, &ctx) {
*target = v;
}
}
for (key, target) in [
("border-left", &mut bl),
("border-right", &mut br),
("border-top", &mut bt),
("border-bottom", &mut bb),
] {
if let Some(i) = t.first(key) {
let first = t.value(i).split_whitespace().next().unwrap_or("");
if let Some(v) = resolve(first) {
*target = v;
}
}
}
let out = (bl, br, bt, bb);
memo_put(&t.border, memo, out);
out
}
pub fn compute_layout(node: &VNode, viewport_w: i32, viewport_h: i32) -> LayoutNode {
#[allow(clippy::too_many_arguments)]
fn at(
node: &VNode,
x: i32,
y: i32,
avail_w: i32,
avail_h: i32,
viewport_w: i32,
viewport_h: i32,
containing: ContainingBlock,
source_index: Option<usize>,
root_font_size: f32,
parent_font_size: f32,
) -> LayoutNode {
match node {
VNode::Text(t) => {
let (w, h) = text_dimensions(t, parent_font_size);
LayoutNode {
rect: Rect { x, y, w, h },
z_index: 0,
display_none: false,
source_index,
scroll_x: 0,
scroll_y: 0,
clip: None,
stacking_context: false,
scroll_height: h,
max_scroll_y: 0,
scrollable: false,
children: vec![],
}
}
VNode::Element {
tag,
props,
children,
} => {
let style = props.attrs.get("style").map(String::as_str);
let my_font_size = style_lookup_font_size(
style,
parent_font_size,
root_font_size,
(viewport_w as f32, viewport_h as f32),
)
.unwrap_or(parent_font_size);
let vw_f = viewport_w as f32;
let vh_f = viewport_h as f32;
let (ml, mr, mt, mb) = style_box_sides_full(
style,
"margin",
containing.w as f32,
parent_font_size,
root_font_size,
vw_f,
vh_f,
);
let (pl, pr, pt, pb) = style_box_sides_full(
style,
"padding",
containing.w as f32,
parent_font_size,
root_font_size,
vw_f,
vh_f,
);
let (bl, br, bt, bb) = style_border_widths(
style,
containing.w as f32,
parent_font_size,
root_font_size,
vw_f,
vh_f,
);
let box_sizing = style_lookup_str(style, "box-sizing")
.map(|s| s.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_BOX_SIZING.to_string());
let is_border_box = box_sizing.eq_ignore_ascii_case("border-box");
let is_root_tag = matches!(tag.as_str(), "body" | "html");
let is_root_index = root_is_viewport_filling(source_index);
let is_viewport_filling = is_viewport_filling(style, is_root_tag || is_root_index);
let has_viewport_height = style_lookup_len_full(
style,
"height",
vh_f,
parent_font_size,
root_font_size,
vw_f,
vh_f,
)
.map(|h| (h as f32 - vh_f).abs() < 0.5)
.unwrap_or(false);
let min_height_vh = style_lookup_len_full(
style,
"min-height",
vh_f,
parent_font_size,
root_font_size,
vw_f,
vh_f,
)
.map(|h| (h as f32 - vh_f).abs() < 0.5)
.unwrap_or(false);
let min_height_is_100pct = {
let raw = style.and_then(|s| {
for decl in s.split(';') {
let d = decl.trim();
if d.is_empty() {
continue;
}
if let Some((k, v)) = d.split_once(':')
&& k.trim().eq_ignore_ascii_case("min-height")
{
return Some(v.trim().to_string());
}
}
None
});
raw.as_deref()
.and_then(crate::Length::parse)
.map(|l| matches!(l, crate::Length::Percent(p) if (p - 100.0).abs() < 0.01))
.unwrap_or(false)
};
let elem_y = y + mt;
let replaced = is_replaced_element(node);
let intrinsic = replaced.then(|| intrinsic_size_of(props)).flatten();
let declared_w = style_lookup_len_full(
style,
"width",
avail_w as f32,
parent_font_size,
root_font_size,
vw_f,
vh_f,
)
.or_else(|| {
replaced.then(|| {
replaced_used_size(
props,
"width",
intrinsic.map(|(w, _)| w),
avail_w as f32,
parent_font_size,
root_font_size,
(vw_f, vh_f),
)
})
});
let (ml_auto, mr_auto) = style_margin_auto_sides(style);
let (ml, mr) = if let Some(dw) = declared_w.filter(|_| ml_auto || mr_auto) {
let outer_w = if is_border_box {
dw as f32
} else {
dw as f32 + pl as f32 + pr as f32 + bl as f32 + br as f32
};
let (l, r) = crate::style::resolve_auto_margins_core(
avail_w as f32,
outer_w,
ml as f32,
mr as f32,
ml_auto,
mr_auto,
);
(l.round() as i32, r.round() as i32)
} else {
(ml, mr)
};
let elem_x = x + ml;
let declared_h = style_lookup_len_full(
style,
"height",
avail_h as f32,
parent_font_size,
root_font_size,
vw_f,
vh_f,
)
.or_else(|| {
replaced.then(|| {
replaced_used_size(
props,
"height",
intrinsic.map(|(_, h)| h),
avail_h as f32,
parent_font_size,
root_font_size,
(vw_f, vh_f),
)
})
});
let has_100p_width = declared_w
.map(|w| (w as f32 - avail_w as f32).abs() < 0.5)
.unwrap_or(false);
let has_100p_height = declared_h
.map(|h| (h as f32 - avail_h as f32).abs() < 0.5)
.unwrap_or(false);
let legacy_pair = has_100p_width && has_100p_height;
let is_viewport_height = is_viewport_filling
|| legacy_pair
|| has_viewport_height
|| min_height_vh
|| min_height_is_100pct;
let padding = Sides {
top: bl,
right: pr,
bottom: br,
left: pl,
};
let margin = Sides {
top: 0,
right: mr,
bottom: 0,
left: ml,
};
let rect_w = floor_to_min_width(
cap_to_max_width(
content_size_for(
declared_w,
avail_w,
is_border_box,
&padding,
&margin,
is_viewport_filling,
legacy_pair,
),
used_max_width(
style,
containing.w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
),
is_border_box,
pl,
pr,
bl,
br,
),
used_min_width(
style,
containing.w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
),
is_border_box,
pl,
pr,
bl,
br,
);
let mut _rect_h = if let Some(dh) = declared_h {
if is_border_box {
dh
} else {
dh + pt + pb + bt + bb
}
} else if is_viewport_height {
(avail_h - mt - mb).max(1)
} else {
declared_h.unwrap_or(avail_h)
};
let content_x = elem_x + bl + pl;
let content_y_start = elem_y + bt + pt;
let content_w = (rect_w - pl - pr - bl - br).max(0);
let content_h_available = (_rect_h - pt - pb - bt - bb).max(0);
let overflow =
style_lookup_str(style, "overflow").unwrap_or_else(|| "visible".to_string());
let raw_scroll_x = style_lookup_len_full(
style,
"scroll-left",
content_w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let raw_scroll_y = style_lookup_len_full(
style,
"scroll-top",
content_h_available as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
if raw_scroll_x.is_some() || raw_scroll_y.is_some() {
static SCROLL_DEPRECATION_WARNED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
if !SCROLL_DEPRECATION_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
eprintln!(
"[velox] warning: scroll-left/scroll-top styles are deprecated; \
use overflow:auto/scroll with wheel scrolling instead"
);
}
}
let scroll_x = raw_scroll_x.unwrap_or(0);
let scroll_y = raw_scroll_y.unwrap_or(0);
let content_x_scrolled = content_x - scroll_x;
let content_y_scrolled = content_y_start - scroll_y;
let display = style_lookup_str(style, "display")
.unwrap_or_else(|| default_display_for_tag(tag).to_string());
let position = style_lookup_str(style, "position")
.unwrap_or_else(|| DEFAULT_POSITION.to_string());
let z_index = if !position.trim().eq_ignore_ascii_case("static") {
style_lookup_i32(style, "z-index").unwrap_or(0)
} else {
0
};
let opacity = style_lookup_str(style, "opacity")
.and_then(|v| {
v.parse::<f32>()
.ok()
.map(|f| if f.is_finite() { f } else { 1.0 })
})
.unwrap_or(1.0);
let transform = style_lookup_str(style, "transform")
.unwrap_or_else(|| DEFAULT_TRANSFORM.to_string());
let stacking_context = opacity < 1.0
|| (!transform.trim().eq_ignore_ascii_case("none") && !transform.is_empty())
|| !position.trim().eq_ignore_ascii_case("static");
if display.trim().eq_ignore_ascii_case("none") {
return LayoutNode {
rect: Rect {
x: elem_x,
y: elem_y,
w: 0,
h: 0,
},
z_index,
display_none: true,
source_index,
scroll_x: 0,
scroll_y: 0,
clip: None,
stacking_context: false,
scroll_height: 0,
max_scroll_y: 0,
scrollable: false,
children: vec![],
};
}
let mut laid_children: Vec<LayoutNode> = Vec::new();
let mut abs_children: Vec<PendingAbsolute> = Vec::new();
let mut max_y_end = content_y_start;
let (rel_dx, rel_dy) = relative_offset_delta(
style,
containing.w,
containing.h,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let out_of_flow_cb = |border_box_h: i32| -> ContainingBlock {
if establishes_containing_block(&position) {
ContainingBlock {
x: elem_x + bl + rel_dx,
y: elem_y + bt + rel_dy,
w: (rect_w - bl - br).max(0),
h: (border_box_h - bt - bb).max(0),
}
} else {
containing
}
};
let descendant_cb = out_of_flow_cb(_rect_h);
if display.trim().eq_ignore_ascii_case("flex") {
let flex_dir = style_lookup_str(style, "flex-direction")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_FLEX_DIRECTION.to_string());
let flex_wrap = style_lookup_str(style, "flex-wrap")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_FLEX_WRAP.to_string());
let justify_content = style_lookup_str(style, "justify-content")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_JUSTIFY_CONTENT.to_string());
let align_items = style_lookup_str(style, "align-items")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_ALIGN_ITEMS.to_string());
let row_gap = style_lookup_len_full(
style,
"row-gap",
0.0,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.or_else(|| {
style_lookup_len_full(
style,
"gap",
0.0,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
})
.unwrap_or(0);
let column_gap = style_lookup_len_full(
style,
"column-gap",
0.0,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.or_else(|| {
style_lookup_len_full(
style,
"gap",
0.0,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
})
.unwrap_or(0);
struct FlexChild<'a> {
index: usize,
node: &'a VNode,
style: Option<&'a str>,
}
let mut flex_children: Vec<FlexChild> = Vec::new();
for (idx, c) in children.iter().enumerate() {
if let VNode::Text(t) = c
&& t.chars().all(|c| c.is_whitespace())
{
continue;
}
let child_style = match c {
VNode::Element { props, .. } => {
props.attrs.get("style").map(String::as_str)
}
_ => None,
};
let child_display = style_lookup_str(child_style, "display")
.unwrap_or_else(|| DEFAULT_DISPLAY_BLOCK.to_string());
if child_display.trim().eq_ignore_ascii_case("none") {
continue;
}
let position = style_lookup_str(child_style, "position")
.unwrap_or_else(|| DEFAULT_POSITION.to_string());
if position.trim().eq_ignore_ascii_case("absolute")
|| position.trim().eq_ignore_ascii_case("fixed")
{
let is_fixed = position.trim().eq_ignore_ascii_case("fixed");
let out_cb = out_of_flow_containing_block(
is_fixed,
descendant_cb,
viewport_w,
viewport_h,
);
let child_ln = at(
c,
content_x_scrolled,
content_y_scrolled,
out_cb.w,
out_cb.h,
viewport_w,
viewport_h,
out_cb,
Some(idx),
root_font_size,
my_font_size,
);
abs_children.push(PendingAbsolute {
style: child_style.map(str::to_string),
is_fixed,
static_x: child_ln.rect.x,
static_y: child_ln.rect.y,
node: child_ln,
});
continue;
}
flex_children.push(FlexChild {
index: idx,
node: c,
style: child_style,
});
}
let mut flex_dir_val = flex_dir.clone();
let mut flex_wrap_val = flex_wrap.clone();
if let Some(flow_raw) = style_lookup_str(style, "flex-flow") {
let tokens: Vec<String> = flow_raw
.split_whitespace()
.map(|s| s.to_ascii_lowercase())
.collect();
for tok in &tokens {
match tok.as_str() {
"row" | "row-reverse" | "column" | "column-reverse" => {
flex_dir_val = tok.clone();
}
"nowrap" | "wrap" | "wrap-reverse" => {
flex_wrap_val = tok.clone();
}
_ => {}
}
}
}
let has_dir = style.is_some_and(|s| {
s.split(';').any(|decl| {
if let Some((k, _)) = decl.split_once(':') {
k.trim().eq_ignore_ascii_case("flex-direction")
} else {
false
}
})
});
let has_wrap = style.is_some_and(|s| {
s.split(';').any(|decl| {
if let Some((k, _)) = decl.split_once(':') {
k.trim().eq_ignore_ascii_case("flex-wrap")
} else {
false
}
})
});
if has_dir {
flex_dir_val = flex_dir.clone();
}
if has_wrap {
flex_wrap_val = flex_wrap.clone();
}
let is_column = flex_dir_val.eq_ignore_ascii_case("column")
|| flex_dir_val.eq_ignore_ascii_case("column-reverse");
let is_reverse = flex_dir_val.eq_ignore_ascii_case("row-reverse")
|| flex_dir_val.eq_ignore_ascii_case("column-reverse");
let is_wrap = flex_wrap_val.eq_ignore_ascii_case("wrap")
|| flex_wrap_val.eq_ignore_ascii_case("wrap-reverse");
let wrap_reverse = flex_wrap_val.eq_ignore_ascii_case("wrap-reverse");
let align_content = style_lookup_str(style, "align-content")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_ALIGN_ITEMS.to_string());
let main_size = if is_column {
content_h_available
} else {
content_w
};
let explicit_h_raw = style_lookup_len_full(
style,
"height",
avail_h as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let explicit_h_content = explicit_h_raw.map(|v| {
if is_border_box {
(v - pt - pb - bt - bb).max(0)
} else {
v
}
});
let _explicit_w = style_lookup_len_full(
style,
"width",
avail_w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let explicit_w_content = _explicit_w.map(|v| {
if is_border_box {
(v - pl - pr - bl - br).max(0)
} else {
v
}
});
let parent_definite = containing.w > 0;
let has_definite_cross_size = has_definite_cross(
is_column,
parent_definite,
if is_column {
explicit_w_content
} else {
explicit_h_content
},
);
let cross_size = if is_column {
content_w
} else if has_definite_cross_size {
explicit_h_content.unwrap_or(content_h_available).max(0)
} else {
0 };
struct FlexItem {
child_index: usize,
layout_node: Option<LayoutNode>,
flex_base_size: f32,
hypothetical_main_size: f32,
target_main_size: f32,
flex_grow: f32,
flex_shrink: f32,
align_self: String,
min_main_size: Option<f32>,
max_main_size: Option<f32>,
pre_measure_main: f32,
}
flex_children
.sort_by_key(|fc| style_lookup_i32(fc.style, "order").unwrap_or(0));
let parse_flex_shorthand =
|style: Option<&str>| -> Option<(f32, f32, Option<String>)> {
let raw = style_lookup_str(style, "flex")?;
let raw = raw.trim();
if raw.is_empty() {
return None;
}
if raw.eq_ignore_ascii_case("auto") {
Some((1.0, 1.0, None))
} else if raw.eq_ignore_ascii_case("none") {
Some((0.0, 0.0, None))
} else if raw.eq_ignore_ascii_case("initial") {
Some((0.0, 1.0, None))
} else {
let toks: Vec<&str> = raw.split_whitespace().collect();
match toks.len() {
1 => {
if let Ok(g) = toks[0].parse::<f32>() {
if g.is_finite() {
Some((g, 1.0, Some("0".to_string())))
} else {
Some((1.0, 1.0, Some(toks[0].to_string())))
}
} else {
Some((1.0, 1.0, Some(toks[0].to_string())))
}
}
2 => {
if let Ok(g) = toks[0].parse::<f32>() {
if g.is_finite() {
if let Ok(s) = toks[1].parse::<f32>() {
if s.is_finite()
&& !toks[1].contains('%')
&& !toks[1].contains("px")
&& !toks[1].contains("rem")
&& !toks[1].contains("em")
&& !toks[1].contains("vw")
&& !toks[1].contains("vh")
&& toks[1] != "auto"
{
Some((g, s, Some("0".to_string())))
} else {
Some((g, 1.0, Some(toks[1].to_string())))
}
} else {
Some((g, 1.0, Some(toks[1].to_string())))
}
} else {
None
}
} else {
None
}
}
3 => {
let g = toks[0]
.parse::<f32>()
.ok()
.filter(|v| v.is_finite())?;
let s = toks[1]
.parse::<f32>()
.ok()
.filter(|v| v.is_finite())?;
Some((g, s, Some(toks[2].to_string())))
}
_ => None,
}
}
};
let mut items: Vec<FlexItem> = Vec::new();
for fc in &flex_children {
let child_avail_main = if is_column {
content_h_available
} else {
main_size
};
let child_avail_cross = if is_column || has_definite_cross_size {
cross_size as f32
} else {
UNCONSTRAINED_CROSS_SIZE };
let (child_avail_w, child_avail_h) = if is_column {
(child_avail_cross, child_avail_main as f32)
} else {
(child_avail_main as f32, child_avail_cross)
};
let flex_basis_val = style_lookup_len_full(
fc.style,
"flex-basis",
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let explicit_main = style_lookup_len_full(
fc.style,
if is_column { "height" } else { "width" },
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let flex_shorthand = parse_flex_shorthand(fc.style);
let (flex_grow, flex_shrink, shorthand_basis): (f32, f32, Option<String>) =
if let Some((g, s, b)) = flex_shorthand.as_ref() {
(*g, *s, b.clone())
} else {
(
style_lookup_str(fc.style, "flex-grow")
.and_then(|v| {
v.parse::<f32>()
.ok()
.map(|f| if f.is_finite() { f } else { 0.0 })
})
.unwrap_or(0.0),
style_lookup_str(fc.style, "flex-shrink")
.and_then(|v| {
v.parse::<f32>()
.ok()
.map(|f| if f.is_finite() { f } else { 1.0 })
})
.unwrap_or(1.0),
None,
)
};
let shorthand_basis_px: Option<f32> =
shorthand_basis.as_deref().and_then(|raw| {
if raw.eq_ignore_ascii_case("auto") {
return None; }
if raw == "0" || raw == "0%" || raw == "0px" {
return Some(0.0);
}
parse_length_value(
raw,
main_size as f32,
my_font_size,
root_font_size,
(vw_f, vh_f),
)
});
let is_flex_one_zero = flex_shorthand.is_some()
&& flex_grow == 1.0
&& flex_shrink == 1.0
&& shorthand_basis.is_none();
let basis_is_content = shorthand_basis_px.is_none()
&& flex_basis_val.is_none()
&& explicit_main.is_none();
let ln = if basis_is_content {
let (pad_l, pad_r, pad_t, pad_b) = style_box_sides_full(
fc.style,
"padding",
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let (bor_l, bor_r, bor_t, bor_b) = style_border_widths(
fc.style,
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let inset = if is_column {
pad_t + pad_b + bor_t + bor_b
} else {
pad_l + pad_r + bor_l + bor_r
};
let keep_w = child_avail_w as i32;
let keep_h = child_avail_h as i32;
let probe = (main_size as i32)
.saturating_mul(4)
.max(4096)
.saturating_add(inset)
.min(i32::MAX / 4);
let probe_layout = at(
fc.node,
0,
0,
if is_column { keep_w } else { probe },
if is_column { probe } else { keep_h },
viewport_w,
viewport_h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: content_h_available,
},
Some(fc.index),
root_font_size,
my_font_size,
);
let target = max_content_width(&probe_layout)
.min(main_size as i32)
.saturating_add(inset)
.max(0);
at(
fc.node,
0,
0,
if is_column { keep_w } else { target },
if is_column { target } else { keep_h },
viewport_w,
viewport_h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: content_h_available,
},
Some(fc.index),
root_font_size,
my_font_size,
)
} else {
at(
fc.node,
0,
0,
child_avail_w as i32,
child_avail_h as i32,
viewport_w,
viewport_h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: content_h_available,
},
Some(fc.index),
root_font_size,
my_font_size,
)
};
let flex_basis = if let Some(v) = shorthand_basis_px {
v
} else if shorthand_basis
.as_deref()
.map(|s| s.eq_ignore_ascii_case("auto"))
.unwrap_or(false)
&& is_flex_one_zero
{
if is_column {
ln.rect.h as f32
} else {
ln.rect.w as f32
}
} else if flex_shorthand.is_some()
&& shorthand_basis.as_deref() == Some("0")
{
0.0
} else if let Some(fb) = flex_basis_val {
fb as f32
} else if let Some(exp) = explicit_main {
if flex_shorthand.is_some() && shorthand_basis == Some("0".to_string())
{
0.0
} else {
exp as f32
}
} else if let Some(raw) = flex_shorthand.as_ref().map(|_| {
style_lookup_str(fc.style, "flex")
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
}) {
let basis_token =
raw.split_whitespace().next_back().unwrap_or_default();
if matches!(
basis_token,
"auto"
| "none"
| "initial"
| "content"
| "max-content"
| "min-content"
| "fit-content"
) {
if is_column {
ln.rect.h as f32
} else {
ln.rect.w as f32
}
} else {
0.0
}
} else if flex_grow > 0.0 {
0.0
} else if is_column {
ln.rect.h as f32
} else {
ln.rect.w as f32
};
let min_key = if is_column { "min-height" } else { "min-width" };
let raw_min = style_lookup_str(fc.style, min_key);
let parsed_min = style_lookup_len_full(
fc.style,
min_key,
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.filter(|&v| v >= 0)
.map(|v| v as f32);
let flex_auto_min_main = || {
if is_column {
ln.rect.h as f32
} else {
let (pad_l, pad_r, _, _) = style_box_sides_full(
fc.style,
"padding",
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let (bor_l, bor_r, _, _) = style_border_widths(
fc.style,
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let inset = pad_l + pad_r + bor_l + bor_r;
let narrow = at(
fc.node,
0,
0,
1,
child_avail_h as i32,
viewport_w,
viewport_h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: content_h_available,
},
Some(fc.index),
root_font_size,
my_font_size,
);
max_content_width(&narrow) as f32 + inset as f32
}
};
let min_main: Option<f32> = match (raw_min.as_deref(), parsed_min) {
(None, _) => Some(flex_auto_min_main()),
(Some(raw), None) if raw.trim().eq_ignore_ascii_case("auto") => {
Some(flex_auto_min_main())
}
(Some(_), parsed) => parsed,
};
let max_main = style_lookup_len_full(
fc.style,
if is_column { "max-height" } else { "max-width" },
main_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.filter(|&v| v >= 0)
.map(|v| v as f32);
let hypothetical_main_size =
clamp_to_min_max(flex_basis, min_main, max_main);
let pre_measure_main = if is_column { ln.rect.h } else { ln.rect.w } as f32;
items.push(FlexItem {
child_index: fc.index,
layout_node: Some(ln),
flex_base_size: flex_basis,
hypothetical_main_size,
target_main_size: flex_basis,
flex_grow,
flex_shrink,
align_self: style_lookup_str(fc.style, "align-self")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| "auto".to_string()),
min_main_size: min_main,
max_main_size: max_main,
pre_measure_main,
});
}
struct FlexLine {
items: Vec<usize>, main_size: f32,
cross_size: f32,
main_positions: Vec<(usize, f32)>, }
let mut lines: Vec<FlexLine> = Vec::new();
if !is_wrap {
lines.push(FlexLine {
items: (0..items.len()).collect(),
main_size: 0.0,
cross_size: 0.0,
main_positions: Vec::new(),
});
} else {
let mut current_line = FlexLine {
items: Vec::new(),
main_size: 0.0,
cross_size: 0.0,
main_positions: Vec::new(),
};
#[allow(clippy::needless_range_loop)]
for i in 0..items.len() {
let item_size = items[i].hypothetical_main_size;
let gap_to_add = if current_line.items.is_empty() {
0.0
} else {
(if is_column { row_gap } else { column_gap }) as f32
};
if !current_line.items.is_empty()
&& current_line.main_size + gap_to_add + item_size
> main_size as f32
{
lines.push(current_line);
current_line = FlexLine {
items: vec![i],
main_size: item_size,
cross_size: 0.0,
main_positions: Vec::new(),
};
} else {
current_line.items.push(i);
current_line.main_size += item_size + gap_to_add;
}
}
if !current_line.items.is_empty() {
lines.push(current_line);
}
}
if lines.is_empty() && !items.is_empty() {
lines.push(FlexLine {
items: (0..items.len()).collect(),
main_size: 0.0,
cross_size: 0.0,
main_positions: Vec::new(),
});
}
let cross_gap = (if is_column { column_gap } else { row_gap }) as f32;
let main_start = if is_column { pt as f32 } else { pl as f32 };
if wrap_reverse {
lines.reverse();
}
let pre_lines_len = lines.len();
for line in &mut lines {
let total_gap = if line.items.len() > 1 {
(line.items.len() as i32 - 1) as f32
* if is_column {
row_gap as f32
} else {
column_gap as f32
}
} else {
0.0
};
let mut frozen = vec![false; line.items.len()];
loop {
let frozen_sum: f32 = line
.items
.iter()
.enumerate()
.filter(|(pos, _)| frozen[*pos])
.map(|(_, &i)| items[i].target_main_size)
.sum();
let unfrozen_base: f32 = line
.items
.iter()
.enumerate()
.filter(|(pos, _)| !frozen[*pos])
.map(|(_, &i)| items[i].flex_base_size)
.sum();
let capacity = main_size as f32 - total_gap - frozen_sum;
let free_space = capacity - unfrozen_base;
for (pos, &i) in line.items.iter().enumerate() {
if !frozen[pos] {
items[i].target_main_size = items[i].flex_base_size;
}
}
if free_space > 0.0 {
let total_grow: f32 = line
.items
.iter()
.enumerate()
.filter(|(pos, _)| !frozen[*pos])
.map(|(_, &i)| items[i].flex_grow)
.sum();
if total_grow > 0.0 {
for (pos, &i) in line.items.iter().enumerate() {
if !frozen[pos] {
let grow_amount =
(items[i].flex_grow / total_grow) * free_space;
items[i].target_main_size += grow_amount;
}
}
}
} else if free_space < 0.0 {
let total_shrink: f32 = line
.items
.iter()
.enumerate()
.filter(|(pos, _)| !frozen[*pos])
.map(|(_, &i)| items[i].flex_shrink * items[i].flex_base_size)
.sum();
if total_shrink > 0.0 {
for (pos, &i) in line.items.iter().enumerate() {
if !frozen[pos] {
let shrink_factor = items[i].flex_shrink
* items[i].flex_base_size
/ total_shrink;
let shrink_amount = shrink_factor * free_space.abs();
items[i].target_main_size -= shrink_amount;
}
}
}
}
let mut newly_frozen = false;
for (pos, &i) in line.items.iter().enumerate() {
if frozen[pos] {
continue;
}
let before = items[i].target_main_size;
let after = clamp_to_min_max(
before,
items[i].min_main_size,
items[i].max_main_size,
);
items[i].target_main_size = after;
if after != before {
frozen[pos] = true;
newly_frozen = true;
}
}
if !newly_frozen {
break;
}
}
line.main_size = line
.items
.iter()
.map(|&i| items[i].target_main_size)
.sum::<f32>()
+ total_gap;
if line.items.is_empty() {
line.main_positions = Vec::new();
line.cross_size = 0.0;
continue;
}
let effective_main = line.main_size;
let extra_space = (main_size as f32 - effective_main).max(0.0);
let gap_val = if is_column { row_gap } else { column_gap };
let n = line.items.len();
let per_gap_extra: f32 = match justify_content.as_str() {
"space-between" => {
if n > 1 {
extra_space / (n as f32 - 1.0)
} else {
0.0
}
}
"space-around" => {
if n > 0 {
extra_space / n as f32
} else {
0.0
}
}
"space-evenly" => {
if n > 0 {
extra_space / (n as f32 + 1.0)
} else {
0.0
}
}
_ => 0.0,
};
#[allow(unused_assignments)]
let mut cursor = main_start;
cursor = match justify_content.as_str() {
"flex-start" | "start" => main_start,
"flex-end" | "end" => main_start + extra_space,
"center" => main_start + extra_space / 2.0,
"space-between" => main_start,
"space-around" => main_start + per_gap_extra / 2.0,
"space-evenly" => main_start + per_gap_extra,
_ => main_start,
};
let mut mpos: Vec<(usize, f32)> = Vec::new();
let extra_for_gap = match justify_content.as_str() {
"space-between" | "space-around" | "space-evenly" => per_gap_extra,
_ => 0.0,
};
for &item_idx in &line.items {
let pos = cursor;
mpos.push((item_idx, pos));
cursor = pos
+ items[item_idx].target_main_size
+ gap_val as f32
+ extra_for_gap;
}
line.main_positions = mpos;
let mut max_cross_size: f32 = 0.0;
for &(item_idx, _) in &line.main_positions {
if let Some(ref ln) = items[item_idx].layout_node {
let cross_sz = if is_column {
ln.rect.w as f32
} else {
ln.rect.h as f32
};
if cross_sz > max_cross_size {
max_cross_size = cross_sz;
}
}
}
let effective_align_items = align_items.clone();
let is_single_line = pre_lines_len == 1;
let can_stretch = has_definite_cross_size
&& effective_align_items.eq_ignore_ascii_case("stretch");
if can_stretch {
let stretch_target = if is_single_line {
cross_size as f32
} else {
max_cross_size
};
for &(item_idx, _) in &line.main_positions {
let child_style = flex_children
.iter()
.find(|fc| fc.index == items[item_idx].child_index)
.map(|fc| fc.style)
.unwrap_or(None);
let explicit_cross = style_lookup_len_full(
child_style,
if is_column { "width" } else { "height" },
cross_size as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let is_baseline = items[item_idx]
.align_self
.eq_ignore_ascii_case("baseline")
|| (items[item_idx].align_self.eq_ignore_ascii_case("auto")
&& effective_align_items.eq_ignore_ascii_case("baseline"));
if explicit_cross.is_none()
&& items[item_idx].align_self.eq_ignore_ascii_case("auto")
&& !is_baseline
&& let Some(ref mut ln) = items[item_idx].layout_node
{
if is_column {
ln.rect.w = stretch_target as i32;
} else {
ln.rect.h = stretch_target as i32;
}
}
}
if is_single_line {
max_cross_size = max_cross_size.max(cross_size as f32);
}
}
line.cross_size = max_cross_size;
}
let total_cross: f32 = if lines.is_empty() {
0.0
} else {
lines.iter().map(|l| l.cross_size).sum::<f32>()
+ cross_gap * (lines.len() as f32 - 1.0)
};
let free_cross = if has_definite_cross_size {
(cross_size as f32 - total_cross).max(0.0)
} else {
0.0
};
let n_lines = lines.len();
let mut cross_start: f32 = 0.0;
let mut cross_extra_per_gap: f32 = 0.0;
#[allow(unused_assignments)]
let mut cross_line_extra: f32 = 0.0;
match align_content.as_str() {
"flex-start" | "start" => cross_start = 0.0,
"flex-end" | "end" => cross_start = free_cross,
"center" => cross_start = free_cross / 2.0,
"space-between" => {
if n_lines > 1 {
cross_extra_per_gap = free_cross / (n_lines as f32 - 1.0);
}
}
"space-around" => {
if n_lines > 0 {
cross_extra_per_gap = free_cross / n_lines as f32;
cross_start = cross_extra_per_gap / 2.0;
}
}
"space-evenly" => {
if n_lines > 0 {
cross_extra_per_gap = free_cross / (n_lines as f32 + 1.0);
cross_start = cross_extra_per_gap;
}
}
"stretch" => {
if has_definite_cross_size && n_lines > 1 && free_cross > 0.0 {
cross_line_extra = free_cross / n_lines as f32;
for line in &mut lines {
line.cross_size += cross_line_extra;
}
if align_items.eq_ignore_ascii_case("stretch") {
for line in &lines {
for &(item_idx, _) in &line.main_positions {
if items[item_idx]
.align_self
.eq_ignore_ascii_case("auto")
&& let Some(ref mut ln) =
items[item_idx].layout_node
{
if !is_column {
ln.rect.h = line.cross_size as i32;
} else {
ln.rect.w = line.cross_size as i32;
}
}
}
}
}
}
}
_ => {}
}
let final_cross_size = if has_definite_cross_size {
cross_size as f32
} else {
total_cross
};
let mut cross_offset: f32 = cross_start;
let single_line_mode = n_lines == 1;
for line in &lines {
for &(item_idx, main_pos) in &line.main_positions {
if let Some(mut ln) = items[item_idx].layout_node.take() {
let fb = items[item_idx].target_main_size.round() as i32;
let item_fc = flex_children
.iter()
.find(|fc| fc.index == items[item_idx].child_index);
let item_style = item_fc.and_then(|fc| fc.style);
let pre_measure_main =
items[item_idx].pre_measure_main.round() as i32;
if fb != pre_measure_main
&& let Some(fc) = item_fc
{
let (rl_w, rl_h, rl_cb) = if is_column {
(
ln.rect.w,
fb,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: fb,
},
)
} else {
(
fb,
ln.rect.h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: fb,
h: content_h_available,
},
)
};
ln = at(
fc.node,
0,
0,
rl_w,
rl_h,
viewport_w,
viewport_h,
rl_cb,
Some(fc.index),
root_font_size,
my_font_size,
);
if is_column {
ln.rect.w = rl_w;
} else {
ln.rect.h = rl_h;
}
}
let pre_x = ln.rect.x;
let pre_y = ln.rect.y;
if is_column {
ln.rect.h = fb;
} else {
ln.rect.w = fb;
}
ln.rect.w = clamp_width_to_max_width(
ln.rect.w,
item_style,
content_w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let item_align =
if items[item_idx].align_self.eq_ignore_ascii_case("auto") {
align_items.clone()
} else {
items[item_idx].align_self.clone()
};
let item_cross_size = if is_column {
ln.rect.w as f32
} else {
ln.rect.h as f32
};
let resolved_align = if item_align.eq_ignore_ascii_case("baseline")
{
"flex-start"
} else {
item_align.as_str()
};
let effective_cross = if single_line_mode {
final_cross_size
} else {
line.cross_size
};
let cross_pos = match resolved_align {
"flex-end" | "end" => {
(effective_cross - item_cross_size).max(0.0)
}
"center" => (effective_cross - item_cross_size) / 2.0,
_ => 0.0,
};
let line_cross = cross_offset + cross_pos;
let (mut resolved_x, mut resolved_y) = if is_column {
(
(content_x_scrolled as f32 + line_cross).round() as i32,
(content_y_scrolled as f32 + (main_pos - main_start))
.round() as i32,
)
} else {
(
(content_x_scrolled as f32 + (main_pos - main_start))
.round() as i32,
(content_y_scrolled as f32 + line_cross).round() as i32,
)
};
if is_reverse {
if is_column {
let container_bottom =
content_y_start + content_h_available;
resolved_y = container_bottom - resolved_y - ln.rect.h;
} else {
let container_right = content_x_scrolled + content_w;
resolved_x = container_right - resolved_x - ln.rect.w;
}
}
translate_layout_subtree(
&mut ln,
resolved_x - pre_x,
resolved_y - pre_y,
);
ln.rect.x = resolved_x;
ln.rect.y = resolved_y;
let child_style = item_style;
apply_relative_position(
child_style,
&mut ln,
content_w,
content_h_available,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
apply_sticky_position(
child_style,
&mut ln,
content_x,
content_y_start,
content_w,
content_h_available,
scroll_x,
scroll_y,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let descendant_dx = ln.rect.x - resolved_x;
let descendant_dy = ln.rect.y - resolved_y;
translate_layout_descendants(&mut ln, descendant_dx, descendant_dy);
max_y_end = max_y_end.max(ln.rect.y + ln.rect.h);
laid_children.push(ln);
}
}
cross_offset += line.cross_size + cross_gap + cross_extra_per_gap;
}
if wrap_reverse {
laid_children.sort_by_key(|n| n.source_index.unwrap_or(usize::MAX));
}
if is_reverse && lines.len() > 1 {
}
} else {
let mut cur_x = content_x_scrolled;
let mut cur_y = content_y_scrolled;
let mut last_bottom_margin = 0;
let container_font_size =
style_lookup_font_size(style, my_font_size, root_font_size, (vw_f, vh_f))
.unwrap_or(my_font_size);
let container_font_family = style_lookup_str(style, "font-family")
.unwrap_or_else(|| DEFAULT_TEXT_FAMILY.to_string());
let container_align = inline_vertical_align(style, VerticalAlign::Baseline);
let container_text_align = style_lookup_str(style, "text-align")
.map(|v| v.trim().to_ascii_lowercase())
.unwrap_or_else(|| DEFAULT_TEXT_ALIGN.to_string());
let container_ws = style_lookup_str(style, "white-space")
.and_then(|v| WhiteSpace::parse(&v))
.unwrap_or_default();
let container_text_overflow = style_lookup_str(style, "text-overflow")
.and_then(|v| TextOverflow::parse(&v))
.unwrap_or_default();
let mut inline_run: Vec<InlineRunItem<'_>> = Vec::new();
for (idx, c) in children.iter().enumerate() {
let is_text = matches!(c, VNode::Text(_));
if should_drop_collapsible_whitespace(children, idx, style) {
continue;
}
let child_style = match c {
VNode::Element { props, .. } => {
props.attrs.get("style").map(String::as_str)
}
_ => None,
};
let child_display = style_lookup_str(child_style, "display")
.unwrap_or_else(|| DEFAULT_DISPLAY_BLOCK.to_string());
if child_display.trim().eq_ignore_ascii_case("none") {
continue;
}
if !is_inline_run_member(c) && !inline_run.is_empty() {
let flushed = flush_inline_run(
&mut inline_run,
&InlineContext {
at,
content_x: content_x_scrolled,
line_limit: content_w,
font_size: container_font_size,
text_align: &container_text_align,
ws: container_ws,
text_overflow: container_text_overflow,
scale: crate::text_wrap::current_scale(),
viewport_w,
viewport_h,
cb: descendant_cb,
root_font_size,
},
&mut laid_children,
cur_y,
);
cur_y = flushed.cur_y;
cur_x = content_x_scrolled;
max_y_end = max_y_end.max(flushed.max_y_end);
}
let mut child_path = vec![idx];
if collect_inline_run(
c,
&mut child_path,
container_font_size,
&container_font_family,
container_align,
&mut inline_run,
) {
continue;
}
let position = style_lookup_str(child_style, "position")
.unwrap_or_else(|| DEFAULT_POSITION.to_string());
if position.trim().eq_ignore_ascii_case("absolute")
|| position.trim().eq_ignore_ascii_case("fixed")
{
let is_fixed = position.trim().eq_ignore_ascii_case("fixed");
let out_cb = out_of_flow_containing_block(
is_fixed,
descendant_cb,
viewport_w,
viewport_h,
);
let (static_x, static_y) = if cur_x != content_x_scrolled {
(content_x_scrolled, cur_y + last_bottom_margin.max(0))
} else {
(cur_x, cur_y)
};
let child_ln = at(
c,
static_x,
static_y,
out_cb.w,
out_cb.h,
viewport_w,
viewport_h,
out_cb,
Some(idx),
root_font_size,
my_font_size,
);
abs_children.push(PendingAbsolute {
style: child_style.map(str::to_string),
is_fixed,
static_x: child_ln.rect.x,
static_y: child_ln.rect.y,
node: child_ln,
});
continue;
}
if !is_text && cur_x != content_x_scrolled {
cur_y += last_bottom_margin.max(0); cur_x = content_x_scrolled;
}
let (_, _, cmt, cmb) = style_box_sides_full(
child_style,
"margin",
content_w as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let collapsed_margin_top = if idx == 0 {
if pt == 0 && bt == 0 {
collapse_margins(mt as f32, cmt as f32).round() as i32
} else {
cmt
}
} else {
collapse_margins(last_bottom_margin as f32, cmt as f32).round() as i32
};
let adjusted_cur_y = cur_y + collapsed_margin_top - cmt;
let mut child_ln = at(
c,
cur_x,
adjusted_cur_y,
(content_w - (cur_x - content_x_scrolled)).max(0),
content_h_available,
viewport_w,
viewport_h,
ContainingBlock {
x: content_x,
y: content_y_start,
w: content_w,
h: content_h_available,
},
Some(idx),
root_font_size,
my_font_size,
);
apply_relative_position(
child_style,
&mut child_ln,
content_w,
content_h_available,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
apply_sticky_position(
child_style,
&mut child_ln,
content_x,
content_y_start,
content_w,
content_h_available,
scroll_x,
scroll_y,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let is_empty_block = child_ln.rect.h == 0 && child_ln.children.is_empty();
if is_empty_block {
last_bottom_margin =
collapse_margins(collapsed_margin_top as f32, cmb as f32).round()
as i32;
cur_x = content_x;
max_y_end = max_y_end.max(cur_y);
laid_children.push(child_ln);
} else {
cur_y = child_ln.rect.y + child_ln.rect.h;
last_bottom_margin = cmb;
cur_x = content_x;
max_y_end = max_y_end.max(child_ln.rect.y + child_ln.rect.h);
laid_children.push(child_ln);
}
}
if !inline_run.is_empty() {
let flushed = flush_inline_run(
&mut inline_run,
&InlineContext {
at,
content_x: content_x_scrolled,
line_limit: content_w,
font_size: container_font_size,
text_align: &container_text_align,
ws: container_ws,
text_overflow: container_text_overflow,
scale: crate::text_wrap::current_scale(),
viewport_w,
viewport_h,
cb: descendant_cb,
root_font_size,
},
&mut laid_children,
cur_y,
);
max_y_end = max_y_end.max(flushed.max_y_end);
}
}
let declared_h2 = style_lookup_len_full(
style,
"height",
avail_h as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.or_else(|| {
replaced.then(|| {
replaced_used_size(
props,
"height",
intrinsic.map(|(_, h)| h),
avail_h as f32,
my_font_size,
root_font_size,
(vw_f, vh_f),
)
})
});
let content_h = (max_y_end - content_y_start).max(0);
let mut rect_h = if let Some(dh) = declared_h2 {
if is_border_box {
dh
} else {
dh + pt + pb + bt + bb
}
} else if is_viewport_height {
_rect_h
} else {
content_h + pt + pb + bt + bb
};
let min_h = style_lookup_len_full(
style,
"min-height",
avail_h as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
)
.unwrap_or(0);
let max_h_val = style_lookup_len_full(
style,
"max-height",
avail_h as f32,
my_font_size,
root_font_size,
vw_f,
vh_f,
);
let max_h = match max_h_val {
Some(v) if v >= 0 => v,
_ => i32::MAX,
};
rect_h = rect_h.max(min_h).min(max_h);
if tag == "button"
&& children.len() == 1
&& let Some(child) = laid_children.get_mut(0)
{
let content_h = (rect_h - pt - pb - bt - bb).max(0);
let child_h = child.rect.h;
let offset_y = ((content_h - child_h).max(0)) / 2;
child.rect.y = elem_y + bt + pt + offset_y;
if let Some(align) = style_lookup_str(style, "text-align") {
let child_w = child.rect.w;
let offset_x = if align.trim().eq_ignore_ascii_case("center") {
((content_w - child_w).max(0)) / 2
} else if align.trim().eq_ignore_ascii_case("right") {
(content_w - child_w).max(0)
} else {
0
};
child.rect.x = content_x + offset_x;
}
}
let scroll_height = content_h + pt + pb + bt + bb;
let overflow_lower = overflow.to_ascii_lowercase();
let scrollable =
is_scrollable(&overflow_lower, scroll_height as f32, rect_h as f32);
let clip = if scrollable || overflow_lower == "hidden" {
Some(Rect {
x: elem_x,
y: elem_y,
w: rect_w,
h: rect_h,
})
} else {
None
};
let max_scroll_y = (scroll_height - rect_h).max(0);
let out_cb = out_of_flow_cb(rect_h);
for mut pending in abs_children {
let cb = out_of_flow_containing_block(
pending.is_fixed,
out_cb,
viewport_w,
viewport_h,
);
apply_absolute_position(
pending.style.as_deref(),
&mut pending.node,
cb,
(pending.static_x, pending.static_y),
my_font_size,
root_font_size,
vw_f,
vh_f,
);
laid_children.push(pending.node);
}
LayoutNode {
rect: Rect {
x: elem_x,
y: elem_y,
w: rect_w,
h: rect_h,
},
z_index,
display_none: false,
source_index,
scroll_x,
scroll_y,
clip,
stacking_context,
scroll_height,
max_scroll_y,
scrollable,
children: laid_children,
}
}
}
}
at(
node,
0,
0,
viewport_w,
viewport_h,
viewport_w,
viewport_h,
ContainingBlock {
x: 0,
y: 0,
w: viewport_w,
h: viewport_h,
},
None,
DEFAULT_ROOT_FONT_SIZE,
DEFAULT_ROOT_FONT_SIZE,
)
}