use crate::{
html::css_parse::{TextAlign, UserSelect},
layout::layout::TermMap,
tdom::node::{NodeArena, NodeId, NodeType},
};
use std::collections::HashMap;
use unicode_segmentation::UnicodeSegmentation;
pub mod layout;
pub mod layoutbox;
pub mod paint;
pub use crate::html::box_edges::{Border, BorderSides, BorderStyle, BoxEdges, EdgeSizes};
pub use crate::html::css_parse::{FontContext, Viewport};
pub use layoutbox::inner_formatting_context;
pub use layoutbox::inner_formatting_context::{
flex::layout_flex_dispatch, flow::layout_tree, flow::layout_tree_with_scroll_offsets,
};
pub use layoutbox::{
BoxKind, ColumnLayout, Cursor, FormattingContext, GeneratedBox, GeneratedBoxKind,
GeneratedPayload, InlineRun, LayoutBox, OuterDisplay, PaintStyle, Rect, ReplacedContent,
ScrollbarLayout, flatten_contents_children, generate_box_tree, generate_node,
layout_generated_box, layout_generated_tree,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LayoutContext<'a> {
pub font: FontContext,
pub viewport: Viewport,
pub scroll_offsets: &'a HashMap<NodeId, u32>,
pub scroll_x_offsets: &'a HashMap<NodeId, u32>,
}
pub static NO_SCROLL_OFFSETS: std::sync::LazyLock<std::collections::HashMap<NodeId, u32>> =
std::sync::LazyLock::new(std::collections::HashMap::new);
impl<'a> LayoutContext<'a> {
pub fn child(self, child_font_size_cells: usize) -> Self {
Self {
font: self.font.child(child_font_size_cells),
viewport: self.viewport,
scroll_offsets: self.scroll_offsets,
scroll_x_offsets: self.scroll_x_offsets,
}
}
}
struct ClippedFragment<'a> {
row: u32,
lo: u32,
hi: u32,
node_id: NodeId,
runs: &'a [InlineRun],
font: usize,
col_start: u32,
}
fn clipped_fragments<'a>(
arena: &'a NodeArena,
boxes: &'a [LayoutBox],
lines: &[(u32, u32, u32)],
) -> Vec<ClippedFragment<'a>> {
let mut text_lines: Vec<FlowTextLine> = Vec::new();
collect_flow_lines(arena, boxes, &mut text_lines);
let mut out = Vec::new();
for &(sel_row, sel_cs, sel_ce) in lines {
for tl in &text_lines {
if tl.row != sel_row {
continue;
}
let line_width: u32 = tl.runs.iter().map(|r| r.width_at(tl.font) as u32).sum();
let line_end = tl.col_start.saturating_add(line_width);
let lo = sel_cs.max(tl.col_start);
let hi = sel_ce.min(line_end);
if lo < hi {
out.push(ClippedFragment {
row: tl.row,
lo,
hi,
node_id: tl.node_id,
runs: tl.runs,
font: tl.font,
col_start: tl.col_start,
});
}
}
}
out
}
pub fn extract_flow_text(
arena: &NodeArena,
boxes: &[LayoutBox],
lines: &[(u32, u32, u32)],
) -> String {
let mut frags: Vec<(u32, u32, NodeId, String)> = Vec::new();
for f in clipped_fragments(arena, boxes, lines) {
let text = extract_line_text(f.runs, f.font, f.lo, f.hi, f.col_start);
if !text.is_empty() {
frags.push((f.row, f.lo, f.node_id, text));
}
}
frags.sort_by_key(|&(row, col, _, _)| (row, col));
let mut out = String::new();
let mut prev_block: Option<NodeId> = None;
for (_, _, node_id, text) in frags {
if !out.is_empty() && prev_block != Some(node_id) {
out.push('\n');
}
out.push_str(&text);
prev_block = Some(node_id);
}
out
}
pub fn user_select_is_none(arena: &NodeArena, start: NodeId) -> bool {
let mut current = Some(start);
while let Some(node_id) = current {
let Some(node) = arena.get(node_id) else {
return false;
};
match node.css_attr.get_user_select() {
UserSelect::None => return true,
UserSelect::Text | UserSelect::All | UserSelect::Contain => return false,
UserSelect::Auto => current = node.parent,
}
}
false
}
fn box_chain_at(boxes: &[LayoutBox], x: u32, y: u32, out: &mut Vec<NodeId>) {
for lb in boxes {
if lb.rect.contains(x, y) {
out.push(lb.node_id);
box_chain_at(&lb.children, x, y, out);
return;
}
}
}
fn box_rect(boxes: &[LayoutBox], node_id: NodeId) -> Option<Rect> {
for lb in boxes {
if lb.node_id == node_id {
return Some(lb.rect);
}
if let Some(found) = box_rect(&lb.children, node_id) {
return Some(found);
}
}
None
}
pub fn clip_flow_lines_scoped(
boxes: &[LayoutBox],
anchor: (u32, u32),
end: (u32, u32),
lines: &[(u32, u32, u32)],
) -> Vec<(u32, u32, u32)> {
let mut anchor_chain = Vec::new();
let mut end_chain = Vec::new();
box_chain_at(boxes, anchor.0, anchor.1, &mut anchor_chain);
box_chain_at(boxes, end.0, end.1, &mut end_chain);
let span = anchor_chain
.iter()
.zip(&end_chain)
.take_while(|(a, b)| a == b)
.map(|(a, _)| *a)
.last()
.and_then(|common| box_rect(boxes, common))
.map(|r| (r.x as u32, r.x as u32 + r.width as u32));
match span {
Some((lo, hi)) => lines
.iter()
.filter_map(|&(y, cs, ce)| {
let lo = cs.max(lo);
let hi = ce.min(hi);
(lo < hi).then_some((y, lo, hi))
})
.collect(),
None => lines.to_vec(),
}
}
pub fn selection_text_lines(
arena: &NodeArena,
boxes: &[LayoutBox],
lines: &[(u32, u32, u32)],
) -> Vec<(u32, u32, u32)> {
clipped_fragments(arena, boxes, lines)
.into_iter()
.map(|f| (f.row, f.lo, f.hi))
.collect()
}
struct FlowTextLine<'a> {
node_id: NodeId,
row: u32,
col_start: u32,
runs: &'a [InlineRun],
font: usize,
}
fn collect_flow_lines<'a>(
arena: &'a NodeArena,
boxes: &'a [LayoutBox],
out: &mut Vec<FlowTextLine<'a>>,
) {
for lb in boxes {
collect_box_lines(arena, lb, out);
collect_flow_lines(arena, &lb.children, out);
}
}
fn collect_box_lines<'a>(arena: &'a NodeArena, lb: &'a LayoutBox, out: &mut Vec<FlowTextLine<'a>>) {
if matches!(lb.kind, crate::layout::BoxKind::Replaced(_))
|| user_select_is_none(arena, lb.node_id)
|| matches!(
arena[lb.node_id].node_type,
NodeType::Progress | NodeType::Meter
)
{
return;
}
let Some(runs) = &lb.paint_style.runs else {
return;
};
if runs.is_empty() {
return;
}
let be = &lb.paint_style.box_edges;
let font = lb.paint_style.font_size_cells.max(1);
let line_h = lb.paint_style.line_height_cells.max(1);
let align = arena[lb.node_id].css_attr.get_text_align();
let glyph_row = crate::html::css_parse::glyph_row_within_line(
lb.paint_style.font_size_cells,
lb.paint_style.line_height_cells,
);
let content_x = lb
.rect
.x
.saturating_add(be.border.left.thickness())
.saturating_add(be.padding.left);
let content_width = lb.rect.width.saturating_sub(be.inner_horizontal()).max(1);
let first_row = lb
.rect
.y
.saturating_add(be.border.top.thickness())
.saturating_add(be.padding.top)
.saturating_add(glyph_row);
for (i, line_runs) in runs.iter().enumerate() {
if line_runs.is_empty() {
continue;
}
let line_width: usize = line_runs.iter().map(|r| r.width_at(font)).sum();
let line_width = line_width.min(content_width);
let offset = match align {
TextAlign::Left => 0,
TextAlign::Center => content_width.saturating_sub(line_width) / 2,
TextAlign::Right => content_width.saturating_sub(line_width),
};
out.push(FlowTextLine {
node_id: lb.node_id,
row: (first_row.saturating_add(i.saturating_mul(line_h))) as u32,
col_start: (content_x.saturating_add(offset)) as u32,
runs: line_runs,
font,
});
}
}
pub fn label_regions(arena: &NodeArena, boxes: &[LayoutBox]) -> Vec<(NodeId, Rect)> {
let mut out = Vec::new();
collect_label_regions(arena, boxes, &mut out);
out
}
fn collect_label_regions(arena: &NodeArena, boxes: &[LayoutBox], out: &mut Vec<(NodeId, Rect)>) {
for lb in boxes {
if let Some(runs) = &lb.paint_style.runs {
collect_box_label_regions(arena, lb, runs, out);
}
collect_label_regions(arena, &lb.children, out);
}
}
fn collect_box_label_regions(
arena: &NodeArena,
lb: &LayoutBox,
runs: &[Vec<InlineRun>],
out: &mut Vec<(NodeId, Rect)>,
) {
if runs.is_empty() {
return;
}
let be = &lb.paint_style.box_edges;
let font = lb.paint_style.font_size_cells.max(1);
let line_h = lb.paint_style.line_height_cells.max(1);
let align = arena[lb.node_id].css_attr.get_text_align();
let glyph_row = crate::html::css_parse::glyph_row_within_line(
lb.paint_style.font_size_cells,
lb.paint_style.line_height_cells,
);
let content_x = lb
.rect
.x
.saturating_add(be.border.left.thickness())
.saturating_add(be.padding.left);
let content_width = lb.rect.width.saturating_sub(be.inner_horizontal()).max(1);
let first_row = lb
.rect
.y
.saturating_add(be.border.top.thickness())
.saturating_add(be.padding.top)
.saturating_add(glyph_row);
for (i, line_runs) in runs.iter().enumerate() {
if line_runs.is_empty() {
continue;
}
let line_width: usize = line_runs.iter().map(|r| r.width_at(font)).sum();
let line_width = line_width.min(content_width);
let offset = match align {
TextAlign::Left => 0,
TextAlign::Center => content_width.saturating_sub(line_width) / 2,
TextAlign::Right => content_width.saturating_sub(line_width),
};
let row = first_row.saturating_add(i.saturating_mul(line_h));
let mut col = content_x.saturating_add(offset);
for run in line_runs {
let w = run.width_at(font);
if let Some(node_id) = run.node_id {
out.push((node_id, Rect::new(col, row, w, 1)));
}
col = col.saturating_add(w);
}
}
}
fn extract_line_text(runs: &[InlineRun], font: usize, lo: u32, hi: u32, line_start: u32) -> String {
let font = font.max(1);
let mut out = String::new();
let mut col = line_start;
for run in runs {
let run_w = run.width_at(font) as u32;
let run_lo = col.max(lo);
let run_hi = col.saturating_add(run_w).min(hi);
if run_lo < run_hi && !run.synthesized {
out.push_str(&run_text_between(
run,
font,
(run_lo - col) as usize,
(run_hi - col) as usize,
));
}
col = col.saturating_add(run_w);
if col >= hi {
break;
}
}
out
}
fn run_text_between(run: &InlineRun, font: usize, lo: usize, hi: usize) -> String {
let font = font.max(1);
let mut out = String::new();
let mut col = 0usize;
for g in run.text.graphemes(true) {
let slot = run.grapheme_advance(g, font);
if col >= hi {
break;
}
if col.saturating_add(slot) > lo {
out.push_str(g);
}
col += slot;
}
out
}
#[derive(Debug, Clone, Copy)]
pub struct BoxConstraint {
pub x: usize,
pub width: usize,
}
impl BoxConstraint {
pub fn root(term_map: &TermMap) -> Self {
Self {
x: 0,
width: term_map.columns as usize,
}
}
}
pub type LayoutFn = fn(
arena: &NodeArena,
node_id: NodeId,
containing: BoxConstraint,
cursor: &mut Cursor,
context: LayoutContext,
pre_allocated_rect: Option<Rect>,
) -> Option<LayoutBox>;
pub static DEFAULT_LAYOUT: &[LayoutFn] = &[
inner_formatting_context::flow::layout_standard_block,
inner_formatting_context::flow::layout_hn,
inner_formatting_context::flow::layout_title,
inner_formatting_context::flow::layout_meta,
inner_formatting_context::flow::layout_a,
inner_formatting_context::flow::layout_hr,
inner_formatting_context::flow::layout_li,
inner_formatting_context::flow::layout_ol,
inner_formatting_context::flow::layout_ul,
inner_formatting_context::flow::layout_menu,
inner_formatting_context::flow::layout_row_group,
inner_formatting_context::flow::layout_td,
inner_formatting_context::flow::layout_th,
inner_formatting_context::flow::layout_tr,
inner_formatting_context::flow::layout_progress,
inner_formatting_context::flow::layout_summary,
inner_formatting_context::flow::layout_inline_blockify,
inner_formatting_context::flow::layout_generic,
];