use zenith_core::{Diagnostic, FontProvider, FontStyle, TextNode};
use zenith_layout::{
RustybuzzEngine, ShapeRequest, TextDirection, TextLayoutEngine, ZenithGlyphRun,
};
use crate::ir::{Color, SceneCommand};
use super::super::paint::resolve_property_color;
use super::super::util::{resolve_geometry_px, rotation_degrees};
use super::ctx::TabLeaderArgs;
use super::shape::{resolve_font_weight, run_to_scene_glyphs};
const TAB_LEADER_GAP_FACTOR: f64 = 1.0;
struct TabLeaderRow {
left_runs: Vec<ZenithGlyphRun>,
left_advance: f64,
right_runs: Vec<ZenithGlyphRun>,
right_advance: f64,
has_tab: bool,
}
fn shape_tab_leader_row(
row: &str,
families: &[String],
font_size: f32,
weight: u16,
engine: &RustybuzzEngine,
fonts: &dyn FontProvider,
) -> TabLeaderRow {
let (left_text, right_text, has_tab) = match row.split_once('\t') {
Some((l, r)) => (l, r, true),
None => (row, "", false),
};
let shape_seg = |seg: &str| -> (Vec<ZenithGlyphRun>, f64) {
if seg.is_empty() {
return (Vec::new(), 0.0);
}
let req = ShapeRequest {
text: seg,
families,
weight,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
match engine.shape_with_fallback(&req, fonts) {
Ok(result) => {
let adv: f64 = result.runs.iter().map(|r| r.advance_width as f64).sum();
(result.runs, adv)
}
Err(_) => (Vec::new(), 0.0),
}
};
let (left_runs, left_advance) = shape_seg(left_text);
let (right_runs, right_advance) = shape_seg(right_text);
TabLeaderRow {
left_runs,
left_advance,
right_runs,
right_advance,
has_tab,
}
}
fn emit_tab_leader_runs(
runs: &[ZenithGlyphRun],
start_x: f64,
y: f64,
color: Color,
glyph_stroke: (Option<Color>, Option<f64>),
commands: &mut Vec<SceneCommand>,
) {
let mut x = start_x;
for run in runs {
commands.push(SceneCommand::DrawGlyphRun {
x,
y,
font_id: run.font_id.clone(),
font_size: run.font_size,
color,
stroke_color: glyph_stroke.0,
stroke_width: glyph_stroke.1,
link: None,
selectable: true,
glyphs: run_to_scene_glyphs(run),
});
x += run.advance_width as f64;
}
}
pub(in crate::compile) fn compile_tab_leader(
text: &TextNode,
leader: &str,
families: &[String],
args: TabLeaderArgs,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
) -> f64 {
let TabLeaderArgs {
font_size,
node_fill_prop,
node_weight_prop,
node_opacity,
resolved,
env,
text_x,
text_y,
ctx,
glyph_stroke,
} = args;
let engine = env.engine;
let fonts = env.fonts;
let combined: String = text.spans.iter().map(|s| s.text.as_str()).collect();
if combined.is_empty() {
return 0.0;
}
let mut color = node_fill_prop
.and_then(|fp| resolve_property_color(fp, resolved, diagnostics, &text.id))
.unwrap_or(Color::srgb(0, 0, 0, 255));
color.a = (color.a as f64 * node_opacity * ctx.opacity).round() as u8;
let weight = resolve_font_weight(node_weight_prop, resolved, 400);
let Some(box_w) = resolve_geometry_px(text.w.as_ref(), resolved) else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"text node '{}' uses tab-leader but has no box width; skipped",
text.id
),
text.source_span,
Some(text.id.clone()),
));
return 0.0;
};
let leader_req = ShapeRequest {
text: leader,
families,
weight,
style: FontStyle::Normal,
font_size,
direction: TextDirection::Ltr,
};
let leader_run = match engine.shape_with_fallback(&leader_req, fonts) {
Ok(result) => result.runs.into_iter().next(),
Err(_) => None,
};
let leader_advance = leader_run
.as_ref()
.map(|r| r.advance_width as f64)
.unwrap_or(0.0);
let rows: Vec<TabLeaderRow> = combined
.split('\n')
.map(|row| shape_tab_leader_row(row, families, font_size, weight, engine, fonts))
.collect();
let (ascent, line_height) = rows
.iter()
.flat_map(|r| r.left_runs.iter().chain(r.right_runs.iter()))
.next()
.or(leader_run.as_ref())
.map(|r| (r.ascent as f64, r.line_height as f64))
.unwrap_or((0.0, 0.0));
let box_h_opt: Option<f64> = resolve_geometry_px(text.h.as_ref(), resolved);
let rot = rotation_degrees(text.rotate.as_ref());
let text_rot = rot
.zip(Some(box_w))
.zip(box_h_opt)
.map(|((a, bw), bh)| (a, text_x + bw / 2.0, text_y + bh / 2.0));
if let Some((angle, cx, cy)) = text_rot {
commands.push(SceneCommand::PushTransform {
angle_deg: angle,
cx,
cy,
});
}
let gap_pad = leader_advance * TAB_LEADER_GAP_FACTOR;
let box_right = text_x + box_w;
for (i, row) in rows.iter().enumerate() {
let baseline_y = text_y + ascent + (i as f64) * line_height;
emit_tab_leader_runs(
&row.left_runs,
text_x,
baseline_y,
color,
glyph_stroke,
commands,
);
if !row.has_tab {
continue;
}
let right_x = box_right - row.right_advance;
emit_tab_leader_runs(
&row.right_runs,
right_x,
baseline_y,
color,
glyph_stroke,
commands,
);
let leader_start = text_x + row.left_advance + gap_pad;
let leader_end = right_x - gap_pad;
let gap = leader_end - leader_start;
if leader_advance <= 0.0 || gap <= 0.0 {
diagnostics.push(Diagnostic::warning(
"text.overflow",
format!(
"text '{}': tab-leader row {} is too long to fit a leader \
in its box; no leader emitted",
text.id,
i + 1
),
text.source_span,
Some(text.id.clone()),
));
continue;
}
let count = (gap / leader_advance).floor() as usize;
if let Some(run) = leader_run.as_ref() {
let mut x = leader_start;
for _ in 0..count {
commands.push(SceneCommand::DrawGlyphRun {
x,
y: baseline_y,
font_id: run.font_id.clone(),
font_size: run.font_size,
color,
stroke_color: glyph_stroke.0,
stroke_width: glyph_stroke.1,
link: None,
selectable: true,
glyphs: run_to_scene_glyphs(run),
});
x += leader_advance;
}
}
}
if text_rot.is_some() {
commands.push(SceneCommand::PopTransform);
}
rows.len() as f64 * line_height
}