use unicode_segmentation::UnicodeSegmentation;
use crate::{
graphics::{char_width, grapheme_width},
html::css_parse::{
AlignItems, AlignSelf, CssDisplay, FlexDirection, Length, LengthContext, Overflow,
WhiteSpace,
},
layout::{
InlineRun, LayoutContext,
layoutbox::generation::{GeneratedBoxKind, generate_node},
layoutbox::geometry::select_options,
layoutbox::inline_tree_has_forced_break,
},
tdom::node::{NodeArena, NodeId, NodeType, effective_font_size_cells},
};
use super::container::{AnonymousTextItem, FLEX_ITEM_MIN_CROSS, FlexItemPrep, FlexItemSource};
pub(super) fn prepare_flex_item(
arena: &NodeArena,
item_id: NodeId,
direction: FlexDirection,
containing_main: usize,
containing_height: Option<usize>,
context: LayoutContext,
align_items: AlignItems,
) -> FlexItemPrep {
let node = &arena[item_id];
let css = &node.css_attr;
let natural = natural_intrinsic_size(arena, item_id, context, Some(containing_main));
let edges = css.get_box_edges(containing_main, context.font, context.viewport);
let margin = edges.margin;
let sizing = css.get_box_sizing();
let (inner_main, inner_cross) = match direction.is_row() {
true => (edges.inner_horizontal(), edges.inner_vertical()),
false => (edges.inner_vertical(), edges.inner_horizontal()),
};
let to_border = |declared: usize, inner: usize| match sizing {
crate::html::css_parse::BoxSizing::BorderBox => declared,
crate::html::css_parse::BoxSizing::ContentBox => declared.saturating_add(inner),
};
let main_containing = if direction.is_row() {
Some(containing_main)
} else {
containing_height
};
let basis_resolved = css
.get_flex_basis_resolved(main_containing, context.font, context.viewport)
.map(|basis| to_border(basis, inner_main))
.or_else(|| {
let explicit = match direction.is_row() {
true => css.get_width(containing_main, context.font, context.viewport),
false => css.get_height(containing_height, context.font, context.viewport),
};
match direction.is_row() {
true => explicit
.map(|w| to_border(w, inner_main))
.or(Some(natural.width)),
false => explicit
.map(|h| to_border(h, inner_main))
.or(Some(natural.height)),
}
})
.unwrap_or(FLEX_ITEM_MIN_CROSS);
let css_min_main = match direction.is_row() {
true => css
.get_min_width(containing_main, context.font, context.viewport)
.map(|v| to_border(v, inner_main)),
false => css
.get_min_height(containing_height, context.font, context.viewport)
.map(|v| to_border(v, inner_main)),
};
let css_max_main = match direction.is_row() {
true => css
.get_max_width(containing_main, context.font, context.viewport)
.map(|v| to_border(v, inner_main)),
false => css
.get_max_height(containing_height, context.font, context.viewport)
.map(|v| to_border(v, inner_main)),
};
let min_main = if css_min_main.is_some() || css.get_overflow() == Overflow::Clip {
0
} else {
match direction.is_row() {
true => longest_breakable_width(arena, item_id),
false => natural.height,
}
};
let css_cross = match direction.is_row() {
true => css
.get_height(containing_height, context.font, context.viewport)
.map(|v| to_border(v, inner_cross)),
false => css
.get_width(containing_main, context.font, context.viewport)
.map(|v| to_border(v, inner_cross)),
};
let own_font_size_cells =
effective_font_size_cells(arena, item_id, context.font, context.viewport);
let line_height_cells = crate::html::css_parse::resolve_line_height_cells(
css.get_line_height(),
own_font_size_cells,
context.font,
context.viewport,
);
let glyph_row =
crate::html::css_parse::glyph_row_within_line(own_font_size_cells, line_height_cells);
let (margin_main_before, margin_main_after, margin_cross_before, margin_cross_after) =
match direction.is_row() {
true => (
margin.left.max(0) as usize,
margin.right.max(0) as usize,
margin.top,
margin.bottom,
),
false => (
margin.top,
margin.bottom,
margin.left.max(0) as usize,
margin.right.max(0) as usize,
),
};
let margin_auto = css.get_margin_auto();
let margin_auto_main = match direction.is_row() {
true => (margin_auto.left, margin_auto.right),
false => (margin_auto.top, margin_auto.bottom),
};
let baseline = edges
.border
.top
.thickness()
.saturating_add(edges.padding.top)
.saturating_add(glyph_row);
FlexItemPrep {
source: FlexItemSource::Node(item_id),
basis: basis_resolved,
min_main,
css_min_main,
css_max_main,
css_cross,
grow: css.get_flex_grow(),
shrink: css.get_flex_shrink(),
align_self: match css.get_align_self() {
AlignSelf::Auto => align_items,
AlignSelf::Value(v) => v,
},
margin_main_before,
margin_main_after,
margin_cross_before,
margin_cross_after,
margin_auto_main,
baseline,
order: css.get_order(),
natural_width: natural.width,
natural_height: natural.height,
}
}
pub(super) fn prepare_anonymous_text_item(
item: AnonymousTextItem,
direction: FlexDirection,
align_items: AlignItems,
) -> FlexItemPrep {
let text_w = runs_total_width(&item.runs);
let natural_height = item.line_height_cells.max(FLEX_ITEM_MIN_CROSS);
let (basis, min_main) = match direction.is_row() {
true => (
text_w.max(FLEX_ITEM_MIN_CROSS),
runs_longest_word(&item.runs),
),
false => (FLEX_ITEM_MIN_CROSS, FLEX_ITEM_MIN_CROSS),
};
FlexItemPrep {
source: FlexItemSource::AnonymousText(item),
basis,
min_main,
css_min_main: None,
css_max_main: None,
css_cross: None,
grow: 0.0,
shrink: 1.0,
align_self: align_items,
margin_main_before: 0,
margin_main_after: 0,
margin_cross_before: 0,
margin_cross_after: 0,
margin_auto_main: (false, false),
baseline: 0,
order: 0,
natural_width: text_w.max(FLEX_ITEM_MIN_CROSS),
natural_height,
}
}
fn runs_total_width(runs: &[InlineRun]) -> usize {
runs.iter()
.filter(|r| !r.force_break)
.map(|r| r.text.graphemes(true).map(grapheme_width).sum::<usize>())
.sum()
}
fn runs_longest_word(runs: &[InlineRun]) -> usize {
runs.iter()
.filter(|r| !r.force_break)
.flat_map(|r| r.text.split_whitespace())
.map(|token| token.graphemes(true).map(grapheme_width).sum::<usize>())
.max()
.unwrap_or(0)
.max(1)
}
pub(super) fn measure_text_content_width(arena: &NodeArena, node_id: NodeId) -> Option<usize> {
let white_space = arena[node_id]
.css_attr
.get_white_space(&arena[node_id].node_type);
let mut w = measure_text_content_width_ws(arena, node_id, white_space)?;
if arena[node_id].node_type == NodeType::Q {
w = w.saturating_add(2);
}
Some(w)
}
fn measure_text_content_width_ws(
arena: &NodeArena,
node_id: NodeId,
white_space: WhiteSpace,
) -> Option<usize> {
let node = &arena[node_id];
let mut total: Option<usize> = Some(0);
for &child_id in &node.children {
let child = &arena[child_id];
match &child.node_type {
NodeType::Text => {
if child.text_is_painted(white_space)
&& let Some(s) = child.data.as_deref()
{
let w: usize = s.graphemes(true).map(grapheme_width).sum();
total = Some(total.unwrap_or(0).saturating_add(w));
}
}
_ if child.node_type.ua_default_display() == CssDisplay::Inline => {
let ws = child.white_space_or(white_space);
let w = measure_text_content_width_ws(arena, child_id, ws).unwrap_or(0);
let w = if child.node_type == NodeType::Q {
w.saturating_add(2)
} else {
w
};
total = Some(total.unwrap_or(0).saturating_add(w));
}
_ => {}
}
}
total
}
pub(super) fn longest_breakable_width(arena: &NodeArena, node_id: NodeId) -> usize {
fn walk(arena: &NodeArena, node_id: NodeId, white_space: WhiteSpace, max_w: &mut usize) {
let node = &arena[node_id];
for &child_id in &node.children {
let child = &arena[child_id];
match &child.node_type {
NodeType::Text => {
if child.text_is_painted(white_space)
&& let Some(s) = child.data.as_deref()
{
for token in s.split_whitespace() {
let w: usize = token.graphemes(true).map(grapheme_width).sum();
if w > *max_w {
*max_w = w;
}
}
}
}
_ if child.node_type.ua_default_display() == CssDisplay::Inline => {
let ws = child.white_space_or(white_space);
walk(arena, child_id, ws, max_w);
}
_ => {}
}
}
}
let white_space = arena[node_id]
.css_attr
.get_white_space(&arena[node_id].node_type);
let mut max_w: usize = FLEX_ITEM_MIN_CROSS;
walk(arena, node_id, white_space, &mut max_w);
max_w
}
fn intrinsic_len(raw: Option<&str>, context: LayoutContext) -> Option<usize> {
let len = Length::parse(raw?)?;
match len {
Length::Auto | Length::Pct(_) => None,
len => Some(
len.resolve(LengthContext {
containing: 0,
font: context.font,
viewport: context.viewport,
})
.max(1),
),
}
}
pub(crate) fn natural_grid_item_size(
arena: &NodeArena,
node_id: NodeId,
context: LayoutContext,
containing_width: Option<usize>,
) -> (usize, usize) {
let natural = natural_intrinsic_size(arena, node_id, context, containing_width);
(natural.width, natural.height)
}
fn natural_intrinsic_size(
arena: &NodeArena,
node_id: NodeId,
context: LayoutContext,
containing_width: Option<usize>,
) -> NaturalIntrinsicSize {
let node = &arena[node_id];
let css = &node.css_attr;
let own_font_size_cells =
effective_font_size_cells(arena, node_id, context.font, context.viewport);
let child_context = context.child(own_font_size_cells);
if matches!(node.node_type, NodeType::Input) {
let is_toggle = node
.get_type()
.is_some_and(|t| matches!(t.to_ascii_lowercase().as_str(), "checkbox" | "radio"));
let edges = css.get_box_edges(0, child_context.font, child_context.viewport);
let (border_h, border_v) = if is_toggle {
(0, 0)
} else {
(
edges.border.horizontal_thickness(),
edges.border.vertical_thickness(),
)
};
let w = if is_toggle {
2
} else {
node.get_width().map(|w| w as usize).unwrap_or(8)
};
return NaturalIntrinsicSize {
width: w.saturating_add(border_h).max(FLEX_ITEM_MIN_CROSS),
height: 1usize.saturating_add(border_v).max(FLEX_ITEM_MIN_CROSS),
};
}
if matches!(node.node_type, NodeType::Select) {
let text_w: usize = select_options(arena, node_id)
.iter()
.map(|opt| opt.label.chars().map(char_width).sum::<usize>())
.max()
.unwrap_or(0);
let edges = css.get_box_edges(0, child_context.font, child_context.viewport);
let w = node
.get_width()
.map(|w| w as usize)
.unwrap_or(text_w.saturating_add(4));
return NaturalIntrinsicSize {
width: w
.saturating_add(edges.border.horizontal_thickness())
.max(FLEX_ITEM_MIN_CROSS),
height: 1usize
.saturating_add(edges.border.vertical_thickness())
.max(FLEX_ITEM_MIN_CROSS),
};
}
let edges = css.get_box_edges(
containing_width.unwrap_or(0),
child_context.font,
child_context.viewport,
);
let generated = generate_node(arena, node_id);
let is_flex_box = generated
.as_ref()
.map(|g| {
g.kind == GeneratedBoxKind::Normal
&& g.inner_formatting_context == crate::layout::FormattingContext::Flex
})
.unwrap_or(false);
let res = if css.display().is_some_and(|d| d.is_flex()) && is_flex_box {
let inner_direction = css.get_flex_direction();
let gap = css.get_gap(child_context.font, child_context.viewport);
let mut child_sizes: Vec<NaturalIntrinsicSize> = Vec::new();
for &child_id in &node.children {
let Some(child_generated) = generate_node(arena, child_id) else {
continue;
};
if child_generated.kind == GeneratedBoxKind::Normal {
child_sizes.push(natural_intrinsic_size(arena, child_id, child_context, None));
}
}
let (inner_w, inner_h) = match inner_direction.is_row() {
true => {
let total_w: usize = child_sizes
.iter()
.map(|s| s.width)
.fold(0usize, usize::saturating_add);
let gaps_w = if child_sizes.len() > 1 {
gap.column.saturating_mul(child_sizes.len() - 1)
} else {
0
};
let max_h = child_sizes
.iter()
.map(|s| s.height)
.max()
.unwrap_or(FLEX_ITEM_MIN_CROSS);
(
total_w.saturating_add(gaps_w).max(FLEX_ITEM_MIN_CROSS),
max_h.max(FLEX_ITEM_MIN_CROSS),
)
}
false => {
let max_w = child_sizes
.iter()
.map(|s| s.width)
.max()
.unwrap_or(FLEX_ITEM_MIN_CROSS);
let total_h: usize = child_sizes
.iter()
.map(|s| s.height)
.fold(0usize, usize::saturating_add);
let gaps_h = if child_sizes.len() > 1 {
gap.row.saturating_mul(child_sizes.len() - 1)
} else {
0
};
(
max_w.max(FLEX_ITEM_MIN_CROSS),
total_h.saturating_add(gaps_h).max(FLEX_ITEM_MIN_CROSS),
)
}
};
let outer_w = inner_w
.saturating_add(edges.padding.horizontal())
.saturating_add(edges.border.horizontal_thickness());
let outer_h = inner_h
.saturating_add(edges.padding.vertical())
.saturating_add(edges.border.vertical_thickness());
NaturalIntrinsicSize {
width: outer_w.max(FLEX_ITEM_MIN_CROSS),
height: outer_h.max(FLEX_ITEM_MIN_CROSS),
}
} else {
let mut content_w: usize = 0;
let mut content_h: usize = 0;
for &child_id in &node.children {
let Some(child_gen) = generate_node(arena, child_id) else {
continue;
};
let is_block =
child_gen.kind == GeneratedBoxKind::Normal && !child_gen.outer_display.is_inline();
if is_block {
let cs = natural_intrinsic_size(arena, child_id, child_context, None);
content_w = content_w.max(cs.width);
content_h = content_h.saturating_add(cs.height);
}
}
let text_w = measure_text_content_width(arena, node_id).unwrap_or(0);
content_w = content_w.max(text_w);
let has_inline = text_w > 0 || inline_tree_has_forced_break(arena, node_id);
if has_inline {
content_h = content_h.saturating_add(own_font_size_cells.max(1));
}
let content_box_w = intrinsic_len(css.0.get("width").map(|s| s.as_ref()), child_context)
.unwrap_or(content_w.max(FLEX_ITEM_MIN_CROSS))
.max(FLEX_ITEM_MIN_CROSS);
let ua_spacing_after = match node.node_type {
NodeType::Hr => 1,
_ => 0,
};
let content_box_h = intrinsic_len(css.0.get("height").map(|s| s.as_ref()), child_context)
.unwrap_or(content_h.max(FLEX_ITEM_MIN_CROSS))
.max(FLEX_ITEM_MIN_CROSS)
.saturating_add(ua_spacing_after);
let width = content_box_w
.saturating_add(edges.padding.horizontal())
.saturating_add(edges.border.horizontal_thickness());
let height = content_box_h
.saturating_add(edges.padding.vertical())
.saturating_add(edges.border.vertical_thickness());
NaturalIntrinsicSize {
width: width.max(FLEX_ITEM_MIN_CROSS),
height: height.max(FLEX_ITEM_MIN_CROSS),
}
};
log::debug!(
target: "htui::intrinsic",
"natural_intrinsic_size node={:?} type={:?} class={:?} flex={} -> {}x{}",
node_id,
node.node_type,
node.attributes.get("class").map(|s| s.as_str()),
is_flex_box,
res.width,
res.height
);
res
}
struct NaturalIntrinsicSize {
width: usize,
height: usize,
}
pub(super) fn natural_cross_axis_size_for_prep(
prep: &FlexItemPrep,
direction: FlexDirection,
) -> usize {
match direction.is_row() {
true => prep.natural_height,
false => prep.natural_width,
}
.max(FLEX_ITEM_MIN_CROSS)
}
pub(super) fn resolve_item_cross_size(
align_self: AlignItems,
line_cross: usize,
natural_cross: usize,
margin_cross: (usize, usize),
explicit_cross: Option<usize>,
) -> usize {
if let Some(size) = explicit_cross {
return size.max(FLEX_ITEM_MIN_CROSS);
}
if matches!(align_self, AlignItems::Stretch) {
line_cross
.saturating_sub(margin_cross.0)
.saturating_sub(margin_cross.1)
.max(FLEX_ITEM_MIN_CROSS)
} else {
natural_cross.max(FLEX_ITEM_MIN_CROSS)
}
}