use zenith_core::{Diagnostic, FrameNode, Node, dim_to_px};
use crate::ir::SceneCommand;
use super::super::paint::{
NodeEffect, resolve_property_filter, resolve_property_mask, resolve_property_shadow,
};
use super::super::util::{
blend_mode_ir, resolve_geometry_px, resolve_property_dimension_px, rotation_degrees,
unsupported_unit_diag,
};
use super::super::{NodeCtx, RenderCtx, compile_node, style_prop};
use super::flow::{node_declared_h, node_declared_w, node_skipped_in_flow, with_flow_box};
use super::wrap::emit_wrapped_container;
#[derive(Clone, Copy)]
struct FrameBox {
x: f64,
y: f64,
w: f64,
h: f64,
}
pub(in crate::compile) fn compile_frame(
frame: &FrameNode,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
connector_strokes: &mut Vec<usize>,
ctx: RenderCtx,
) {
if frame.visible == Some(false) {
return;
}
let (Some(x_dim), Some(y_dim), Some(w_dim), Some(h_dim)) =
(&frame.x, &frame.y, &frame.w, &frame.h)
else {
diagnostics.push(Diagnostic::advisory(
"scene.missing_geometry",
format!(
"frame '{}' is missing one or more geometry properties (x, y, w, h); \
skipped",
frame.id
),
frame.source_span,
Some(frame.id.clone()),
));
return;
};
let Some(frame_x) = resolve_geometry_px(Some(x_dim), cx.resolved) else {
diagnostics.push(unsupported_unit_diag(
"frame",
&frame.id,
"x",
frame.source_span,
));
return;
};
let Some(frame_y) = resolve_geometry_px(Some(y_dim), cx.resolved) else {
diagnostics.push(unsupported_unit_diag(
"frame",
&frame.id,
"y",
frame.source_span,
));
return;
};
let Some(frame_w) = resolve_geometry_px(Some(w_dim), cx.resolved) else {
diagnostics.push(unsupported_unit_diag(
"frame",
&frame.id,
"w",
frame.source_span,
));
return;
};
let Some(frame_h) = resolve_geometry_px(Some(h_dim), cx.resolved) else {
diagnostics.push(unsupported_unit_diag(
"frame",
&frame.id,
"h",
frame.source_span,
));
return;
};
let frame_rot = rotation_degrees(frame.rotate.as_ref());
if let Some(angle) = frame_rot {
let cx_pivot = ctx.dx + frame_x + frame_w / 2.0;
let cy_pivot = ctx.dy + frame_y + frame_h / 2.0;
commands.push(SceneCommand::PushTransform {
angle_deg: angle,
cx: cx_pivot,
cy: cy_pivot,
});
}
let frame_opacity = frame.opacity.unwrap_or(1.0).clamp(0.0, 1.0);
let blend = blend_mode_ir(frame.blend_mode.as_deref());
let child_opacity = match blend {
Some(blend_mode) => {
commands.push(SceneCommand::PushLayer {
opacity: ctx.opacity * frame_opacity,
blend_mode: Some(blend_mode),
});
ctx.opacity
}
None => ctx.opacity * frame_opacity,
};
let blur_sigma = frame
.blur
.as_ref()
.and_then(|d| dim_to_px(d.value, &d.unit))
.filter(|&s| s > 0.0);
let effect: Option<NodeEffect> = if let Some(sigma) = blur_sigma {
Some(NodeEffect::Blur(sigma))
} else if let Some(shadows) = frame
.shadow
.as_ref()
.and_then(|p| resolve_property_shadow(p, cx.resolved, &frame.id))
{
Some(NodeEffect::Shadow(shadows))
} else {
frame
.filter
.as_ref()
.and_then(|p| resolve_property_filter(p, cx.resolved, &frame.id))
.map(NodeEffect::Filter)
};
let mask = frame
.mask
.as_ref()
.and_then(|p| resolve_property_mask(p, cx.resolved, (frame_x, frame_y, frame_w, frame_h)));
let child_ctx = RenderCtx {
opacity: child_opacity,
dx: ctx.dx, dy: ctx.dy, baseline_grid: ctx.baseline_grid,
};
let fbox = FrameBox {
x: frame_x,
y: frame_y,
w: frame_w,
h: frame_h,
};
if effect.is_none() && mask.is_none() {
compile_frame_clipped_children(
frame,
fbox,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
} else {
let mut draws = Vec::new();
let mut local_connector_strokes = Vec::new();
compile_frame_clipped_children(
frame,
fbox,
cx,
&mut draws,
diagnostics,
&mut local_connector_strokes,
child_ctx,
);
emit_wrapped_container(
commands,
draws,
effect,
mask,
connector_strokes,
local_connector_strokes,
);
}
if blend.is_some() {
commands.push(SceneCommand::PopLayer);
}
if frame_rot.is_some() {
commands.push(SceneCommand::PopTransform);
}
}
fn compile_frame_clipped_children(
frame: &FrameNode,
fbox: FrameBox,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
connector_strokes: &mut Vec<usize>,
child_ctx: RenderCtx,
) {
commands.push(SceneCommand::PushClip {
x: fbox.x,
y: fbox.y,
w: fbox.w,
h: fbox.h,
});
match frame.layout.as_deref() {
Some("flow") => {
compile_frame_flow(
frame,
fbox,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
}
Some("grid") => {
compile_frame_grid(
frame,
fbox,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
}
_ => {
for child in &frame.children {
compile_node(
child,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
}
}
}
commands.push(SceneCommand::PopClip);
}
fn frame_pad_gap(frame: &FrameNode, cx: NodeCtx) -> (f64, f64) {
let pad = resolve_property_dimension_px(
style_prop(&frame.style, cx.style_map, "padding"),
cx.resolved,
0.0,
);
let gap = resolve_property_dimension_px(
style_prop(&frame.style, cx.style_map, "gap"),
cx.resolved,
0.0,
);
(pad, gap)
}
fn compile_frame_flow(
frame: &FrameNode,
fbox: FrameBox,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
connector_strokes: &mut Vec<usize>,
child_ctx: RenderCtx,
) {
let FrameBox {
x: frame_x,
y: frame_y,
w: frame_w,
..
} = fbox;
let (pad, gap) = frame_pad_gap(frame, cx);
let content_left = frame_x + pad;
let content_top = frame_y + pad;
let content_w = (frame_w - 2.0 * pad).max(0.0);
let laid_out: Vec<&Node> = frame
.children
.iter()
.filter(|c| !node_skipped_in_flow(c))
.collect();
let last_idx = laid_out.len().saturating_sub(1);
let mut cursor_y = content_top;
for (i, child) in laid_out.iter().enumerate() {
let child_w = node_declared_w(child, cx.resolved).unwrap_or(content_w);
let declared_h = node_declared_h(child, cx.resolved);
let positioned = with_flow_box(child, content_left, cursor_y, child_w, declared_h);
let measured_h = compile_node(
&positioned,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
let advance = declared_h.unwrap_or(measured_h);
cursor_y += advance;
if i != last_idx {
cursor_y += gap;
}
}
}
fn compile_frame_grid(
frame: &FrameNode,
fbox: FrameBox,
cx: NodeCtx,
commands: &mut Vec<SceneCommand>,
diagnostics: &mut Vec<Diagnostic>,
connector_strokes: &mut Vec<usize>,
child_ctx: RenderCtx,
) {
let FrameBox {
x: frame_x,
y: frame_y,
w: frame_w,
h: frame_h,
} = fbox;
let (pad, gap) = frame_pad_gap(frame, cx);
let content_left = frame_x + pad;
let content_top = frame_y + pad;
let content_w = (frame_w - 2.0 * pad).max(0.0);
let content_h = (frame_h - 2.0 * pad).max(0.0);
let participating: Vec<&Node> = frame
.children
.iter()
.filter(|c| !node_skipped_in_flow(c))
.collect();
let n = participating.len();
let cols = frame.columns.unwrap_or(1).max(1) as usize;
let effective_rows = frame
.rows
.map(|r| r.max(1) as usize)
.unwrap_or_else(|| n.div_ceil(cols).max(1));
let col_w = ((content_w - (cols - 1) as f64 * gap) / cols as f64).max(0.0);
let row_h = ((content_h - (effective_rows - 1) as f64 * gap) / effective_rows as f64).max(0.0);
for (i, child) in participating.iter().enumerate() {
let col = i % cols;
let row = i / cols;
let cell_x = content_left + col as f64 * (col_w + gap);
let cell_y = content_top + row as f64 * (row_h + gap);
let positioned = with_flow_box(child, cell_x, cell_y, col_w, Some(row_h));
let _ = compile_node(
&positioned,
cx,
commands,
diagnostics,
connector_strokes,
child_ctx,
);
}
}