Skip to main content

hti_scene/
lib.rs

1use hti_core::*;
2use std::collections::HashMap;
3
4pub fn build_scene(
5    nodes: &[LayoutNode],
6    assets: &HashMap<String, Vec<u8>>,
7    viewport: Rect,
8    warnings: &mut Vec<RenderWarning>,
9) -> Scene {
10    let mut scene_nodes: Vec<SceneNode> = Vec::new();
11    let mut clip_map: HashMap<String, u32> = HashMap::new();
12    let mut clip_counter: u32 = 0;
13    let mut gradient_defs: Vec<GradientDef> = Vec::new();
14    let mut grad_counter: u32 = 0;
15    let mut blur_filters: Vec<BlurFilterDef> = Vec::new();
16    let mut filter_counter: u32 = 0;
17
18    for node in nodes {
19        let bounds = node.layout.bounds();
20
21        // Culling: bỏ node ngoài viewport (chỉ cull non-div)
22        if !bounds.intersects(&viewport) && node.dom.tag != DomTag::Div {
23            continue;
24        }
25
26        // Resolve clip_id từ clip rect của node
27        let clip_id = node.layout.clip.map(|clip_rect| {
28            let key = format!(
29                "{:.0},{:.0},{:.0},{:.0}",
30                clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height
31            );
32            *clip_map.entry(key).or_insert_with(|| {
33                let id = clip_counter;
34                clip_counter += 1;
35                scene_nodes.push(SceneNode::Clip(ClipSceneNode {
36                    id,
37                    rect: clip_rect,
38                    border_radius: node.style.border_radius,
39                }));
40                id
41            })
42        });
43
44        match node.dom.tag {
45            DomTag::Img => {
46                let src_key = node.dom.src.clone().unwrap_or_default();
47                let (image_bytes, image_mime, intrinsic_w, intrinsic_h) =
48                    if let Some(bytes) = assets.get(&src_key).filter(|b| !b.is_empty()) {
49                        let (iw, ih) = read_image_size(bytes);
50                        (bytes.clone(), guess_mime(&src_key), iw, ih)
51                    } else {
52                        if assets.get(&src_key).map_or(true, |b| b.is_empty()) {
53                            warnings.push(RenderWarning::ImageDecodeFailed {
54                                src: src_key.clone(),
55                                reason: "asset not found or empty".to_string(),
56                            });
57                        }
58                        (
59                            placeholder_png(),
60                            "image/png".to_string(),
61                            bounds.width,
62                            bounds.height,
63                        )
64                    };
65
66                if let Some(shadow) = node.style.box_shadow {
67                    push_shadow_rect(
68                        &mut scene_nodes,
69                        &mut blur_filters,
70                        &mut filter_counter,
71                        bounds,
72                        shadow,
73                        node.style.border_radius,
74                        clip_id,
75                    );
76                }
77
78                scene_nodes.push(SceneNode::Image(ImageSceneNode {
79                    bounds,
80                    src_key,
81                    image_bytes,
82                    image_mime,
83                    object_fit: node.style.object_fit,
84                    object_position: (node.style.object_position_x, node.style.object_position_y),
85                    intrinsic_width: intrinsic_w,
86                    intrinsic_height: intrinsic_h,
87                    border_radius: node.style.border_radius,
88                    opacity: node.style.opacity,
89                    clip_id,
90                    transform: node.style.transform,
91                }));
92            }
93
94            DomTag::Div | DomTag::Span if node.dom.is_leaf_text() => {
95                if let Some(metrics) = &node.text_metrics {
96                    scene_nodes.push(SceneNode::Text(TextSceneNode {
97                        bounds,
98                        lines: metrics.lines.clone(),
99                        font_size: node.style.font_size,
100                        font_weight: node.style.font_weight,
101                        font_family: node.style.font_family.clone(),
102                        color: node.style.color,
103                        line_height_px: metrics.line_height_px,
104                        letter_spacing: node.style.letter_spacing,
105                        text_align: node.style.text_align,
106                        text_overflow: node.style.text_overflow,
107                        white_space: node.style.white_space,
108                        opacity: node.style.opacity,
109                        clip_id,
110                        transform: node.style.transform,
111                    }));
112                }
113            }
114
115            DomTag::Div | DomTag::Span => {
116                let has_visual = !matches!(node.style.background, Background::None)
117                    || node.style.border_width > 0.0
118                    || node.style.box_shadow.is_some();
119                if !has_visual {
120                    continue;
121                }
122
123                // Gradient def — ID gắn trực tiếp vào RectSceneNode
124                let (background, gradient_id) =
125                    if let Background::LinearGradient(ref grad) = node.style.background {
126                        let id = format!("grad{}", grad_counter);
127                        grad_counter += 1;
128                        gradient_defs.push(GradientDef {
129                            id: id.clone(),
130                            gradient: grad.clone(),
131                        });
132                        (node.style.background.clone(), Some(id))
133                    } else {
134                        (node.style.background.clone(), None)
135                    };
136
137                if let Some(shadow) = node.style.box_shadow {
138                    push_shadow_rect(
139                        &mut scene_nodes,
140                        &mut blur_filters,
141                        &mut filter_counter,
142                        bounds,
143                        shadow,
144                        node.style.border_radius,
145                        clip_id,
146                    );
147                }
148
149                scene_nodes.push(SceneNode::Rect(RectSceneNode {
150                    bounds,
151                    background,
152                    gradient_id,
153                    border_width: node.style.border_width,
154                    border_color: node.style.border_color,
155                    border_radius: node.style.border_radius,
156                    opacity: node.style.opacity,
157                    clip_id,
158                    transform: node.style.transform,
159                    backdrop_blur_radius: node.style.backdrop_blur_radius,
160                    blur_radius: 0.0,
161                    filter_id: None,
162                }));
163            }
164        }
165    }
166
167    let content_size = nodes.iter().fold(Size::new(0.0, 0.0), |acc, n| {
168        Size::new(
169            f32::max(acc.width, n.layout.x + n.layout.width),
170            f32::max(acc.height, n.layout.y + n.layout.height),
171        )
172    });
173
174    Scene {
175        nodes: scene_nodes,
176        gradient_defs,
177        blur_filters,
178        viewport,
179        content_size,
180    }
181}
182
183fn push_shadow_rect(
184    nodes: &mut Vec<SceneNode>,
185    blur_filters: &mut Vec<BlurFilterDef>,
186    filter_counter: &mut u32,
187    bounds: Rect,
188    shadow: BoxShadow,
189    border_radius: f32,
190    clip_id: Option<u32>,
191) {
192    let shadow_bounds = Rect::new(
193        bounds.x + shadow.offset_x - shadow.spread_radius,
194        bounds.y + shadow.offset_y - shadow.spread_radius,
195        bounds.width + shadow.spread_radius * 2.0,
196        bounds.height + shadow.spread_radius * 2.0,
197    );
198
199    // Tạo blur filter nếu có blur_radius > 0
200    let filter_id = if shadow.blur_radius > 0.0 {
201        let id = format!("blur{}", filter_counter);
202        *filter_counter += 1;
203        blur_filters.push(BlurFilterDef {
204            id: id.clone(),
205            std_deviation: shadow.blur_radius / 2.0, // CSS blur → SVG stdDeviation
206        });
207        Some(id)
208    } else {
209        None
210    };
211
212    nodes.push(SceneNode::Rect(RectSceneNode {
213        bounds: shadow_bounds,
214        background: Background::Color(shadow.color),
215        gradient_id: None,
216        border_width: 0.0,
217        border_color: Color::TRANSPARENT,
218        border_radius: border_radius + shadow.spread_radius,
219        opacity: shadow.color.a as f32 / 255.0,
220        clip_id,
221        transform: Transform::default(),
222        backdrop_blur_radius: 0.0,
223        blur_radius: shadow.blur_radius,
224        filter_id,
225    }));
226}
227
228/// Đọc intrinsic size của ảnh từ bytes (chỉ decode header).
229fn read_image_size(bytes: &[u8]) -> (f32, f32) {
230    use image::ImageReader;
231    use std::io::Cursor;
232    if let Ok(reader) = ImageReader::new(Cursor::new(bytes)).with_guessed_format() {
233        if let Ok((w, h)) = reader.into_dimensions() {
234            return (w as f32, h as f32);
235        }
236    }
237    (0.0, 0.0) // unknown → 0 means "use bounds"
238}
239
240fn guess_mime(src: &str) -> String {
241    if src.ends_with(".png") {
242        "image/png".into()
243    } else if src.ends_with(".jpg") || src.ends_with(".jpeg") {
244        "image/jpeg".into()
245    } else if src.ends_with(".webp") {
246        "image/webp".into()
247    } else if src.ends_with(".gif") {
248        "image/gif".into()
249    } else {
250        "image/png".into()
251    }
252}
253
254fn placeholder_png() -> Vec<u8> {
255    // 1×1 grey PNG
256    vec![
257        0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
258        0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90,
259        0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xd8,
260        0xd8, 0xd8, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00,
261        0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
262    ]
263}