Skip to main content

compose_rust/
protocol.rs

1//! Fixed-layout little-endian boundary protocol.
2
3use crate::schema::{
4    AssetKind, ColorScheme, DesignSystem, Key, MaterialRole, MessageDuration, Modifier, MotionRole,
5    Paint, PropertyKind, Selection, ShapeRole, SpaceRole, TYPE_ROLE_COUNT, Theme, WidgetKind,
6};
7use core::fmt;
8
9const TAG_ENVELOPE: u16 = 0;
10const TAG_CREATE: u16 = 1;
11const TAG_SET_PROP: u16 = 2;
12const TAG_SET_MODIFIER: u16 = 3;
13const TAG_INSERT: u16 = 4;
14const TAG_MOVE: u16 = 5;
15const TAG_REMOVE: u16 = 6;
16const TAG_SET_TEXT: u16 = 7;
17const TAG_APPEND_TEXT: u16 = 8;
18const TAG_SET_THEME: u16 = 9;
19const TAG_REGISTER_ASSET: u16 = 10;
20const TAG_RELEASE_ASSET: u16 = 11;
21const TAG_SHOW_MESSAGE: u16 = 12;
22const TAG_SET_WINDOW: u16 = 13;
23const ENVELOPE_LEN: usize = 12;
24/// The shortest mutation record on the wire (`Remove`: 4-byte header + `node_id`).
25const MIN_RECORD_LEN: usize = 8;
26
27const VALUE_NONE: u16 = 0;
28const VALUE_STRING: u16 = 1;
29const VALUE_BOOL: u16 = 2;
30const VALUE_I64: u16 = 3;
31const VALUE_F32: u16 = 4;
32/// An opaque byte run in the arena. Same `(offset, length)` layout as a string, with no
33/// UTF-8 check, because a Canvas command list is not text.
34const VALUE_BYTES: u16 = 5;
35
36#[derive(Clone, Debug, PartialEq)]
37pub enum PropertyValue<'a> {
38    None,
39    String(&'a str),
40    Bool(bool),
41    Integer(i64),
42    Float(f32),
43    Bytes(&'a [u8]),
44}
45
46#[derive(Clone, Debug, PartialEq)]
47pub enum Mutation<'a> {
48    Create {
49        node_id: u32,
50        widget: WidgetKind,
51    },
52    SetProp {
53        node_id: u32,
54        property: PropertyKind,
55        value: PropertyValue<'a>,
56    },
57    SetModifier {
58        node_id: u32,
59        index: u16,
60        modifier: Modifier,
61    },
62    Insert {
63        parent_id: u32,
64        node_id: u32,
65        index: u32,
66    },
67    Move {
68        parent_id: u32,
69        node_id: u32,
70        index: u32,
71    },
72    Remove {
73        node_id: u32,
74    },
75    SetText {
76        node_id: u32,
77        text: &'a str,
78        selection: Option<Selection>,
79    },
80    /// Appends the streamed tail to a Text node instead of resending the whole string.
81    AppendText {
82        node_id: u32,
83        text: &'a str,
84    },
85    /// The root theme. Sent once as the first record of the initial batch.
86    SetTheme(Theme),
87    /// What the application asked of its own window.
88    ///
89    /// Written once per rebuild beside the theme, and read by the Renderer before it
90    /// stands the window up: `dioxus_compose_host_init` answers with the first batch, so
91    /// the setting is in hand before there is a window to apply it to. Platforms where
92    /// the window is not ours ignore it, which is not an error but an absence of anywhere
93    /// to put it.
94    SetWindow(crate::schema::Window),
95    /// Hands the Renderer the bytes of one asset. The Renderer copies them into its own
96    /// cache inside this call, because the batch buffer is only valid for the call that
97    /// carries it and an image has to outlive the frame that draws it.
98    RegisterAsset {
99        asset_id: u32,
100        kind: AssetKind,
101        bytes: &'a [u8],
102    },
103    /// Drops an asset from the Renderer's cache. The Host owns the lifetime.
104    ReleaseAsset {
105        asset_id: u32,
106    },
107    /// Says a sentence to the user once. It is not a node, because the Host would then
108    /// have to hold "showing until four seconds from now" and run a render to take it
109    /// away again. How long it stays, where it sits and what happens when a second one
110    /// arrives while the first is up are all the Renderer's.
111    ///
112    /// `handler_id` is what the action label reports when it is pressed, or 0 when the
113    /// message has no action, in which case `action` is empty too.
114    ShowMessage {
115        handler_id: u64,
116        text: &'a str,
117        action: &'a str,
118        duration: MessageDuration,
119    },
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub enum ProtocolError {
124    Truncated,
125    InvalidEnvelope,
126    InvalidRecordLength,
127    InvalidTag(u16),
128    InvalidWidget(u16),
129    InvalidProperty(u16),
130    InvalidValueKind(u16),
131    InvalidModifier(u16),
132    InvalidTheme(u16),
133    InvalidAssetKind(u16),
134    InvalidMessageDuration(u16),
135    InvalidStringRange,
136    InvalidUtf8,
137    LengthOverflow,
138}
139
140#[derive(Clone, Debug, PartialEq)]
141pub struct HostEvent<'a> {
142    pub node_id: u32,
143    pub handler_id: u64,
144    pub payload: crate::schema::EventPayload<'a>,
145}
146
147const EVENT_CLICK: u16 = 1;
148const EVENT_TEXT_CHANGED: u16 = 2;
149const EVENT_TEXT_SUBMITTED: u16 = 3;
150const EVENT_FOCUS_LOST: u16 = 4;
151const EVENT_PROTOCOL_ERROR: u16 = 5;
152const EVENT_KEY_DOWN: u16 = 6;
153const EVENT_RANGE_REQUESTED: u16 = 7;
154// Tags 8 to 15 are reserved for the pointer gesture events, so this one starts at 16
155// and the window size report continues from 17.
156const EVENT_VALUE_CHANGED: u16 = 16;
157const EVENT_WINDOW_SIZE_CHANGED: u16 = 17;
158const EVENT_RESYNC: u16 = 18;
159const EVENT_LIFECYCLE_START: u16 = 19;
160const EVENT_LIFECYCLE_STOP: u16 = 20;
161const EVENT_DESIGN_SYSTEM_RESOLVED: u16 = 21;
162const EVENT_FILES_ENTERED: u16 = 22;
163const EVENT_FILES_DROPPED: u16 = 23;
164
165const MODIFIER_SHIFT: u8 = 1 << 0;
166const MODIFIER_CTRL: u8 = 1 << 1;
167const MODIFIER_ALT: u8 = 1 << 2;
168const MODIFIER_META: u8 = 1 << 3;
169
170/// Decode one Renderer-to-Host event record.
171pub fn decode_event(bytes: &[u8]) -> Result<HostEvent<'_>, ProtocolError> {
172    if bytes.len() < 16 {
173        return Err(ProtocolError::Truncated);
174    }
175    let tag = read_u16(bytes, 0)?;
176    let record_len = usize::from(read_u16(bytes, 2)?);
177    if record_len < 16 || record_len % 4 != 0 || record_len > bytes.len() {
178        return Err(ProtocolError::InvalidRecordLength);
179    }
180    let node_id = read_u32(bytes, 4)?;
181    let handler_id = read_u64(bytes, 8)?;
182    let payload = match tag {
183        EVENT_CLICK if record_len == 16 => crate::schema::EventPayload::Clicked,
184        EVENT_TEXT_CHANGED if record_len == 24 => {
185            crate::schema::EventPayload::TextChanged(read_string(bytes, 16, record_len)?)
186        }
187        EVENT_TEXT_SUBMITTED if record_len == 24 => {
188            crate::schema::EventPayload::TextSubmitted(read_string(bytes, 16, record_len)?)
189        }
190        EVENT_FOCUS_LOST if record_len == 16 => crate::schema::EventPayload::FocusLost,
191        EVENT_PROTOCOL_ERROR if record_len == 28 => crate::schema::EventPayload::ProtocolError {
192            code: read_u32(bytes, 16)?,
193            message: read_string(bytes, 20, record_len)?,
194        },
195        EVENT_KEY_DOWN if record_len == 20 => {
196            let raw_key = read_u16(bytes, 16)?;
197            let modifiers = *bytes.get(18).ok_or(ProtocolError::Truncated)?;
198            crate::schema::EventPayload::KeyDown {
199                key: Key::try_from(raw_key)
200                    .map_err(|()| ProtocolError::InvalidValueKind(raw_key))?,
201                shift_key: modifiers & MODIFIER_SHIFT != 0,
202                ctrl_key: modifiers & MODIFIER_CTRL != 0,
203                alt_key: modifiers & MODIFIER_ALT != 0,
204                meta_key: modifiers & MODIFIER_META != 0,
205            }
206        }
207        EVENT_RANGE_REQUESTED if record_len == 24 => crate::schema::EventPayload::RangeRequested {
208            start: read_u32(bytes, 16)?,
209            count: read_u32(bytes, 20)?,
210        },
211        EVENT_VALUE_CHANGED if record_len == 24 => {
212            crate::schema::EventPayload::ValueChanged(f64::from_bits(read_u64(bytes, 16)?))
213        }
214        EVENT_WINDOW_SIZE_CHANGED if record_len == 32 => {
215            let raw_class = read_u32(bytes, 24)?;
216            let class = u16::try_from(raw_class)
217                .ok()
218                .and_then(|value| crate::schema::WindowSizeClass::try_from(value).ok())
219                .ok_or(ProtocolError::InvalidValueKind(raw_class as u16))?;
220            let raw_height_class = read_u32(bytes, 28)?;
221            let height_class = u16::try_from(raw_height_class)
222                .ok()
223                .and_then(|value| crate::schema::WindowHeightClass::try_from(value).ok())
224                .ok_or(ProtocolError::InvalidValueKind(raw_height_class as u16))?;
225            crate::schema::EventPayload::WindowSizeChanged {
226                width_dp: f32::from_bits(read_u32(bytes, 16)?),
227                height_dp: f32::from_bits(read_u32(bytes, 20)?),
228                class,
229                height_class,
230            }
231        }
232        EVENT_DESIGN_SYSTEM_RESOLVED if record_len == 20 => {
233            let raw = read_u32(bytes, 16)?;
234            let system = u16::try_from(raw)
235                .ok()
236                .and_then(|tag| crate::schema::DesignSystem::try_from(tag).ok())
237                .ok_or(ProtocolError::InvalidValueKind(raw as u16))?;
238            crate::schema::EventPayload::DesignSystemResolved(system)
239        }
240        EVENT_FILES_ENTERED if record_len == 16 => crate::schema::EventPayload::FilesEntered,
241        EVENT_FILES_DROPPED if record_len == 24 => {
242            crate::schema::EventPayload::FilesDropped(read_string(bytes, 16, record_len)?)
243        }
244        EVENT_RESYNC if record_len == 16 => crate::schema::EventPayload::Resync,
245        EVENT_LIFECYCLE_START if record_len == 16 => crate::schema::EventPayload::LifecycleStart,
246        EVENT_LIFECYCLE_STOP if record_len == 16 => crate::schema::EventPayload::LifecycleStop,
247        EVENT_CLICK..=EVENT_RANGE_REQUESTED | EVENT_VALUE_CHANGED..=EVENT_LIFECYCLE_STOP => {
248            return Err(ProtocolError::InvalidRecordLength);
249        }
250        other => return Err(ProtocolError::InvalidTag(other)),
251    };
252    Ok(HostEvent {
253        node_id,
254        handler_id,
255        payload,
256    })
257}
258
259/// Test/mock helper for constructing a Renderer-to-Host event.
260pub fn encode_event(event: &HostEvent<'_>, output: &mut Vec<u8>) -> Result<(), ProtocolError> {
261    output.clear();
262    if let crate::schema::EventPayload::KeyDown {
263        key,
264        shift_key,
265        ctrl_key,
266        alt_key,
267        meta_key,
268    } = event.payload
269    {
270        output.extend_from_slice(&EVENT_KEY_DOWN.to_le_bytes());
271        output.extend_from_slice(&20_u16.to_le_bytes());
272        output.extend_from_slice(&event.node_id.to_le_bytes());
273        output.extend_from_slice(&event.handler_id.to_le_bytes());
274        output.extend_from_slice(&(key as u16).to_le_bytes());
275        let modifiers = (u8::from(shift_key) * MODIFIER_SHIFT)
276            | (u8::from(ctrl_key) * MODIFIER_CTRL)
277            | (u8::from(alt_key) * MODIFIER_ALT)
278            | (u8::from(meta_key) * MODIFIER_META);
279        output.push(modifiers);
280        output.push(0);
281        return Ok(());
282    }
283    if let crate::schema::EventPayload::ValueChanged(value) = event.payload {
284        output.extend_from_slice(&EVENT_VALUE_CHANGED.to_le_bytes());
285        output.extend_from_slice(&24_u16.to_le_bytes());
286        output.extend_from_slice(&event.node_id.to_le_bytes());
287        output.extend_from_slice(&event.handler_id.to_le_bytes());
288        output.extend_from_slice(&value.to_bits().to_le_bytes());
289        return Ok(());
290    }
291    if let crate::schema::EventPayload::WindowSizeChanged {
292        width_dp,
293        height_dp,
294        class,
295        height_class,
296    } = event.payload
297    {
298        output.extend_from_slice(&EVENT_WINDOW_SIZE_CHANGED.to_le_bytes());
299        output.extend_from_slice(&32_u16.to_le_bytes());
300        output.extend_from_slice(&event.node_id.to_le_bytes());
301        output.extend_from_slice(&event.handler_id.to_le_bytes());
302        output.extend_from_slice(&width_dp.to_bits().to_le_bytes());
303        output.extend_from_slice(&height_dp.to_bits().to_le_bytes());
304        output.extend_from_slice(&u32::from(u16::from(class)).to_le_bytes());
305        output.extend_from_slice(&u32::from(u16::from(height_class)).to_le_bytes());
306        return Ok(());
307    }
308    if let crate::schema::EventPayload::DesignSystemResolved(system) = event.payload {
309        output.extend_from_slice(&EVENT_DESIGN_SYSTEM_RESOLVED.to_le_bytes());
310        output.extend_from_slice(&20_u16.to_le_bytes());
311        output.extend_from_slice(&event.node_id.to_le_bytes());
312        output.extend_from_slice(&event.handler_id.to_le_bytes());
313        // The tag in a word, which keeps the record a multiple of four the way every
314        // other record here is.
315        output.extend_from_slice(&u32::from(u16::from(system)).to_le_bytes());
316        return Ok(());
317    }
318    if let crate::schema::EventPayload::RangeRequested { start, count } = event.payload {
319        output.extend_from_slice(&EVENT_RANGE_REQUESTED.to_le_bytes());
320        output.extend_from_slice(&24_u16.to_le_bytes());
321        output.extend_from_slice(&event.node_id.to_le_bytes());
322        output.extend_from_slice(&event.handler_id.to_le_bytes());
323        output.extend_from_slice(&start.to_le_bytes());
324        output.extend_from_slice(&count.to_le_bytes());
325        return Ok(());
326    }
327    let (tag, record_len, text, error_code) = match &event.payload {
328        crate::schema::EventPayload::Clicked => (EVENT_CLICK, 16_u16, None, None),
329        crate::schema::EventPayload::TextChanged(value) => {
330            (EVENT_TEXT_CHANGED, 24, Some(*value), None)
331        }
332        crate::schema::EventPayload::TextSubmitted(value) => {
333            (EVENT_TEXT_SUBMITTED, 24, Some(*value), None)
334        }
335        crate::schema::EventPayload::FocusLost => (EVENT_FOCUS_LOST, 16, None, None),
336        crate::schema::EventPayload::FilesEntered => (EVENT_FILES_ENTERED, 16, None, None),
337        crate::schema::EventPayload::FilesDropped(paths) => {
338            (EVENT_FILES_DROPPED, 24, Some(*paths), None)
339        }
340        crate::schema::EventPayload::Resync => (EVENT_RESYNC, 16, None, None),
341        crate::schema::EventPayload::LifecycleStart => (EVENT_LIFECYCLE_START, 16, None, None),
342        crate::schema::EventPayload::LifecycleStop => (EVENT_LIFECYCLE_STOP, 16, None, None),
343        crate::schema::EventPayload::ProtocolError { code, message } => {
344            (EVENT_PROTOCOL_ERROR, 28, Some(*message), Some(*code))
345        }
346        crate::schema::EventPayload::KeyDown { .. }
347        | crate::schema::EventPayload::RangeRequested { .. }
348        | crate::schema::EventPayload::ValueChanged(_)
349        | crate::schema::EventPayload::WindowSizeChanged { .. }
350        | crate::schema::EventPayload::DesignSystemResolved(_) => unreachable!(),
351    };
352    output.extend_from_slice(&tag.to_le_bytes());
353    output.extend_from_slice(&record_len.to_le_bytes());
354    output.extend_from_slice(&event.node_id.to_le_bytes());
355    output.extend_from_slice(&event.handler_id.to_le_bytes());
356    if let Some(code) = error_code {
357        output.extend_from_slice(&code.to_le_bytes());
358    }
359    if let Some(text) = text {
360        let offset = u32::from(record_len);
361        let len = u32::try_from(text.len()).map_err(|_| ProtocolError::LengthOverflow)?;
362        output.extend_from_slice(&offset.to_le_bytes());
363        output.extend_from_slice(&len.to_le_bytes());
364        output.extend_from_slice(text.as_bytes());
365    }
366    Ok(())
367}
368
369impl fmt::Display for ProtocolError {
370    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
371        write!(formatter, "protocol error: {self:?}")
372    }
373}
374
375impl std::error::Error for ProtocolError {}
376
377/// Reusable encoder. Once capacities are warmed, `clear` and `finish` allocate no memory.
378pub struct BatchEncoder {
379    records: Vec<u8>,
380    strings: Vec<u8>,
381    string_fixups: Vec<usize>,
382    arena: Vec<u8>,
383    record_count: u32,
384}
385
386impl Default for BatchEncoder {
387    fn default() -> Self {
388        Self::with_capacity(4096, 1024, 64)
389    }
390}
391
392impl BatchEncoder {
393    pub fn with_capacity(record_bytes: usize, string_bytes: usize, string_count: usize) -> Self {
394        let mut encoder = Self {
395            records: Vec::with_capacity(record_bytes),
396            strings: Vec::with_capacity(string_bytes),
397            string_fixups: Vec::with_capacity(string_count),
398            arena: Vec::with_capacity(record_bytes + string_bytes),
399            record_count: 0,
400        };
401        encoder.clear();
402        encoder
403    }
404
405    pub fn clear(&mut self) {
406        self.records.clear();
407        self.strings.clear();
408        self.string_fixups.clear();
409        self.arena.clear();
410        self.record_count = 0;
411        self.begin_record(TAG_ENVELOPE, 8);
412        self.put_u32(0);
413        self.put_u32(0);
414    }
415
416    pub fn encode(&mut self, mutation: &Mutation<'_>) -> Result<(), ProtocolError> {
417        match mutation {
418            Mutation::Create { node_id, widget } => {
419                self.begin_record(TAG_CREATE, 8);
420                self.put_u32(*node_id);
421                self.put_u16(*widget as u16);
422                self.put_u16(0);
423            }
424            Mutation::SetProp {
425                node_id,
426                property,
427                value,
428            } => {
429                self.begin_record(TAG_SET_PROP, 20);
430                self.put_u32(*node_id);
431                self.put_u16(*property as u16);
432                match value {
433                    PropertyValue::None => {
434                        self.put_u16(VALUE_NONE);
435                        self.put_u64(0);
436                        self.put_u32(0);
437                    }
438                    PropertyValue::String(value) => {
439                        self.put_u16(VALUE_STRING);
440                        self.put_string_ref(value)?;
441                        self.put_u32(0);
442                    }
443                    PropertyValue::Bool(value) => {
444                        self.put_u16(VALUE_BOOL);
445                        self.put_u64(u64::from(*value));
446                        self.put_u32(0);
447                    }
448                    PropertyValue::Integer(value) => {
449                        self.put_u16(VALUE_I64);
450                        self.put_u64(*value as u64);
451                        self.put_u32(0);
452                    }
453                    PropertyValue::Float(value) => {
454                        self.put_u16(VALUE_F32);
455                        self.put_u64(u64::from(value.to_bits()));
456                        self.put_u32(0);
457                    }
458                    PropertyValue::Bytes(value) => {
459                        self.put_u16(VALUE_BYTES);
460                        self.put_bytes_ref(value)?;
461                        self.put_u32(0);
462                    }
463                }
464            }
465            Mutation::SetModifier {
466                node_id,
467                index,
468                modifier,
469            } => {
470                self.begin_record(TAG_SET_MODIFIER, 24);
471                self.put_u32(*node_id);
472                self.put_u16(*index);
473                let (tag, first, second) = modifier_fields(modifier);
474                self.put_u16(tag);
475                self.put_u64(first);
476                self.put_u64(second);
477            }
478            Mutation::Insert {
479                parent_id,
480                node_id,
481                index,
482            } => {
483                self.begin_record(TAG_INSERT, 12);
484                self.put_u32(*parent_id);
485                self.put_u32(*node_id);
486                self.put_u32(*index);
487            }
488            Mutation::Move {
489                parent_id,
490                node_id,
491                index,
492            } => {
493                self.begin_record(TAG_MOVE, 12);
494                self.put_u32(*parent_id);
495                self.put_u32(*node_id);
496                self.put_u32(*index);
497            }
498            Mutation::Remove { node_id } => {
499                self.begin_record(TAG_REMOVE, 4);
500                self.put_u32(*node_id);
501            }
502            Mutation::SetText {
503                node_id,
504                text,
505                selection,
506            } => {
507                self.begin_record(TAG_SET_TEXT, 20);
508                self.put_u32(*node_id);
509                self.put_string_ref(text)?;
510                let (start, end) =
511                    selection.map_or((u32::MAX, u32::MAX), |value| (value.start, value.end));
512                self.put_u32(start);
513                self.put_u32(end);
514            }
515            Mutation::AppendText { node_id, text } => {
516                self.begin_record(TAG_APPEND_TEXT, 12);
517                self.put_u32(*node_id);
518                self.put_string_ref(text)?;
519            }
520            Mutation::RegisterAsset {
521                asset_id,
522                kind,
523                bytes,
524            } => {
525                self.begin_record(TAG_REGISTER_ASSET, 16);
526                self.put_u32(*asset_id);
527                self.put_u16(*kind as u16);
528                self.put_u16(0);
529                self.put_bytes_ref(bytes)?;
530            }
531            Mutation::ReleaseAsset { asset_id } => {
532                self.begin_record(TAG_RELEASE_ASSET, 4);
533                self.put_u32(*asset_id);
534            }
535            Mutation::ShowMessage {
536                handler_id,
537                text,
538                action,
539                duration,
540            } => {
541                self.begin_record(TAG_SHOW_MESSAGE, 28);
542                self.put_u64(*handler_id);
543                self.put_string_ref(text)?;
544                self.put_string_ref(action)?;
545                self.put_u16(*duration as u16);
546                self.put_u16(0);
547            }
548            Mutation::SetTheme(theme) => {
549                self.begin_record(TAG_SET_THEME, 8 + 4 * TYPE_ROLE_COUNT as u16);
550                self.put_u16(theme.design_system as u16);
551                self.put_u16(theme.fallback as u16);
552                self.put_u16(theme.color_scheme as u16);
553                self.put_u16(u16::from(theme.adaptive));
554                // One slot per type role, zero where the role keeps the system font. Sent
555                // whether or not any of them is filled, because the record is fixed
556                // layout and a theme change is one record rather than a per frame cost.
557                for asset in theme.fonts {
558                    self.put_u32(asset);
559                }
560            }
561            Mutation::SetWindow(window) => {
562                self.begin_record(TAG_SET_WINDOW, 28);
563                self.put_u16(window.chrome as u16);
564                self.put_u16(window.title_bar as u16);
565                self.put_u16(window.width);
566                self.put_u16(window.height);
567                self.put_u16(window.min_width);
568                self.put_u16(window.min_height);
569                self.put_u16(u16::from(window.resizable));
570                // The padding that keeps the record a multiple of four, which the envelope
571                // requires. Seven of these words is an odd number of them.
572                self.put_u16(0);
573                self.put_string_ref(window.title)?;
574                self.put_u32(window.icon);
575            }
576        }
577        self.record_count = self
578            .record_count
579            .checked_add(1)
580            .ok_or(ProtocolError::LengthOverflow)?;
581        Ok(())
582    }
583
584    pub fn finish(&mut self) -> Result<&[u8], ProtocolError> {
585        let records_len =
586            u32::try_from(self.records.len()).map_err(|_| ProtocolError::LengthOverflow)?;
587        self.records[4..8].copy_from_slice(&records_len.to_le_bytes());
588        self.records[8..12].copy_from_slice(&self.record_count.to_le_bytes());
589        let total = self
590            .records
591            .len()
592            .checked_add(self.strings.len())
593            .ok_or(ProtocolError::LengthOverflow)?;
594        self.arena.clear();
595        if self.arena.capacity() < total {
596            self.arena.reserve(total);
597        }
598        self.arena.extend_from_slice(&self.records);
599        self.arena.extend_from_slice(&self.strings);
600        for &position in &self.string_fixups {
601            let relative = read_u32(&self.arena, position)?;
602            let absolute = records_len
603                .checked_add(relative)
604                .ok_or(ProtocolError::LengthOverflow)?;
605            self.arena[position..position + 4].copy_from_slice(&absolute.to_le_bytes());
606        }
607        Ok(&self.arena)
608    }
609
610    /// Where the arena a finished batch lives in starts, and how much of it is mapped.
611    ///
612    /// A runtime that cannot read the Host's memory directly wraps this range once and
613    /// keeps the view: every batch is a prefix of the arena, so it is read where it lies
614    /// and no byte is copied out. Growing the arena moves it, which is why the address is
615    /// reported again on every call rather than once at startup.
616    pub fn arena(&self) -> (*const u8, usize) {
617        (self.arena.as_ptr(), self.arena.capacity())
618    }
619
620    pub fn capacity(&self) -> usize {
621        self.records.capacity() + self.strings.capacity() + self.arena.capacity()
622    }
623
624    fn begin_record(&mut self, tag: u16, payload_len: u16) {
625        self.put_u16(tag);
626        self.put_u16(payload_len + 4);
627    }
628
629    fn put_string_ref(&mut self, value: &str) -> Result<(), ProtocolError> {
630        self.put_bytes_ref(value.as_bytes())
631    }
632
633    /// Asset bytes ride in the same trailing region as strings, with the same
634    /// `(offset, len)` reference and the same fixup, so the record stays fixed layout.
635    fn put_bytes_ref(&mut self, value: &[u8]) -> Result<(), ProtocolError> {
636        let offset =
637            u32::try_from(self.strings.len()).map_err(|_| ProtocolError::LengthOverflow)?;
638        let len = u32::try_from(value.len()).map_err(|_| ProtocolError::LengthOverflow)?;
639        self.string_fixups.push(self.records.len());
640        self.put_u32(offset);
641        self.put_u32(len);
642        self.strings.extend_from_slice(value);
643        Ok(())
644    }
645
646    fn put_u16(&mut self, value: u16) {
647        self.records.extend_from_slice(&value.to_le_bytes());
648    }
649
650    fn put_u32(&mut self, value: u32) {
651        self.records.extend_from_slice(&value.to_le_bytes());
652    }
653
654    fn put_u64(&mut self, value: u64) {
655        self.records.extend_from_slice(&value.to_le_bytes());
656    }
657}
658
659pub fn decode_batch(bytes: &[u8]) -> Result<Vec<Mutation<'_>>, ProtocolError> {
660    if bytes.len() < ENVELOPE_LEN
661        || read_u16(bytes, 0)? != TAG_ENVELOPE
662        || read_u16(bytes, 2)? != 12
663    {
664        return Err(ProtocolError::InvalidEnvelope);
665    }
666    let records_len =
667        usize::try_from(read_u32(bytes, 4)?).map_err(|_| ProtocolError::LengthOverflow)?;
668    let count = usize::try_from(read_u32(bytes, 8)?).map_err(|_| ProtocolError::LengthOverflow)?;
669    if records_len < ENVELOPE_LEN || records_len > bytes.len() || records_len % 4 != 0 {
670        return Err(ProtocolError::InvalidEnvelope);
671    }
672    // `count` is attacker-controlled, so it only sizes the buffer up to what the
673    // record region could actually hold. The shortest mutation record is `Remove` at 8
674    // bytes, so that is the ceiling. Reserving `count` directly let a 12-byte message ask
675    // for a >100 GB allocation.
676    let capacity = count.min((records_len - ENVELOPE_LEN) / MIN_RECORD_LEN);
677    let mut output = Vec::with_capacity(capacity);
678    let mut position = ENVELOPE_LEN;
679    while position < records_len {
680        let tag = read_u16(bytes, position)?;
681        let len = usize::from(read_u16(bytes, position + 2)?);
682        if len < 4 || len % 4 != 0 || position + len > records_len {
683            return Err(ProtocolError::InvalidRecordLength);
684        }
685        let payload = position + 4;
686        let mutation = match tag {
687            TAG_CREATE if len == 12 => Mutation::Create {
688                node_id: read_u32(bytes, payload)?,
689                widget: WidgetKind::try_from(read_u16(bytes, payload + 4)?).map_err(|()| {
690                    ProtocolError::InvalidWidget(read_u16(bytes, payload + 4).unwrap_or(0))
691                })?,
692            },
693            TAG_SET_PROP if len == 24 => {
694                let property_raw = read_u16(bytes, payload + 4)?;
695                let value_kind = read_u16(bytes, payload + 6)?;
696                let value = match value_kind {
697                    VALUE_NONE => PropertyValue::None,
698                    VALUE_STRING => {
699                        PropertyValue::String(read_string(bytes, payload + 8, records_len)?)
700                    }
701                    VALUE_BOOL => PropertyValue::Bool(read_u64(bytes, payload + 8)? != 0),
702                    VALUE_I64 => PropertyValue::Integer(read_u64(bytes, payload + 8)? as i64),
703                    VALUE_F32 => {
704                        PropertyValue::Float(f32::from_bits(read_u64(bytes, payload + 8)? as u32))
705                    }
706                    VALUE_BYTES => {
707                        PropertyValue::Bytes(read_bytes(bytes, payload + 8, records_len)?)
708                    }
709                    other => return Err(ProtocolError::InvalidValueKind(other)),
710                };
711                Mutation::SetProp {
712                    node_id: read_u32(bytes, payload)?,
713                    property: PropertyKind::try_from(property_raw)
714                        .map_err(|()| ProtocolError::InvalidProperty(property_raw))?,
715                    value,
716                }
717            }
718            TAG_SET_MODIFIER if len == 28 => Mutation::SetModifier {
719                node_id: read_u32(bytes, payload)?,
720                index: read_u16(bytes, payload + 4)?,
721                modifier: decode_modifier(
722                    read_u16(bytes, payload + 6)?,
723                    read_u64(bytes, payload + 8)?,
724                    read_u64(bytes, payload + 16)?,
725                )?,
726            },
727            TAG_INSERT if len == 16 => Mutation::Insert {
728                parent_id: read_u32(bytes, payload)?,
729                node_id: read_u32(bytes, payload + 4)?,
730                index: read_u32(bytes, payload + 8)?,
731            },
732            TAG_MOVE if len == 16 => Mutation::Move {
733                parent_id: read_u32(bytes, payload)?,
734                node_id: read_u32(bytes, payload + 4)?,
735                index: read_u32(bytes, payload + 8)?,
736            },
737            TAG_REMOVE if len == 8 => Mutation::Remove {
738                node_id: read_u32(bytes, payload)?,
739            },
740            TAG_SET_TEXT if len == 24 => {
741                let start = read_u32(bytes, payload + 12)?;
742                let end = read_u32(bytes, payload + 16)?;
743                Mutation::SetText {
744                    node_id: read_u32(bytes, payload)?,
745                    text: read_string(bytes, payload + 4, records_len)?,
746                    selection: (start != u32::MAX || end != u32::MAX)
747                        .then_some(Selection { start, end }),
748                }
749            }
750            TAG_APPEND_TEXT if len == 16 => Mutation::AppendText {
751                node_id: read_u32(bytes, payload)?,
752                text: read_string(bytes, payload + 4, records_len)?,
753            },
754            TAG_SET_THEME if len == 12 + 4 * TYPE_ROLE_COUNT => {
755                let design_system = read_u16(bytes, payload)?;
756                let fallback = read_u16(bytes, payload + 2)?;
757                let color_scheme = read_u16(bytes, payload + 4)?;
758                let adaptive = read_u16(bytes, payload + 6)?;
759                if adaptive > 1 {
760                    return Err(ProtocolError::InvalidTheme(adaptive));
761                }
762                let mut fonts = [0u32; TYPE_ROLE_COUNT];
763                for (index, slot) in fonts.iter_mut().enumerate() {
764                    *slot = read_u32(bytes, payload + 8 + 4 * index)?;
765                }
766                Mutation::SetTheme(Theme {
767                    design_system: DesignSystem::try_from(design_system)
768                        .map_err(|()| ProtocolError::InvalidTheme(design_system))?,
769                    fallback: DesignSystem::try_from(fallback)
770                        .map_err(|()| ProtocolError::InvalidTheme(fallback))?,
771                    color_scheme: ColorScheme::try_from(color_scheme)
772                        .map_err(|()| ProtocolError::InvalidTheme(color_scheme))?,
773                    adaptive: adaptive == 1,
774                    fonts,
775                })
776            }
777            TAG_SET_WINDOW if len == 32 => {
778                let chrome = read_u16(bytes, payload)?;
779                let title_bar = read_u16(bytes, payload + 2)?;
780                let resizable = read_u16(bytes, payload + 12)?;
781                if resizable > 1 {
782                    return Err(ProtocolError::InvalidValueKind(resizable));
783                }
784                Mutation::SetWindow(crate::schema::Window {
785                    chrome: crate::schema::Chrome::try_from(chrome)
786                        .map_err(|()| ProtocolError::InvalidValueKind(chrome))?,
787                    title_bar: crate::schema::TitleBar::try_from(title_bar)
788                        .map_err(|()| ProtocolError::InvalidValueKind(title_bar))?,
789                    width: read_u16(bytes, payload + 4)?,
790                    height: read_u16(bytes, payload + 6)?,
791                    min_width: read_u16(bytes, payload + 8)?,
792                    min_height: read_u16(bytes, payload + 10)?,
793                    resizable: resizable == 1,
794                    // Leaked on purpose, once per window. The record's owner is the
795                    // window, the window outlives the batch it arrived in, and there is
796                    // one of these per process.
797                    icon: read_u32(bytes, payload + 24)?,
798                    title: Box::leak(
799                        read_string(bytes, payload + 16, records_len)?
800                            .to_owned()
801                            .into_boxed_str(),
802                    ),
803                })
804            }
805            TAG_REGISTER_ASSET if len == 20 => {
806                let raw_kind = read_u16(bytes, payload + 4)?;
807                Mutation::RegisterAsset {
808                    asset_id: read_u32(bytes, payload)?,
809                    kind: AssetKind::try_from(raw_kind)
810                        .map_err(|()| ProtocolError::InvalidAssetKind(raw_kind))?,
811                    bytes: read_bytes(bytes, payload + 8, records_len)?,
812                }
813            }
814            TAG_RELEASE_ASSET if len == 8 => Mutation::ReleaseAsset {
815                asset_id: read_u32(bytes, payload)?,
816            },
817            TAG_SHOW_MESSAGE if len == 32 => {
818                let raw_duration = read_u16(bytes, payload + 24)?;
819                Mutation::ShowMessage {
820                    handler_id: read_u64(bytes, payload)?,
821                    text: read_string(bytes, payload + 8, records_len)?,
822                    action: read_string(bytes, payload + 16, records_len)?,
823                    duration: MessageDuration::try_from(raw_duration)
824                        .map_err(|()| ProtocolError::InvalidMessageDuration(raw_duration))?,
825                }
826            }
827            TAG_CREATE..=TAG_SHOW_MESSAGE => {
828                return Err(ProtocolError::InvalidRecordLength);
829            }
830            other => return Err(ProtocolError::InvalidTag(other)),
831        };
832        output.push(mutation);
833        position += len;
834    }
835    if output.len() != count {
836        return Err(ProtocolError::InvalidEnvelope);
837    }
838    Ok(output)
839}
840
841/// Two `f32` share one `u64`, with the first value in the low 32 bits.
842const fn pack_floats(low: f32, high: f32) -> u64 {
843    (low.to_bits() as u64) | ((high.to_bits() as u64) << 32)
844}
845
846const fn unpack_low(word: u64) -> f32 {
847    f32::from_bits(word as u32)
848}
849
850const fn unpack_high(word: u64) -> f32 {
851    f32::from_bits((word >> 32) as u32)
852}
853
854fn modifier_fields(modifier: &Modifier) -> (u16, u64, u64) {
855    match modifier {
856        Modifier::Empty => (0, 0, 0),
857        Modifier::Padding(value) => (1, u64::from(value.to_bits()), 0),
858        Modifier::FillMaxWidth => (2, 0, 0),
859        Modifier::FillMaxHeight => (3, 0, 0),
860        Modifier::Width(value) => (4, u64::from(value.to_bits()), 0),
861        Modifier::Height(value) => (5, u64::from(value.to_bits()), 0),
862        Modifier::Size { width, height } => {
863            (6, u64::from(width.to_bits()), u64::from(height.to_bits()))
864        }
865        Modifier::Background(paint) => (7, paint.to_bits(), 0),
866        Modifier::Clickable { handler_id } => (8, *handler_id, 0),
867        Modifier::PaddingRole(role) => (9, *role as u64, 0),
868        Modifier::PaddingEach {
869            start,
870            top,
871            end,
872            bottom,
873        } => (10, pack_floats(*start, *top), pack_floats(*end, *bottom)),
874        Modifier::Weight(value) => (11, u64::from(value.to_bits()), 0),
875        Modifier::Shape {
876            top_start,
877            top_end,
878            bottom_end,
879            bottom_start,
880        } => (
881            12,
882            pack_floats(*top_start, *top_end),
883            pack_floats(*bottom_end, *bottom_start),
884        ),
885        Modifier::ShapeRole(role) => (13, *role as u64, 0),
886        Modifier::Border { width, paint } => (14, u64::from(width.to_bits()), paint.to_bits()),
887        Modifier::Elevation(dp) => (15, u64::from(dp.to_bits()), 0),
888        Modifier::ObserveSize { token } => (16, u64::from(*token), 0),
889        Modifier::Motion(role) => (17, *role as u64, 0),
890        Modifier::Material(role) => (18, *role as u64, 0),
891    }
892}
893
894fn decode_role<T: TryFrom<u16, Error = ()>>(first: u64) -> Result<T, ProtocolError> {
895    let tag = u16::try_from(first).map_err(|_| ProtocolError::InvalidModifier(0))?;
896    T::try_from(tag).map_err(|()| ProtocolError::InvalidModifier(tag))
897}
898
899fn decode_paint(bits: u64) -> Result<Paint, ProtocolError> {
900    Paint::from_bits(bits).ok_or(ProtocolError::InvalidModifier(0))
901}
902
903fn decode_modifier(tag: u16, first: u64, second: u64) -> Result<Modifier, ProtocolError> {
904    match tag {
905        0 => Ok(Modifier::Empty),
906        1 => Ok(Modifier::Padding(f32::from_bits(first as u32))),
907        2 => Ok(Modifier::FillMaxWidth),
908        3 => Ok(Modifier::FillMaxHeight),
909        4 => Ok(Modifier::Width(f32::from_bits(first as u32))),
910        5 => Ok(Modifier::Height(f32::from_bits(first as u32))),
911        6 => Ok(Modifier::Size {
912            width: f32::from_bits(first as u32),
913            height: f32::from_bits(second as u32),
914        }),
915        7 => Ok(Modifier::Background(decode_paint(first)?)),
916        8 => Ok(Modifier::Clickable { handler_id: first }),
917        9 => Ok(Modifier::PaddingRole(decode_role::<SpaceRole>(first)?)),
918        10 => Ok(Modifier::PaddingEach {
919            start: unpack_low(first),
920            top: unpack_high(first),
921            end: unpack_low(second),
922            bottom: unpack_high(second),
923        }),
924        11 => Ok(Modifier::Weight(f32::from_bits(first as u32))),
925        12 => Ok(Modifier::Shape {
926            top_start: unpack_low(first),
927            top_end: unpack_high(first),
928            bottom_end: unpack_low(second),
929            bottom_start: unpack_high(second),
930        }),
931        13 => Ok(Modifier::ShapeRole(decode_role::<ShapeRole>(first)?)),
932        14 => Ok(Modifier::Border {
933            width: f32::from_bits(first as u32),
934            paint: decode_paint(second)?,
935        }),
936        15 => Ok(Modifier::Elevation(f32::from_bits(first as u32))),
937        16 => Ok(Modifier::ObserveSize {
938            token: first as u32,
939        }),
940        17 => Ok(Modifier::Motion(decode_role::<MotionRole>(first)?)),
941        18 => Ok(Modifier::Material(decode_role::<MaterialRole>(first)?)),
942        other => Err(ProtocolError::InvalidModifier(other)),
943    }
944}
945
946/// Reads a `(offset: u32, len: u32)` string reference at `position`.
947///
948/// Strings live in the arena that follows the records, so `arena_start` is the
949/// first byte a string may legally point at. Without that floor a hostile Renderer can
950/// aim a string at the record region and have the decoder reinterpret record headers as
951/// text, garbage decoding into a valid-looking mutation.
952fn read_string(bytes: &[u8], position: usize, arena_start: usize) -> Result<&str, ProtocolError> {
953    let value = read_bytes(bytes, position, arena_start)?;
954    std::str::from_utf8(value).map_err(|_| ProtocolError::InvalidUtf8)
955}
956
957/// The same arena range a string uses, without the UTF-8 requirement.
958fn read_bytes(bytes: &[u8], position: usize, arena_start: usize) -> Result<&[u8], ProtocolError> {
959    let offset =
960        usize::try_from(read_u32(bytes, position)?).map_err(|_| ProtocolError::LengthOverflow)?;
961    let len = usize::try_from(read_u32(bytes, position + 4)?)
962        .map_err(|_| ProtocolError::LengthOverflow)?;
963    let end = offset
964        .checked_add(len)
965        .ok_or(ProtocolError::InvalidStringRange)?;
966    if offset < arena_start {
967        return Err(ProtocolError::InvalidStringRange);
968    }
969    bytes
970        .get(offset..end)
971        .ok_or(ProtocolError::InvalidStringRange)
972}
973
974fn read_u16(bytes: &[u8], position: usize) -> Result<u16, ProtocolError> {
975    let value = bytes
976        .get(position..position + 2)
977        .ok_or(ProtocolError::Truncated)?;
978    Ok(u16::from_le_bytes([value[0], value[1]]))
979}
980
981fn read_u32(bytes: &[u8], position: usize) -> Result<u32, ProtocolError> {
982    let value = bytes
983        .get(position..position + 4)
984        .ok_or(ProtocolError::Truncated)?;
985    Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
986}
987
988fn read_u64(bytes: &[u8], position: usize) -> Result<u64, ProtocolError> {
989    let value = bytes
990        .get(position..position + 8)
991        .ok_or(ProtocolError::Truncated)?;
992    Ok(u64::from_le_bytes(
993        value.try_into().map_err(|_| ProtocolError::Truncated)?,
994    ))
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000    use crate::schema::TypeRole;
1001
1002    #[test]
1003    fn round_trips_all_m0_mutations() {
1004        let mutations = [
1005            Mutation::Create {
1006                node_id: 1,
1007                widget: WidgetKind::Column,
1008            },
1009            Mutation::SetProp {
1010                node_id: 2,
1011                property: PropertyKind::Text,
1012                value: PropertyValue::String("μ•ˆλ…•"),
1013            },
1014            Mutation::SetProp {
1015                node_id: 3,
1016                property: PropertyKind::Enabled,
1017                value: PropertyValue::Bool(true),
1018            },
1019            Mutation::SetModifier {
1020                node_id: 1,
1021                index: 0,
1022                modifier: Modifier::Padding(16.0),
1023            },
1024            Mutation::Insert {
1025                parent_id: 1,
1026                node_id: 2,
1027                index: 0,
1028            },
1029            Mutation::Move {
1030                parent_id: 1,
1031                node_id: 2,
1032                index: 1,
1033            },
1034            Mutation::Remove { node_id: 3 },
1035            Mutation::SetText {
1036                node_id: 4,
1037                text: "compose",
1038                selection: Some(Selection { start: 1, end: 4 }),
1039            },
1040            Mutation::AppendText {
1041                node_id: 4,
1042                text: " token",
1043            },
1044        ];
1045        let mut encoder = BatchEncoder::default();
1046        for mutation in &mutations {
1047            encoder.encode(mutation).unwrap();
1048        }
1049        let decoded = decode_batch(encoder.finish().unwrap()).unwrap();
1050        assert_eq!(decoded, mutations);
1051    }
1052
1053    /// A role Paint and a literal Paint occupy the same record length and
1054    /// decode back to the value that was encoded.
1055    #[test]
1056    fn fr13_paint_role_and_literal_share_one_record_length() {
1057        let encode_one = |modifier| {
1058            let mut encoder = BatchEncoder::default();
1059            encoder
1060                .encode(&Mutation::SetModifier {
1061                    node_id: 1,
1062                    index: 0,
1063                    modifier,
1064                })
1065                .unwrap();
1066            encoder.finish().unwrap().to_vec()
1067        };
1068        let role = encode_one(Modifier::Background(Paint::Role(
1069            crate::schema::ColorRole::Surface,
1070        )));
1071        let literal = encode_one(Modifier::Background(Paint::Literal(
1072            crate::schema::Color::rgb(0x1b1b1f),
1073        )));
1074        assert_eq!(role.len(), literal.len());
1075        assert_eq!(
1076            decode_batch(&role).unwrap(),
1077            [Mutation::SetModifier {
1078                node_id: 1,
1079                index: 0,
1080                modifier: Modifier::Background(Paint::Role(crate::schema::ColorRole::Surface)),
1081            }]
1082        );
1083        assert_eq!(
1084            decode_batch(&literal).unwrap(),
1085            [Mutation::SetModifier {
1086                node_id: 1,
1087                index: 0,
1088                modifier: Modifier::Background(Paint::Literal(crate::schema::Color::rgb(0x1b1b1f))),
1089            }]
1090        );
1091    }
1092
1093    /// Every design primitive fits in `(tag, u64, u64)` and survives a round trip.
1094    #[test]
1095    fn fr13_design_modifiers_round_trip_in_two_words() {
1096        let modifiers = [
1097            Modifier::PaddingRole(SpaceRole::Md),
1098            Modifier::PaddingEach {
1099                start: 1.0,
1100                top: 2.0,
1101                end: 3.0,
1102                bottom: 4.0,
1103            },
1104            Modifier::Weight(0.25),
1105            Modifier::Shape {
1106                top_start: 4.0,
1107                top_end: 8.0,
1108                bottom_end: 12.0,
1109                bottom_start: 16.0,
1110            },
1111            Modifier::ShapeRole(ShapeRole::Full),
1112            Modifier::Border {
1113                width: 2.0,
1114                paint: Paint::Role(crate::schema::ColorRole::Outline),
1115            },
1116            Modifier::Elevation(6.0),
1117        ];
1118        let mut encoder = BatchEncoder::default();
1119        let expected: Vec<_> = modifiers
1120            .iter()
1121            .enumerate()
1122            .map(|(index, modifier)| Mutation::SetModifier {
1123                node_id: 1,
1124                index: index as u16,
1125                modifier: modifier.clone(),
1126            })
1127            .collect();
1128        for mutation in &expected {
1129            encoder.encode(mutation).unwrap();
1130        }
1131        let bytes = encoder.finish().unwrap();
1132        // Every modifier record is the same fixed 28 bytes.
1133        assert_eq!(bytes.len(), ENVELOPE_LEN + expected.len() * 28);
1134        assert_eq!(decode_batch(bytes).unwrap(), expected);
1135    }
1136
1137    /// `SetTheme` is four `u16` fields and one font slot per type role.
1138    ///
1139    /// Fixed length whether or not any font is named, because the record is fixed layout
1140    /// and a theme is sent once rather than per frame. The slots cost 36 bytes on a
1141    /// message that appears once in a session.
1142    #[test]
1143    fn fr23_set_theme_round_trips_with_a_slot_for_every_type_role() {
1144        let theme = Theme::adaptive(DesignSystem::Cupertino)
1145            .with_color_scheme(ColorScheme::Dark)
1146            .with_font(TypeRole::Display, 7);
1147        let length = 12 + 4 * TYPE_ROLE_COUNT as u16;
1148        let mut encoder = BatchEncoder::default();
1149        encoder.encode(&Mutation::SetTheme(theme)).unwrap();
1150        let bytes = encoder.finish().unwrap();
1151        assert_eq!(bytes.len(), ENVELOPE_LEN + length as usize);
1152        assert_eq!(read_u16(bytes, ENVELOPE_LEN).unwrap(), TAG_SET_THEME);
1153        assert_eq!(read_u16(bytes, ENVELOPE_LEN + 2).unwrap(), length);
1154        assert_eq!(decode_batch(bytes).unwrap(), [Mutation::SetTheme(theme)]);
1155    }
1156
1157    /// A font named for one role leaves every other role on the system font.
1158    #[test]
1159    fn fr23_a_font_is_named_for_one_role_and_no_others() {
1160        let theme = Theme::unified(DesignSystem::Material3).with_font(TypeRole::Display, 3);
1161        assert_eq!(theme.font(TypeRole::Display), Some(3));
1162        for role in [
1163            TypeRole::Body,
1164            TypeRole::Title,
1165            TypeRole::Mono,
1166            TypeRole::Caption,
1167        ] {
1168            assert_eq!(theme.font(role), None);
1169        }
1170    }
1171
1172    /// An out-of-range theme tag is an error, never a panic.
1173    #[test]
1174    fn fr14_rejects_unknown_theme_tags() {
1175        let mut encoder = BatchEncoder::default();
1176        encoder
1177            .encode(&Mutation::SetTheme(Theme::default()))
1178            .unwrap();
1179        let mut bytes = encoder.finish().unwrap().to_vec();
1180        bytes[ENVELOPE_LEN + 4..ENVELOPE_LEN + 6].copy_from_slice(&99_u16.to_le_bytes());
1181        assert_eq!(decode_batch(&bytes), Err(ProtocolError::InvalidTheme(99)));
1182    }
1183
1184    #[test]
1185    fn rejects_unknown_tags_without_panicking() {
1186        let mut bytes = vec![0, 0, 12, 0, 20, 0, 0, 0, 1, 0, 0, 0];
1187        bytes.extend_from_slice(&999_u16.to_le_bytes());
1188        bytes.extend_from_slice(&8_u16.to_le_bytes());
1189        bytes.extend_from_slice(&0_u32.to_le_bytes());
1190        assert_eq!(decode_batch(&bytes), Err(ProtocolError::InvalidTag(999)));
1191    }
1192
1193    #[test]
1194    fn pr4_renderer_events_round_trip_fixed_layout() {
1195        let events = [
1196            HostEvent {
1197                node_id: 7,
1198                handler_id: 11,
1199                payload: crate::EventPayload::Clicked,
1200            },
1201            HostEvent {
1202                node_id: 8,
1203                handler_id: 12,
1204                payload: crate::EventPayload::TextChanged("ν•œκΈ€"),
1205            },
1206            HostEvent {
1207                node_id: 8,
1208                handler_id: 13,
1209                payload: crate::EventPayload::TextSubmitted("done"),
1210            },
1211            HostEvent {
1212                node_id: 8,
1213                handler_id: 14,
1214                payload: crate::EventPayload::FocusLost,
1215            },
1216            HostEvent {
1217                node_id: 9,
1218                handler_id: 16,
1219                payload: crate::EventPayload::RangeRequested {
1220                    start: 100,
1221                    count: 20,
1222                },
1223            },
1224            HostEvent {
1225                node_id: 8,
1226                handler_id: 15,
1227                payload: crate::EventPayload::KeyDown {
1228                    key: Key::Enter,
1229                    shift_key: true,
1230                    ctrl_key: false,
1231                    alt_key: true,
1232                    meta_key: false,
1233                },
1234            },
1235        ];
1236        let mut bytes = Vec::new();
1237        for event in events {
1238            encode_event(&event, &mut bytes).unwrap();
1239            assert_eq!(decode_event(&bytes).unwrap(), event);
1240        }
1241    }
1242
1243    #[test]
1244    fn fr20_window_size_changed_round_trips_in_thirty_two_bytes() {
1245        let event = HostEvent {
1246            node_id: 0,
1247            handler_id: 0,
1248            payload: crate::EventPayload::WindowSizeChanged {
1249                width_dp: 841.5,
1250                height_dp: 600.25,
1251                class: crate::WindowSizeClass::Expanded,
1252                height_class: crate::WindowHeightClass::Medium,
1253            },
1254        };
1255        let mut bytes = Vec::new();
1256        encode_event(&event, &mut bytes).unwrap();
1257        assert_eq!(bytes.len(), 32);
1258        assert_eq!(decode_event(&bytes).unwrap(), event);
1259    }
1260
1261    #[test]
1262    fn fr20_unknown_window_size_class_is_a_protocol_error() {
1263        let event = HostEvent {
1264            node_id: 0,
1265            handler_id: 0,
1266            payload: crate::EventPayload::WindowSizeChanged {
1267                width_dp: 320.0,
1268                height_dp: 640.0,
1269                class: crate::WindowSizeClass::Compact,
1270                height_class: crate::WindowHeightClass::Medium,
1271            },
1272        };
1273        let mut bytes = Vec::new();
1274        encode_event(&event, &mut bytes).unwrap();
1275        bytes[24..28].copy_from_slice(&99_u32.to_le_bytes());
1276        assert_eq!(
1277            decode_event(&bytes),
1278            Err(ProtocolError::InvalidValueKind(99))
1279        );
1280    }
1281
1282    #[test]
1283    fn fr20_unknown_window_height_class_is_a_protocol_error() {
1284        let event = HostEvent {
1285            node_id: 0,
1286            handler_id: 0,
1287            payload: crate::EventPayload::WindowSizeChanged {
1288                width_dp: 320.0,
1289                height_dp: 640.0,
1290                class: crate::WindowSizeClass::Compact,
1291                height_class: crate::WindowHeightClass::Medium,
1292            },
1293        };
1294        let mut bytes = Vec::new();
1295        encode_event(&event, &mut bytes).unwrap();
1296        bytes[28..32].copy_from_slice(&99_u32.to_le_bytes());
1297        assert_eq!(
1298            decode_event(&bytes),
1299            Err(ProtocolError::InvalidValueKind(99))
1300        );
1301    }
1302
1303    #[test]
1304    fn fr20_window_size_classes_follow_the_material_boundaries() {
1305        use crate::WindowSizeClass;
1306        assert_eq!(
1307            WindowSizeClass::from_width_dp(599.9),
1308            WindowSizeClass::Compact
1309        );
1310        assert_eq!(
1311            WindowSizeClass::from_width_dp(600.0),
1312            WindowSizeClass::Medium
1313        );
1314        assert_eq!(
1315            WindowSizeClass::from_width_dp(839.9),
1316            WindowSizeClass::Medium
1317        );
1318        assert_eq!(
1319            WindowSizeClass::from_width_dp(840.0),
1320            WindowSizeClass::Expanded
1321        );
1322    }
1323
1324    #[test]
1325    fn fr20_window_height_classes_follow_the_material_boundaries() {
1326        use crate::WindowHeightClass;
1327        assert_eq!(
1328            WindowHeightClass::from_height_dp(479.9),
1329            WindowHeightClass::Compact
1330        );
1331        assert_eq!(
1332            WindowHeightClass::from_height_dp(480.0),
1333            WindowHeightClass::Medium
1334        );
1335        assert_eq!(
1336            WindowHeightClass::from_height_dp(899.9),
1337            WindowHeightClass::Medium
1338        );
1339        assert_eq!(
1340            WindowHeightClass::from_height_dp(900.0),
1341            WindowHeightClass::Expanded
1342        );
1343    }
1344}