use std::collections::BTreeMap;
use zenith_core::{
Diagnostic, FontProvider, PropertyValue, ResolvedToken, ShapeNode, Style, TextNode,
};
use zenith_layout::RustybuzzEngine;
use crate::ir::{Paint, SceneCommand, StrokeAlign};
use super::super::RenderCtx;
use super::super::anchor::AnchorMap;
use super::super::chain::ChainAssignments;
use super::super::paint::resolve_property_color;
use super::super::style_prop;
use super::super::text::{
MeasureEnv, TextCompileEnv, compile_text, empty_md_blocks, measure_text_wrapped_height,
resolve_text_families,
};
use super::super::util::{
AxisTarget, missing_geometry_diag, px_prop, resolve_anchored_axis, resolve_geometry_px,
resolve_property_dimension_px, rotation_degrees, unsupported_unit_diag,
};
#[derive(Clone, Copy)]
pub(in crate::compile) struct ShapeCompileEnv<'a> {
pub(in crate::compile) resolved: &'a BTreeMap<String, ResolvedToken>,
pub(in crate::compile) style_map: &'a BTreeMap<&'a str, &'a Style>,
pub(in crate::compile) fonts: &'a dyn FontProvider,
pub(in crate::compile) engine: &'a RustybuzzEngine,
pub(in crate::compile) chains: &'a ChainAssignments,
pub(in crate::compile) footnote_markers: &'a BTreeMap<String, String>,
pub(in crate::compile) node_boxes: &'a BTreeMap<String, (f64, f64, f64, f64)>,
pub(in crate::compile) anchors: &'a AnchorMap,
pub(in crate::compile) ctx: RenderCtx,
}
#[derive(Clone, Copy)]
struct ShapeBox {
x: f64,
y: f64,
w: f64,
h: f64,
}
#[derive(Clone, Copy)]
struct ShapeBg<'a> {
x: f64,
y: f64,
w: f64,
h: f64,
color_op: f64,
fill_prop: Option<&'a PropertyValue>,
stroke_prop: Option<&'a PropertyValue>,
stroke_width: f64,
}
pub(in crate::compile) fn compile_shape(
shape: &ShapeNode,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
env: ShapeCompileEnv,
) {
let resolved = env.resolved;
let style_map = env.style_map;
let ctx = env.ctx;
if shape.visible == Some(false) {
return;
}
let (Some(w_dim), Some(h_dim)) = (&shape.w, &shape.h) else {
diagnostics.push(missing_geometry_diag("shape", &shape.id, shape.source_span));
return;
};
let Some(w) = resolve_geometry_px(Some(w_dim), resolved) else {
diagnostics.push(unsupported_unit_diag(
"shape",
&shape.id,
"w",
shape.source_span,
));
return;
};
let Some(h) = resolve_geometry_px(Some(h_dim), resolved) else {
diagnostics.push(unsupported_unit_diag(
"shape",
&shape.id,
"h",
shape.source_span,
));
return;
};
let anchor_xy = env.anchors.get(&shape.id).copied();
let Some(x_raw) = resolve_anchored_axis(
AxisTarget {
kind: "shape",
node_id: &shape.id,
axis: "x",
},
shape.x.as_ref(),
resolved,
anchor_xy.map(|(ax, _)| ax),
shape.source_span,
diagnostics,
) else {
return;
};
let Some(y_raw) = resolve_anchored_axis(
AxisTarget {
kind: "shape",
node_id: &shape.id,
axis: "y",
},
shape.y.as_ref(),
resolved,
anchor_xy.map(|(_, ay)| ay),
shape.source_span,
diagnostics,
) else {
return;
};
let x = x_raw + ctx.dx;
let y = y_raw + ctx.dy;
let geom = ShapeBox { x, y, w, h };
let node_opacity = shape.opacity.unwrap_or(1.0).clamp(0.0, 1.0);
let color_op = node_opacity * ctx.opacity;
let fill_prop: Option<&PropertyValue> = shape
.fill
.as_ref()
.or_else(|| style_prop(&shape.style, style_map, "fill"));
let stroke_prop = shape
.stroke
.as_ref()
.or_else(|| style_prop(&shape.style, style_map, "stroke"));
let stroke_width = {
let sw = shape
.stroke_width
.clone()
.or_else(|| style_prop(&shape.style, style_map, "stroke-width").cloned());
resolve_property_dimension_px(sw.as_ref(), resolved, 1.0)
};
let bg = ShapeBg {
x,
y,
w,
h,
color_op,
fill_prop,
stroke_prop,
stroke_width,
};
let rot = rotation_degrees(shape.rotate.as_ref());
if let Some(angle) = rot {
let cx = x + w / 2.0;
let cy = y + h / 2.0;
commands.push(SceneCommand::PushTransform {
angle_deg: angle,
cx,
cy,
});
}
match shape.kind.as_deref() {
Some("ellipse") => {
emit_shape_ellipse(shape, resolved, diagnostics, commands, bg);
}
Some("decision") => {
emit_shape_decision(shape, resolved, diagnostics, commands, bg);
}
Some("terminator") => {
emit_shape_rounded_rect(shape, resolved, diagnostics, commands, h / 2.0, bg);
}
_ => {
let radius_prop = shape
.radius
.clone()
.or_else(|| style_prop(&shape.style, style_map, "radius").cloned());
let radius = resolve_property_dimension_px(radius_prop.as_ref(), resolved, 0.0);
emit_shape_rounded_rect(shape, resolved, diagnostics, commands, radius, bg);
}
}
emit_shape_label(shape, commands, diagnostics, env, geom);
if rot.is_some() {
commands.push(SceneCommand::PopTransform);
}
}
fn emit_shape_label(
shape: &ShapeNode,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
env: ShapeCompileEnv,
geom: ShapeBox,
) {
let ShapeCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers,
node_boxes,
anchors,
ctx,
} = env;
let ShapeBox { x, y, w, h } = geom;
if shape.spans.is_empty() {
return;
}
let pad = resolve_property_dimension_px(shape.padding.as_ref(), resolved, 0.0);
let content_x = x + pad;
let content_y = y + pad;
let content_w = (w - 2.0 * pad).max(0.0);
let content_h = (h - 2.0 * pad).max(0.0);
if content_w <= 0.0 || content_h <= 0.0 {
return;
}
let align = match shape.h_align.as_deref() {
Some("end") => Some("end".to_owned()),
Some("start") => Some("start".to_owned()),
_ => Some("center".to_owned()),
};
let mut synth = TextNode {
id: format!("{}/label", shape.id),
name: None,
role: None,
x: Some(px_prop(content_x)),
y: Some(px_prop(content_y)),
w: Some(px_prop(content_w)),
h: Some(px_prop(content_h)),
align,
v_align: None,
direction: None,
overflow: None,
overflow_wrap: None,
style: shape.text_style.clone(),
fill: None,
stroke: None,
stroke_width: None,
contrast_bg: None,
font_family: None,
font_size: None,
font_size_min: None,
font_weight: None,
shadow: None,
filter: None,
mask: None,
blend_mode: None,
blur: None,
opacity: None,
visible: None,
locked: None,
selectable: None,
rotate: None,
chain: None,
drop_cap_lines: None,
hyphenate: None,
widow_orphan: None,
tab_leader: None,
text_exclusion: None,
padding_left: None,
text_indent: None,
content_format: None,
src: None,
bullet: None,
bullet_gap: None,
anchor: None,
anchor_zone: None,
anchor_sibling: None,
anchor_edge: None,
anchor_gap: None,
anchor_parent: None,
spans: shape.spans.clone(),
block_styles: Vec::new(),
source_span: shape.source_span,
unknown_props: BTreeMap::new(),
};
let families = resolve_text_families(&synth, resolved, style_map, fonts, diagnostics);
let wrapped_h = measure_text_wrapped_height(
&synth,
content_w,
&families,
MeasureEnv {
resolved,
style_map,
fonts,
engine,
},
diagnostics,
)
.unwrap_or(0.0);
let v_offset = match shape.v_align.as_deref() {
Some("top") => 0.0,
Some("bottom") => (content_h - wrapped_h).max(0.0),
_ => ((content_h - wrapped_h) / 2.0).max(0.0),
};
synth.y = Some(px_prop(content_y + v_offset));
let label_ctx = RenderCtx {
dx: 0.0,
dy: 0.0,
..ctx
};
let _ = compile_text(
&synth,
TextCompileEnv {
resolved,
style_map,
fonts,
engine,
chains,
footnote_markers,
node_boxes,
anchors,
md_blocks: empty_md_blocks(),
page_block_styles: &[],
doc_block_styles: &[],
},
commands,
diagnostics,
label_ctx,
);
}
fn emit_shape_rounded_rect(
shape: &ShapeNode,
resolved: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
commands: &mut Vec<SceneCommand>,
radius: f64,
bg: ShapeBg,
) {
let ShapeBg {
x,
y,
w,
h,
color_op,
fill_prop,
stroke_prop,
stroke_width,
} = bg;
let is_rounded = radius > 0.0;
if let Some(fill_prop) = fill_prop
&& let Some(mut color) = resolve_property_color(fill_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
if is_rounded {
commands.push(SceneCommand::FillRoundedRect {
x,
y,
w,
h,
radius,
radii: None,
paint: Paint::solid(color),
});
} else {
commands.push(SceneCommand::FillRect {
x,
y,
w,
h,
paint: Paint::solid(color),
});
}
}
if let Some(stroke_prop) = stroke_prop
&& let Some(mut color) =
resolve_property_color(stroke_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
let half = stroke_width / 2.0;
let adjust_inside = |v: f64| if v > 0.0 { (v - half).max(0.0) } else { 0.0 };
let adjust_outside = |v: f64| if v > 0.0 { v + half } else { 0.0 };
let (sx, sy, sw_geom, sh_geom, sradius) = match shape.stroke_alignment.as_deref() {
Some("inside") => (
x + half,
y + half,
w - stroke_width,
h - stroke_width,
adjust_inside(radius),
),
Some("outside") => (
x - half,
y - half,
w + stroke_width,
h + stroke_width,
adjust_outside(radius),
),
_ => (x, y, w, h, radius),
};
let stroke_is_rounded = sradius > 0.0;
if sw_geom > 0.0 && sh_geom > 0.0 {
if stroke_is_rounded {
commands.push(SceneCommand::StrokeRoundedRect {
x: sx,
y: sy,
w: sw_geom,
h: sh_geom,
radius: sradius,
radii: None,
color,
stroke_width,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
} else {
commands.push(SceneCommand::StrokeRect {
x: sx,
y: sy,
w: sw_geom,
h: sh_geom,
color,
stroke_width,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
}
}
}
}
fn emit_shape_ellipse(
shape: &ShapeNode,
resolved: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
commands: &mut Vec<SceneCommand>,
bg: ShapeBg,
) {
let ShapeBg {
x,
y,
w,
h,
color_op,
fill_prop,
stroke_prop,
stroke_width,
} = bg;
if let Some(fill_prop) = fill_prop
&& let Some(mut color) = resolve_property_color(fill_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
commands.push(SceneCommand::FillEllipse {
x,
y,
w,
h,
rx: None,
ry: None,
paint: Paint::solid(color),
});
}
if let Some(stroke_prop) = stroke_prop
&& let Some(mut color) =
resolve_property_color(stroke_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
commands.push(SceneCommand::StrokeEllipse {
x,
y,
w,
h,
rx: None,
ry: None,
color,
stroke_width,
stroke_dash: None,
stroke_gap: None,
stroke_linecap: None,
});
}
}
fn emit_shape_decision(
shape: &ShapeNode,
resolved: &BTreeMap<String, ResolvedToken>,
diagnostics: &mut Vec<Diagnostic>,
commands: &mut Vec<SceneCommand>,
bg: ShapeBg,
) {
let ShapeBg {
x,
y,
w,
h,
color_op,
fill_prop,
stroke_prop,
stroke_width,
} = bg;
let flat_points = vec![
x + w / 2.0,
y, x + w,
y + h / 2.0, x + w / 2.0,
y + h, x,
y + h / 2.0, ];
if let Some(fill_prop) = fill_prop
&& let Some(mut color) = resolve_property_color(fill_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
commands.push(SceneCommand::FillPolygon {
points: flat_points.clone(),
paint: Paint::solid(color),
even_odd: false,
});
}
if let Some(stroke_prop) = stroke_prop
&& let Some(mut color) =
resolve_property_color(stroke_prop, resolved, diagnostics, &shape.id)
{
color.a = (color.a as f64 * color_op).round() as u8;
commands.push(SceneCommand::StrokePolyline {
points: flat_points,
color,
stroke_width,
closed: true,
align: StrokeAlign::Center,
fill_even_odd: false,
});
}
}