///|
// Viewport skeleton fast-path state and helpers.
//
// Large documents can switch below-fold nodes to lightweight skeleton nodes
// while the renderer builds the visible viewport.
///|
/// Viewport culling: estimated cumulative Y position during build_node.
/// Elements estimated below viewport_cutoff use skeleton mode (fast path).
let viewport_estimated_y : Ref[Double] = { val: 0.0 }
///|
let viewport_cutoff : Ref[Double] = { val: 1.0e10 }
///|
let viewport_skeleton_count : Ref[Int] = { val: 0 }
///|
let viewport_full_node_count : Ref[Int] = { val: 0 }
///|
let viewport_full_node_cutoff : Ref[Int] = { val: 2000 }
///|
let viewport_skeleton_enabled : Ref[Bool] = { val: true }
///|
let empty_indexed_stylesheets : Ref[Array[@css.IndexedStylesheet]] = { val: [] }
///|
fn should_use_viewport_skeleton() -> Bool {
viewport_skeleton_enabled.val &&
(
viewport_estimated_y.val > viewport_cutoff.val ||
viewport_full_node_count.val > viewport_full_node_cutoff.val
)
}
///|
fn parse_skeleton_px_length(value : String) -> Double? {
let lower = value.trim().to_owned().to_lower()
if lower.is_empty() || lower == "auto" {
return None
}
let raw = if lower.has_suffix("px") {
lower.unsafe_substring(start=0, end=lower.length() - 2).trim().to_owned()
} else {
lower
}
let parsed = @string.parse_double(raw) catch { _ => return None }
if parsed >= 0.0 {
Some(parsed)
} else {
None
}
}
///|
fn apply_skeleton_inline_style_hints(
style : @style.Style,
inline_css : String,
) -> @style.Style {
let result = apply_skeleton_inline_display_hint(style, inline_css)
apply_skeleton_inline_height_hint(result, inline_css)
}
///|
fn apply_skeleton_inline_display_hint(
style : @style.Style,
inline_css : String,
) -> @style.Style {
let result = style
match get_cached_inline_style_display(inline_css) {
Some(@types.Display::None) => result.display = @types.Display::None
Some(_) | None => ()
}
result
}
///|
fn apply_skeleton_inline_height_hint(
style : @style.Style,
inline_css : String,
) -> @style.Style {
let result = style
for declaration in parse_inline_declarations_cached(inline_css) {
let (prop, value) = declaration
if prop == "height" {
match parse_skeleton_px_length(value) {
Some(height) => result.height = @types.Length(height)
None => ()
}
}
}
result
}
///|
fn viewport_skeleton_advance(style : @style.Style) -> Double {
match style.height {
@types.Length(height) => height
_ => style.line_height
}
}
///|
fn skeleton_parent_allows_explicit_size_hints(
parent_style : @style.Style?,
) -> Bool {
match parent_style {
Some(style) =>
match (style.overflow_x, style.overflow_y) {
(@types.Scroll | @types.Auto, _) | (_, @types.Scroll | @types.Auto) =>
true
_ => false
}
None => false
}
}
///|
fn should_collapse_viewport_skeleton_subtree(style : @style.Style) -> Bool {
match style.display {
@types.Display::None | @types.Contents => false
_ =>
match style.height {
@types.Length(_) => true
_ => false
}
}
}
///|
/// Advance the viewport culling heuristic only for in-flow block containers.
/// Counting floated descendants as vertical progress causes small tables with
/// many float-based fixtures to enter skeleton mode too early.
fn should_advance_viewport_estimate(style : @style.Style) -> Bool {
let is_in_flow = match style.position {
@types.Absolute | @types.Fixed => false
_ =>
match style.float {
@types.Float::Left | @types.Float::Right => false
@types.Float::None => true
}
}
if !is_in_flow {
return false
}
match style.display {
@types.Block | @types.Flex | @types.Grid | @types.Table => true
_ => false
}
}
///|
fn build_viewport_skeleton_node(
elem : @html.Element,
selector_elem : @css.Element,
ctx : RenderContext,
stylesheets : Array[@css.Stylesheet],
indexed_stylesheets : Array[@css.IndexedStylesheet],
parent_style : @style.Style?,
css_vars : Map[String, String],
incoming_counters : Map[String, CounterEntry],
owner_id : String,
) -> @node.Node {
viewport_skeleton_count.val += 1
let skeleton_style = match parent_style {
Some(ps) => {
let s = @style.Style::default()
s.font_size = ps.font_size
s.line_height = ps.line_height
s.color = ps.color
s
}
None => @style.Style::default()
}
let allow_size_hints = skeleton_parent_allows_explicit_size_hints(
parent_style,
)
let skeleton_style = match elem.style {
Some(inline_css) =>
if allow_size_hints {
apply_skeleton_inline_style_hints(skeleton_style, inline_css)
} else {
apply_skeleton_inline_display_hint(skeleton_style, inline_css)
}
None => skeleton_style
}
if elem.attributes.contains("hidden") {
skeleton_style.display = @types.Display::None
}
let tag_lower = elem.tag.to_lower()
if tag_lower == "dialog" && !elem.attributes.contains("open") {
skeleton_style.display = @types.Display::None
}
if skeleton_style.display == @types.Display::None {
return @node.Node::leaf(make_node_id(elem), skeleton_style)
}
if should_advance_viewport_estimate(skeleton_style) {
viewport_estimated_y.val += viewport_skeleton_advance(skeleton_style)
}
if allow_size_hints &&
should_collapse_viewport_skeleton_subtree(skeleton_style) {
return @node.Node::leaf(make_node_id(elem), skeleton_style)
}
let children : Array[@node.Node] = []
for child in elem.children {
match child {
@html.Node::Element(child_elem) => {
if should_skip_element(child_elem.tag) {
continue
}
let child_selector = html_to_selector_element_minimal(
child_elem,
Some(selector_elem),
)
let child_node = element_to_node_with_styles_internal(
child_elem,
child_selector,
ctx,
stylesheets,
indexed_stylesheets,
Some(skeleton_style),
css_vars,
incoming_counters,
owner_id,
)
children.push(child_node)
}
@html.Node::Text(text) =>
if !text.is_empty() {
children.push(create_text_node(text, skeleton_style))
}
}
}
@node.Node::new(make_node_id(elem), skeleton_style, children)
}