use hti_core::*;
use std::collections::HashMap;
pub fn build_scene(
nodes: &[LayoutNode],
assets: &HashMap<String, Vec<u8>>,
viewport: Rect,
warnings: &mut Vec<RenderWarning>,
) -> Scene {
let mut scene_nodes: Vec<SceneNode> = Vec::new();
let mut clip_map: HashMap<String, u32> = HashMap::new();
let mut clip_counter: u32 = 0;
let mut gradient_defs: Vec<GradientDef> = Vec::new();
let mut grad_counter: u32 = 0;
let mut blur_filters: Vec<BlurFilterDef> = Vec::new();
let mut filter_counter: u32 = 0;
for node in nodes {
let bounds = node.layout.bounds();
if !bounds.intersects(&viewport) && node.dom.tag != DomTag::Div {
continue;
}
let clip_id = node.layout.clip.map(|clip_rect| {
let key = format!(
"{:.0},{:.0},{:.0},{:.0}",
clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height
);
*clip_map.entry(key).or_insert_with(|| {
let id = clip_counter;
clip_counter += 1;
scene_nodes.push(SceneNode::Clip(ClipSceneNode {
id,
rect: clip_rect,
border_radius: node.style.border_radius,
}));
id
})
});
match node.dom.tag {
DomTag::Img => {
let src_key = node.dom.src.clone().unwrap_or_default();
let (image_bytes, image_mime, intrinsic_w, intrinsic_h) =
if let Some(bytes) = assets.get(&src_key).filter(|b| !b.is_empty()) {
let (iw, ih) = read_image_size(bytes);
(bytes.clone(), guess_mime(&src_key), iw, ih)
} else {
if assets.get(&src_key).map_or(true, |b| b.is_empty()) {
warnings.push(RenderWarning::ImageDecodeFailed {
src: src_key.clone(),
reason: "asset not found or empty".to_string(),
});
}
(
placeholder_png(),
"image/png".to_string(),
bounds.width,
bounds.height,
)
};
if let Some(shadow) = node.style.box_shadow {
push_shadow_rect(
&mut scene_nodes,
&mut blur_filters,
&mut filter_counter,
bounds,
shadow,
node.style.border_radius,
clip_id,
);
}
scene_nodes.push(SceneNode::Image(ImageSceneNode {
bounds,
src_key,
image_bytes,
image_mime,
object_fit: node.style.object_fit,
object_position: (node.style.object_position_x, node.style.object_position_y),
intrinsic_width: intrinsic_w,
intrinsic_height: intrinsic_h,
border_radius: node.style.border_radius,
opacity: node.style.opacity,
clip_id,
transform: node.style.transform,
}));
}
DomTag::Div | DomTag::Span if node.dom.is_leaf_text() => {
if let Some(metrics) = &node.text_metrics {
scene_nodes.push(SceneNode::Text(TextSceneNode {
bounds,
lines: metrics.lines.clone(),
font_size: node.style.font_size,
font_weight: node.style.font_weight,
font_family: node.style.font_family.clone(),
color: node.style.color,
line_height_px: metrics.line_height_px,
letter_spacing: node.style.letter_spacing,
text_align: node.style.text_align,
text_overflow: node.style.text_overflow,
white_space: node.style.white_space,
opacity: node.style.opacity,
clip_id,
transform: node.style.transform,
}));
}
}
DomTag::Div | DomTag::Span => {
let has_visual = !matches!(node.style.background, Background::None)
|| node.style.border_width > 0.0
|| node.style.box_shadow.is_some();
if !has_visual {
continue;
}
let (background, gradient_id) =
if let Background::LinearGradient(ref grad) = node.style.background {
let id = format!("grad{}", grad_counter);
grad_counter += 1;
gradient_defs.push(GradientDef {
id: id.clone(),
gradient: grad.clone(),
});
(node.style.background.clone(), Some(id))
} else {
(node.style.background.clone(), None)
};
if let Some(shadow) = node.style.box_shadow {
push_shadow_rect(
&mut scene_nodes,
&mut blur_filters,
&mut filter_counter,
bounds,
shadow,
node.style.border_radius,
clip_id,
);
}
scene_nodes.push(SceneNode::Rect(RectSceneNode {
bounds,
background,
gradient_id,
border_width: node.style.border_width,
border_color: node.style.border_color,
border_radius: node.style.border_radius,
opacity: node.style.opacity,
clip_id,
transform: node.style.transform,
backdrop_blur_radius: node.style.backdrop_blur_radius,
blur_radius: 0.0,
filter_id: None,
}));
}
}
}
let content_size = nodes.iter().fold(Size::new(0.0, 0.0), |acc, n| {
Size::new(
f32::max(acc.width, n.layout.x + n.layout.width),
f32::max(acc.height, n.layout.y + n.layout.height),
)
});
Scene {
nodes: scene_nodes,
gradient_defs,
blur_filters,
viewport,
content_size,
}
}
fn push_shadow_rect(
nodes: &mut Vec<SceneNode>,
blur_filters: &mut Vec<BlurFilterDef>,
filter_counter: &mut u32,
bounds: Rect,
shadow: BoxShadow,
border_radius: f32,
clip_id: Option<u32>,
) {
let shadow_bounds = Rect::new(
bounds.x + shadow.offset_x - shadow.spread_radius,
bounds.y + shadow.offset_y - shadow.spread_radius,
bounds.width + shadow.spread_radius * 2.0,
bounds.height + shadow.spread_radius * 2.0,
);
let filter_id = if shadow.blur_radius > 0.0 {
let id = format!("blur{}", filter_counter);
*filter_counter += 1;
blur_filters.push(BlurFilterDef {
id: id.clone(),
std_deviation: shadow.blur_radius / 2.0, });
Some(id)
} else {
None
};
nodes.push(SceneNode::Rect(RectSceneNode {
bounds: shadow_bounds,
background: Background::Color(shadow.color),
gradient_id: None,
border_width: 0.0,
border_color: Color::TRANSPARENT,
border_radius: border_radius + shadow.spread_radius,
opacity: shadow.color.a as f32 / 255.0,
clip_id,
transform: Transform::default(),
backdrop_blur_radius: 0.0,
blur_radius: shadow.blur_radius,
filter_id,
}));
}
fn read_image_size(bytes: &[u8]) -> (f32, f32) {
use image::ImageReader;
use std::io::Cursor;
if let Ok(reader) = ImageReader::new(Cursor::new(bytes)).with_guessed_format() {
if let Ok((w, h)) = reader.into_dimensions() {
return (w as f32, h as f32);
}
}
(0.0, 0.0) }
fn guess_mime(src: &str) -> String {
if src.ends_with(".png") {
"image/png".into()
} else if src.ends_with(".jpg") || src.ends_with(".jpeg") {
"image/jpeg".into()
} else if src.ends_with(".webp") {
"image/webp".into()
} else if src.ends_with(".gif") {
"image/gif".into()
} else {
"image/png".into()
}
}
fn placeholder_png() -> Vec<u8> {
vec![
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xd8,
0xd8, 0xd8, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]
}