use crate::{
graphics::grapheme_width,
html::css_parse::TextAlign,
layout::layout::TermMap,
tdom::node::{NodeArena, NodeId},
};
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};
pub use layoutbox::{
BoxKind, ColumnLayout, Cursor, FormattingContext, GeneratedBox, GeneratedBoxKind, InlineRun,
LayoutBox, OuterDisplay, PaintStyle, Rect, ReplacedContent, generate_box_tree, generate_node,
layout_generated_box, layout_generated_tree,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LayoutContext {
pub font: FontContext,
pub viewport: Viewport,
}
impl LayoutContext {
pub fn child(self, child_font_size_cells: usize) -> Self {
Self {
font: self.font.child(child_font_size_cells),
viewport: self.viewport,
}
}
}
pub fn extract_flow_text(
arena: &NodeArena,
boxes: &[LayoutBox],
lines: &[(u32, u32, u32)],
) -> String {
let mut text_lines: Vec<FlowTextLine> = Vec::new();
collect_flow_lines(arena, boxes, &mut text_lines);
let mut frags: Vec<(u32, u32, NodeId, String)> = 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 {
continue;
}
let text = extract_line_text(tl.runs, tl.font, lo, hi, tl.col_start);
if !text.is_empty() {
frags.push((tl.row, lo, tl.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 selection_text_lines(
arena: &NodeArena,
boxes: &[LayoutBox],
lines: &[(u32, u32, u32)],
) -> Vec<(u32, u32, u32)> {
let mut text_lines: Vec<FlowTextLine> = Vec::new();
collect_flow_lines(arena, boxes, &mut text_lines);
let mut out: Vec<(u32, u32, u32)> = 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 text_end = tl.col_start.saturating_add(line_width);
let lo = sel_cs.max(tl.col_start);
let hi = sel_ce.min(text_end);
if lo < hi {
out.push((sel_row, lo, hi));
}
}
}
out
}
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>>) {
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 = if line_h > font {
(line_h - font) / 2
} else {
font.saturating_sub(1) / 2
};
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 = if line_h > font {
(line_h - font) / 2
} else {
font.saturating_sub(1) / 2
};
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 {
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 = grapheme_width(g).max(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,
];