cotis_web/renderer.rs
1//! Application-facing renderer type for browser/WASM targets.
2//!
3//! [`HTMLRenderer`] is the main entry point: wire it into [`CotisApp`](cotis::cotis_app::CotisApp)
4//! with a layout manager and render-list pipe, then drive frames through
5//! [`AsyncRenderApp::compute_frame_async`](cotis::cotis_app::AsyncRenderApp::compute_frame_async).
6//!
7//! Each frame:
8//!
9//! 1. [`CotisRendererAsync::draw_frame`] awaits the JS `waitForNextFrame` helper.
10//! 2. Render commands (types implementing [`HTMLDrawable`]) are
11//! drawn via [`draw_html_render_commands`].
12//!
13//! Cotis context traits ([`CotisWindowContext`](cotis_utils::traits::CotisWindowContext),
14//! [`CotisFrameContext`](cotis_utils::traits::CotisFrameContext), text measuring, etc.) are
15//! implemented in [`crate::cotis_traits`].
16//!
17//! Custom DOM hosts and form reading: [`crate::custom_data`] and
18//! [`HTMLRenderer::get_custom_element_html`] / [`HTMLRenderer::get_custom_element_properties`].
19//!
20//! Image URLs: [`crate::images`].
21
22use crate::rendering::{HTMLDrawable, draw_html_render_commands};
23use crate::web_functions::primitives::{load_font, wait_for_next_frame};
24use cotis::renders::CotisRendererAsync;
25use cotis_defaults::element_configs::text_config::TextConfig;
26use cotis_utils::font_manager::FontManager;
27use cotis_utils::math::Dimensions;
28
29/// Reserved type alias for a text-measuring callback (currently unused).
30pub type MeasureFunType = fn(&str, &TextConfig) -> Dimensions;
31
32/// Reserved type alias for a synchronous loop callback (currently unused).
33pub type LoopRoutine = Box<dyn FnMut() + Send>;
34
35/// Async HTML/CSS renderer for Cotis applications running in the browser.
36///
37/// Implements [`CotisRendererAsync`] and Cotis context traits (see [`crate::cotis_traits`]).
38/// Holds the page origin (`window.location.origin`) for resolving relative asset URLs.
39///
40/// # WASM
41///
42/// Requires a browser environment with a global `window` object. Construct after
43/// `init_wasm()` and before starting the async frame loop.
44///
45/// # Example
46///
47/// ```rust,ignore
48/// use cotis_web::renderer::HTMLRenderer;
49///
50/// let mut renderer = HTMLRenderer::new();
51/// // Wire into CotisApp, then compute_frame_async in a loop.
52/// ```
53pub struct HTMLRenderer {
54 _url: String,
55}
56
57impl<HTMLDrawableType: HTMLDrawable> CotisRendererAsync<HTMLDrawableType> for HTMLRenderer {
58 /// Draw one frame: wait for the next animation frame, then update the DOM.
59 ///
60 /// Calls the JS `waitForNextFrame` helper, then runs each command through
61 /// [`HTMLCanvas`](crate::rendering::HTMLCanvas) and
62 /// [`draw_html_render_commands`].
63 /// Unused `cotis-{id}` elements from previous frames are removed in
64 /// [`HTMLCanvas::finish`](crate::rendering::HTMLCanvas::finish).
65 ///
66 /// # WASM
67 ///
68 /// Requires an initialized DOM root (see bundled `renderer.js` / `init_html_root`).
69 async fn draw_frame(&mut self, render_commands: impl Iterator<Item = HTMLDrawableType>) {
70 wait_for_next_frame().await;
71 draw_html_render_commands(render_commands);
72 }
73}
74
75impl Default for HTMLRenderer {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl HTMLRenderer {
82 /// Creates a renderer bound to the current page origin.
83 ///
84 /// Reads `window.location.origin` (e.g. `"https://example.com:8080"`) for asset URL
85 /// resolution.
86 ///
87 /// # Panics
88 ///
89 /// Panics if there is no global `window` or if `location.origin()` is unavailable.
90 pub fn new() -> Self {
91 use web_sys::window;
92 let window = window().expect("no global `window` exists");
93 let location = window.location();
94
95 let origin = location.origin().expect("could not get origin");
96 Self { _url: origin }
97 }
98
99 /// Registers a font URL with the browser and assigns it an id in `manager`.
100 ///
101 /// Pushes `url` into `manager`, then calls the JS `loadFont` helper so CSS
102 /// `font-family: font{id}` (used by text drawables) resolves correctly.
103 ///
104 /// # WASM
105 ///
106 /// Font loading is asynchronous in the browser; allow a frame or two before relying on
107 /// measured text dimensions.
108 pub fn load_font(&mut self, manager: &FontManager<String>, url: &str) {
109 let id = manager.push_font(url.to_string());
110 load_font(url, id);
111 }
112
113 /// Returns the `innerHTML` of the custom host element for the given render command id.
114 ///
115 /// The element id in the DOM is `cotis-{element_id}`. Returns `None` if no such element
116 /// exists in the current document.
117 ///
118 /// # WASM
119 ///
120 /// Queries the live DOM; call after the frame that created or updated the element.
121 pub fn get_custom_element_html(&self, element_id: u64) -> Option<String> {
122 crate::web_functions::primitives::get_custom_element_html(element_id)
123 }
124
125 /// Returns JSON describing properties of a custom host element or a descendant.
126 ///
127 /// Use `selector` to target a child, e.g. `Some(r#"input[name="username"]"#)` to read an
128 /// input's `value`. With `None`, the JS helper auto-picks the first form-like element or
129 /// the host itself.
130 ///
131 /// Parse the returned string with [`serde_json`] to access fields like `value`,
132 /// `placeholder`, `checked`, etc.
133 ///
134 /// # WASM
135 ///
136 /// Queries the live DOM; call after the frame that created or updated the element.
137 ///
138 /// # Example
139 ///
140 /// ```rust,ignore
141 /// # use cotis_web::renderer::HTMLRenderer;
142 /// let renderer = HTMLRenderer::new();
143 /// if let Some(json) = renderer.get_custom_element_properties(42, Some("input")) {
144 /// let props: serde_json::Value = serde_json::from_str(&json).unwrap();
145 /// let value = props["value"].as_str();
146 /// }
147 /// ```
148 pub fn get_custom_element_properties(
149 &self,
150 element_id: u64,
151 selector: Option<&str>,
152 ) -> Option<String> {
153 crate::web_functions::primitives::get_custom_element_properties(element_id, selector)
154 }
155}