Skip to main content

appscale_core/
ir.rs

1//! Binary IR Layer — the transport contract between React's reconciler and Rust.
2//!
3//! This is the framework's biggest innovation. Instead of:
4//! - JSON bridge (React Native old arch: ~16-32ms per call)
5//! - JSI object passing (React Native new arch: <1ms but still JS objects)
6//!
7//! We use a binary IR (FlatBuffers) that is:
8//! - Deterministic: same input → same output
9//! - Zero-copy deserializable
10//! - Cross-language safe (JS → Rust with no marshaling)
11//! - Replayable (critical for debugging, testing, and AI)
12//!
13//! Two transport modes:
14//! - JSON (Phase 1 — DevTools, testing, debugging)
15//! - FlatBuffers (Phase 2 — production performance)
16
17use 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/// A single command from the reconciler to the engine.
25/// Each React commit produces a batch of these commands.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(tag = "type")]
28pub enum IrCommand {
29    /// Create a new node with initial props and style.
30    #[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    /// Update props on an existing node (only changed props).
41    #[serde(rename = "update_props")]
42    UpdateProps {
43        id: NodeId,
44        diff: PropsDiff,
45    },
46
47    /// Update layout style on an existing node.
48    #[serde(rename = "update_style")]
49    UpdateStyle {
50        id: NodeId,
51        style: LayoutStyle,
52    },
53
54    /// Append a child to a parent (at the end).
55    #[serde(rename = "append_child")]
56    AppendChild {
57        parent: NodeId,
58        child: NodeId,
59    },
60
61    /// Insert a child before another child.
62    #[serde(rename = "insert_before")]
63    InsertBefore {
64        parent: NodeId,
65        child: NodeId,
66        before: NodeId,
67    },
68
69    /// Remove a child from its parent (and destroy the subtree).
70    #[serde(rename = "remove_child")]
71    RemoveChild {
72        parent: NodeId,
73        child: NodeId,
74    },
75
76    /// Set the root node of the tree.
77    #[serde(rename = "set_root")]
78    SetRootNode {
79        id: NodeId,
80    },
81}
82
83/// A batch of IR commands from a single React commit.
84/// The engine processes these atomically.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct IrBatch {
87    /// Monotonically increasing commit ID.
88    pub commit_id: u64,
89
90    /// Timestamp (ms since app start).
91    pub timestamp_ms: f64,
92
93    /// The commands to execute, in order.
94    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
119// === JSON Transport (Phase 1 — bootstrap) ===
120
121/// Decode an IR batch from JSON bytes.
122/// Phase 1 uses JSON; Phase 2 replaces with FlatBuffers.
123pub fn decode_batch(bytes: &[u8]) -> Result<IrBatch, IrError> {
124    serde_json::from_slice(bytes).map_err(|e| IrError::DecodeFailed(e.to_string()))
125}
126
127/// Encode an IR batch to JSON bytes.
128/// Used for DevTools replay and testing.
129pub 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
145// === FlatBuffers Transport (Phase 2 — production) ===
146
147/// Decode an IR batch from FlatBuffers binary bytes.
148/// Zero-copy deserialization — the buffer is read in-place.
149pub 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
233/// Encode an IR batch to FlatBuffers binary bytes.
234pub 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
339// === Type conversion helpers ===
340
341fn 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
684// === Serde impls for NodeId (serialize as u64) ===
685
686impl 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        // Encode to FlatBuffers
790        let encoded = encode_batch_flatbuf(&batch);
791        assert!(!encoded.is_empty());
792
793        // Decode from FlatBuffers
794        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        // Verify CreateNode
800        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        // Verify second CreateNode with props and style
809        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        // Verify AppendChild
825        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        // Verify SetRootNode
834        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        // FlatBuffers should be smaller than JSON for structured data
910        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    /// Build a realistic IrBatch with N nodes for benchmarking.
916    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        // JSON encode
955        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        // JSON decode
963        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        // FlatBuffers encode
970        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        // FlatBuffers decode
978        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        // FlatBuffers should be faster for decoding (zero-copy)
997        // We don't assert on speed (CI variability), just that both work
998        assert_eq!(decode_batch_flatbuf(&fb_bytes).unwrap().commands.len(),
999                   decode_batch(&json_bytes).unwrap().commands.len());
1000    }
1001}