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}