inkferro_core/dom/decode.rs
1//! Op-buffer decoder: flat byte buffer -> [`Vec<Op>`].
2//!
3//! This is the FFI wire format for M3: the JS reconciler (M3-G) emits a flat
4//! `Buffer` of `[opcode, id, payload...]` records, napi (M3-D) hands the bytes
5//! through here as `&[u8]`, and the result drives [`apply`](super::apply). This
6//! module is the **decode** half only — Rust never emits this format at runtime
7//! (JS is the sole emitter), so there is intentionally no `pub` encoder here. A
8//! test-only encoder lives in the test submodule for round-tripping. The
9//! cross-language contract M3-G's JS emitter targets is the wire-format table
10//! documented below — there is no persisted byte artifact; the inline
11//! literal-byte fixtures in the test submodule pin that table Rust-side.
12//!
13//! # Trust boundary
14//!
15//! The input is attacker-shaped (a JS `Buffer` of arbitrary bytes). Decoding
16//! therefore **never panics**: every read is bounds-checked through [`Reader`]
17//! and every malformed input returns a typed [`DecodeError`]. This is distinct
18//! from [`apply`](super::apply), which is *total over semantics* (unknown ids
19//! are silent no-ops, mirroring ink's JS guards). The decoder rejects malformed
20//! *bytes*; `apply` tolerates malformed *meaning*. Keep the two boundaries
21//! separate: a truncated buffer is a decode error, an op referencing a dead id
22//! is not.
23//!
24//! # Wire format (v1) — the cross-language contract
25//!
26//! A buffer is a flat sequence of op records decoded until end-of-input. There
27//! is no outer length/count prefix and no version byte; the buffer is exactly
28//! the concatenation of `encode_op(op)` for each op in order. Mid-record
29//! truncation yields [`DecodeError::UnexpectedEof`].
30//!
31//! ## Primitives
32//!
33//! * **u8** — 1 byte. Used for opcodes, enum tags, and field ids.
34//! * **u32** — 4 bytes, **little-endian**. Used for every node id and for
35//! string length prefixes.
36//! * **f32** — 4 bytes, little-endian IEEE-754. Used for every `Style` float
37//! (`flex_grow`, `flex_shrink`, `aspect_ratio`, `gap`, `column_gap`,
38//! `row_gap`) and for the inner value of `Dim::Points`/`Dim::Percent`,
39//! `Lp::Points`/`Lp::Percent`.
40//! * **f64** — 8 bytes, little-endian IEEE-754. Used for exactly one thing:
41//! `AttrValue::Number` (JS numbers are f64).
42//! * **bool** — 1 byte: `0x00` = false, `0x01` = true. Any other value is
43//! [`DecodeError::InvalidBool`].
44//! * **string** — a u32 length prefix (LE) followed by that many bytes of
45//! UTF-8. Invalid UTF-8 is [`DecodeError::InvalidUtf8`].
46//!
47//! ## Opcodes (record = `[opcode:u8][fields...]`)
48//!
49//! | opcode | variant | fields after opcode |
50//! |--------|---------------|----------------------------------------------------|
51//! | `0x00` | Create | `id:u32`, `kind:u8` |
52//! | `0x01` | AppendChild | `parent:u32`, `child:u32` |
53//! | `0x02` | InsertBefore | `parent:u32`, `child:u32`, `before:u32` |
54//! | `0x03` | RemoveChild | `parent:u32`, `child:u32` |
55//! | `0x04` | SetText | `id:u32`, `text:string` |
56//! | `0x05` | SetStyle | `id:u32`, `style` (see below) |
57//! | `0x06` | SetAttribute | `id:u32`, `key:string`, `value:AttrValue` |
58//! | `0x07` | SetTransform | `id:u32`, `has:bool` |
59//! | `0x08` | SetStatic | `id:u32`, `value:bool` |
60//! | `0x09` | Hide | `id:u32` |
61//! | `0x0A` | Unhide | `id:u32` |
62//! | `0x0B` | Free | `id:u32` |
63//! | `0x0C` | SetTextStyle | `id:u32`, `TextStyle` (see below) |
64//! | `0x0D` | ClearTextStyle| `id:u32` |
65//!
66//! Any other opcode is [`DecodeError::UnknownOpcode`].
67//!
68//! ## `TextStyle` — tagged field-id scheme (P5.1 SET_TEXT_STYLE)
69//!
70//! Same self-describing scheme as `Style`:
71//!
72//! ```text
73//! [field_count:u32] then field_count repetitions of:
74//! [field_id:u8][typed value for that field]
75//! ```
76//!
77//! The decoder starts from `TextStyle::default()` and writes each decoded field.
78//!
79//! | id | field | value encoding |
80//! |-----|------------------|----------------|
81//! | `0` | color | `string` |
82//! | `1` | background_color | `string` |
83//! | `2` | bold | `bool` |
84//! | `3` | italic | `bool` |
85//! | `4` | underline | `bool` |
86//! | `5` | strikethrough | `bool` |
87//! | `6` | inverse | `bool` |
88//! | `7` | dim_color | `bool` |
89//!
90//! Any other field id is [`DecodeError::UnknownFieldId`].
91//!
92//! ## `Kind` tag (u8)
93//!
94//! `0x00` Root, `0x01` Box, `0x02` Text, `0x03` VirtualText. Other =
95//! [`DecodeError::UnknownTag`].
96//!
97//! ## `AttrValue` (`[tag:u8][payload]`)
98//!
99//! * `0x00` Bool — payload `bool` (1 byte).
100//! * `0x01` Str — payload `string`.
101//! * `0x02` Number — payload `f64` (8 bytes, LE).
102//!
103//! ## `Style` — tagged field-id scheme
104//!
105//! `Style` has ~60 optional fields. Rather than make JS know the Rust struct
106//! layout (a fixed 60-slot record), the style payload is **self-describing**:
107//!
108//! ```text
109//! [field_count:u32] then field_count repetitions of:
110//! [field_id:u8][typed value for that field]
111//! ```
112//!
113//! Only fields that are `Some` (or, for the non-`Option` visual strings, set)
114//! are emitted; the decoder starts from `Style::default()` and writes each
115//! decoded field. A field id present in the buffer means the field is `Some`;
116//! its absence means `None`. This was chosen over a positional fixed-layout
117//! record because (1) it decouples the JS emitter from the exact Rust field
118//! order and count — M3-G writes `[id][value]` pairs from the doc table alone,
119//! never mirroring `struct Style`; (2) it is compact for the common sparse case
120//! (a Box usually sets a handful of props); and (3) adding a future style field
121//! is a backward-compatible new field-id, not a breaking width change.
122//!
123//! ### Style field ids and their value encodings
124//!
125//! | id | field | value encoding |
126//! |------|-----------------------|---------------------------------------------|
127//! | `0` | position | `Position` tag (u8) |
128//! | `1` | top | `Dim` |
129//! | `2` | right | `Dim` |
130//! | `3` | bottom | `Dim` |
131//! | `4` | left | `Dim` |
132//! | `5` | margin | `Lp` |
133//! | `6` | margin_x | `Lp` |
134//! | `7` | margin_y | `Lp` |
135//! | `8` | margin_top | `Lp` |
136//! | `9` | margin_right | `Lp` |
137//! | `10` | margin_bottom | `Lp` |
138//! | `11` | margin_left | `Lp` |
139//! | `12` | padding | `Lp` |
140//! | `13` | padding_x | `Lp` |
141//! | `14` | padding_y | `Lp` |
142//! | `15` | padding_top | `Lp` |
143//! | `16` | padding_right | `Lp` |
144//! | `17` | padding_bottom | `Lp` |
145//! | `18` | padding_left | `Lp` |
146//! | `19` | flex_direction | `FlexDir` tag (u8) |
147//! | `20` | flex_wrap | `FlexWrap` tag (u8) |
148//! | `21` | flex_grow | `f32` |
149//! | `22` | flex_shrink | `f32` |
150//! | `23` | flex_basis | `Dim` |
151//! | `24` | align_items | `Align` tag (u8) |
152//! | `25` | align_self | `Align` tag (u8) |
153//! | `26` | align_content | `ContentAlign` tag (u8) |
154//! | `27` | justify_content | `ContentAlign` tag (u8) |
155//! | `28` | width | `Dim` |
156//! | `29` | height | `Dim` |
157//! | `30` | min_width | `Dim` |
158//! | `31` | min_height | `Dim` |
159//! | `32` | max_width | `Dim` |
160//! | `33` | max_height | `Dim` |
161//! | `34` | aspect_ratio | `f32` |
162//! | `35` | display | `Display` tag (u8) |
163//! | `36` | border_style | `BorderStyle` |
164//! | `37` | border_top | `bool` |
165//! | `38` | border_right | `bool` |
166//! | `39` | border_bottom | `bool` |
167//! | `40` | border_left | `bool` |
168//! | `41` | gap | `f32` |
169//! | `42` | column_gap | `f32` |
170//! | `43` | row_gap | `f32` |
171//! | `44` | text_wrap | `TextWrap` tag (u8) |
172//! | `45` | overflow_x | `Overflow` tag (u8) |
173//! | `46` | overflow_y | `Overflow` tag (u8) |
174//! | `47` | background_color | `string` |
175//! | `48` | border_color | `string` |
176//! | `49` | border_top_color | `string` |
177//! | `50` | border_right_color | `string` |
178//! | `51` | border_bottom_color | `string` |
179//! | `52` | border_left_color | `string` |
180//! | `53` | border_background_color | `string` |
181//! | `54` | border_top_background_color | `string` |
182//! | `55` | border_right_background_color | `string` |
183//! | `56` | border_bottom_background_color | `string` |
184//! | `57` | border_left_background_color | `string` |
185//! | `58` | border_dim_color | `bool` |
186//! | `59` | border_top_dim_color | `bool` |
187//! | `60` | border_right_dim_color | `bool` |
188//! | `61` | border_bottom_dim_color | `bool` |
189//! | `62` | border_left_dim_color | `bool` |
190//!
191//! Any other field id is [`DecodeError::UnknownFieldId`].
192//!
193//! ### Nested enum encodings
194//!
195//! * **`Dim`** — `[tag:u8]` then: `0x00` Points → `f32`; `0x01` Percent → `f32`;
196//! `0x02` Auto → (no payload).
197//! * **`Lp`** — `[tag:u8]` then: `0x00` Points → `f32`; `0x01` Percent → `f32`.
198//! * **`Position`** — `0x00` Relative, `0x01` Absolute, `0x02` Static.
199//! * **`FlexDir`** — `0x00` Row, `0x01` Column, `0x02` RowReverse, `0x03`
200//! ColumnReverse.
201//! * **`FlexWrap`** — `0x00` NoWrap, `0x01` Wrap, `0x02` WrapReverse.
202//! * **`Align`** — `0x00` Stretch, `0x01` FlexStart, `0x02` Center, `0x03`
203//! FlexEnd, `0x04` Baseline.
204//! * **`ContentAlign`** — `0x00` FlexStart, `0x01` Center, `0x02` FlexEnd,
205//! `0x03` SpaceBetween, `0x04` SpaceAround, `0x05` SpaceEvenly, `0x06`
206//! Stretch.
207//! * **`Display`** — `0x00` Flex, `0x01` None.
208//! * **`TextWrap`** — `0x00` Wrap, `0x01` Hard, `0x02` TruncateEnd, `0x03`
209//! TruncateMiddle, `0x04` TruncateStart.
210//! * **`Overflow`** — `0x00` Visible, `0x01` Hidden.
211//! * **`BorderStyle`** — `[tag:u8]` then: `0x00` Named → `string`; `0x01`
212//! Custom → eight `string`s in order `top_left, top, top_right, right,
213//! bottom_right, bottom, bottom_left, left`.
214//!
215//! Any out-of-range nested tag is [`DecodeError::UnknownTag`].
216
217use super::node::{
218 Align, AttrValue, BorderStyle, ContentAlign, Dim, Display, FlexDir, FlexWrap, Kind, Lp,
219 Overflow, Position, Style, TextStyle, TextWrap,
220};
221use super::op::Op;
222
223/// A typed decode failure. No variant is reachable by a *valid* buffer; every
224/// one corresponds to a specific malformation at the FFI trust boundary.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum DecodeError {
227 /// The buffer ended in the middle of a record/primitive. Carries the number
228 /// of bytes that were still required.
229 UnexpectedEof,
230 /// An opcode byte that does not map to any [`Op`] variant.
231 UnknownOpcode(u8),
232 /// An enum tag byte (Kind, Dim, AttrValue, a Style sub-enum, …) out of
233 /// range for its position.
234 UnknownTag(u8),
235 /// A `Style` field id with no assigned meaning.
236 UnknownFieldId(u8),
237 /// A `bool` byte that was neither `0x00` nor `0x01`.
238 InvalidBool(u8),
239 /// A length-prefixed string whose bytes were not valid UTF-8.
240 InvalidUtf8,
241}
242
243impl core::fmt::Display for DecodeError {
244 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
245 match self {
246 DecodeError::UnexpectedEof => write!(f, "unexpected end of op buffer"),
247 DecodeError::UnknownOpcode(b) => write!(f, "unknown opcode 0x{b:02X}"),
248 DecodeError::UnknownTag(b) => write!(f, "unknown enum tag 0x{b:02X}"),
249 DecodeError::UnknownFieldId(b) => write!(f, "unknown style field id {b}"),
250 DecodeError::InvalidBool(b) => write!(f, "invalid bool byte 0x{b:02X}"),
251 DecodeError::InvalidUtf8 => write!(f, "invalid utf-8 in length-prefixed string"),
252 }
253 }
254}
255
256impl std::error::Error for DecodeError {}
257
258type Result<T> = core::result::Result<T, DecodeError>;
259
260/// A bounds-checked cursor over the input bytes. Every primitive read goes
261/// through one of these methods, so truncation anywhere is an `UnexpectedEof`
262/// rather than a panic.
263struct Reader<'a> {
264 buf: &'a [u8],
265 pos: usize,
266}
267
268impl<'a> Reader<'a> {
269 fn new(buf: &'a [u8]) -> Self {
270 Self { buf, pos: 0 }
271 }
272
273 fn at_end(&self) -> bool {
274 self.pos >= self.buf.len()
275 }
276
277 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
278 let end = self.pos.checked_add(n).ok_or(DecodeError::UnexpectedEof)?;
279 let slice = self
280 .buf
281 .get(self.pos..end)
282 .ok_or(DecodeError::UnexpectedEof)?;
283 self.pos = end;
284 Ok(slice)
285 }
286
287 fn u8(&mut self) -> Result<u8> {
288 Ok(self.take(1)?[0])
289 }
290
291 fn u32(&mut self) -> Result<u32> {
292 let b = self.take(4)?;
293 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
294 }
295
296 fn f32(&mut self) -> Result<f32> {
297 let b = self.take(4)?;
298 Ok(f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
299 }
300
301 fn f64(&mut self) -> Result<f64> {
302 let b = self.take(8)?;
303 Ok(f64::from_le_bytes([
304 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
305 ]))
306 }
307
308 fn bool(&mut self) -> Result<bool> {
309 match self.u8()? {
310 0 => Ok(false),
311 1 => Ok(true),
312 other => Err(DecodeError::InvalidBool(other)),
313 }
314 }
315
316 fn string(&mut self) -> Result<String> {
317 let len = self.u32()? as usize;
318 let bytes = self.take(len)?;
319 core::str::from_utf8(bytes)
320 .map(str::to_owned)
321 .map_err(|_| DecodeError::InvalidUtf8)
322 }
323}
324
325/// Decode a flat op buffer into a [`Vec<Op>`].
326///
327/// The bytes are the cross-language wire format documented at the module level:
328/// a sequence of `[opcode, id, payload]` records with no outer count, decoded
329/// until end-of-input. Returns a typed [`DecodeError`] on any malformation; it
330/// never panics, even on truncated or garbage input (FFI trust boundary).
331///
332/// # Errors
333///
334/// Returns [`DecodeError`] if the buffer is truncated mid-record
335/// ([`DecodeError::UnexpectedEof`]), contains an unknown opcode, enum tag, or
336/// style field id, an invalid bool byte, or invalid UTF-8 in a string.
337pub fn decode_ops(buf: &[u8]) -> Result<Vec<Op>> {
338 let mut r = Reader::new(buf);
339 let mut ops = Vec::new();
340 while !r.at_end() {
341 ops.push(decode_op(&mut r)?);
342 }
343 Ok(ops)
344}
345
346fn decode_op(r: &mut Reader<'_>) -> Result<Op> {
347 let opcode = r.u8()?;
348 let op = match opcode {
349 0x00 => Op::Create {
350 id: r.u32()?,
351 kind: decode_kind(r)?,
352 },
353 0x01 => Op::AppendChild {
354 parent: r.u32()?,
355 child: r.u32()?,
356 },
357 0x02 => Op::InsertBefore {
358 parent: r.u32()?,
359 child: r.u32()?,
360 before: r.u32()?,
361 },
362 0x03 => Op::RemoveChild {
363 parent: r.u32()?,
364 child: r.u32()?,
365 },
366 0x04 => Op::SetText {
367 id: r.u32()?,
368 text: r.string()?,
369 },
370 0x05 => Op::SetStyle {
371 id: r.u32()?,
372 style: Box::new(decode_style(r)?),
373 },
374 0x06 => Op::SetAttribute {
375 id: r.u32()?,
376 key: r.string()?,
377 value: decode_attr_value(r)?,
378 },
379 0x07 => Op::SetTransform {
380 id: r.u32()?,
381 has: r.bool()?,
382 },
383 0x08 => Op::SetStatic {
384 id: r.u32()?,
385 value: r.bool()?,
386 },
387 0x09 => Op::Hide { id: r.u32()? },
388 0x0A => Op::Unhide { id: r.u32()? },
389 0x0B => Op::Free { id: r.u32()? },
390 0x0C => Op::SetTextStyle {
391 id: r.u32()?,
392 style: decode_text_style(r)?,
393 },
394 0x0D => Op::ClearTextStyle { id: r.u32()? },
395 other => return Err(DecodeError::UnknownOpcode(other)),
396 };
397 Ok(op)
398}
399
400fn decode_kind(r: &mut Reader<'_>) -> Result<Kind> {
401 match r.u8()? {
402 0x00 => Ok(Kind::Root),
403 0x01 => Ok(Kind::Box),
404 0x02 => Ok(Kind::Text),
405 0x03 => Ok(Kind::VirtualText),
406 other => Err(DecodeError::UnknownTag(other)),
407 }
408}
409
410fn decode_attr_value(r: &mut Reader<'_>) -> Result<AttrValue> {
411 match r.u8()? {
412 0x00 => Ok(AttrValue::Bool(r.bool()?)),
413 0x01 => Ok(AttrValue::Str(r.string()?)),
414 0x02 => Ok(AttrValue::Number(r.f64()?)),
415 other => Err(DecodeError::UnknownTag(other)),
416 }
417}
418
419fn decode_dim(r: &mut Reader<'_>) -> Result<Dim> {
420 match r.u8()? {
421 0x00 => Ok(Dim::Points(r.f32()?)),
422 0x01 => Ok(Dim::Percent(r.f32()?)),
423 0x02 => Ok(Dim::Auto),
424 other => Err(DecodeError::UnknownTag(other)),
425 }
426}
427
428fn decode_lp(r: &mut Reader<'_>) -> Result<Lp> {
429 match r.u8()? {
430 0x00 => Ok(Lp::Points(r.f32()?)),
431 0x01 => Ok(Lp::Percent(r.f32()?)),
432 other => Err(DecodeError::UnknownTag(other)),
433 }
434}
435
436fn decode_position(r: &mut Reader<'_>) -> Result<Position> {
437 match r.u8()? {
438 0x00 => Ok(Position::Relative),
439 0x01 => Ok(Position::Absolute),
440 0x02 => Ok(Position::Static),
441 other => Err(DecodeError::UnknownTag(other)),
442 }
443}
444
445fn decode_flex_dir(r: &mut Reader<'_>) -> Result<FlexDir> {
446 match r.u8()? {
447 0x00 => Ok(FlexDir::Row),
448 0x01 => Ok(FlexDir::Column),
449 0x02 => Ok(FlexDir::RowReverse),
450 0x03 => Ok(FlexDir::ColumnReverse),
451 other => Err(DecodeError::UnknownTag(other)),
452 }
453}
454
455fn decode_flex_wrap(r: &mut Reader<'_>) -> Result<FlexWrap> {
456 match r.u8()? {
457 0x00 => Ok(FlexWrap::NoWrap),
458 0x01 => Ok(FlexWrap::Wrap),
459 0x02 => Ok(FlexWrap::WrapReverse),
460 other => Err(DecodeError::UnknownTag(other)),
461 }
462}
463
464fn decode_align(r: &mut Reader<'_>) -> Result<Align> {
465 match r.u8()? {
466 0x00 => Ok(Align::Stretch),
467 0x01 => Ok(Align::FlexStart),
468 0x02 => Ok(Align::Center),
469 0x03 => Ok(Align::FlexEnd),
470 0x04 => Ok(Align::Baseline),
471 other => Err(DecodeError::UnknownTag(other)),
472 }
473}
474
475fn decode_content_align(r: &mut Reader<'_>) -> Result<ContentAlign> {
476 match r.u8()? {
477 0x00 => Ok(ContentAlign::FlexStart),
478 0x01 => Ok(ContentAlign::Center),
479 0x02 => Ok(ContentAlign::FlexEnd),
480 0x03 => Ok(ContentAlign::SpaceBetween),
481 0x04 => Ok(ContentAlign::SpaceAround),
482 0x05 => Ok(ContentAlign::SpaceEvenly),
483 0x06 => Ok(ContentAlign::Stretch),
484 other => Err(DecodeError::UnknownTag(other)),
485 }
486}
487
488fn decode_display(r: &mut Reader<'_>) -> Result<Display> {
489 match r.u8()? {
490 0x00 => Ok(Display::Flex),
491 0x01 => Ok(Display::None),
492 other => Err(DecodeError::UnknownTag(other)),
493 }
494}
495
496fn decode_text_wrap(r: &mut Reader<'_>) -> Result<TextWrap> {
497 match r.u8()? {
498 0x00 => Ok(TextWrap::Wrap),
499 0x01 => Ok(TextWrap::Hard),
500 0x02 => Ok(TextWrap::TruncateEnd),
501 0x03 => Ok(TextWrap::TruncateMiddle),
502 0x04 => Ok(TextWrap::TruncateStart),
503 other => Err(DecodeError::UnknownTag(other)),
504 }
505}
506
507fn decode_overflow(r: &mut Reader<'_>) -> Result<Overflow> {
508 match r.u8()? {
509 0x00 => Ok(Overflow::Visible),
510 0x01 => Ok(Overflow::Hidden),
511 other => Err(DecodeError::UnknownTag(other)),
512 }
513}
514
515fn decode_border_style(r: &mut Reader<'_>) -> Result<BorderStyle> {
516 match r.u8()? {
517 0x00 => Ok(BorderStyle::Named(r.string()?)),
518 0x01 => Ok(BorderStyle::Custom {
519 top_left: r.string()?,
520 top: r.string()?,
521 top_right: r.string()?,
522 right: r.string()?,
523 bottom_right: r.string()?,
524 bottom: r.string()?,
525 bottom_left: r.string()?,
526 left: r.string()?,
527 }),
528 other => Err(DecodeError::UnknownTag(other)),
529 }
530}
531
532fn decode_style(r: &mut Reader<'_>) -> Result<Style> {
533 let mut style = Style::default();
534 let field_count = r.u32()?;
535 for _ in 0..field_count {
536 let field_id = r.u8()?;
537 match field_id {
538 0 => style.position = Some(decode_position(r)?),
539 1 => style.top = Some(decode_dim(r)?),
540 2 => style.right = Some(decode_dim(r)?),
541 3 => style.bottom = Some(decode_dim(r)?),
542 4 => style.left = Some(decode_dim(r)?),
543 5 => style.margin = Some(decode_lp(r)?),
544 6 => style.margin_x = Some(decode_lp(r)?),
545 7 => style.margin_y = Some(decode_lp(r)?),
546 8 => style.margin_top = Some(decode_lp(r)?),
547 9 => style.margin_right = Some(decode_lp(r)?),
548 10 => style.margin_bottom = Some(decode_lp(r)?),
549 11 => style.margin_left = Some(decode_lp(r)?),
550 12 => style.padding = Some(decode_lp(r)?),
551 13 => style.padding_x = Some(decode_lp(r)?),
552 14 => style.padding_y = Some(decode_lp(r)?),
553 15 => style.padding_top = Some(decode_lp(r)?),
554 16 => style.padding_right = Some(decode_lp(r)?),
555 17 => style.padding_bottom = Some(decode_lp(r)?),
556 18 => style.padding_left = Some(decode_lp(r)?),
557 19 => style.flex_direction = Some(decode_flex_dir(r)?),
558 20 => style.flex_wrap = Some(decode_flex_wrap(r)?),
559 21 => style.flex_grow = Some(r.f32()?),
560 22 => style.flex_shrink = Some(r.f32()?),
561 23 => style.flex_basis = Some(decode_dim(r)?),
562 24 => style.align_items = Some(decode_align(r)?),
563 25 => style.align_self = Some(decode_align(r)?),
564 26 => style.align_content = Some(decode_content_align(r)?),
565 27 => style.justify_content = Some(decode_content_align(r)?),
566 28 => style.width = Some(decode_dim(r)?),
567 29 => style.height = Some(decode_dim(r)?),
568 30 => style.min_width = Some(decode_dim(r)?),
569 31 => style.min_height = Some(decode_dim(r)?),
570 32 => style.max_width = Some(decode_dim(r)?),
571 33 => style.max_height = Some(decode_dim(r)?),
572 34 => style.aspect_ratio = Some(r.f32()?),
573 35 => style.display = Some(decode_display(r)?),
574 36 => style.border_style = Some(decode_border_style(r)?),
575 37 => style.border_top = Some(r.bool()?),
576 38 => style.border_right = Some(r.bool()?),
577 39 => style.border_bottom = Some(r.bool()?),
578 40 => style.border_left = Some(r.bool()?),
579 41 => style.gap = Some(r.f32()?),
580 42 => style.column_gap = Some(r.f32()?),
581 43 => style.row_gap = Some(r.f32()?),
582 44 => style.text_wrap = Some(decode_text_wrap(r)?),
583 45 => style.overflow_x = Some(decode_overflow(r)?),
584 46 => style.overflow_y = Some(decode_overflow(r)?),
585 47 => style.background_color = Some(r.string()?),
586 48 => style.border_color = Some(r.string()?),
587 49 => style.border_top_color = Some(r.string()?),
588 50 => style.border_right_color = Some(r.string()?),
589 51 => style.border_bottom_color = Some(r.string()?),
590 52 => style.border_left_color = Some(r.string()?),
591 53 => style.border_background_color = Some(r.string()?),
592 54 => style.border_top_background_color = Some(r.string()?),
593 55 => style.border_right_background_color = Some(r.string()?),
594 56 => style.border_bottom_background_color = Some(r.string()?),
595 57 => style.border_left_background_color = Some(r.string()?),
596 58 => style.border_dim_color = Some(r.bool()?),
597 59 => style.border_top_dim_color = Some(r.bool()?),
598 60 => style.border_right_dim_color = Some(r.bool()?),
599 61 => style.border_bottom_dim_color = Some(r.bool()?),
600 62 => style.border_left_dim_color = Some(r.bool()?),
601 other => return Err(DecodeError::UnknownFieldId(other)),
602 }
603 }
604 Ok(style)
605}
606
607/// Decode a `TextStyle` (P5.1 SET_TEXT_STYLE) from the self-describing tagged
608/// field-id scheme. Mirrors [`decode_style`]: start from default, write each
609/// decoded field. The render walk reads the stored `text_styling` via
610/// `resolve_transform` for the native simple-`<Text>` path (P5.1b); a later
611/// `ClearTextStyle` (0x0D) resets it to `None` on a styled→plain rerender.
612fn decode_text_style(r: &mut Reader<'_>) -> Result<TextStyle> {
613 let mut style = TextStyle::default();
614 let field_count = r.u32()?;
615 for _ in 0..field_count {
616 let field_id = r.u8()?;
617 match field_id {
618 0 => style.color = Some(r.string()?),
619 1 => style.background_color = Some(r.string()?),
620 2 => style.bold = r.bool()?,
621 3 => style.italic = r.bool()?,
622 4 => style.underline = r.bool()?,
623 5 => style.strikethrough = r.bool()?,
624 6 => style.inverse = r.bool()?,
625 7 => style.dim_color = r.bool()?,
626 other => return Err(DecodeError::UnknownFieldId(other)),
627 }
628 }
629 Ok(style)
630}
631
632#[cfg(test)]
633#[path = "decode_tests.rs"]
634mod decode_tests;