use std::sync::Arc;
use super::model::{Block, InlineStyle, ListKind, RichDoc};
use crate::text::{measure_advance, Font};
use crate::widgets::text_area::TextHAlign;
pub const INDENT_PX: f64 = 24.0;
pub const LIST_GUTTER_PX: f64 = 24.0;
pub const MARKER_GAP_PX: f64 = 6.0;
pub const LINE_SPACING: f64 = 1.35;
pub type FontResolver<'a> = dyn Fn(&InlineStyle) -> Arc<Font> + 'a;
#[derive(Clone)]
pub struct LineFragment {
pub text: String,
pub style: InlineStyle,
pub font: Arc<Font>,
pub font_size: f64,
pub x: f64,
pub width: f64,
pub ascent: f64,
pub descent: f64,
pub start_byte: usize,
}
#[derive(Clone)]
pub struct LineLayout {
pub fragments: Vec<LineFragment>,
pub width: f64,
pub height: f64,
pub ascent: f64,
pub descent: f64,
pub align_dx: f64,
pub baseline_from_top: f64,
pub start_byte: usize,
pub end_byte: usize,
}
#[derive(Clone)]
pub struct BlockLayout {
pub lines: Vec<LineLayout>,
pub text_left: f64,
pub marker: Option<String>,
pub marker_font: Option<Arc<Font>>,
pub marker_font_size: f64,
pub marker_x: f64,
pub height: f64,
pub width: f64,
}
#[derive(Clone)]
pub struct DocLayout {
pub blocks: Vec<BlockLayout>,
pub width: f64,
pub height: f64,
}
fn run_size(style: &InlineStyle, default_size: f64) -> f64 {
style.font_size.unwrap_or(default_size)
}
pub fn layout_doc(
doc: &RichDoc,
width: f64,
default_font_size: f64,
resolver: &FontResolver,
) -> DocLayout {
let numbers = ordered_numbers(&doc.blocks);
let mut blocks = Vec::with_capacity(doc.blocks.len());
let mut y = 0.0f64;
for (bi, block) in doc.blocks.iter().enumerate() {
let bl = layout_block(block, numbers[bi], width, default_font_size, resolver);
y += bl.height;
blocks.push(bl);
}
let max_w = blocks.iter().map(|b| b.width).fold(0.0f64, f64::max);
DocLayout {
blocks,
width: width.max(max_w),
height: y,
}
}
fn ordered_numbers(blocks: &[Block]) -> Vec<usize> {
let mut out = vec![0usize; blocks.len()];
let mut counters = [0usize; (MAX_LEVELS)];
let mut active = [false; MAX_LEVELS];
for (i, block) in blocks.iter().enumerate() {
let d = (block.indent as usize).min(MAX_LEVELS - 1);
match block.list {
ListKind::Ordered => {
counters[d] = if active[d] { counters[d] + 1 } else { 1 };
out[i] = counters[d];
for (k, a) in active.iter_mut().enumerate() {
*a = k == d;
}
}
_ => {
for a in active.iter_mut() {
*a = false;
}
}
}
}
out
}
const MAX_LEVELS: usize = 16;
fn layout_block(
block: &Block,
ordinal: usize,
width: f64,
default_font_size: f64,
resolver: &FontResolver,
) -> BlockLayout {
let base_indent = block.indent as f64 * INDENT_PX;
let is_list = block.list != ListKind::None;
let text_left = base_indent + if is_list { LIST_GUTTER_PX } else { 0.0 };
let avail = (width - text_left).max(1.0);
let lead_style = block
.runs
.first()
.map(|r| r.style.clone())
.unwrap_or_default();
let marker_font = resolver(&lead_style);
let marker_font_size = run_size(&lead_style, default_font_size);
let marker = match block.list {
ListKind::None => None,
ListKind::Bullet => Some("\u{2022}".to_string()),
ListKind::Ordered => Some(format!("{ordinal}.")),
};
let marker_x = if let Some(m) = &marker {
let mw = measure_advance(&marker_font, m, marker_font_size);
(text_left - MARKER_GAP_PX - mw).max(base_indent)
} else {
base_indent
};
let pieces = tokenize(block, default_font_size, resolver);
let mut lines = wrap(pieces, avail);
if lines.is_empty() {
let font = resolver(&lead_style);
let size = run_size(&lead_style, default_font_size);
lines.push(LineLayout {
fragments: Vec::new(),
width: 0.0,
height: (font.ascender_px(size) + font.descender_px(size)) * LINE_SPACING,
ascent: font.ascender_px(size),
descent: font.descender_px(size),
align_dx: 0.0,
baseline_from_top: 0.0,
start_byte: 0,
end_byte: 0,
});
}
let mut height = 0.0f64;
let mut max_line_w = 0.0f64;
for line in &mut lines {
finalize_line(line, avail, block.align);
height += line.height;
max_line_w = max_line_w.max(line.width);
}
BlockLayout {
lines,
text_left,
marker,
marker_font: Some(marker_font),
marker_font_size,
marker_x,
height,
width: text_left + max_line_w,
}
}
struct Piece {
text: String,
style: InlineStyle,
font: Arc<Font>,
font_size: f64,
width: f64,
ascent: f64,
descent: f64,
is_ws: bool,
start_byte: usize,
}
fn tokenize(block: &Block, default_font_size: f64, resolver: &FontResolver) -> Vec<Piece> {
let mut pieces = Vec::new();
let mut run_start = 0usize;
for run in &block.runs {
if run.text.is_empty() {
continue;
}
let font = resolver(&run.style);
let size = run_size(&run.style, default_font_size);
let ascent = font.ascender_px(size);
let descent = font.descender_px(size);
let mut chunk_off = 0usize;
for chunk in split_ws(&run.text) {
let is_ws = chunk.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
let width = measure_advance(&font, chunk, size);
pieces.push(Piece {
text: chunk.to_string(),
style: run.style.clone(),
font: Arc::clone(&font),
font_size: size,
width,
ascent,
descent,
is_ws,
start_byte: run_start + chunk_off,
});
chunk_off += chunk.len();
}
run_start += run.text.len();
}
pieces
}
fn split_ws(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut start = 0usize;
let mut prev_ws: Option<bool> = None;
for (i, c) in s.char_indices() {
let ws = c.is_whitespace();
match prev_ws {
Some(p) if p != ws => {
out.push(&s[start..i]);
start = i;
}
_ => {}
}
prev_ws = Some(ws);
}
if start < s.len() {
out.push(&s[start..]);
}
out
}
fn wrap(pieces: Vec<Piece>, avail: f64) -> Vec<LineLayout> {
let mut lines: Vec<LineLayout> = Vec::new();
let mut cur: Vec<Piece> = Vec::new();
let mut cur_w = 0.0f64;
let mut pending_space: Option<Piece> = None;
let mut iter = pieces.into_iter().peekable();
while let Some(piece) = iter.next() {
if piece.is_ws {
if !cur.is_empty() {
pending_space = Some(piece);
}
continue;
}
let mut word = vec![piece];
while let Some(next) = iter.peek() {
if next.is_ws {
break;
}
word.push(iter.next().unwrap());
}
let word_w: f64 = word.iter().map(|p| p.width).sum();
let space_w = pending_space.as_ref().map(|p| p.width).unwrap_or(0.0);
if !cur.is_empty() && cur_w + space_w + word_w > avail {
lines.push(finish_pieces(std::mem::take(&mut cur)));
cur_w = 0.0;
pending_space = None;
} else if let Some(sp) = pending_space.take() {
cur_w += sp.width;
cur.push(sp);
}
cur_w += word_w;
cur.extend(word);
}
if !cur.is_empty() {
lines.push(finish_pieces(cur));
}
lines
}
fn finish_pieces(pieces: Vec<Piece>) -> LineLayout {
let mut fragments: Vec<LineFragment> = Vec::new();
let mut x = 0.0f64;
let mut ascent = 0.0f64;
let mut descent = 0.0f64;
for p in pieces {
ascent = ascent.max(p.ascent);
descent = descent.max(p.descent);
if let Some(last) = fragments.last_mut() {
if last.style == p.style
&& Arc::ptr_eq(&last.font, &p.font)
&& (last.font_size - p.font_size).abs() < 1e-9
{
last.text.push_str(&p.text);
last.width += p.width;
x += p.width;
continue;
}
}
fragments.push(LineFragment {
text: p.text,
style: p.style,
font: p.font,
font_size: p.font_size,
x,
width: p.width,
ascent: p.ascent,
descent: p.descent,
start_byte: p.start_byte,
});
x += p.width;
}
let (start_byte, end_byte) = match (fragments.first(), fragments.last()) {
(Some(first), Some(last)) => (first.start_byte, last.start_byte + last.text.len()),
_ => (0, 0),
};
LineLayout {
fragments,
width: x,
height: 0.0,
ascent,
descent,
align_dx: 0.0,
baseline_from_top: 0.0,
start_byte,
end_byte,
}
}
fn finalize_line(line: &mut LineLayout, avail: f64, align: TextHAlign) {
let content = line.ascent + line.descent;
line.height = content * LINE_SPACING;
let leading = line.height - content;
line.baseline_from_top = leading * 0.5 + line.ascent;
line.align_dx = match align {
TextHAlign::Left => 0.0,
TextHAlign::Center => ((avail - line.width) * 0.5).max(0.0),
TextHAlign::Right => (avail - line.width).max(0.0),
};
}
#[cfg(test)]
#[path = "layout_tests.rs"]
mod layout_tests;