Skip to main content

pathland_core/
lib.rs

1//! # pathland-core
2//!
3//! The 16-byte fixed-size opcode engine for Pathland: fixed-size opcodes, a
4//! single-producer ring buffer, a bump arena for variable-length data, and the
5//! linear-memory layout shared with host renderers.
6//!
7//! This crate is `no_std` + `alloc`. It has no runtime dependencies.
8//!
9//! ## Producer (guest engine)
10//!
11//! ```no_run
12//! use pathland_core::{init_memory, Guest, MemoryLayout};
13//!
14//! let layout = MemoryLayout::default();
15//! let mut mem = vec![0u8; layout.total_bytes()];
16//! init_memory(&mut mem, &layout);
17//!
18//! let mut guest = Guest::new(&mut mem, &layout);
19//! guest.begin_frame();
20//! guest.create_node(1, pathland_core::component_type::VSTACK).unwrap();
21//! guest.insert_child(0, 1, pathland_core::APPEND).unwrap();
22//! guest.set_property(1, pathland_core::property_id::SPACING, pathland_core::value_type::F32, 4.0f32.to_bits()).unwrap();
23//! guest.end_frame(); // publishes frameCount
24//! ```
25//!
26//! ## Consumer (host renderer)
27//!
28//! ```no_run
29//! use pathland_core::{init_memory, Guest, Host, MemoryLayout};
30//! # let layout = MemoryLayout::default();
31//! # let mut mem = vec![0u8; layout.total_bytes()];
32//! # init_memory(&mut mem, &layout);
33//! # let mut guest = Guest::new(&mut mem, &layout);
34//! # guest.begin_frame();
35//! # guest.create_node(1, 1).unwrap();
36//! # guest.end_frame();
37//! let mut host = Host::new(&mut mem, &layout);
38//! for frame in host.frames() {
39//!     for op in frame.opcodes() {
40//!         println!("{:?}", op);
41//!     }
42//! }
43//! ```
44
45#![no_std]
46#![forbid(unsafe_op_in_unsafe_fn)]
47
48#[cfg(test)]
49extern crate std;
50
51extern crate alloc;
52
53mod arena;
54pub mod attributed;
55mod conformance;
56mod constants;
57mod events;
58pub mod fit;
59mod memory;
60mod opcode;
61mod ring;
62pub mod tokens;
63mod window;
64
65pub use arena::ArenaError;
66pub use attributed::{
67    AttributedError, AttributedSegment, AttributedString, LinkRun, StyleRun, TextAttribute,
68};
69pub use constants::*;
70pub use events::{Event, EventError};
71pub use memory::MemoryLayout;
72pub use opcode::{Opcode, OPCODE_SIZE};
73pub use ring::RingError;
74pub use window::WindowMeta;
75
76use alloc::vec;
77use alloc::vec::Vec;
78use arena as arena_fn;
79use memory::header;
80use memory::{OFF_FRAME_COUNT, OFF_READ_CURSOR, OFF_SLOT_COUNT, OFF_WRITE_CURSOR};
81use ring as ring_fn;
82
83/// Initialize the linear memory header for a layout. Call once at startup
84/// before creating a `Guest` or `Host`.
85pub fn init_memory(mem: &mut [u8], layout: &MemoryLayout) {
86    header::init(mem, layout);
87}
88
89/// Accessor for a shared-memory ring header, for consumers that read the ring
90/// in place (zero-copy) without a `Host` handle — e.g. a foreign driver that
91/// holds a raw pointer to the shared linear memory.
92///
93/// Field offsets match [`memory::OFF_*`] constants and are part of the stable
94/// wire layout (see `spec/OPCODE.md`).
95pub struct SharedHeader<'a> {
96    mem: &'a [u8],
97}
98
99impl<'a> SharedHeader<'a> {
100    /// Wrap an immutable view of the shared linear memory.
101    pub fn new(mem: &'a [u8]) -> Self {
102        Self { mem }
103    }
104
105    fn u32(&self, off: usize) -> u32 {
106        u32::from_le_bytes(self.mem[off..off + 4].try_into().unwrap())
107    }
108
109    /// `frameCount` — the completed-frame watermark.
110    pub fn frame_count(&self) -> u32 {
111        self.u32(memory::OFF_FRAME_COUNT)
112    }
113
114    /// `readCursor` — the slot the consumer has consumed up to (masked).
115    pub fn read_cursor(&self) -> u32 {
116        self.u32(memory::OFF_READ_CURSOR)
117    }
118
119    /// `writeCursor` — the slot the producer has written up to (masked).
120    pub fn write_cursor(&self) -> u32 {
121        self.u32(memory::OFF_WRITE_CURSOR)
122    }
123
124    /// `slotCount` — number of ring slots (power of two).
125    pub fn slot_count(&self) -> u32 {
126        self.u32(memory::OFF_SLOT_COUNT)
127    }
128
129    /// Byte offset of the ring region within the linear memory.
130    pub fn ring_offset(&self) -> usize {
131        self.u32(memory::OFF_RING_OFFSET) as usize
132    }
133
134    /// Byte length of the ring region.
135    pub fn ring_bytes(&self) -> usize {
136        self.u32(memory::OFF_RING_BYTES) as usize
137    }
138
139    /// Byte offset of the arena region within the linear memory.
140    pub fn arena_offset(&self) -> usize {
141        self.u32(memory::OFF_ARENA_OFFSET) as usize
142    }
143
144    /// Byte offset of the arena cursor.
145    pub fn arena_cursor(&self) -> u32 {
146        self.u32(memory::OFF_ARENA_CURSOR)
147    }
148}
149
150/// Number of ring slots per the header.
151fn slot_count(header: &[u8]) -> usize {
152    header::get_u32(header, OFF_SLOT_COUNT) as usize
153}
154
155/// Ring mask per the header (slot count is a power of two).
156fn slot_mask(header: &[u8]) -> usize {
157    let slots = slot_count(header);
158    debug_assert!(slots.is_power_of_two());
159    slots - 1
160}
161
162// ---------------------------------------------------------------------------
163// Guest (producer)
164// ---------------------------------------------------------------------------
165
166/// Producer side: the guest engine writing opcodes and arena data into shared
167/// linear memory. Owns the header, ring, and arena regions as disjoint slices.
168pub struct Guest<'a> {
169    header: &'a mut [u8],
170    slots: &'a mut [u8],
171    /// Host → guest event ring (guest reads raw inputs here).
172    event_slots: &'a mut [u8],
173    arena: &'a mut [u8],
174    /// Host → guest event arena (host-owned bump; the guest resolves `TEXT_CHANGED`
175    /// text here over the shared ring).
176    event_arena: &'a mut [u8],
177    /// Write cursor captured at `begin_frame`.
178    frame_start: u32,
179}
180
181impl<'a> Guest<'a> {
182    /// Create a guest view over the linear memory. `init_memory` MUST have been
183    /// called on the same memory beforehand.
184    pub fn new(mem: &'a mut [u8], layout: &MemoryLayout) -> Self {
185        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
186        let (slots, rest) = rest.split_at_mut(layout.ring_bytes());
187        let (event_slots, rest) = rest.split_at_mut(layout.event_ring_bytes());
188        let (arena, event_arena) = rest.split_at_mut(layout.arena_bytes);
189        let frame_start = header::get_u32(header, OFF_WRITE_CURSOR);
190        Self {
191            header,
192            slots,
193            event_slots,
194            arena,
195            event_arena,
196            frame_start,
197        }
198    }
199
200    fn mask(&self) -> usize {
201        slot_mask(self.header)
202    }
203
204    /// Record the start of a new frame.
205    pub fn begin_frame(&mut self) {
206        self.frame_start = header::get_u32(self.header, OFF_WRITE_CURSOR);
207    }
208
209    /// Number of opcodes written since the last `begin_frame`.
210    pub fn pending(&self) -> usize {
211        let write = header::get_u32(self.header, OFF_WRITE_CURSOR);
212        let mask = self.mask() as u32;
213        (write.wrapping_sub(self.frame_start) & mask) as usize
214    }
215
216    /// End the frame: increments `frameCount`.
217    pub fn end_frame(&mut self) {
218        let count = header::get_u32(self.header, OFF_FRAME_COUNT);
219        header::set_u32(self.header, OFF_FRAME_COUNT, count.wrapping_add(1));
220    }
221
222    /// Total frames produced so far.
223    pub fn frame_count(&self) -> u32 {
224        header::get_u32(self.header, OFF_FRAME_COUNT)
225    }
226
227    // --- TREE ---
228
229    pub fn create_node(&mut self, node_id: u32, component_type: u16) -> Result<(), RingError> {
230        ring_fn::push_create_node(self.slots, self.header, self.mask(), node_id, component_type)
231    }
232
233    pub fn delete_node(&mut self, node_id: u32) -> Result<(), RingError> {
234        ring_fn::push_delete_node(self.slots, self.header, self.mask(), node_id)
235    }
236
237    pub fn insert_child(&mut self, parent: u32, child: u32, index: u32) -> Result<(), RingError> {
238        ring_fn::push_insert_child(self.slots, self.header, self.mask(), parent, child, index)
239    }
240
241    pub fn remove_child(&mut self, parent: u32, child: u32) -> Result<(), RingError> {
242        ring_fn::push_remove_child(self.slots, self.header, self.mask(), parent, child)
243    }
244
245    pub fn move_child(
246        &mut self,
247        parent: u32,
248        child: u32,
249        new_index: u32,
250    ) -> Result<(), RingError> {
251        ring_fn::push_move_child(self.slots, self.header, self.mask(), parent, child, new_index)
252    }
253
254    // --- Raw opcode push (guest → host) ---
255
256    /// Push a caller-built raw opcode into the ring. This is the escape hatch
257    /// for foreign hosts (C ABI / FFM) that construct the 16-byte opcode
258    /// themselves and write it into the shared ring in place.
259    pub fn push_opcode(&mut self, op: Opcode) -> Result<(), RingError> {
260        ring_fn::push(self.slots, self.header, self.mask(), &op)
261    }
262
263    // --- STYLE ---
264
265    pub fn set_property(
266        &mut self,
267        node_id: u32,
268        property_id: u16,
269        value_type: u8,
270        value: u32,
271    ) -> Result<(), RingError> {
272        ring_fn::push_set_property(
273            self.slots,
274            self.header,
275            self.mask(),
276            node_id,
277            property_id,
278            value_type,
279            value,
280        )
281    }
282
283    /// Set a node's text content via the arena.
284    pub fn set_text(&mut self, node_id: u32, text: &str) -> Result<u32, RingError> {
285        let arena_ref = arena_fn::alloc_str(self.arena, self.header, text).map_err(|_| RingError::Full)?;
286        ring_fn::push_set_text(self.slots, self.header, self.mask(), node_id, arena_ref)?;
287        Ok(arena_ref)
288    }
289
290    /// Set a node's text plus style runs and links via `PARAMETER::SET_ATTRIBUTED_TEXT`.
291    ///
292    /// The payload is encoded (see [`crate::AttributedString::encode`]) into one
293    /// length-prefixed arena entry. Returns that entry's byte offset.
294    pub fn set_attributed_text(
295        &mut self,
296        node_id: u32,
297        text: &crate::AttributedString,
298    ) -> Result<u32, RingError> {
299        let payload = text.encode().map_err(|_| RingError::InvalidPayload)?;
300        let arena_ref = arena_fn::alloc(self.arena, self.header, &payload).map_err(|_| RingError::Full)?;
301        ring_fn::push_set_attributed_text(self.slots, self.header, self.mask(), node_id, arena_ref)?;
302        Ok(arena_ref)
303    }
304
305    /// Set a node's date value via `PARAMETER::SET_DATE` (draft): `days` since the
306    /// epoch (I32, pre-1970 negative) and `millis` of day (U32, 0..86,400,000).
307    pub fn set_date(&mut self, node_id: u32, days: i32, millis: u32) -> Result<(), RingError> {
308        let op = Opcode::new(
309            category::PARAMETER,
310            crate::parameter::SET_DATE,
311            0,
312            node_id,
313            days as u32,
314            millis,
315        );
316        ring_fn::push(self.slots, self.header, self.mask(), &op)
317    }
318
319    /// Override a design token globally via `PARAMETER::SET_DESIGN_TOKEN`:
320    /// `path` (arena string) → `value` of `value_type`. Applies everywhere the
321    /// renderer resolves tokens (spec/TOKENS.md). A `dark.`-prefixed path
322    /// overrides the dark variant. Returns the arena offset of the path.
323    pub fn set_design_token(
324        &mut self,
325        path: &str,
326        value_type: u8,
327        value: u32,
328    ) -> Result<u32, RingError> {
329        let arena_ref = arena_fn::alloc_str(self.arena, self.header, path)
330            .map_err(|_| RingError::Full)?;
331        let op = Opcode::new(
332            category::PARAMETER,
333            crate::parameter::SET_DESIGN_TOKEN,
334            0,
335            arena_ref,
336            u32::from(value_type),
337            value,
338        );
339        ring_fn::push(self.slots, self.header, self.mask(), &op)?;
340        Ok(arena_ref)
341    }
342
343    /// Override a design token with a **STRING value** via `PARAMETER::SET_DESIGN_TOKEN`
344    /// (spec/TOKENS.md): both the token path and the value are arena strings —
345    /// `A` = path ref, `B` = `STRING` value type, `C` = value ref. Returns the
346    /// arena offset of the path.
347    pub fn set_design_token_string(
348        &mut self,
349        path: &str,
350        value: &str,
351    ) -> Result<u32, RingError> {
352        let arena_ref = arena_fn::alloc_str(self.arena, self.header, path)
353            .map_err(|_| RingError::Full)?;
354        let value_ref = arena_fn::alloc_str(self.arena, self.header, value)
355            .map_err(|_| RingError::Full)?;
356        let op = Opcode::new(
357            category::PARAMETER,
358            crate::parameter::SET_DESIGN_TOKEN,
359            0,
360            arena_ref,
361            u32::from(crate::value_type::STRING),
362            value_ref,
363        );
364        ring_fn::push(self.slots, self.header, self.mask(), &op)?;
365        Ok(arena_ref)
366    }
367
368    /// Allocate arbitrary bytes into the arena, returning the offset.
369    pub fn alloc(&mut self, bytes: &[u8]) -> Result<u32, ArenaError> {
370        arena_fn::alloc(self.arena, self.header, bytes)
371    }
372
373    /// Allocate a string into the arena, returning the offset.
374    pub fn alloc_str(&mut self, s: &str) -> Result<u32, ArenaError> {
375        arena_fn::alloc_str(self.arena, self.header, s)
376    }
377
378    // --- META ---
379
380    pub fn reset(&mut self) -> Result<(), RingError> {
381        ring_fn::push_reset(self.slots, self.header, self.mask())
382    }
383
384    /// Force the renderer-wide color scheme (`META::SET_COLOR_SCHEME`):
385    /// `0` = platform-derived, `1` = light, `2` = dark (spec/TOKENS.md).
386    pub fn set_color_scheme(&mut self, mode: u32) -> Result<(), RingError> {
387        let op = Opcode::new(category::META, crate::meta::SET_COLOR_SCHEME, 0, mode, 0, 0);
388        ring_fn::push(self.slots, self.header, self.mask(), &op)
389    }
390
391    /// Clear every application design-token override (`META::RESET_THEME`).
392    pub fn reset_theme(&mut self) -> Result<(), RingError> {
393        let op = Opcode::new(category::META, crate::meta::RESET_THEME, 0, 0, 0, 0);
394        ring_fn::push(self.slots, self.header, self.mask(), &op)
395    }
396
397    // --- EVENT ingress (host → guest) ---
398
399    /// Drain all pending raw-input events written by the host into the event
400    /// ring, decoding each 16-byte `EVENT` opcode into a typed [`Event`].
401    ///
402    /// The guest (app/engine) polls this to receive renderer-reported inputs.
403    pub fn drain_events(&mut self) -> Vec<Event> {
404        let mask = self.mask();
405        let ops = ring_fn::read_events(&self.event_slots, self.header, mask, usize::MAX);
406        ops.into_iter()
407            .filter_map(|op| crate::events::decode_event(&op, self.event_arena))
408            .collect()
409    }
410
411    /// Drain raw `EVENT`-category opcodes from the host → guest event ring as
412    /// their 16-byte wire form (no decoding). Used by foreign hosts that want
413    /// the raw bytes.
414    pub fn drain_event_opcodes(&mut self) -> Vec<Opcode> {
415        let mask = self.mask();
416        ring_fn::read_events(&self.event_slots, self.header, mask, usize::MAX)
417    }
418
419    /// Drain at most `max` raw `EVENT`-category opcodes, leaving the rest
420    /// pending for a later call (so a caller with a fixed-size buffer never
421    /// drops events it couldn't copy).
422    pub fn drain_event_opcodes_max(&mut self, max: usize) -> Vec<Opcode> {
423        let mask = self.mask();
424        ring_fn::read_events(&self.event_slots, self.header, mask, max)
425    }
426
427    /// The host → guest event arena bump cursor (diagnostics/tests).
428    pub fn event_arena_cursor(&self) -> u32 {
429        header::get_u32(self.header, memory::OFF_EVENT_ARENA_CURSOR)
430    }
431
432    /// Push a raw `EVENT`-category opcode into the host → guest event ring.
433    ///
434    /// This is the host-side write (the renderer reporting an input). Exposed
435    /// here (guest can hold the ring too) so a C-ABI shim can inject raw event
436    /// opcodes without round-tripping through the typed [`Event`] enum.
437    pub fn push_event_opcode(&mut self, op: Opcode) -> Result<(), RingError> {
438        ring_fn::push_event(self.event_slots, self.header, self.mask(), &op)
439    }
440}
441
442// ---------------------------------------------------------------------------
443// Host (consumer)
444// ---------------------------------------------------------------------------
445
446/// A completed frame produced by the guest: a sequence of opcodes plus arena
447/// access.
448///
449/// `Frame` is the **transport seam**: both the shared-memory ring (`Host`) and
450/// network batch decoders produce frame views, so consumers such as
451/// `RenderTree::apply_frame` work identically regardless of origin.
452pub struct Frame<'a> {
453    slots: &'a [u8],
454    arena: &'a [u8],
455    start: usize,
456    end: usize,
457}
458
459impl<'a> Frame<'a> {
460    /// Construct a frame view over contiguous 16-byte opcode slots plus an
461    /// arena region (`start`/`end` are byte offsets into `slots`).
462    ///
463    /// Used by transport backends to produce `Frame` views over their own
464    /// buffers; `Host` builds these internally from the ring.
465    pub fn from_parts(slots: &'a [u8], arena: &'a [u8], start: usize, end: usize) -> Self {
466        Self {
467            slots,
468            arena,
469            start,
470            end,
471        }
472    }
473
474    /// Iterate the opcodes in this frame, in order.
475    pub fn opcodes(&self) -> FrameIter<'a> {
476        FrameIter {
477            slots: self.slots,
478            cursor: self.start,
479            end: self.end,
480        }
481    }
482
483    /// Number of opcodes in the frame.
484    pub fn len(&self) -> usize {
485        (self.end - self.start) / Opcode::SIZE
486    }
487
488    pub fn is_empty(&self) -> bool {
489        self.len() == 0
490    }
491
492    /// Read an arena entry by offset (see `Guest::alloc` / `Guest::alloc_str`).
493    pub fn arena_get(&self, offset: u32) -> Result<&'a [u8], ArenaError> {
494        arena_fn::get(self.arena, offset)
495    }
496
497    /// Read an arena string by offset.
498    pub fn arena_str(&self, offset: u32) -> Result<&'a str, ArenaError> {
499        arena_fn::get_str(self.arena, offset)
500    }
501
502    /// The full arena region (for transport serialization of arena deltas).
503    pub fn arena(&self) -> &'a [u8] {
504        self.arena
505    }
506}
507
508/// Iterator over the opcodes in a `Frame`.
509pub struct FrameIter<'a> {
510    slots: &'a [u8],
511    cursor: usize,
512    end: usize,
513}
514
515impl<'a> Iterator for FrameIter<'a> {
516    type Item = Opcode;
517
518    fn next(&mut self) -> Option<Opcode> {
519        if self.cursor >= self.end {
520            return None;
521        }
522        let bytes: [u8; 16] = self.slots[self.cursor..self.cursor + 16].try_into().unwrap();
523        self.cursor += 16;
524        Some(Opcode::from_bytes(&bytes))
525    }
526}
527
528/// Consumer side: the host renderer reading completed frames.
529///
530/// SPSC contract: the host never reads past the producer's published
531/// `writeCursor`, and the producer never overwrites an unconsumed `readCursor`.
532/// If the host is slow, multiple produced frames are coalesced into one `Frame`
533/// (opcodes remain in order); the `frameCount` simply signals "new data".
534pub struct Host<'a> {
535    header: &'a mut [u8],
536    slots: &'a [u8],
537    /// Host → guest event ring (host writes raw inputs here).
538    event_slots: &'a mut [u8],
539    arena: &'a [u8],
540    /// Host → guest event arena (host-owned bump; the host writes event string
541    /// payloads here, e.g. `TEXT_CHANGED` text over the shared ring).
542    event_arena: &'a mut [u8],
543    /// Number of frames already consumed by this host view.
544    consumed: u32,
545}
546
547impl<'a> Host<'a> {
548    /// Create a host view over the linear memory.
549    pub fn new(mem: &'a mut [u8], layout: &MemoryLayout) -> Self {
550        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
551        let (slots, rest) = rest.split_at_mut(layout.ring_bytes());
552        let (event_slots, rest) = rest.split_at_mut(layout.event_ring_bytes());
553        let (arena, event_arena) = rest.split_at_mut(layout.arena_bytes);
554        let slots: &'a [u8] = slots;
555        let arena: &'a [u8] = arena;
556        Self {
557            header,
558            slots,
559            event_slots,
560            arena,
561            event_arena,
562            consumed: 0,
563        }
564    }
565
566    /// Total frames produced by the guest (for diagnostics).
567    pub fn frame_count(&self) -> u32 {
568        header::get_u32(self.header, OFF_FRAME_COUNT)
569    }
570
571    /// Consume all frames produced since the last call, advancing `readCursor`.
572    ///
573    /// Returns at most one `Frame` (all new opcodes since the last drain). If
574    /// the producer produced multiple frames before this call they coalesce.
575    pub fn frames(&mut self) -> Vec<Frame<'a>> {
576        let frame_count = header::get_u32(self.header, OFF_FRAME_COUNT);
577        if self.consumed >= frame_count {
578            return Vec::new();
579        }
580        let mask = slot_mask(self.header);
581        let start = ring_fn::cursor(self.header, OFF_READ_CURSOR, mask);
582        let end = ring_fn::cursor(self.header, OFF_WRITE_CURSOR, mask);
583
584        // The masked write cursor can wrap past the read cursor (the producer
585        // laps the reader numerically). The pending run is counted modulo the
586        // ring size; a plain `end < start` guard would permanently deadlock the
587        // ring at the wrap boundary (the reader never advances again). A
588        // wrapped run is drained in two calls — first the [start..slots) head,
589        // then the [0..end) tail — advancing `readCursor` between them, so the
590        // opcodes still reach the consumer in ring order.
591        let count = end.wrapping_sub(start) & mask;
592        if count == 0 {
593            return Vec::new();
594        }
595        let run_end = (start + count) & mask; // exclusive slot index of the full run
596        let (drain_to_slot, frame_byte_end) = if run_end > start {
597            (run_end, run_end * Opcode::SIZE)
598        } else {
599            (0, self.slots.len()) // wrapped: drain the head now, the tail next call
600        };
601
602        let frames = vec![Frame {
603            slots: self.slots,
604            arena: self.arena,
605            start: start * Opcode::SIZE,
606            end: frame_byte_end,
607        }];
608
609        header::set_u32(self.header, OFF_READ_CURSOR, drain_to_slot as u32);
610        self.consumed = frame_count;
611
612        // A `META::RESET` invalidates prior state: reset the host → guest event
613        // arena cursor so the host starts bump-allocating fresh (matching the
614        // guest arena's reset-on-RESET semantics).
615        let drained_end_byte = if drain_to_slot > start {
616            drain_to_slot * Opcode::SIZE
617        } else {
618            self.slots.len() // wrapped head: drained [start .. ring end)
619        };
620        let reset_seen = self.slots[start * Opcode::SIZE..drained_end_byte]
621            .chunks_exact(Opcode::SIZE)
622            .any(|slot| {
623                let bytes: [u8; 16] = slot.try_into().unwrap();
624                let op = Opcode::from_bytes(&bytes);
625                op.category() == category::META && op.command() == crate::meta::RESET
626            });
627        if reset_seen {
628            memory::header::set_u32(self.header, memory::OFF_EVENT_ARENA_CURSOR, 0);
629        }
630
631        frames
632    }
633
634    /// Write a raw-input event (host → guest) into the event ring, encoded as a
635    /// 16-byte `EVENT` opcode. The renderer reports inputs here; the guest
636    /// drains them via [`Guest::drain_events`].
637    ///
638    /// `TextChanged` text and `Navigate` URLs are bump-allocated into the
639    /// host → guest **event arena** and referenced by their absolute offset in
640    /// `B` (the `SET_TEXT` shared-memory convention); the guest resolves them
641    /// from the same region. If the event arena is full the event is rejected
642    /// with [`RingError::Full`] (backpressure).
643    pub fn send_event(&mut self, event: &Event) -> Result<(), RingError> {
644        let mask = slot_mask(self.header);
645        let op = match event {
646            Event::TextChanged { target, value } => {
647                let offset = crate::arena::alloc_event(&mut self.event_arena, self.header, value.as_bytes())
648                    .map_err(|_| RingError::Full)?;
649                Opcode::new(category::EVENT, crate::event::TEXT_CHANGED, 0, *target, offset, 0)
650            }
651            Event::Navigate { url: Some(url) } => {
652                let offset = crate::arena::alloc_event(&mut self.event_arena, self.header, url.as_bytes())
653                    .map_err(|_| RingError::Full)?;
654                Opcode::new(
655                    category::EVENT,
656                    crate::event::NAVIGATE,
657                    crate::flag::NAVIGATE_URL,
658                    0,
659                    offset,
660                    0,
661                )
662            }
663            _ => event.encode(),
664        };
665        ring_fn::push_event(self.event_slots, self.header, mask, &op)
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672    use crate::constants::{category, component_type, property_id, value_type};
673    use alloc::vec;
674
675    #[test]
676    fn guest_host_round_trip() {
677        let layout = MemoryLayout::default();
678        let mut mem = vec![0u8; layout.total_bytes()];
679        init_memory(&mut mem, &layout);
680
681        let mut guest = Guest::new(&mut mem, &layout);
682        guest.begin_frame();
683        guest.create_node(1, component_type::VSTACK).unwrap();
684        guest.insert_child(0, 1, APPEND).unwrap();
685        guest.set_property(1, property_id::SPACING, value_type::F32, 4.0f32.to_bits()).unwrap();
686        guest
687            .set_property(1, property_id::PADDING, value_type::F32, 8.0f32.to_bits())
688            .unwrap();
689        let arena_ref = guest.set_text(1, "Hello").unwrap();
690        assert_eq!(guest.pending(), 5);
691        guest.end_frame();
692        assert_eq!(guest.frame_count(), 1);
693
694        let mut host = Host::new(&mut mem, &layout);
695        assert_eq!(host.frame_count(), 1);
696        let frames = host.frames();
697        assert_eq!(frames.len(), 1);
698        let frame = &frames[0];
699        let ops: Vec<Opcode> = frame.opcodes().collect();
700        assert_eq!(ops.len(), 5);
701        assert_eq!(ops[0].category(), category::TREE);
702        assert_eq!(ops[0].a(), 1);
703        assert_eq!(ops[1].a(), 0); // parent root
704        assert_eq!(ops[2].b() as u16, property_id::SPACING);
705        assert_eq!(ops[2].c_f32(), 4.0);
706        assert_eq!(frame.arena_str(arena_ref).unwrap(), "Hello");
707        // Second drain is empty.
708        assert!(host.frames().is_empty());
709    }
710
711    #[test]
712    fn set_design_token_round_trips() {
713        let layout = MemoryLayout::default();
714        let mut mem = vec![0u8; layout.total_bytes()];
715        init_memory(&mut mem, &layout);
716
717        let mut guest = Guest::new(&mut mem, &layout);
718        guest.begin_frame();
719        let arena_ref = guest
720            .set_design_token("color.primary", value_type::COLOR, 0xFF_2563EB)
721            .unwrap();
722        let dark_ref = guest
723            .set_design_token("dark.color.primary", value_type::COLOR, 0xFF_60A5FA)
724            .unwrap();
725        guest.end_frame();
726
727        let mut host = Host::new(&mut mem, &layout);
728        let frame = &host.frames()[0];
729        let ops: Vec<Opcode> = frame.opcodes().collect();
730        assert_eq!(ops.len(), 2);
731        assert_eq!(ops[0].category(), category::PARAMETER);
732        assert_eq!(ops[0].command(), parameter::SET_DESIGN_TOKEN);
733        assert_eq!(ops[0].b(), u32::from(value_type::COLOR));
734        assert_eq!(ops[0].c(), 0xFF_2563EB);
735        assert_eq!(frame.arena_str(arena_ref).unwrap(), "color.primary");
736        assert_eq!(frame.arena_str(dark_ref).unwrap(), "dark.color.primary");
737    }
738
739    #[test]
740    fn set_design_token_string_round_trips() {
741        let layout = MemoryLayout::default();
742        let mut mem = vec![0u8; layout.total_bytes()];
743        init_memory(&mut mem, &layout);
744
745        let mut guest = Guest::new(&mut mem, &layout);
746        guest.begin_frame();
747        let path_ref = guest.set_design_token_string("font.body.family", "Inter").unwrap();
748        guest.end_frame();
749
750        let mut host = Host::new(&mut mem, &layout);
751        let frame = &host.frames()[0];
752        let ops: Vec<Opcode> = frame.opcodes().collect();
753        assert_eq!(ops.len(), 1);
754        assert_eq!(ops[0].category(), category::PARAMETER);
755        assert_eq!(ops[0].command(), parameter::SET_DESIGN_TOKEN);
756        assert_eq!(ops[0].b(), u32::from(value_type::STRING));
757        assert_eq!(frame.arena_str(ops[0].a()).unwrap(), "font.body.family");
758        assert_eq!(frame.arena_str(ops[0].c()).unwrap(), "Inter");
759        assert_eq!(path_ref, ops[0].a());
760    }
761
762    #[test]
763    fn ring_full_is_reported() {
764        let mut layout = MemoryLayout::default();
765        layout.slot_count = 4; // tiny ring
766        let mut mem = vec![0u8; layout.total_bytes()];
767        init_memory(&mut mem, &layout);
768
769        let mut guest = Guest::new(&mut mem, &layout);
770        // 4 slots -> capacity is slot_count-1 = 3 opcodes before full.
771        assert!(guest.create_node(1, component_type::VSTACK).is_ok());
772        assert!(guest.create_node(2, component_type::HSTACK).is_ok());
773        assert!(guest.create_node(3, component_type::TEXT).is_ok());
774        assert_eq!(
775            guest.create_node(4, component_type::TEXT).unwrap_err(),
776            RingError::Full
777        );
778    }
779
780    #[test]
781    fn host_event_round_trips_to_guest() {
782        let layout = MemoryLayout::default();
783        let mut mem = vec![0u8; layout.total_bytes()];
784        init_memory(&mut mem, &layout);
785
786        // Host (renderer) writes an event.
787        let mut host = Host::new(&mut mem, &layout);
788        host.send_event(&Event::PointerUp {
789            target: 4,
790            x: 10.0,
791            y: 20.0,
792            secondary: false,
793        })
794        .unwrap();
795        drop(host);
796
797        // Guest drains and decodes it.
798        let mut guest = Guest::new(&mut mem, &layout);
799        let events = guest.drain_events();
800        assert_eq!(
801            events,
802            vec![Event::PointerUp {
803                target: 4,
804                x: 10.0,
805                y: 20.0,
806                secondary: false,
807            }]
808        );
809        // Second drain is empty.
810        assert!(guest.drain_events().is_empty());
811    }
812
813    #[test]
814    fn drain_events_skips_unknown_event_commands() {
815        let layout = MemoryLayout::default();
816        let mut mem = vec![0u8; layout.total_bytes()];
817        init_memory(&mut mem, &layout);
818
819        // Write a valid event into the event ring, then release the host borrow.
820        {
821            let mut host = Host::new(&mut mem, &layout);
822            host.send_event(&Event::PointerUp {
823                target: 4,
824                x: 10.0,
825                y: 20.0,
826                secondary: false,
827            })
828            .unwrap();
829        }
830
831        // Push a second, malformed EVENT opcode directly into the ring.
832        let mask = slot_mask(&mem[..memory::HEADER_SIZE]);
833        let (header, rest) = mem.split_at_mut(memory::HEADER_SIZE);
834        let (_slots, rest) = rest.split_at_mut(layout.ring_bytes());
835        let (event_slots, _arena) = rest.split_at_mut(layout.event_ring_bytes());
836        let bogus = Opcode::new(crate::category::EVENT, 0xFF, 0, 1, 0, 0);
837        ring_fn::push_event(event_slots, header, mask, &bogus).unwrap();
838
839        // The guest sees only the decodable event; the unknown command is dropped.
840        let mut guest = Guest::new(&mut mem, &layout);
841        let events = guest.drain_events();
842        assert_eq!(events.len(), 1);
843        assert_eq!(
844            events[0],
845            Event::PointerUp {
846                target: 4,
847                x: 10.0,
848                y: 20.0,
849                secondary: false,
850            }
851        );
852    }
853
854    #[test]
855    fn host_frames_survives_a_wrapped_write_cursor() {
856        let layout = MemoryLayout {
857            slot_count: 4,
858            arena_bytes: MemoryLayout::default().arena_bytes,
859            event_arena_bytes: MemoryLayout::default().event_arena_bytes,
860        };
861        let mut mem = vec![0u8; layout.total_bytes()];
862        init_memory(&mut mem, &layout);
863
864        // Guest writes three opcodes (slots 0,1,2; capacity is slot_count-1).
865        {
866            let mut guest = Guest::new(&mut mem, &layout);
867            guest.begin_frame();
868            guest.create_node(1, component_type::VSTACK).unwrap();
869            guest.create_node(2, component_type::HSTACK).unwrap();
870            guest.create_node(3, component_type::TEXT).unwrap();
871            guest.end_frame();
872        }
873        // Host drains everything -> readCursor = 3.
874        {
875            let mut host = Host::new(&mut mem, &layout);
876            assert_eq!(host.frames().len(), 1);
877        }
878        // Guest writes one more opcode -> slot 3, writeCursor wraps 3 -> 0.
879        // Now masked write(0) < masked read(3) — the wrap that used to
880        // permanently deadlock the frame ring.
881        {
882            let mut guest = Guest::new(&mut mem, &layout);
883            guest.begin_frame();
884            guest.create_node(4, component_type::TEXT).unwrap();
885            guest.end_frame();
886        }
887
888        // The wrapped run is the head [3..4) + a (here empty) tail [0..0): the
889        // first read drains the head in ring order...
890        let mut host = Host::new(&mut mem, &layout);
891        let frames = host.frames();
892        assert_eq!(frames.len(), 1);
893        assert_eq!(frames[0].opcodes().count(), 1);
894        assert_eq!(frames[0].opcodes().next().unwrap().a(), 4);
895        // ...and readCursor advanced to 0, so nothing is left pending.
896        assert!(host.frames().is_empty());
897        // And the producer can wrap again without ever deadlocking.
898        {
899            let mut guest = Guest::new(&mut mem, &layout);
900            guest.begin_frame();
901            guest.create_node(5, component_type::TEXT).unwrap();
902            guest.end_frame();
903        }
904        let mut host = Host::new(&mut mem, &layout);
905        assert_eq!(host.frames()[0].opcodes().next().unwrap().a(), 5);
906    }
907
908    #[test]
909    fn icon_vocabulary_names_are_canonical_and_unique() {
910        use crate::constants::icon;
911        use alloc::collections::BTreeSet;
912
913        let mut seen = BTreeSet::new();
914        for name in icon::NAMES {
915            assert!(icon::is_canonical(name), "{} not canonical", name);
916            assert!(seen.insert(name), "duplicate canonical icon {}", name);
917        }
918        // Every documented constant is in the canonical list.
919        for name in [
920            icon::HOME,
921            icon::SETTINGS,
922            icon::PLAY,
923            icon::SKIP_FORWARD,
924            icon::VOLUME_MUTE,
925            icon::FILTER,
926        ] {
927            assert!(icon::is_canonical(name));
928        }
929        assert!(!icon::is_canonical("definitely-not-an-icon"));
930    }
931}