Skip to main content

kozan_platform/
context.rs

1//! View context — the user-facing API inside a view thread.
2//!
3//! Like Chrome's `LocalFrame` + `LocalDOMWindow` — the entry point for
4//! accessing the document, spawning async work, and communicating
5//! back to the main thread.
6//!
7//! Zero windowing-backend knowledge. Communicates through `PlatformHost` trait.
8
9use std::cell::RefCell;
10use std::future::Future;
11use std::pin::Pin;
12use std::rc::Rc;
13use std::sync::Arc;
14use std::sync::mpsc;
15
16use kozan_core::Document;
17use kozan_core::compositor::layer_tree::LayerTree;
18use kozan_core::paint::DisplayList;
19use kozan_core::scroll::ScrollOffsets;
20use kozan_core::scroll::ScrollTree;
21use kozan_core::widget::FrameWidget;
22use kozan_scheduler::WakeSender;
23
24use crate::host::PlatformHost;
25use crate::id::WindowId;
26use crate::pipeline::render_loop::RenderEvent;
27
28/// Pinned boxed future that can live on the view thread.
29///
30/// `!Send` is fine — these are spawned into the `LocalExecutor` which
31/// only runs on the view thread. `'static` so captured DOM handles
32/// (which are `'static` index types) can cross `.await` points.
33pub(crate) type StagedFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
34
35/// A frame callback — like `requestAnimationFrame(callback)`.
36/// Returns `true` to keep for next frame (loop), `false` for one-shot.
37pub(crate) type FrameCallback = Box<dyn FnMut(kozan_scheduler::FrameInfo) -> bool + 'static>;
38
39/// Everything the view thread produces per frame — posted to the render thread.
40///
41/// Chrome: `LayerTreeHost::FinishCommit()` posts this to the compositor's
42/// task queue. Ownership transfers — no shared state, no mutex.
43pub struct FrameOutput {
44    pub display_list: Arc<DisplayList>,
45    pub layer_tree: LayerTree,
46    pub scroll_tree: ScrollTree,
47}
48
49/// The user-facing API inside a view.
50///
51/// Passed to the view's init closure. Provides access to:
52/// - The document (DOM tree, via `FrameWidget`)
53/// - The cross-thread sender (for giving to background tasks)
54/// - The platform host (for requesting redraws, setting title, etc.)
55/// - The window identity (which window this view belongs to)
56///
57/// # Example
58///
59/// ```ignore
60/// app.window(WindowConfig::default(), |ctx| {
61///     let doc = ctx.document();
62///     let btn = doc.create::<HtmlButtonElement>();
63///     btn.set_text("Hello!");
64///     doc.root().append(btn);
65/// });
66/// ```
67pub struct ViewContext {
68    /// The engine entry point — DOM, layout, paint.
69    /// Chrome equivalent: `LocalFrameView`.
70    frame: FrameWidget,
71
72    wake_sender: WakeSender,
73    host: Arc<dyn PlatformHost>,
74    window_id: WindowId,
75
76    /// Channel to post frames to the render thread.
77    /// Chrome: `ProxyMain` posts commits to compositor's task queue.
78    render_sender: mpsc::Sender<RenderEvent>,
79
80    /// Futures queued via `spawn()` during the init closure.
81    ///
82    /// After init returns, `run_view_thread` drains these into the
83    /// `LocalExecutor`. Using `Rc<RefCell<...>>` (not Arc/Mutex) because
84    /// `ViewContext` is `!Send` and lives entirely on the view thread.
85    staged_futures: Rc<RefCell<Vec<StagedFuture>>>,
86
87    /// Frame callbacks queued via `request_frame()`.
88    /// Drained into the scheduler each tick — like `requestAnimationFrame`.
89    staged_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
90
91    /// Last computed FPS — updated each frame by the scheduler.
92    /// Shared via `Rc<Cell>` so async tasks can read it.
93    last_fps: Rc<std::cell::Cell<f64>>,
94}
95
96impl ViewContext {
97    /// Create a new view context. Called internally by the view thread.
98    pub(crate) fn new(
99        frame: FrameWidget,
100        wake_sender: WakeSender,
101        host: Arc<dyn PlatformHost>,
102        window_id: WindowId,
103        render_sender: mpsc::Sender<RenderEvent>,
104    ) -> Self {
105        Self {
106            frame,
107            wake_sender,
108            host,
109            window_id,
110            render_sender,
111            staged_futures: Rc::new(RefCell::new(Vec::new())),
112            staged_frame_callbacks: Rc::new(RefCell::new(Vec::new())),
113            last_fps: Rc::new(std::cell::Cell::new(0.0)),
114        }
115    }
116
117    /// Read-only access to the document.
118    #[inline]
119    pub fn document(&self) -> &Document {
120        self.frame.document()
121    }
122
123    /// Register custom font data (TTF/OTF/TTC bytes) into the font system.
124    ///
125    /// Chrome equivalent: `document.fonts.add(new FontFace(...))`.
126    /// After registration, the font's family name is available for CSS
127    /// `font-family` matching. The family name is auto-detected from
128    /// the font file's `name` table.
129    ///
130    /// Accepts `&'static [u8]` (zero-copy for `include_bytes!()`),
131    /// `Vec<u8>` (runtime-loaded), or `Arc<[u8]>` (pre-shared).
132    ///
133    /// Returns the registered family names.
134    ///
135    /// # Example
136    ///
137    /// ```ignore
138    /// // Static — zero copy:
139    /// ctx.register_font(include_bytes!("../assets/Cairo.ttf") as &[u8]);
140    ///
141    /// // Runtime — from a file:
142    /// ctx.register_font(std::fs::read("font.ttf").unwrap());
143    /// ```
144    pub fn register_font(
145        &self,
146        data: impl Into<kozan_core::layout::inline::font_system::FontBlob>,
147    ) -> Vec<String> {
148        self.frame.font_system().register_font(data)
149    }
150
151    /// Spawn a `!Send` async task on the view thread's executor.
152    ///
153    /// The future runs on the view thread — it can safely capture and
154    /// mutate DOM handles across `.await` points. No `Arc`, no `Mutex`,
155    /// no `WakeSender` needed.
156    ///
157    /// ```ignore
158    /// ctx.spawn(async move {
159    ///     kozan_platform::time::sleep(Duration::from_millis(500)).await;
160    ///     card.set_style(activated_style());
161    /// });
162    /// ```
163    ///
164    /// If called during the init closure the future is queued and started
165    /// on the first scheduler tick. If called later (e.g. from an event
166    /// handler posted via `WakeSender`) it is spawned into the executor
167    /// immediately — use `WakeSender::post` for that case.
168    pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
169        self.staged_futures.borrow_mut().push(Box::pin(future));
170    }
171
172    /// Drain futures queued by `spawn()` during init.
173    ///
174    /// Called by `run_view_thread` after the init closure returns.
175    pub(crate) fn take_staged_futures(&self) -> Vec<StagedFuture> {
176        self.staged_futures.borrow_mut().drain(..).collect()
177    }
178
179    /// Take frame callbacks queued via `request_frame()`.
180    ///
181    /// Called by the event loop each tick to register them with the scheduler.
182    /// Uses `mem::take` — zero allocation, swaps with empty Vec.
183    pub(crate) fn take_staged_frame_callbacks(&self) -> Vec<FrameCallback> {
184        std::mem::take(&mut *self.staged_frame_callbacks.borrow_mut())
185    }
186
187    /// Get a clone of the cross-thread sender.
188    ///
189    /// Give this to background threads so they can send results
190    /// back to this view's scheduler.
191    #[inline]
192    pub fn wake_sender(&self) -> WakeSender {
193        self.wake_sender.clone()
194    }
195
196    /// Request a redraw for this view's window.
197    pub fn request_redraw(&self) {
198        self.host.request_redraw(self.window_id);
199    }
200
201    /// Set the window title.
202    pub fn set_title(&self, title: &str) {
203        self.host.set_title(self.window_id, title);
204    }
205
206    /// Close this view's window.
207    pub fn close_window(&self) {
208        self.host.close_window(self.window_id);
209    }
210
211    /// The `WindowId` this view belongs to.
212    #[inline]
213    pub fn window_id(&self) -> WindowId {
214        self.window_id
215    }
216
217    /// Current FPS — updated each frame by the scheduler.
218    ///
219    /// Returns 0.0 on the first frame. Use this to build FPS overlays.
220    ///
221    /// ```ignore
222    /// let fps_rc = ctx.fps_cell();
223    /// ctx.spawn(async move {
224    ///     loop {
225    ///         sleep(Duration::from_millis(200)).await;
226    ///         label.set_text(&format!("{:.0} FPS", fps_rc.get()));
227    ///     }
228    /// });
229    /// ```
230    #[inline]
231    pub fn fps(&self) -> f64 {
232        self.last_fps.get()
233    }
234
235    /// Returns the shared FPS counter — callers may read or write.
236    #[inline]
237    pub fn fps_cell(&self) -> Rc<std::cell::Cell<f64>> {
238        Rc::clone(&self.last_fps)
239    }
240
241    /// Register a frame callback — like `requestAnimationFrame`.
242    ///
243    /// Returns `bool`: `true` = keep for next frame, `false` = one-shot.
244    ///
245    /// ```ignore
246    /// // One-shot:
247    /// ctx.request_frame(|_info| { do_something(); false });
248    ///
249    /// // Render loop (like requestAnimationFrame in a loop):
250    /// ctx.request_frame(move |info| {
251    ///     fps_label.set_content(format!("{:.0} FPS", info.fps));
252    ///     true // keep running
253    /// });
254    /// ```
255    pub fn request_frame(
256        &self,
257        callback: impl FnMut(kozan_scheduler::FrameInfo) -> bool + 'static,
258    ) {
259        self.staged_frame_callbacks
260            .borrow_mut()
261            .push(Box::new(callback));
262    }
263
264    // ── Internal (view thread only) ───────────────────────────────────────
265
266    /// Update FPS from the scheduler's frame info.
267    pub(crate) fn set_last_fps(&self, fps: f64) {
268        self.last_fps.set(fps);
269    }
270
271    /// Previous frame's pipeline timing.
272    pub(crate) fn last_frame_timing(&self) -> kozan_primitives::timing::FrameTiming {
273        self.frame.last_timing()
274    }
275
276    /// Check if the document has pending changes that need a frame.
277    ///
278    /// Called after `scheduler.tick()` — spawned tasks may have mutated the DOM
279    /// (e.g. `style().w(pct(...))`) without requesting a frame.
280    /// Chrome equivalent: checking `Document::NeedsStyleRecalc()` after microtask checkpoint.
281    pub(crate) fn document_needs_frame(&self) -> bool {
282        self.frame.document().needs_visual_update()
283    }
284
285    /// Apply scroll offsets received from the compositor.
286    /// Chrome: main thread applies scroll deltas posted from compositor thread.
287    pub(crate) fn apply_scroll_sync(&mut self, offsets: ScrollOffsets) {
288        self.frame.apply_compositor_scroll(&offsets);
289    }
290
291    /// Run style → layout → paint, then post the result to the render thread.
292    /// Chrome: `LocalFrameView::UpdateLifecyclePhases()` then `FinishCommit()`.
293    pub(crate) fn update_lifecycle_and_commit(&mut self) {
294        self.frame.update_lifecycle();
295
296        let dl = self.frame.last_display_list();
297        let layer_tree = self.frame.take_layer_tree();
298
299        if let (Some(dl), Some(tree)) = (dl, layer_tree) {
300            let (scroll_tree, _scroll_offsets) = self.frame.scroll_state_snapshot();
301            let _ = self.render_sender.send(RenderEvent::Commit(FrameOutput {
302                display_list: dl,
303                layer_tree: tree,
304                scroll_tree,
305            }));
306        }
307    }
308
309    /// Process an input event — hit test + DOM dispatch.
310    ///
311    /// Returns `true` if DOM state changed (hover/focus/active changed,
312    /// or event listeners were dispatched that may have mutated the DOM).
313    pub(crate) fn on_input(&mut self, input: kozan_core::InputEvent) -> bool {
314        self.frame.handle_input(input)
315    }
316
317    /// Mark that styles need recalculation.
318    ///
319    /// Called when `ElementState` changes (hover, focus, active) or when
320    /// event listeners may have mutated the DOM. Chrome equivalent:
321    /// `Document::SetNeedsStyleRecalc()`.
322    pub(crate) fn invalidate_style(&mut self) {
323        self.frame.mark_needs_update();
324    }
325
326    /// Notify the frame of a resize event.
327    pub(crate) fn on_resize(&mut self, width: u32, height: u32) {
328        self.frame.resize(width, height);
329    }
330
331    /// Notify the frame of a scale factor change.
332    pub(crate) fn on_scale_factor_changed(&mut self, factor: f64) {
333        self.frame.set_scale_factor(factor);
334    }
335}