use crate::font::registry::FontRegistry;
use crate::font::resolve::{ResolvedFont, resolve_font};
use crate::layout::line::LayoutLine;
use crate::layout::paragraph::{Alignment, break_into_lines};
use crate::shaping::run::ShapedRun;
use crate::shaping::shaper::{FontMetricsPx, font_metrics_px, shape_text};
pub struct BlockLayout {
pub block_id: usize,
pub position: usize,
pub lines: Vec<LayoutLine>,
pub y: f32,
pub height: f32,
pub top_margin: f32,
pub bottom_margin: f32,
pub left_margin: f32,
pub right_margin: f32,
pub list_marker: Option<ShapedListMarker>,
pub background_color: Option<[f32; 4]>,
}
pub struct ShapedListMarker {
pub run: ShapedRun,
pub x: f32,
}
#[derive(Clone)]
pub struct BlockLayoutParams {
pub block_id: usize,
pub position: usize,
pub text: String,
pub fragments: Vec<FragmentParams>,
pub alignment: Alignment,
pub top_margin: f32,
pub bottom_margin: f32,
pub left_margin: f32,
pub right_margin: f32,
pub text_indent: f32,
pub list_marker: String,
pub list_indent: f32,
pub tab_positions: Vec<f32>,
pub line_height_multiplier: Option<f32>,
pub non_breakable_lines: bool,
pub checkbox: Option<bool>,
pub background_color: Option<[f32; 4]>,
}
#[derive(Clone)]
pub struct FragmentParams {
pub text: String,
pub offset: usize,
pub length: usize,
pub font_family: Option<String>,
pub font_weight: Option<u32>,
pub font_bold: Option<bool>,
pub font_italic: Option<bool>,
pub font_point_size: Option<u32>,
pub underline_style: crate::types::UnderlineStyle,
pub overline: bool,
pub strikeout: bool,
pub is_link: bool,
pub letter_spacing: f32,
pub word_spacing: f32,
pub foreground_color: Option<[f32; 4]>,
pub underline_color: Option<[f32; 4]>,
pub background_color: Option<[f32; 4]>,
pub anchor_href: Option<String>,
pub tooltip: Option<String>,
pub vertical_alignment: crate::types::VerticalAlignment,
pub image_name: Option<String>,
pub image_width: f32,
pub image_height: f32,
}
pub fn layout_block(
registry: &FontRegistry,
params: &BlockLayoutParams,
available_width: f32,
scale_factor: f32,
) -> BlockLayout {
let effective_left_margin = params.left_margin + params.list_indent;
let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
let mut shaped_runs = Vec::new();
let mut default_metrics: Option<FontMetricsPx> = None;
for frag in ¶ms.fragments {
if let Some(ref image_name) = frag.image_name {
use crate::shaping::run::{ShapedGlyph, ShapedRun};
let image_glyph = ShapedGlyph {
glyph_id: 0,
cluster: 0,
x_advance: frag.image_width,
y_advance: 0.0,
x_offset: 0.0,
y_offset: 0.0,
font_face_id: crate::types::FontFaceId(0),
};
let run = ShapedRun {
font_face_id: crate::types::FontFaceId(0),
size_px: 0.0,
weight: 400,
glyphs: vec![image_glyph],
advance_width: frag.image_width,
text_range: frag.offset..frag.offset + frag.text.len(),
underline_style: frag.underline_style,
overline: false,
strikeout: false,
is_link: frag.is_link,
foreground_color: None,
underline_color: None,
background_color: None,
anchor_href: frag.anchor_href.clone(),
tooltip: frag.tooltip.clone(),
vertical_alignment: crate::types::VerticalAlignment::Normal,
image_name: Some(image_name.clone()),
image_height: frag.image_height,
};
shaped_runs.push(run);
continue;
}
let font_point_size = match frag.vertical_alignment {
crate::types::VerticalAlignment::SuperScript
| crate::types::VerticalAlignment::SubScript => frag
.font_point_size
.map(|s| ((s as f32 * 0.65) as u32).max(1)),
crate::types::VerticalAlignment::Normal => frag.font_point_size,
};
let resolved = resolve_font(
registry,
frag.font_family.as_deref(),
frag.font_weight,
frag.font_bold,
frag.font_italic,
font_point_size,
scale_factor,
);
if let Some(resolved) = resolved {
if default_metrics.is_none() {
default_metrics = font_metrics_px(registry, &resolved);
}
if let Some(mut run) = shape_text(registry, &resolved, &frag.text, frag.offset) {
run.underline_style = frag.underline_style;
run.overline = frag.overline;
run.strikeout = frag.strikeout;
run.is_link = frag.is_link;
run.foreground_color = frag.foreground_color;
run.underline_color = frag.underline_color;
run.background_color = frag.background_color;
run.anchor_href = frag.anchor_href.clone();
run.tooltip = frag.tooltip.clone();
run.vertical_alignment = frag.vertical_alignment;
if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
}
if !params.tab_positions.is_empty() {
apply_tab_stops(&mut run, &frag.text, ¶ms.tab_positions);
}
shaped_runs.push(run);
}
}
}
let metrics = default_metrics.unwrap_or_else(|| get_default_metrics(registry, scale_factor));
let wrap_width = if params.non_breakable_lines {
f32::INFINITY
} else {
content_width
};
let mut lines = break_into_lines(
shaped_runs,
¶ms.text,
wrap_width,
params.alignment,
params.text_indent,
&metrics,
);
let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
let mut y = 0.0f32;
for line in &mut lines {
if line_height_mul != 1.0 {
line.line_height *= line_height_mul;
}
line.y = y + line.ascent; y += line.line_height;
}
let content_height = y;
let total_height = params.top_margin + content_height + params.bottom_margin;
let list_marker = if params.checkbox.is_some() {
shape_checkbox_marker(registry, &metrics, params, scale_factor)
} else if !params.list_marker.is_empty() {
shape_list_marker(registry, &metrics, params, scale_factor)
} else {
None
};
BlockLayout {
block_id: params.block_id,
position: params.position,
lines,
y: 0.0, height: total_height,
top_margin: params.top_margin,
bottom_margin: params.bottom_margin,
left_margin: effective_left_margin,
right_margin: params.right_margin,
list_marker,
background_color: params.background_color,
}
}
fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
let mut extra_advance = 0.0f32;
for glyph in &mut run.glyphs {
glyph.x_advance += letter_spacing;
extra_advance += letter_spacing;
if word_spacing != 0.0 {
let byte_offset = glyph.cluster as usize;
if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
&& ch == ' '
{
glyph.x_advance += word_spacing;
extra_advance += word_spacing;
}
}
}
run.advance_width += extra_advance;
}
fn shape_list_marker(
registry: &FontRegistry,
_metrics: &FontMetricsPx,
params: &BlockLayoutParams,
scale_factor: f32,
) -> Option<ShapedListMarker> {
let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
let run = shape_text(registry, &resolved, ¶ms.list_marker, 0)?;
let gap = 4.0; let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
let marker_x = marker_x.max(params.left_margin);
Some(ShapedListMarker { run, x: marker_x })
}
fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
let default_tab = 48.0; let mut pen_x = 0.0f32;
for glyph in &mut run.glyphs {
let byte_offset = glyph.cluster as usize;
if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
&& ch == '\t'
{
let next_stop = tab_positions
.iter()
.find(|&&stop| stop > pen_x + 1.0)
.copied()
.unwrap_or_else(|| {
let last = tab_positions.last().copied().unwrap_or(0.0);
let increment = if tab_positions.len() >= 2 {
tab_positions[1] - tab_positions[0]
} else {
default_tab
};
let mut stop = last + increment;
while stop <= pen_x + 1.0 {
stop += increment;
}
stop
});
let tab_advance = next_stop - pen_x;
let delta = tab_advance - glyph.x_advance;
glyph.x_advance = tab_advance;
run.advance_width += delta;
}
pen_x += glyph.x_advance;
}
}
fn shape_checkbox_marker(
registry: &FontRegistry,
_metrics: &FontMetricsPx,
params: &BlockLayoutParams,
scale_factor: f32,
) -> Option<ShapedListMarker> {
let checked = params.checkbox?;
let marker_text = if checked { "\u{2611}" } else { "\u{2610}" };
let resolved = resolve_font(registry, None, None, None, None, None, scale_factor)?;
let run = shape_text(registry, &resolved, marker_text, 0)?;
let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
let fallback_text = if checked { "[x]" } else { "[ ]" };
shape_text(registry, &resolved, fallback_text, 0)?
} else {
run
};
let gap = 4.0;
let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
let marker_x = marker_x.max(params.left_margin);
Some(ShapedListMarker { run, x: marker_x })
}
fn get_default_metrics(registry: &FontRegistry, scale_factor: f32) -> FontMetricsPx {
if let Some(default_id) = registry.default_font() {
let resolved = ResolvedFont {
font_face_id: default_id,
size_px: registry.default_size_px(),
face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
swash_cache_key: registry
.get(default_id)
.map(|e| e.swash_cache_key)
.unwrap_or_default(),
scale_factor,
weight: 400,
};
if let Some(m) = font_metrics_px(registry, &resolved) {
return m;
}
}
FontMetricsPx {
ascent: 14.0,
descent: 4.0,
leading: 0.0,
underline_offset: -2.0,
strikeout_offset: 5.0,
stroke_size: 1.0,
}
}