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 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 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 fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
219}
220
221pub trait ToRenderRunner {
222 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 #[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 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 async fn render(&self, view: View) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
264
265 async fn render_partial(
269 &self,
270 partial: PartialView,
271 ) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
272
273 #[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 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 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}