1use crate::tree::NodeId;
18use crate::platform::{ViewType, PropsDiff, PropValue};
19use crate::layout::LayoutStyle;
20use crate::generated::flatbuf;
21use serde::{Serialize, Deserialize};
22use std::collections::HashMap;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "type")]
28pub enum IrCommand {
29 #[serde(rename = "create")]
31 CreateNode {
32 id: NodeId,
33 view_type: ViewType,
34 #[serde(default)]
35 props: HashMap<String, PropValue>,
36 #[serde(default)]
37 style: LayoutStyle,
38 },
39
40 #[serde(rename = "update_props")]
42 UpdateProps {
43 id: NodeId,
44 diff: PropsDiff,
45 },
46
47 #[serde(rename = "update_style")]
49 UpdateStyle {
50 id: NodeId,
51 style: LayoutStyle,
52 },
53
54 #[serde(rename = "append_child")]
56 AppendChild {
57 parent: NodeId,
58 child: NodeId,
59 },
60
61 #[serde(rename = "insert_before")]
63 InsertBefore {
64 parent: NodeId,
65 child: NodeId,
66 before: NodeId,
67 },
68
69 #[serde(rename = "remove_child")]
71 RemoveChild {
72 parent: NodeId,
73 child: NodeId,
74 },
75
76 #[serde(rename = "set_root")]
78 SetRootNode {
79 id: NodeId,
80 },
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct IrBatch {
87 pub commit_id: u64,
89
90 pub timestamp_ms: f64,
92
93 pub commands: Vec<IrCommand>,
95}
96
97impl IrBatch {
98 pub fn new(commit_id: u64) -> Self {
99 Self {
100 commit_id,
101 timestamp_ms: 0.0,
102 commands: Vec::new(),
103 }
104 }
105
106 pub fn push(&mut self, cmd: IrCommand) {
107 self.commands.push(cmd);
108 }
109
110 pub fn is_empty(&self) -> bool {
111 self.commands.is_empty()
112 }
113
114 pub fn len(&self) -> usize {
115 self.commands.len()
116 }
117}
118
119pub fn decode_batch(bytes: &[u8]) -> Result<IrBatch, IrError> {
124 serde_json::from_slice(bytes).map_err(|e| IrError::DecodeFailed(e.to_string()))
125}
126
127pub fn encode_batch(batch: &IrBatch) -> Result<Vec<u8>, IrError> {
130 serde_json::to_vec(batch).map_err(|e| IrError::EncodeFailed(e.to_string()))
131}
132
133#[derive(Debug, thiserror::Error)]
134pub enum IrError {
135 #[error("IR decode failed: {0}")]
136 DecodeFailed(String),
137
138 #[error("IR encode failed: {0}")]
139 EncodeFailed(String),
140
141 #[error("Unknown command type in FlatBuffers IR")]
142 UnknownCommand,
143}
144
145pub fn decode_batch_flatbuf(bytes: &[u8]) -> Result<IrBatch, IrError> {
150 let fb_batch = flatbuf::root_as_ir_batch(bytes)
151 .map_err(|e| IrError::DecodeFailed(e.to_string()))?;
152
153 let mut batch = IrBatch {
154 commit_id: fb_batch.commit_id(),
155 timestamp_ms: fb_batch.timestamp_ms(),
156 commands: Vec::new(),
157 };
158
159 if let Some(commands) = fb_batch.commands() {
160 batch.commands.reserve(commands.len());
161 for fb_cmd in commands {
162 let cmd = decode_fb_command(&fb_cmd)?;
163 batch.commands.push(cmd);
164 }
165 }
166
167 Ok(batch)
168}
169
170fn decode_fb_command(fb_cmd: &flatbuf::IrCommand<'_>) -> Result<IrCommand, IrError> {
171 match fb_cmd.cmd_type() {
172 flatbuf::Command::CreateNode => {
173 let cn = fb_cmd.cmd_as_create_node().ok_or(IrError::UnknownCommand)?;
174 let view_type = fb_view_type_to_engine(cn.view_type(), cn.custom_type());
175 let props = cn.props().map(|p| fb_props_to_engine(&p)).unwrap_or_default();
176 let style = cn.style().map(|s| fb_layout_to_engine(&s)).unwrap_or_default();
177 Ok(IrCommand::CreateNode {
178 id: NodeId(cn.id()),
179 view_type,
180 props,
181 style,
182 })
183 }
184 flatbuf::Command::UpdateProps => {
185 let up = fb_cmd.cmd_as_update_props().ok_or(IrError::UnknownCommand)?;
186 let diff = up.diff().map(|p| {
187 let changes = fb_props_to_engine(&p);
188 PropsDiff { changes }
189 }).unwrap_or_default();
190 Ok(IrCommand::UpdateProps {
191 id: NodeId(up.id()),
192 diff,
193 })
194 }
195 flatbuf::Command::UpdateStyle => {
196 let us = fb_cmd.cmd_as_update_style().ok_or(IrError::UnknownCommand)?;
197 let style = us.style().map(|s| fb_layout_to_engine(&s)).unwrap_or_default();
198 Ok(IrCommand::UpdateStyle {
199 id: NodeId(us.id()),
200 style,
201 })
202 }
203 flatbuf::Command::AppendChild => {
204 let ac = fb_cmd.cmd_as_append_child().ok_or(IrError::UnknownCommand)?;
205 Ok(IrCommand::AppendChild {
206 parent: NodeId(ac.parent()),
207 child: NodeId(ac.child()),
208 })
209 }
210 flatbuf::Command::InsertBefore => {
211 let ib = fb_cmd.cmd_as_insert_before().ok_or(IrError::UnknownCommand)?;
212 Ok(IrCommand::InsertBefore {
213 parent: NodeId(ib.parent()),
214 child: NodeId(ib.child()),
215 before: NodeId(ib.before()),
216 })
217 }
218 flatbuf::Command::RemoveChild => {
219 let rc = fb_cmd.cmd_as_remove_child().ok_or(IrError::UnknownCommand)?;
220 Ok(IrCommand::RemoveChild {
221 parent: NodeId(rc.parent()),
222 child: NodeId(rc.child()),
223 })
224 }
225 flatbuf::Command::SetRoot => {
226 let sr = fb_cmd.cmd_as_set_root().ok_or(IrError::UnknownCommand)?;
227 Ok(IrCommand::SetRootNode { id: NodeId(sr.id()) })
228 }
229 _ => Err(IrError::UnknownCommand),
230 }
231}
232
233pub fn encode_batch_flatbuf(batch: &IrBatch) -> Vec<u8> {
235 let mut fbb = flatbuffers::FlatBufferBuilder::with_capacity(1024);
236
237 let cmd_offsets: Vec<_> = batch.commands.iter().map(|cmd| {
238 encode_fb_command(&mut fbb, cmd)
239 }).collect();
240
241 let commands = fbb.create_vector(&cmd_offsets);
242
243 let fb_batch = flatbuf::IrBatch::create(&mut fbb, &flatbuf::IrBatchArgs {
244 commit_id: batch.commit_id,
245 timestamp_ms: batch.timestamp_ms,
246 commands: Some(commands),
247 });
248
249 flatbuf::finish_ir_batch_buffer(&mut fbb, fb_batch);
250 fbb.finished_data().to_vec()
251}
252
253fn encode_fb_command<'a>(
254 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
255 cmd: &IrCommand,
256) -> flatbuffers::WIPOffset<flatbuf::IrCommand<'a>> {
257 match cmd {
258 IrCommand::CreateNode { id, view_type, props, style } => {
259 let (fb_vt, custom_str) = engine_view_type_to_fb(fbb, view_type);
260 let fb_props = if props.is_empty() { None } else {
261 Some(engine_props_to_fb(fbb, props))
262 };
263 let fb_style = Some(engine_layout_to_fb(fbb, style));
264 let cn = flatbuf::CreateNode::create(fbb, &flatbuf::CreateNodeArgs {
265 id: id.0,
266 view_type: fb_vt,
267 custom_type: custom_str,
268 props: fb_props,
269 style: fb_style,
270 });
271 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
272 cmd_type: flatbuf::Command::CreateNode,
273 cmd: Some(cn.as_union_value()),
274 })
275 }
276 IrCommand::UpdateProps { id, diff } => {
277 let fb_diff = engine_props_to_fb_diff(fbb, diff);
278 let up = flatbuf::UpdateProps::create(fbb, &flatbuf::UpdatePropsArgs {
279 id: id.0,
280 diff: Some(fb_diff),
281 });
282 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
283 cmd_type: flatbuf::Command::UpdateProps,
284 cmd: Some(up.as_union_value()),
285 })
286 }
287 IrCommand::UpdateStyle { id, style } => {
288 let fb_style = engine_layout_to_fb(fbb, style);
289 let us = flatbuf::UpdateStyle::create(fbb, &flatbuf::UpdateStyleArgs {
290 id: id.0,
291 style: Some(fb_style),
292 });
293 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
294 cmd_type: flatbuf::Command::UpdateStyle,
295 cmd: Some(us.as_union_value()),
296 })
297 }
298 IrCommand::AppendChild { parent, child } => {
299 let ac = flatbuf::AppendChild::create(fbb, &flatbuf::AppendChildArgs {
300 parent: parent.0,
301 child: child.0,
302 });
303 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
304 cmd_type: flatbuf::Command::AppendChild,
305 cmd: Some(ac.as_union_value()),
306 })
307 }
308 IrCommand::InsertBefore { parent, child, before } => {
309 let ib = flatbuf::InsertBefore::create(fbb, &flatbuf::InsertBeforeArgs {
310 parent: parent.0,
311 child: child.0,
312 before: before.0,
313 });
314 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
315 cmd_type: flatbuf::Command::InsertBefore,
316 cmd: Some(ib.as_union_value()),
317 })
318 }
319 IrCommand::RemoveChild { parent, child } => {
320 let rc = flatbuf::RemoveChild::create(fbb, &flatbuf::RemoveChildArgs {
321 parent: parent.0,
322 child: child.0,
323 });
324 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
325 cmd_type: flatbuf::Command::RemoveChild,
326 cmd: Some(rc.as_union_value()),
327 })
328 }
329 IrCommand::SetRootNode { id } => {
330 let sr = flatbuf::SetRoot::create(fbb, &flatbuf::SetRootArgs { id: id.0 });
331 flatbuf::IrCommand::create(fbb, &flatbuf::IrCommandArgs {
332 cmd_type: flatbuf::Command::SetRoot,
333 cmd: Some(sr.as_union_value()),
334 })
335 }
336 }
337}
338
339fn fb_view_type_to_engine(vt: flatbuf::ViewType, custom: Option<&str>) -> ViewType {
342 match vt {
343 flatbuf::ViewType::Container => ViewType::Container,
344 flatbuf::ViewType::Text => ViewType::Text,
345 flatbuf::ViewType::TextInput => ViewType::TextInput,
346 flatbuf::ViewType::Image => ViewType::Image,
347 flatbuf::ViewType::ScrollView => ViewType::ScrollView,
348 flatbuf::ViewType::Button => ViewType::Button,
349 flatbuf::ViewType::Switch => ViewType::Switch,
350 flatbuf::ViewType::Slider => ViewType::Slider,
351 flatbuf::ViewType::ActivityIndicator => ViewType::ActivityIndicator,
352 flatbuf::ViewType::DatePicker => ViewType::DatePicker,
353 flatbuf::ViewType::Modal => ViewType::Modal,
354 flatbuf::ViewType::BottomSheet => ViewType::BottomSheet,
355 flatbuf::ViewType::MenuBar => ViewType::MenuBar,
356 flatbuf::ViewType::TitleBar => ViewType::TitleBar,
357 flatbuf::ViewType::Custom => ViewType::Custom(custom.unwrap_or("").to_string()),
358 _ => ViewType::Container,
359 }
360}
361
362fn engine_view_type_to_fb<'a>(
363 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
364 vt: &ViewType,
365) -> (flatbuf::ViewType, Option<flatbuffers::WIPOffset<&'a str>>) {
366 match vt {
367 ViewType::Container => (flatbuf::ViewType::Container, None),
368 ViewType::Text => (flatbuf::ViewType::Text, None),
369 ViewType::TextInput => (flatbuf::ViewType::TextInput, None),
370 ViewType::Image => (flatbuf::ViewType::Image, None),
371 ViewType::ScrollView => (flatbuf::ViewType::ScrollView, None),
372 ViewType::Button => (flatbuf::ViewType::Button, None),
373 ViewType::Switch => (flatbuf::ViewType::Switch, None),
374 ViewType::Slider => (flatbuf::ViewType::Slider, None),
375 ViewType::ActivityIndicator => (flatbuf::ViewType::ActivityIndicator, None),
376 ViewType::DatePicker => (flatbuf::ViewType::DatePicker, None),
377 ViewType::Modal => (flatbuf::ViewType::Modal, None),
378 ViewType::BottomSheet => (flatbuf::ViewType::BottomSheet, None),
379 ViewType::MenuBar => (flatbuf::ViewType::MenuBar, None),
380 ViewType::TitleBar => (flatbuf::ViewType::TitleBar, None),
381 ViewType::Custom(name) => {
382 let s = fbb.create_string(name);
383 (flatbuf::ViewType::Custom, Some(s))
384 }
385 }
386}
387
388fn fb_props_to_engine(diff: &flatbuf::PropsDiff<'_>) -> HashMap<String, PropValue> {
389 let mut map = HashMap::new();
390 if let Some(changes) = diff.changes() {
391 for entry in changes {
392 let key = entry.key().to_string();
393 let value = match entry.value_type() {
394 flatbuf::PropValueUnion::StringVal => {
395 entry.value_as_string_val()
396 .map(|v| PropValue::String(v.value().unwrap_or("").to_string()))
397 .unwrap_or(PropValue::Null)
398 }
399 flatbuf::PropValueUnion::FloatVal => {
400 entry.value_as_float_val()
401 .map(|v| PropValue::F64(v.value() as f64))
402 .unwrap_or(PropValue::Null)
403 }
404 flatbuf::PropValueUnion::IntVal => {
405 entry.value_as_int_val()
406 .map(|v| PropValue::I32(v.value()))
407 .unwrap_or(PropValue::Null)
408 }
409 flatbuf::PropValueUnion::BoolVal => {
410 entry.value_as_bool_val()
411 .map(|v| PropValue::Bool(v.value()))
412 .unwrap_or(PropValue::Null)
413 }
414 flatbuf::PropValueUnion::ColorVal => {
415 entry.value_as_color_val()
416 .and_then(|v| v.value().map(|c| PropValue::Color(crate::platform::Color {
417 r: c.r(),
418 g: c.g(),
419 b: c.b(),
420 a: c.a(),
421 })))
422 .unwrap_or(PropValue::Null)
423 }
424 _ => PropValue::Null,
425 };
426 map.insert(key, value);
427 }
428 }
429 map
430}
431
432fn engine_props_to_fb<'a>(
433 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
434 props: &HashMap<String, PropValue>,
435) -> flatbuffers::WIPOffset<flatbuf::PropsDiff<'a>> {
436 let entries: Vec<_> = props.iter().map(|(key, value)| {
437 encode_prop_entry(fbb, key, value)
438 }).collect();
439 let changes = fbb.create_vector(&entries);
440 flatbuf::PropsDiff::create(fbb, &flatbuf::PropsDiffArgs {
441 changes: Some(changes),
442 })
443}
444
445fn engine_props_to_fb_diff<'a>(
446 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
447 diff: &PropsDiff,
448) -> flatbuffers::WIPOffset<flatbuf::PropsDiff<'a>> {
449 engine_props_to_fb(fbb, &diff.changes)
450}
451
452fn encode_prop_entry<'a>(
453 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
454 key: &str,
455 value: &PropValue,
456) -> flatbuffers::WIPOffset<flatbuf::PropEntry<'a>> {
457 let key_offset = fbb.create_string(key);
458 let (value_type, value_offset) = match value {
459 PropValue::String(s) => {
460 let sv = fbb.create_string(s);
461 let val = flatbuf::StringVal::create(fbb, &flatbuf::StringValArgs { value: Some(sv) });
462 (flatbuf::PropValueUnion::StringVal, val.as_union_value())
463 }
464 PropValue::F32(f) => {
465 let val = flatbuf::FloatVal::create(fbb, &flatbuf::FloatValArgs { value: *f });
466 (flatbuf::PropValueUnion::FloatVal, val.as_union_value())
467 }
468 PropValue::F64(f) => {
469 let val = flatbuf::FloatVal::create(fbb, &flatbuf::FloatValArgs { value: *f as f32 });
470 (flatbuf::PropValueUnion::FloatVal, val.as_union_value())
471 }
472 PropValue::I32(i) => {
473 let val = flatbuf::IntVal::create(fbb, &flatbuf::IntValArgs { value: *i });
474 (flatbuf::PropValueUnion::IntVal, val.as_union_value())
475 }
476 PropValue::Bool(b) => {
477 let val = flatbuf::BoolVal::create(fbb, &flatbuf::BoolValArgs { value: *b });
478 (flatbuf::PropValueUnion::BoolVal, val.as_union_value())
479 }
480 PropValue::Color(c) => {
481 let fb_color = flatbuf::Color::new(c.r, c.g, c.b, c.a);
482 let val = flatbuf::ColorVal::create(fbb, &flatbuf::ColorValArgs {
483 value: Some(&fb_color),
484 });
485 (flatbuf::PropValueUnion::ColorVal, val.as_union_value())
486 }
487 PropValue::Rect { .. } | PropValue::Null => {
488 (flatbuf::PropValueUnion::NONE, flatbuffers::WIPOffset::<flatbuf::StringVal>::new(0).as_union_value())
489 }
490 };
491 flatbuf::PropEntry::create(fbb, &flatbuf::PropEntryArgs {
492 key: Some(key_offset),
493 value_type,
494 value: Some(value_offset),
495 })
496}
497
498fn fb_layout_to_engine(ls: &flatbuf::LayoutStyle<'_>) -> LayoutStyle {
499 use crate::layout;
500
501 let display = match ls.display() {
502 flatbuf::Display::Flex => layout::Display::Flex,
503 flatbuf::Display::Grid => layout::Display::Grid,
504 flatbuf::Display::None => layout::Display::None,
505 _ => layout::Display::Flex,
506 };
507 let position = match ls.position() {
508 flatbuf::Position::Relative => layout::Position::Relative,
509 flatbuf::Position::Absolute => layout::Position::Absolute,
510 _ => layout::Position::Relative,
511 };
512 let flex_direction = match ls.flex_direction() {
513 flatbuf::FlexDirection::Column => layout::FlexDirection::Column,
514 flatbuf::FlexDirection::Row => layout::FlexDirection::Row,
515 flatbuf::FlexDirection::ColumnReverse => layout::FlexDirection::ColumnReverse,
516 flatbuf::FlexDirection::RowReverse => layout::FlexDirection::RowReverse,
517 _ => layout::FlexDirection::Column,
518 };
519 let flex_wrap = match ls.flex_wrap() {
520 flatbuf::FlexWrap::NoWrap => layout::FlexWrap::NoWrap,
521 flatbuf::FlexWrap::Wrap => layout::FlexWrap::Wrap,
522 flatbuf::FlexWrap::WrapReverse => layout::FlexWrap::WrapReverse,
523 _ => layout::FlexWrap::NoWrap,
524 };
525 let justify_content = match ls.justify_content() {
526 flatbuf::JustifyContent::FlexStart => Some(layout::JustifyContent::FlexStart),
527 flatbuf::JustifyContent::FlexEnd => Some(layout::JustifyContent::FlexEnd),
528 flatbuf::JustifyContent::Center => Some(layout::JustifyContent::Center),
529 flatbuf::JustifyContent::SpaceBetween => Some(layout::JustifyContent::SpaceBetween),
530 flatbuf::JustifyContent::SpaceAround => Some(layout::JustifyContent::SpaceAround),
531 flatbuf::JustifyContent::SpaceEvenly => Some(layout::JustifyContent::SpaceEvenly),
532 _ => None,
533 };
534 let align_items = match ls.align_items() {
535 flatbuf::AlignItems::FlexStart => Some(layout::AlignItems::FlexStart),
536 flatbuf::AlignItems::FlexEnd => Some(layout::AlignItems::FlexEnd),
537 flatbuf::AlignItems::Center => Some(layout::AlignItems::Center),
538 flatbuf::AlignItems::Stretch => Some(layout::AlignItems::Stretch),
539 flatbuf::AlignItems::Baseline => Some(layout::AlignItems::Baseline),
540 _ => None,
541 };
542 let overflow = match ls.overflow() {
543 flatbuf::Overflow::Visible => layout::Overflow::Visible,
544 flatbuf::Overflow::Hidden => layout::Overflow::Hidden,
545 flatbuf::Overflow::Scroll => layout::Overflow::Scroll,
546 _ => layout::Overflow::Visible,
547 };
548
549 LayoutStyle {
550 display,
551 position,
552 flex_direction,
553 flex_wrap,
554 flex_grow: ls.flex_grow(),
555 flex_shrink: ls.flex_shrink(),
556 justify_content,
557 align_items,
558 width: fb_dimension_to_engine(ls.width()),
559 height: fb_dimension_to_engine(ls.height()),
560 min_width: fb_dimension_to_engine(ls.min_width()),
561 min_height: fb_dimension_to_engine(ls.min_height()),
562 max_width: fb_dimension_to_engine(ls.max_width()),
563 max_height: fb_dimension_to_engine(ls.max_height()),
564 aspect_ratio: if ls.aspect_ratio() == 0.0 { None } else { Some(ls.aspect_ratio()) },
565 margin: ls.margin().map(fb_edges_to_engine).unwrap_or_default(),
566 padding: ls.padding().map(fb_edges_to_engine).unwrap_or_default(),
567 gap: ls.gap(),
568 overflow,
569 }
570}
571
572fn fb_dimension_to_engine(d: Option<&flatbuf::Dimension>) -> crate::layout::Dimension {
573 match d {
574 None => crate::layout::Dimension::Auto,
575 Some(dim) => match dim.type_() {
576 flatbuf::DimensionType::Auto => crate::layout::Dimension::Auto,
577 flatbuf::DimensionType::Points => crate::layout::Dimension::Points(dim.value()),
578 flatbuf::DimensionType::Percent => crate::layout::Dimension::Percent(dim.value()),
579 _ => crate::layout::Dimension::Auto,
580 },
581 }
582}
583
584fn fb_edges_to_engine(e: &flatbuf::Edges) -> crate::layout::Edges {
585 crate::layout::Edges {
586 top: e.top(),
587 right: e.right(),
588 bottom: e.bottom(),
589 left: e.left(),
590 }
591}
592
593fn engine_layout_to_fb<'a>(
594 fbb: &mut flatbuffers::FlatBufferBuilder<'a>,
595 style: &LayoutStyle,
596) -> flatbuffers::WIPOffset<flatbuf::LayoutStyle<'a>> {
597 use crate::layout;
598
599 let display = match style.display {
600 layout::Display::Flex => flatbuf::Display::Flex,
601 layout::Display::Grid => flatbuf::Display::Grid,
602 layout::Display::None => flatbuf::Display::None,
603 };
604 let position = match style.position {
605 layout::Position::Relative => flatbuf::Position::Relative,
606 layout::Position::Absolute => flatbuf::Position::Absolute,
607 };
608 let flex_direction = match style.flex_direction {
609 layout::FlexDirection::Column => flatbuf::FlexDirection::Column,
610 layout::FlexDirection::Row => flatbuf::FlexDirection::Row,
611 layout::FlexDirection::ColumnReverse => flatbuf::FlexDirection::ColumnReverse,
612 layout::FlexDirection::RowReverse => flatbuf::FlexDirection::RowReverse,
613 };
614 let flex_wrap = match style.flex_wrap {
615 layout::FlexWrap::NoWrap => flatbuf::FlexWrap::NoWrap,
616 layout::FlexWrap::Wrap => flatbuf::FlexWrap::Wrap,
617 layout::FlexWrap::WrapReverse => flatbuf::FlexWrap::WrapReverse,
618 };
619 let justify_content = match style.justify_content {
620 Some(layout::JustifyContent::FlexStart) | None => flatbuf::JustifyContent::FlexStart,
621 Some(layout::JustifyContent::FlexEnd) => flatbuf::JustifyContent::FlexEnd,
622 Some(layout::JustifyContent::Center) => flatbuf::JustifyContent::Center,
623 Some(layout::JustifyContent::SpaceBetween) => flatbuf::JustifyContent::SpaceBetween,
624 Some(layout::JustifyContent::SpaceAround) => flatbuf::JustifyContent::SpaceAround,
625 Some(layout::JustifyContent::SpaceEvenly) => flatbuf::JustifyContent::SpaceEvenly,
626 };
627 let align_items = match style.align_items {
628 Some(layout::AlignItems::FlexStart) | None => flatbuf::AlignItems::FlexStart,
629 Some(layout::AlignItems::FlexEnd) => flatbuf::AlignItems::FlexEnd,
630 Some(layout::AlignItems::Center) => flatbuf::AlignItems::Center,
631 Some(layout::AlignItems::Stretch) => flatbuf::AlignItems::Stretch,
632 Some(layout::AlignItems::Baseline) => flatbuf::AlignItems::Baseline,
633 };
634 let overflow = match style.overflow {
635 layout::Overflow::Visible => flatbuf::Overflow::Visible,
636 layout::Overflow::Hidden => flatbuf::Overflow::Hidden,
637 layout::Overflow::Scroll => flatbuf::Overflow::Scroll,
638 };
639
640 let width = engine_dimension_to_fb(&style.width);
641 let height = engine_dimension_to_fb(&style.height);
642 let min_width = engine_dimension_to_fb(&style.min_width);
643 let min_height = engine_dimension_to_fb(&style.min_height);
644 let max_width = engine_dimension_to_fb(&style.max_width);
645 let max_height = engine_dimension_to_fb(&style.max_height);
646 let margin = flatbuf::Edges::new(
647 style.margin.top, style.margin.right, style.margin.bottom, style.margin.left,
648 );
649 let padding = flatbuf::Edges::new(
650 style.padding.top, style.padding.right, style.padding.bottom, style.padding.left,
651 );
652
653 flatbuf::LayoutStyle::create(fbb, &flatbuf::LayoutStyleArgs {
654 display,
655 position,
656 flex_direction,
657 flex_wrap,
658 flex_grow: style.flex_grow,
659 flex_shrink: style.flex_shrink,
660 justify_content,
661 align_items,
662 width: Some(&width),
663 height: Some(&height),
664 min_width: Some(&min_width),
665 min_height: Some(&min_height),
666 max_width: Some(&max_width),
667 max_height: Some(&max_height),
668 aspect_ratio: style.aspect_ratio.unwrap_or(0.0),
669 margin: Some(&margin),
670 padding: Some(&padding),
671 gap: style.gap,
672 overflow,
673 })
674}
675
676fn engine_dimension_to_fb(dim: &crate::layout::Dimension) -> flatbuf::Dimension {
677 match dim {
678 crate::layout::Dimension::Auto => flatbuf::Dimension::new(flatbuf::DimensionType::Auto, 0.0),
679 crate::layout::Dimension::Points(v) => flatbuf::Dimension::new(flatbuf::DimensionType::Points, *v),
680 crate::layout::Dimension::Percent(v) => flatbuf::Dimension::new(flatbuf::DimensionType::Percent, *v),
681 }
682}
683
684impl Serialize for NodeId {
687 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
688 serializer.serialize_u64(self.0)
689 }
690}
691
692impl<'de> Deserialize<'de> for NodeId {
693 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
694 let id = u64::deserialize(deserializer)?;
695 Ok(NodeId(id))
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_roundtrip_json() {
705 let mut batch = IrBatch::new(1);
706 batch.push(IrCommand::CreateNode {
707 id: NodeId(1),
708 view_type: ViewType::Container,
709 props: HashMap::new(),
710 style: LayoutStyle::default(),
711 });
712 batch.push(IrCommand::CreateNode {
713 id: NodeId(2),
714 view_type: ViewType::Text,
715 props: {
716 let mut p = HashMap::new();
717 p.insert("text".to_string(), PropValue::String("Hello".to_string()));
718 p
719 },
720 style: LayoutStyle::default(),
721 });
722 batch.push(IrCommand::AppendChild {
723 parent: NodeId(1),
724 child: NodeId(2),
725 });
726 batch.push(IrCommand::SetRootNode { id: NodeId(1) });
727
728 let encoded = encode_batch(&batch).unwrap();
729 let decoded = decode_batch(&encoded).unwrap();
730
731 assert_eq!(decoded.commit_id, 1);
732 assert_eq!(decoded.commands.len(), 4);
733 }
734
735 #[test]
736 fn test_roundtrip_flatbuf() {
737 let mut batch = IrBatch::new(42);
738 batch.timestamp_ms = 123.456;
739 batch.push(IrCommand::CreateNode {
740 id: NodeId(1),
741 view_type: ViewType::Container,
742 props: HashMap::new(),
743 style: LayoutStyle::default(),
744 });
745 batch.push(IrCommand::CreateNode {
746 id: NodeId(2),
747 view_type: ViewType::Text,
748 props: {
749 let mut p = HashMap::new();
750 p.insert("text".to_string(), PropValue::String("Hello FlatBuf".to_string()));
751 p.insert("fontSize".to_string(), PropValue::F32(16.0));
752 p.insert("bold".to_string(), PropValue::Bool(true));
753 p.insert("count".to_string(), PropValue::I32(7));
754 p
755 },
756 style: LayoutStyle {
757 display: crate::layout::Display::Flex,
758 flex_direction: crate::layout::FlexDirection::Row,
759 width: crate::layout::Dimension::Points(100.0),
760 height: crate::layout::Dimension::Percent(50.0),
761 margin: crate::layout::Edges { top: 8.0, right: 8.0, bottom: 8.0, left: 8.0 },
762 ..LayoutStyle::default()
763 },
764 });
765 batch.push(IrCommand::AppendChild { parent: NodeId(1), child: NodeId(2) });
766 batch.push(IrCommand::UpdateProps {
767 id: NodeId(2),
768 diff: {
769 let mut d = PropsDiff::new();
770 d.set("text", PropValue::String("Updated".to_string()));
771 d
772 },
773 });
774 batch.push(IrCommand::UpdateStyle {
775 id: NodeId(2),
776 style: LayoutStyle {
777 flex_grow: 1.0,
778 ..LayoutStyle::default()
779 },
780 });
781 batch.push(IrCommand::InsertBefore {
782 parent: NodeId(1),
783 child: NodeId(3),
784 before: NodeId(2),
785 });
786 batch.push(IrCommand::RemoveChild { parent: NodeId(1), child: NodeId(3) });
787 batch.push(IrCommand::SetRootNode { id: NodeId(1) });
788
789 let encoded = encode_batch_flatbuf(&batch);
791 assert!(!encoded.is_empty());
792
793 let decoded = decode_batch_flatbuf(&encoded).unwrap();
795 assert_eq!(decoded.commit_id, 42);
796 assert_eq!(decoded.timestamp_ms, 123.456);
797 assert_eq!(decoded.commands.len(), 8);
798
799 match &decoded.commands[0] {
801 IrCommand::CreateNode { id, view_type, .. } => {
802 assert_eq!(*id, NodeId(1));
803 assert_eq!(*view_type, ViewType::Container);
804 }
805 _ => panic!("Expected CreateNode"),
806 }
807
808 match &decoded.commands[1] {
810 IrCommand::CreateNode { id, view_type, props, style } => {
811 assert_eq!(*id, NodeId(2));
812 assert_eq!(*view_type, ViewType::Text);
813 assert_eq!(props.len(), 4);
814 match &props["text"] {
815 PropValue::String(s) => assert_eq!(s, "Hello FlatBuf"),
816 _ => panic!("Expected String prop"),
817 }
818 assert!(matches!(style.flex_direction, crate::layout::FlexDirection::Row));
819 assert!(matches!(style.width, crate::layout::Dimension::Points(v) if (v - 100.0).abs() < 0.01));
820 }
821 _ => panic!("Expected CreateNode"),
822 }
823
824 match &decoded.commands[2] {
826 IrCommand::AppendChild { parent, child } => {
827 assert_eq!(*parent, NodeId(1));
828 assert_eq!(*child, NodeId(2));
829 }
830 _ => panic!("Expected AppendChild"),
831 }
832
833 match &decoded.commands[7] {
835 IrCommand::SetRootNode { id } => assert_eq!(*id, NodeId(1)),
836 _ => panic!("Expected SetRootNode"),
837 }
838 }
839
840 #[test]
841 fn test_flatbuf_custom_view_type() {
842 let mut batch = IrBatch::new(1);
843 batch.push(IrCommand::CreateNode {
844 id: NodeId(99),
845 view_type: ViewType::Custom("MyWidget".to_string()),
846 props: HashMap::new(),
847 style: LayoutStyle::default(),
848 });
849
850 let encoded = encode_batch_flatbuf(&batch);
851 let decoded = decode_batch_flatbuf(&encoded).unwrap();
852
853 match &decoded.commands[0] {
854 IrCommand::CreateNode { view_type, .. } => {
855 assert_eq!(*view_type, ViewType::Custom("MyWidget".to_string()));
856 }
857 _ => panic!("Expected CreateNode"),
858 }
859 }
860
861 #[test]
862 fn test_flatbuf_color_prop() {
863 let mut batch = IrBatch::new(1);
864 batch.push(IrCommand::CreateNode {
865 id: NodeId(1),
866 view_type: ViewType::Container,
867 props: {
868 let mut p = HashMap::new();
869 p.insert("bg".to_string(), PropValue::Color(crate::platform::Color::rgba(255, 0, 128, 0.5)));
870 p
871 },
872 style: LayoutStyle::default(),
873 });
874
875 let encoded = encode_batch_flatbuf(&batch);
876 let decoded = decode_batch_flatbuf(&encoded).unwrap();
877
878 match &decoded.commands[0] {
879 IrCommand::CreateNode { props, .. } => {
880 match &props["bg"] {
881 PropValue::Color(c) => {
882 assert_eq!(c.r, 255);
883 assert_eq!(c.g, 0);
884 assert_eq!(c.b, 128);
885 assert!((c.a - 0.5).abs() < 0.01);
886 }
887 _ => panic!("Expected Color prop"),
888 }
889 }
890 _ => panic!("Expected CreateNode"),
891 }
892 }
893
894 #[test]
895 fn test_flatbuf_size_vs_json() {
896 let mut batch = IrBatch::new(1);
897 for i in 0..100 {
898 batch.push(IrCommand::CreateNode {
899 id: NodeId(i),
900 view_type: ViewType::Container,
901 props: HashMap::new(),
902 style: LayoutStyle::default(),
903 });
904 }
905
906 let json_bytes = encode_batch(&batch).unwrap();
907 let fb_bytes = encode_batch_flatbuf(&batch);
908
909 assert!(fb_bytes.len() < json_bytes.len(),
911 "FlatBuffers ({} bytes) should be smaller than JSON ({} bytes)",
912 fb_bytes.len(), json_bytes.len());
913 }
914
915 fn make_bench_batch(n: u64) -> IrBatch {
917 let mut batch = IrBatch::new(1);
918 for i in 1..=n {
919 batch.push(IrCommand::CreateNode {
920 id: NodeId(i),
921 view_type: if i % 5 == 0 { ViewType::Text } else { ViewType::Container },
922 props: {
923 let mut p = HashMap::new();
924 p.insert("label".to_string(), PropValue::String(format!("node_{i}")));
925 if i % 3 == 0 {
926 p.insert("fontSize".to_string(), PropValue::F32(14.0));
927 p.insert("bold".to_string(), PropValue::Bool(true));
928 }
929 p
930 },
931 style: LayoutStyle {
932 width: crate::layout::Dimension::Points(100.0),
933 height: crate::layout::Dimension::Points(40.0),
934 margin: crate::layout::Edges { top: 4.0, right: 4.0, bottom: 4.0, left: 4.0 },
935 ..LayoutStyle::default()
936 },
937 });
938 if i > 1 {
939 batch.push(IrCommand::AppendChild {
940 parent: NodeId(((i - 2) / 4) + 1),
941 child: NodeId(i),
942 });
943 }
944 }
945 batch.push(IrCommand::SetRootNode { id: NodeId(1) });
946 batch
947 }
948
949 #[test]
950 fn bench_encode_decode_json_vs_flatbuf() {
951 let batch = make_bench_batch(500);
952 let iterations = 100;
953
954 let json_start = std::time::Instant::now();
956 let mut json_bytes = Vec::new();
957 for _ in 0..iterations {
958 json_bytes = encode_batch(&batch).unwrap();
959 }
960 let json_encode_us = json_start.elapsed().as_micros() / iterations;
961
962 let json_decode_start = std::time::Instant::now();
964 for _ in 0..iterations {
965 let _ = decode_batch(&json_bytes).unwrap();
966 }
967 let json_decode_us = json_decode_start.elapsed().as_micros() / iterations;
968
969 let fb_start = std::time::Instant::now();
971 let mut fb_bytes = Vec::new();
972 for _ in 0..iterations {
973 fb_bytes = encode_batch_flatbuf(&batch);
974 }
975 let fb_encode_us = fb_start.elapsed().as_micros() / iterations;
976
977 let fb_decode_start = std::time::Instant::now();
979 for _ in 0..iterations {
980 let _ = decode_batch_flatbuf(&fb_bytes).unwrap();
981 }
982 let fb_decode_us = fb_decode_start.elapsed().as_micros() / iterations;
983
984 println!("\n┌──────────────────────────────────────────────────────────┐");
985 println!("│ IR Encode/Decode Benchmark (500 nodes, {iterations} iterations) │");
986 println!("├───────────────────┬──────────────┬──────────────────────┤");
987 println!("│ │ JSON (µs) │ FlatBuffers (µs) │");
988 println!("├───────────────────┼──────────────┼──────────────────────┤");
989 println!("│ Encode │ {json_encode_us:>10} │ {fb_encode_us:>18} │");
990 println!("│ Decode │ {json_decode_us:>10} │ {fb_decode_us:>18} │");
991 println!("├───────────────────┼──────────────┼──────────────────────┤");
992 println!("│ Size (bytes) │ {json_size:>10} │ {fb_size:>18} │",
993 json_size = json_bytes.len(), fb_size = fb_bytes.len());
994 println!("└───────────────────┴──────────────┴──────────────────────┘\n");
995
996 assert_eq!(decode_batch_flatbuf(&fb_bytes).unwrap().commands.len(),
999 decode_batch(&json_bytes).unwrap().commands.len());
1000 }
1001}