hyperchad_renderer/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3#![allow(clippy::multiple_crate_versions)]
4
5#[cfg(feature = "assets")]
6pub mod assets;
7#[cfg(feature = "canvas")]
8pub mod canvas;
9#[cfg(feature = "viewport")]
10pub mod viewport;
11
12use std::{future::Future, pin::Pin};
13
14use async_trait::async_trait;
15pub use hyperchad_color::Color;
16use hyperchad_transformer::{Container, ResponsiveTrigger, html::ParseError};
17pub use switchy_async::runtime::Handle;
18
19pub use hyperchad_transformer as transformer;
20
21#[derive(Debug)]
22pub enum RendererEvent {
23    View(View),
24    Partial(PartialView),
25    #[cfg(feature = "canvas")]
26    CanvasUpdate(canvas::CanvasUpdate),
27    Event {
28        name: String,
29        value: Option<String>,
30    },
31}
32
33pub enum Content {
34    View(View),
35    PartialView(PartialView),
36    #[cfg(feature = "json")]
37    Json(serde_json::Value),
38}
39
40impl Content {
41    #[must_use]
42    pub fn view(view: impl Into<View>) -> Self {
43        Self::View(view.into())
44    }
45
46    /// # Errors
47    ///
48    /// * If the `view` fails to convert to a `View`
49    pub fn try_view<T: TryInto<View>>(view: T) -> Result<Self, T::Error> {
50        Ok(Self::View(view.try_into()?))
51    }
52
53    #[must_use]
54    pub fn partial_view(target: impl Into<String>, container: impl Into<Container>) -> Self {
55        Self::PartialView(PartialView {
56            target: target.into(),
57            container: container.into(),
58        })
59    }
60
61    /// # Errors
62    ///
63    /// * If the `container` fails to convert to a `Container`
64    pub fn try_partial_view<T: TryInto<Container>>(
65        target: impl Into<String>,
66        container: T,
67    ) -> Result<Self, T::Error> {
68        Ok(Self::PartialView(PartialView {
69            target: target.into(),
70            container: container.try_into()?,
71        }))
72    }
73}
74
75#[derive(Default, Debug, Clone)]
76pub struct PartialView {
77    pub target: String,
78    pub container: Container,
79}
80
81#[derive(Default)]
82pub struct View {
83    pub future: Option<Pin<Box<dyn Future<Output = Container> + Send>>>,
84    pub immediate: Container,
85}
86
87impl std::fmt::Debug for View {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("View")
90            .field("future", &self.future.is_some())
91            .field("immediate", &self.immediate)
92            .finish()
93    }
94}
95
96#[cfg(feature = "json")]
97impl TryFrom<serde_json::Value> for Content {
98    type Error = serde_json::Error;
99
100    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
101        Ok(Self::Json(value))
102    }
103}
104
105impl<'a> TryFrom<&'a str> for Content {
106    type Error = ParseError;
107
108    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
109        Ok(Self::View(View {
110            future: None,
111            immediate: value.try_into()?,
112        }))
113    }
114}
115
116impl TryFrom<String> for Content {
117    type Error = ParseError;
118
119    fn try_from(value: String) -> Result<Self, Self::Error> {
120        Ok(Self::View(View {
121            future: None,
122            immediate: value.try_into()?,
123        }))
124    }
125}
126
127impl From<Container> for Content {
128    fn from(value: Container) -> Self {
129        Self::View(View {
130            future: None,
131            immediate: value,
132        })
133    }
134}
135
136#[allow(clippy::fallible_impl_from)]
137impl From<Vec<Container>> for Content {
138    fn from(value: Vec<Container>) -> Self {
139        if value.len() == 1 {
140            return Self::View(View {
141                future: None,
142                immediate: value.into_iter().next().unwrap(),
143            });
144        }
145
146        Container {
147            children: value,
148            ..Default::default()
149        }
150        .into()
151    }
152}
153
154impl From<View> for Content {
155    fn from(value: View) -> Self {
156        Self::View(value)
157    }
158}
159
160impl From<PartialView> for Content {
161    fn from(value: PartialView) -> Self {
162        Self::PartialView(value)
163    }
164}
165
166impl<'a> TryFrom<&'a str> for View {
167    type Error = ParseError;
168
169    fn try_from(value: &'a str) -> Result<Self, Self::Error> {
170        Ok(Self {
171            future: None,
172            immediate: value.try_into()?,
173        })
174    }
175}
176
177impl TryFrom<String> for View {
178    type Error = ParseError;
179
180    fn try_from(value: String) -> Result<Self, Self::Error> {
181        Ok(Self {
182            future: None,
183            immediate: value.try_into()?,
184        })
185    }
186}
187
188impl From<Container> for View {
189    fn from(value: Container) -> Self {
190        Self {
191            future: None,
192            immediate: value,
193        }
194    }
195}
196
197#[allow(clippy::fallible_impl_from)]
198impl From<Vec<Container>> for View {
199    fn from(value: Vec<Container>) -> Self {
200        if value.len() == 1 {
201            return Self {
202                future: None,
203                immediate: value.into_iter().next().unwrap(),
204            };
205        }
206
207        Self {
208            future: None,
209            immediate: value.into(),
210        }
211    }
212}
213
214pub trait RenderRunner: Send + Sync {
215    /// # Errors
216    ///
217    /// Will error if fails to run
218    fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
219}
220
221pub trait ToRenderRunner {
222    /// # Errors
223    ///
224    /// * If failed to convert the value to a `RenderRunner`
225    fn to_runner(
226        self,
227        handle: Handle,
228    ) -> Result<Box<dyn RenderRunner>, Box<dyn std::error::Error + Send>>;
229}
230
231#[async_trait]
232pub trait Renderer: ToRenderRunner + Send + Sync {
233    /// # Errors
234    ///
235    /// Will error if `Renderer` implementation app fails to start
236    #[allow(clippy::too_many_arguments)]
237    async fn init(
238        &mut self,
239        width: f32,
240        height: f32,
241        x: Option<i32>,
242        y: Option<i32>,
243        background: Option<Color>,
244        title: Option<&str>,
245        description: Option<&str>,
246        viewport: Option<&str>,
247    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
248
249    fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger);
250
251    /// # Errors
252    ///
253    /// Will error if `Renderer` implementation fails to emit the event.
254    async fn emit_event(
255        &self,
256        event_name: String,
257        event_value: Option<String>,
258    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
259
260    /// # Errors
261    ///
262    /// Will error if `Renderer` implementation fails to render the view.
263    async fn render(&self, view: View) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
264
265    /// # Errors
266    ///
267    /// Will error if `Renderer` implementation fails to render the partial elements.
268    async fn render_partial(
269        &self,
270        partial: PartialView,
271    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
272
273    /// # Errors
274    ///
275    /// Will error if `Renderer` implementation fails to render the canvas update.
276    #[cfg(feature = "canvas")]
277    async fn render_canvas(
278        &self,
279        update: canvas::CanvasUpdate,
280    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
281        unimplemented!("Unable to render canvas update={update:?}")
282    }
283}
284
285#[cfg(feature = "html")]
286pub trait HtmlTagRenderer {
287    fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger);
288
289    /// # Errors
290    ///
291    /// * If the `HtmlTagRenderer` fails to write the element attributes
292    fn element_attrs_to_html(
293        &self,
294        f: &mut dyn std::io::Write,
295        container: &Container,
296        is_flex_child: bool,
297    ) -> Result<(), std::io::Error>;
298
299    /// # Errors
300    ///
301    /// * If the `HtmlTagRenderer` fails to write the css media-queries
302    fn reactive_conditions_to_css(
303        &self,
304        _f: &mut dyn std::io::Write,
305        _container: &Container,
306    ) -> Result<(), std::io::Error> {
307        Ok(())
308    }
309
310    fn partial_html(
311        &self,
312        headers: &std::collections::HashMap<String, String>,
313        container: &Container,
314        content: String,
315        viewport: Option<&str>,
316        background: Option<Color>,
317    ) -> String;
318
319    #[allow(clippy::too_many_arguments)]
320    fn root_html(
321        &self,
322        headers: &std::collections::HashMap<String, String>,
323        container: &Container,
324        content: String,
325        viewport: Option<&str>,
326        background: Option<Color>,
327        title: Option<&str>,
328        description: Option<&str>,
329    ) -> String;
330}