Skip to main content

rlvgl_playit/
executor.rs

1//! The playit executor: polls a transport, parses commands, and dispatches
2//! them against the widget tree.
3
4use crate::command::{Command, DumpSpec, EventSpec, QuerySpec};
5use crate::framebuffer::FramebufferReader;
6use crate::protocol::{format_event_spec, format_response, parse_command, write_hex_u32};
7use crate::recorder::EventRecorder;
8use crate::response::{Response, StatusData};
9use crate::tag::{find_by_tag, find_by_tag_mut};
10use crate::transport::PlayitTransport;
11use rlvgl_core::WidgetNode;
12use rlvgl_core::event::Event;
13
14/// Maximum line length accepted by the executor.
15const MAX_LINE: usize = 128;
16
17/// Event transform pipeline (e.g. gesture recognition).
18///
19/// The executor feeds each injected raw event through [`process`](Self::process)
20/// and calls [`tick`](Self::tick) once per [`PlayitExecutor::poll`] invocation.
21/// Returned events are dispatched to the widget tree.
22///
23/// Two return slots cover the worst case of `DoubleTapRecognizer::process`
24/// which may emit a buffered first tap plus the current event.
25///
26/// Use [`NullPipeline`] to skip gesture processing (direct dispatch).
27///
28/// # Example
29///
30/// ```
31/// use rlvgl_core::event::Event;
32/// use rlvgl_playit::EventPipeline;
33///
34/// /// Pipeline that swallows Tick events and passes everything else through.
35/// struct FilterTicks;
36///
37/// impl EventPipeline for FilterTicks {
38///     fn process(&mut self, event: Event) -> (Option<Event>, Option<Event>) {
39///         match event {
40///             Event::Tick => (None, None),
41///             other => (Some(other), None),
42///         }
43///     }
44///     fn tick(&mut self) -> (Option<Event>, Option<Event>) {
45///         (None, None)
46///     }
47/// }
48///
49/// let mut pipeline = FilterTicks;
50/// let (a, _b) = pipeline.process(Event::Tick);
51/// assert!(a.is_none());
52///
53/// let (a, _b) = pipeline.process(Event::PressRelease { x: 10, y: 20 });
54/// assert!(a.is_some());
55/// ```
56pub trait EventPipeline {
57    /// Transform one input event into zero, one, or two output events.
58    fn process(&mut self, event: Event) -> (Option<Event>, Option<Event>);
59    /// Advance internal timers.  Called once per frame.
60    fn tick(&mut self) -> (Option<Event>, Option<Event>);
61}
62
63/// No-op pipeline that passes events through unchanged.
64pub struct NullPipeline;
65
66impl EventPipeline for NullPipeline {
67    fn process(&mut self, event: Event) -> (Option<Event>, Option<Event>) {
68        (Some(event), None)
69    }
70    fn tick(&mut self) -> (Option<Event>, Option<Event>) {
71        (None, None)
72    }
73}
74
75/// State for an in-progress framebuffer dump.
76struct DumpState {
77    spec: DumpSpec,
78    remaining: u8,
79    last_present_seen: u32,
80}
81
82/// Processes playit commands arriving over a [`PlayitTransport`] and executes
83/// them against a [`WidgetNode`] tree.
84///
85/// The const generic `REC_CAP` sets the maximum number of events the built-in
86/// recorder can capture (default 256).
87///
88/// Call [`poll`](Self::poll) once per frame from your main loop.
89pub struct PlayitExecutor<T: PlayitTransport, const REC_CAP: usize = 256> {
90    transport: T,
91    line_buf: [u8; MAX_LINE],
92    line_len: usize,
93    dump: Option<DumpState>,
94    recorder: EventRecorder<REC_CAP>,
95}
96
97impl<T: PlayitTransport, const REC_CAP: usize> PlayitExecutor<T, REC_CAP> {
98    /// Create a new executor wrapping the given transport.
99    pub fn new(transport: T) -> Self {
100        Self {
101            transport,
102            line_buf: [0; MAX_LINE],
103            line_len: 0,
104            dump: None,
105            recorder: EventRecorder::new(),
106        }
107    }
108
109    /// Drain incoming bytes, parse complete command lines, and execute them.
110    ///
111    /// * `root` — mutable reference to the widget tree root.
112    /// * `status` — current telemetry snapshot for `?` commands.
113    /// * `fb` — optional framebuffer reader for `D` pixel dump commands.
114    /// * `pipeline` — event pipeline for gesture processing.  Pass
115    ///   [`&mut NullPipeline`](NullPipeline) for direct dispatch.
116    /// * `on_extension` — callback for application-defined extension commands.
117    pub fn poll<P: EventPipeline, F>(
118        &mut self,
119        root: &mut WidgetNode,
120        status: &StatusData,
121        fb: Option<&dyn FramebufferReader>,
122        pipeline: &mut P,
123        on_extension: F,
124    ) where
125        F: FnMut(&[u8]),
126    {
127        self.poll_with_callback(root, status, fb, pipeline, on_extension, |_event| {});
128    }
129
130    /// Variant of [`poll`](Self::poll) that invokes `after_dispatch` after
131    /// every event delivered to the widget tree.
132    pub fn poll_with_callback<P: EventPipeline, F, A>(
133        &mut self,
134        root: &mut WidgetNode,
135        status: &StatusData,
136        fb: Option<&dyn FramebufferReader>,
137        pipeline: &mut P,
138        mut on_extension: F,
139        mut after_dispatch: A,
140    ) where
141        F: FnMut(&[u8]),
142        A: FnMut(&Event),
143    {
144        // Drain transport bytes and accumulate lines.
145        while let Some(byte) = self.transport.read_byte() {
146            if byte == b'\n' || byte == b'\r' {
147                if self.line_len > 0 {
148                    let len = self.line_len;
149                    let mut line = [0u8; MAX_LINE];
150                    line[..len].copy_from_slice(&self.line_buf[..len]);
151                    self.line_len = 0;
152                    self.handle_line(
153                        &line[..len],
154                        root,
155                        status,
156                        pipeline,
157                        &mut on_extension,
158                        &mut after_dispatch,
159                    );
160                }
161            } else if self.line_len < MAX_LINE {
162                self.line_buf[self.line_len] = byte;
163                self.line_len += 1;
164            }
165        }
166
167        // Advance pipeline timers and dispatch any deferred gesture events.
168        self.dispatch_output_events(root, pipeline.tick(), &mut after_dispatch);
169
170        // Advance recorder tick counter.
171        self.recorder.tick();
172
173        // Emit queued framebuffer dump rows if a new present has occurred.
174        if let Some(reader) = fb {
175            self.emit_dump_if_ready(reader);
176        }
177    }
178
179    /// Dispatch one runtime input event through the same gesture pipeline used
180    /// for transport commands.
181    pub fn dispatch_event<P: EventPipeline, A>(
182        &mut self,
183        event: Event,
184        root: &mut WidgetNode,
185        pipeline: &mut P,
186        mut after_dispatch: A,
187    ) where
188        A: FnMut(&Event),
189    {
190        self.dispatch_output_events(root, pipeline.process(event), &mut after_dispatch);
191    }
192
193    // ------------------------------------------------------------------
194    // Internal dispatch
195    // ------------------------------------------------------------------
196
197    fn handle_line<P: EventPipeline, F, A>(
198        &mut self,
199        line: &[u8],
200        root: &mut WidgetNode,
201        status: &StatusData,
202        pipeline: &mut P,
203        on_extension: &mut F,
204        after_dispatch: &mut A,
205    ) where
206        F: FnMut(&[u8]),
207        A: FnMut(&Event),
208    {
209        let Some(cmd) = parse_command(line) else {
210            self.send_response(&Response::Error("parse error"));
211            return;
212        };
213
214        match cmd {
215            Command::Status => {
216                self.send_response(&Response::Status(*status));
217            }
218
219            Command::Inject(spec) => {
220                self.inject_event(spec, root, pipeline, after_dispatch);
221                self.send_response(&Response::Ok);
222            }
223
224            Command::InjectTagged(tag, spec) => {
225                if let Some(node) = find_by_tag_mut(root, tag) {
226                    // Tagged injection bypasses pipeline — targets a specific widget.
227                    let event = spec.to_event();
228                    if self.recorder.is_running() {
229                        self.recorder.record(spec);
230                    }
231                    node.dispatch_event(&event);
232                    after_dispatch(&event);
233                    self.send_response(&Response::Ok);
234                } else {
235                    self.send_response(&Response::Error("tag not found"));
236                }
237            }
238
239            Command::Query(q) => self.handle_query(root, &q),
240
241            Command::DumpPixels(spec) => {
242                let present = spec.frames;
243                self.dump = Some(DumpState {
244                    spec,
245                    remaining: present,
246                    last_present_seen: 0,
247                });
248                self.send_str(b"DUMP:queued\r\n");
249            }
250
251            Command::RecordStart => {
252                self.recorder.start();
253                self.send_str(b"REC:recording\r\n");
254            }
255
256            Command::RecordStop => {
257                self.recorder.stop();
258                self.dump_recording();
259            }
260
261            Command::RecordDump => {
262                self.dump_recording();
263            }
264
265            Command::Extension(payload) => {
266                on_extension(payload);
267                self.send_response(&Response::Ok);
268            }
269        }
270    }
271
272    /// Inject an event through the pipeline and into the widget tree,
273    /// recording if the recorder is active.
274    fn inject_event<P: EventPipeline, A>(
275        &mut self,
276        spec: EventSpec,
277        root: &mut WidgetNode,
278        pipeline: &mut P,
279        after_dispatch: &mut A,
280    ) where
281        A: FnMut(&Event),
282    {
283        if self.recorder.is_running() {
284            self.recorder.record(spec);
285        }
286
287        let event = spec.to_event();
288        self.dispatch_output_events(root, pipeline.process(event), after_dispatch);
289    }
290
291    fn dispatch_output_events<A>(
292        &mut self,
293        root: &mut WidgetNode,
294        outputs: (Option<Event>, Option<Event>),
295        after_dispatch: &mut A,
296    ) where
297        A: FnMut(&Event),
298    {
299        if let Some(evt) = outputs.0 {
300            root.dispatch_event(&evt);
301            after_dispatch(&evt);
302        }
303        if let Some(evt) = outputs.1 {
304            root.dispatch_event(&evt);
305            after_dispatch(&evt);
306        }
307    }
308
309    fn handle_query(&mut self, root: &WidgetNode, q: &QuerySpec<'_>) {
310        match q {
311            QuerySpec::Bounds(tag) => {
312                if let Some(node) = find_by_tag(root, tag) {
313                    let b = node.widget.borrow().bounds();
314                    self.send_response(&Response::Bounds {
315                        x: b.x,
316                        y: b.y,
317                        width: b.width,
318                        height: b.height,
319                    });
320                } else {
321                    self.send_response(&Response::Error("tag not found"));
322                }
323            }
324            QuerySpec::Exists(tag) => {
325                let found = find_by_tag(root, tag).is_some();
326                self.send_response(&Response::Exists(found));
327            }
328            QuerySpec::ChildCount(tag) => {
329                if let Some(node) = find_by_tag(root, tag) {
330                    let count = node.children.len().min(u16::MAX as usize) as u16;
331                    self.send_response(&Response::ChildCount(count));
332                } else {
333                    self.send_response(&Response::Error("tag not found"));
334                }
335            }
336        }
337    }
338
339    // ------------------------------------------------------------------
340    // Recording dump
341    // ------------------------------------------------------------------
342
343    fn dump_recording(&mut self) {
344        let len = self.recorder.len();
345        // Header: REC:START,<count>
346        {
347            let mut hdr = [0u8; 32];
348            let mut w = crate::protocol::BufWriter::new(&mut hdr);
349            w.write_str("REC:START,");
350            w.write_i32(len as i32);
351            w.write_str("\r\n");
352            let n = w.pos;
353            self.transport.write_bytes(&hdr[..n]);
354        }
355
356        // Entries: @<delta> <command>\r\n
357        // Drain to avoid borrowing self.recorder while writing to transport.
358        for entry in self.recorder.drain() {
359            // Format prefix: @<delta>
360            let mut prefix = [0u8; 16];
361            let pn = {
362                let mut w = crate::protocol::BufWriter::new(&mut prefix);
363                w.write_byte(b'@');
364                w.write_i32(entry.tick_delta as i32);
365                w.write_byte(b' ');
366                w.pos
367            };
368            self.transport.write_bytes(&prefix[..pn]);
369
370            // Format event spec
371            let mut spec_buf = [0u8; 128];
372            let sn = format_event_spec(&entry.spec, &mut spec_buf);
373            self.transport.write_bytes(&spec_buf[..sn]);
374            self.transport.write_bytes(b"\r\n");
375        }
376
377        self.transport.write_bytes(b"REC:END\r\n");
378    }
379
380    // ------------------------------------------------------------------
381    // Framebuffer dump
382    // ------------------------------------------------------------------
383
384    fn emit_dump_if_ready(&mut self, reader: &dyn FramebufferReader) {
385        let Some(state) = self.dump.as_mut() else {
386            return;
387        };
388
389        let current_present = reader.present_count();
390        if state.last_present_seen == 0 && state.remaining == state.spec.frames {
391            state.last_present_seen = current_present;
392            return;
393        }
394        if current_present == state.last_present_seen {
395            return;
396        }
397
398        self.transport.write_bytes(b"F\r\n");
399
400        let spec = state.spec;
401        let mut row_buf = [0u32; 40];
402        for row in 0..spec.height {
403            let n = reader.read_row(
404                spec.x,
405                spec.y + row as i32,
406                spec.width,
407                &mut row_buf[..spec.width as usize],
408            );
409            for (i, &pixel) in row_buf[..n].iter().enumerate() {
410                let mut hex = [0u8; 8];
411                write_hex_u32(pixel, &mut hex);
412                self.transport.write_bytes(&hex);
413                if i + 1 < n {
414                    self.transport.write_bytes(b" ");
415                }
416            }
417            self.transport.write_bytes(b"\r\n");
418        }
419
420        state.last_present_seen = current_present;
421        state.remaining -= 1;
422
423        if state.remaining == 0 {
424            self.dump = None;
425            let mut buf = [0u8; 16];
426            let n = format_response(&Response::DumpEnd, &mut buf);
427            self.transport.write_bytes(&buf[..n]);
428        }
429    }
430
431    // ------------------------------------------------------------------
432    // Output helpers
433    // ------------------------------------------------------------------
434
435    fn send_response(&mut self, resp: &Response<'_>) {
436        let mut buf = [0u8; 128];
437        let n = format_response(resp, &mut buf);
438        self.transport.write_bytes(&buf[..n]);
439    }
440
441    fn send_str(&mut self, s: &[u8]) {
442        self.transport.write_bytes(s);
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use crate::transport::PlayitTransport;
450    use rlvgl_core::renderer::Renderer;
451    use rlvgl_core::widget::{Rect, Widget};
452    use std::cell::RefCell;
453    use std::collections::VecDeque;
454    use std::rc::Rc;
455
456    #[derive(Default)]
457    struct VecTransport {
458        incoming: VecDeque<u8>,
459        outgoing: Vec<u8>,
460    }
461
462    impl VecTransport {
463        fn with_input(input: &[u8]) -> Self {
464            Self {
465                incoming: input.iter().copied().collect(),
466                outgoing: Vec::new(),
467            }
468        }
469    }
470
471    impl PlayitTransport for VecTransport {
472        fn read_byte(&mut self) -> Option<u8> {
473            self.incoming.pop_front()
474        }
475
476        fn write_bytes(&mut self, bytes: &[u8]) {
477            self.outgoing.extend_from_slice(bytes);
478        }
479    }
480
481    type TestExecutor = PlayitExecutor<VecTransport, 16>;
482
483    struct RecordingWidget {
484        bounds: Rect,
485        events: Rc<RefCell<Vec<Event>>>,
486    }
487
488    impl Widget for RecordingWidget {
489        fn bounds(&self) -> Rect {
490            self.bounds
491        }
492
493        fn draw(&self, _renderer: &mut dyn Renderer) {}
494
495        fn handle_event(&mut self, event: &Event) -> bool {
496            self.events.borrow_mut().push(event.clone());
497            false
498        }
499    }
500
501    fn recording_node(
502        tag: Option<&'static str>,
503        events: Rc<RefCell<Vec<Event>>>,
504        bounds: Rect,
505    ) -> WidgetNode {
506        WidgetNode {
507            widget: Rc::new(RefCell::new(RecordingWidget { bounds, events })),
508            children: Vec::new(),
509            tag,
510        }
511    }
512
513    struct TickPipeline {
514        emit_tick: bool,
515    }
516
517    impl EventPipeline for TickPipeline {
518        fn process(&mut self, event: Event) -> (Option<Event>, Option<Event>) {
519            (Some(event), None)
520        }
521
522        fn tick(&mut self) -> (Option<Event>, Option<Event>) {
523            if self.emit_tick {
524                self.emit_tick = false;
525                (Some(Event::Tick), None)
526            } else {
527                (None, None)
528            }
529        }
530    }
531
532    #[test]
533    fn poll_with_callback_reports_dispatches_from_transport_and_pipeline_tick() {
534        let widget_events = Rc::new(RefCell::new(Vec::new()));
535        let mut root = recording_node(
536            Some("root"),
537            widget_events.clone(),
538            Rect {
539                x: 0,
540                y: 0,
541                width: 10,
542                height: 10,
543            },
544        );
545        let mut executor = TestExecutor::new(VecTransport::with_input(b"T10,20\r\n"));
546        let mut pipeline = TickPipeline { emit_tick: true };
547        let mut callback_events = Vec::new();
548
549        executor.poll_with_callback(
550            &mut root,
551            &StatusData::default(),
552            None,
553            &mut pipeline,
554            |_payload| {},
555            |event| callback_events.push(event.clone()),
556        );
557
558        assert_eq!(
559            callback_events,
560            vec![Event::PressRelease { x: 10, y: 20 }, Event::Tick]
561        );
562        assert_eq!(*widget_events.borrow(), callback_events);
563    }
564
565    #[test]
566    fn tagged_inject_and_runtime_dispatch_both_invoke_after_dispatch() {
567        let root_events = Rc::new(RefCell::new(Vec::new()));
568        let child_events = Rc::new(RefCell::new(Vec::new()));
569        let mut root = recording_node(
570            Some("root"),
571            root_events,
572            Rect {
573                x: 0,
574                y: 0,
575                width: 10,
576                height: 10,
577            },
578        );
579        root.children.push(recording_node(
580            Some("target"),
581            child_events.clone(),
582            Rect {
583                x: 5,
584                y: 6,
585                width: 20,
586                height: 30,
587            },
588        ));
589
590        let mut executor = TestExecutor::new(VecTransport::with_input(b"T@target:15,16\r\n"));
591        let mut pipeline = TickPipeline { emit_tick: false };
592        let mut callback_events = Vec::new();
593
594        executor.poll_with_callback(
595            &mut root,
596            &StatusData::default(),
597            None,
598            &mut pipeline,
599            |_payload| {},
600            |event| callback_events.push(event.clone()),
601        );
602        executor.dispatch_event(
603            Event::KeyDown {
604                key: rlvgl_core::event::Key::Enter,
605            },
606            &mut root,
607            &mut pipeline,
608            |event| callback_events.push(event.clone()),
609        );
610
611        assert_eq!(
612            callback_events,
613            vec![
614                Event::PressRelease { x: 15, y: 16 },
615                Event::KeyDown {
616                    key: rlvgl_core::event::Key::Enter
617                }
618            ]
619        );
620        assert_eq!(
621            *child_events.borrow(),
622            vec![
623                Event::PressRelease { x: 15, y: 16 },
624                Event::KeyDown {
625                    key: rlvgl_core::event::Key::Enter
626                }
627            ]
628        );
629    }
630}