1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3
4use std::{
5 collections::HashMap,
6 pin::Pin,
7 str::FromStr as _,
8 sync::{atomic::AtomicI32, Arc, LazyLock, Mutex, RwLock},
9};
10
11use fltk::{
12 app::{self, App},
13 enums::{self, Event},
14 frame::{self, Frame},
15 group,
16 image::{RgbImage, SharedImage},
17 prelude::*,
18 window::{DoubleWindow, Window},
19};
20use flume::{Receiver, Sender};
21use futures::Future;
22use gigachad_transformer::{
23 calc::{calc_number, Calc as _},
24 ContainerElement, Element, ElementList, HeaderSize, LayoutDirection, LayoutOverflow,
25};
26use thiserror::Error;
27
28type RouteFunc = Arc<
29 Box<
30 dyn (Fn() -> Pin<
31 Box<dyn Future<Output = Result<ElementList, Box<dyn std::error::Error>>> + Send>,
32 >) + Send
33 + Sync,
34 >,
35>;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum RoutePath {
39 Literal(String),
40 Literals(Vec<String>),
41}
42
43impl RoutePath {
44 #[must_use]
45 pub fn matches(&self, path: &str) -> bool {
46 match self {
47 Self::Literal(route_path) => route_path == path,
48 Self::Literals(route_paths) => route_paths.iter().any(|x| x == path),
49 }
50 }
51}
52
53impl From<&str> for RoutePath {
54 fn from(value: &str) -> Self {
55 Self::Literal(value.to_owned())
56 }
57}
58
59impl From<&[&str; 1]> for RoutePath {
60 fn from(value: &[&str; 1]) -> Self {
61 Self::Literals(value.iter().map(ToString::to_string).collect())
62 }
63}
64
65impl From<&[&str; 2]> for RoutePath {
66 fn from(value: &[&str; 2]) -> Self {
67 Self::Literals(value.iter().map(ToString::to_string).collect())
68 }
69}
70
71impl From<&[&str; 3]> for RoutePath {
72 fn from(value: &[&str; 3]) -> Self {
73 Self::Literals(value.iter().map(ToString::to_string).collect())
74 }
75}
76
77impl From<&[&str; 4]> for RoutePath {
78 fn from(value: &[&str; 4]) -> Self {
79 Self::Literals(value.iter().map(ToString::to_string).collect())
80 }
81}
82
83impl From<&[&str; 5]> for RoutePath {
84 fn from(value: &[&str; 5]) -> Self {
85 Self::Literals(value.iter().map(ToString::to_string).collect())
86 }
87}
88
89impl From<&[&str; 6]> for RoutePath {
90 fn from(value: &[&str; 6]) -> Self {
91 Self::Literals(value.iter().map(ToString::to_string).collect())
92 }
93}
94
95impl From<&[&str; 7]> for RoutePath {
96 fn from(value: &[&str; 7]) -> Self {
97 Self::Literals(value.iter().map(ToString::to_string).collect())
98 }
99}
100
101impl From<&[&str; 8]> for RoutePath {
102 fn from(value: &[&str; 8]) -> Self {
103 Self::Literals(value.iter().map(ToString::to_string).collect())
104 }
105}
106
107impl From<&[&str; 9]> for RoutePath {
108 fn from(value: &[&str; 9]) -> Self {
109 Self::Literals(value.iter().map(ToString::to_string).collect())
110 }
111}
112
113impl From<&[&str; 10]> for RoutePath {
114 fn from(value: &[&str; 10]) -> Self {
115 Self::Literals(value.iter().map(ToString::to_string).collect())
116 }
117}
118
119impl From<&[&str]> for RoutePath {
120 fn from(value: &[&str]) -> Self {
121 Self::Literals(value.iter().map(ToString::to_string).collect())
122 }
123}
124
125impl From<Vec<&str>> for RoutePath {
126 fn from(value: Vec<&str>) -> Self {
127 Self::Literals(value.into_iter().map(ToString::to_string).collect())
128 }
129}
130
131impl From<String> for RoutePath {
132 fn from(value: String) -> Self {
133 Self::Literal(value)
134 }
135}
136
137impl From<&[String]> for RoutePath {
138 fn from(value: &[String]) -> Self {
139 Self::Literals(value.iter().map(ToString::to_string).collect())
140 }
141}
142
143impl From<&[&String]> for RoutePath {
144 fn from(value: &[&String]) -> Self {
145 Self::Literals(value.iter().map(ToString::to_string).collect())
146 }
147}
148
149impl From<Vec<String>> for RoutePath {
150 fn from(value: Vec<String>) -> Self {
151 Self::Literals(value)
152 }
153}
154
155#[derive(Debug, Error)]
156pub enum LoadImageError {
157 #[error(transparent)]
158 Reqwest(#[from] reqwest::Error),
159 #[error(transparent)]
160 Image(#[from] image::ImageError),
161 #[error(transparent)]
162 Fltk(#[from] FltkError),
163}
164
165#[derive(Debug, Clone)]
166pub enum AppEvent {
167 Navigate {
168 href: String,
169 },
170 LoadImage {
171 source: String,
172 width: Option<f32>,
173 height: Option<f32>,
174 frame: Frame,
175 },
176}
177
178#[derive(Clone)]
179pub struct Renderer {
180 app: Option<App>,
181 window: Option<DoubleWindow>,
182 elements: Arc<Mutex<ElementList>>,
183 root: Arc<RwLock<Option<group::Flex>>>,
184 routes: Arc<RwLock<Vec<(RoutePath, RouteFunc)>>>,
185 width: Arc<AtomicI32>,
186 height: Arc<AtomicI32>,
187 event_sender: Option<Sender<AppEvent>>,
188 event_receiver: Option<Receiver<AppEvent>>,
189}
190
191impl Default for Renderer {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197impl Renderer {
198 #[must_use]
199 pub fn new() -> Self {
200 Self {
201 app: None,
202 window: None,
203 elements: Arc::new(Mutex::new(ElementList::default())),
204 root: Arc::new(RwLock::new(None)),
205 routes: Arc::new(RwLock::new(vec![])),
206 width: Arc::new(AtomicI32::new(0)),
207 height: Arc::new(AtomicI32::new(0)),
208 event_sender: None,
209 event_receiver: None,
210 }
211 }
212
213 pub fn start(mut self, width: u16, height: u16) -> Result<Self, FltkError> {
221 let app = app::App::default();
222 self.app.replace(app);
223
224 let mut window = Window::default()
225 .with_size(i32::from(width), i32::from(height))
226 .with_label("MoosicBox");
227
228 self.window.replace(window.clone());
229 self.width
230 .store(i32::from(width), std::sync::atomic::Ordering::SeqCst);
231 self.height
232 .store(i32::from(height), std::sync::atomic::Ordering::SeqCst);
233
234 app::set_background_color(24, 26, 27);
235 app::set_foreground_color(255, 255, 255);
236
237 let (tx, rx) = flume::unbounded();
238 self.event_sender.replace(tx);
239 self.event_receiver.replace(rx);
240
241 window.handle({
242 let renderer = self.clone();
243 move |window, ev| match ev {
244 Event::Resize => {
245 if renderer.width.load(std::sync::atomic::Ordering::SeqCst) != window.width()
246 || renderer.height.load(std::sync::atomic::Ordering::SeqCst)
247 != window.height()
248 {
249 renderer
250 .width
251 .store(window.width(), std::sync::atomic::Ordering::SeqCst);
252 renderer
253 .height
254 .store(window.height(), std::sync::atomic::Ordering::SeqCst);
255 log::debug!(
256 "event resize: width={} height={}",
257 window.width(),
258 window.height()
259 );
260
261 #[allow(clippy::cast_precision_loss)]
262 {
263 renderer
264 .elements
265 .lock()
266 .unwrap()
267 .calc(window.width() as f32, window.height() as f32);
268 }
269
270 if let Err(e) = renderer.perform_render() {
271 log::error!("Failed to draw elements: {e:?}");
272 }
273 }
274 true
275 }
276 _ => false,
277 }
278 });
279
280 window.set_callback(|_| {
281 if fltk::app::event() == fltk::enums::Event::Close {
282 app::quit();
283 }
284 });
285
286 window = window.center_screen();
287 window.end();
288 window.make_resizable(true);
289 window.show();
290
291 Ok(self)
292 }
293
294 #[must_use]
298 pub fn with_route<
299 F: Future<Output = Result<ElementList, E>> + Send + 'static,
300 E: Into<Box<dyn std::error::Error>>,
301 >(
302 self,
303 route: impl Into<RoutePath>,
304 handler: impl Fn() -> F + Send + Sync + Clone + 'static,
305 ) -> Self {
306 self.routes.write().unwrap().push((
307 route.into(),
308 Arc::new(Box::new(move || {
309 Box::pin({
310 let handler = handler.clone();
311 async move { handler().await.map_err(Into::into) }
312 })
313 })),
314 ));
315 self
316 }
317
318 async fn load_image(
319 source: String,
320 width: Option<f32>,
321 height: Option<f32>,
322 mut frame: Frame,
323 ) -> Result<(), LoadImageError> {
324 type ImageCache = LazyLock<Arc<tokio::sync::RwLock<HashMap<String, SharedImage>>>>;
325 static IMAGE_CACHE: ImageCache =
326 LazyLock::new(|| Arc::new(tokio::sync::RwLock::new(HashMap::new())));
327
328 let key = format!("{source}:{width:?}:{height:?}");
329
330 let cached_image = { IMAGE_CACHE.read().await.get(&key).cloned() };
331
332 let image = if let Some(image) = cached_image {
333 image
334 } else {
335 let data = reqwest::get(source).await?.bytes().await?;
336 let image = image::load_from_memory_with_format(&data, image::ImageFormat::WebP)?;
337 let image = RgbImage::new(
338 image.as_bytes(),
339 image.width().try_into().unwrap(),
340 image.height().try_into().unwrap(),
341 enums::ColorDepth::Rgb8,
342 )?;
343 let image = SharedImage::from_image(image)?;
344 IMAGE_CACHE.write().await.insert(key, image.clone());
345
346 image
347 };
348
349 if width.is_some() || height.is_some() {
350 #[allow(clippy::cast_possible_truncation)]
351 #[allow(clippy::cast_precision_loss)]
352 let width = width.unwrap_or(image.width() as f32).round() as i32;
353 #[allow(clippy::cast_possible_truncation)]
354 #[allow(clippy::cast_precision_loss)]
355 let height = height.unwrap_or(image.height() as f32).round() as i32;
356
357 frame.set_size(width, height);
358 }
359
360 frame.set_image_scaled(Some(image));
361 frame.set_damage(true);
362 app::awake();
363
364 Ok(())
365 }
366
367 pub async fn listen(&self) {
368 let Some(rx) = self.event_receiver.clone() else {
369 moosicbox_assert::die_or_panic!("Cannot listen before app is started");
370 };
371 let mut renderer = self.clone();
372 while let Ok(event) = rx.recv_async().await {
373 log::debug!("received event {event:?}");
374 match event {
375 AppEvent::Navigate { href } => {
376 if let Err(e) = renderer.navigate(&href).await {
377 log::error!("Failed to navigate: {e:?}");
378 }
379 }
380 AppEvent::LoadImage {
381 source,
382 width,
383 height,
384 frame,
385 } => {
386 moosicbox_task::spawn("renderer: load_image", async move {
387 Self::load_image(source, width, height, frame).await
388 });
389 }
390 }
391 }
392 }
393
394 pub async fn navigate(&mut self, path: &str) -> Result<(), FltkError> {
402 let handler = {
403 self.routes
404 .read()
405 .unwrap()
406 .iter()
407 .find(|(route, _)| route.matches(path))
408 .cloned()
409 .map(|(_, handler)| handler)
410 };
411 if let Some(handler) = handler {
412 match handler().await {
413 Ok(elements) => {
414 self.render(elements)?;
415 }
416 Err(e) => {
417 log::error!("Failed to fetch route elements: {e:?}");
418 }
419 }
420 } else {
421 log::warn!("Invalid navigation path={path:?}");
422 }
423
424 Ok(())
425 }
426
427 fn perform_render(&self) -> Result<(), FltkError> {
428 let (Some(mut window), Some(tx)) = (self.window.clone(), self.event_sender.clone()) else {
429 moosicbox_assert::die_or_panic!("Cannot perform_render before app is started");
430 };
431 log::debug!("perform_render: started");
432 {
433 let mut root = self.root.write().unwrap();
434 if let Some(root) = root.take() {
435 window.remove(&root);
436 log::debug!("perform_render: removed root");
437 }
438 window.begin();
439 log::debug!("perform_render: begin");
440 let elements: &[Element] = &self.elements.lock().unwrap();
441 root.replace(draw_elements(
442 elements,
443 #[allow(clippy::cast_precision_loss)]
444 Context::new(window.width() as f32, window.height() as f32),
445 tx,
446 )?);
447 }
448 window.end();
449 window.flush();
450 app::awake();
451 log::debug!("perform_render: finished");
452 Ok(())
453 }
454
455 pub fn render(&mut self, mut elements: ElementList) -> Result<(), FltkError> {
463 log::debug!("render: {elements:?}");
464
465 #[allow(clippy::cast_precision_loss)]
466 {
467 elements.calc(
468 self.width.load(std::sync::atomic::Ordering::SeqCst) as f32,
469 self.height.load(std::sync::atomic::Ordering::SeqCst) as f32,
470 );
471
472 *self.elements.lock().unwrap() = elements;
473 }
474
475 self.perform_render()?;
476
477 Ok(())
478 }
479
480 pub fn run(self) -> Result<(), FltkError> {
484 let Some(app) = self.app else {
485 moosicbox_assert::die_or_panic!("Cannot listen before app is started");
486 };
487 app.run()
488 }
489}
490
491#[derive(Clone)]
492struct Context {
493 size: u16,
494 direction: LayoutDirection,
495 overflow: LayoutOverflow,
496 width: f32,
497 height: f32,
498}
499
500impl Context {
501 fn new(width: f32, height: f32) -> Self {
502 Self {
503 size: 12,
504 direction: LayoutDirection::default(),
505 overflow: LayoutOverflow::default(),
506 width,
507 height,
508 }
509 }
510
511 fn with_container(mut self, container: &ContainerElement) -> Self {
512 self.direction = container.direction;
513 self.overflow = container.overflow;
514 self.width = container
515 .calculated_width
516 .or_else(|| container.width.map(|x| calc_number(x, self.width)))
517 .unwrap_or(self.width);
518 self.height = container
519 .calculated_height
520 .or_else(|| container.height.map(|x| calc_number(x, self.height)))
521 .unwrap_or(self.height);
522 self
523 }
524}
525
526fn draw_elements(
527 elements: &[Element],
528 context: Context,
529 event_sender: Sender<AppEvent>,
530) -> Result<group::Flex, FltkError> {
531 log::debug!("draw_elements: elements={elements:?}");
532
533 let outer_flex = match context.overflow {
534 LayoutOverflow::Scroll | LayoutOverflow::Squash => None,
535 LayoutOverflow::Wrap => Some(match context.direction {
536 LayoutDirection::Row => group::Flex::default_fill().column(),
537 LayoutDirection::Column => group::Flex::default_fill().row(),
538 }),
539 };
540
541 let flex = group::Flex::default_fill();
542 let mut flex = match context.direction {
543 LayoutDirection::Row => flex.row(),
544 LayoutDirection::Column => flex.column(),
545 };
546
547 let Some(first) = elements.first() else {
548 flex.end();
549 if let Some(outer) = outer_flex {
550 outer.end();
551 return Ok(outer);
552 }
553 return Ok(flex);
554 };
555
556 let (mut row, mut col) = first
557 .container_element()
558 .and_then(|x| {
559 x.calculated_position.as_ref().and_then(|x| match x {
560 gigachad_transformer::LayoutPosition::Wrap { row, col } => Some((*row, *col)),
561 gigachad_transformer::LayoutPosition::Default => None,
562 })
563 })
564 .unwrap_or((0, 0));
565
566 for (i, element) in elements.iter().enumerate() {
567 let (current_row, current_col) = element
568 .container_element()
569 .and_then(|x| {
570 x.calculated_position.as_ref().and_then(|x| match x {
571 gigachad_transformer::LayoutPosition::Wrap { row, col } => Some((*row, *col)),
572 gigachad_transformer::LayoutPosition::Default => None,
573 })
574 })
575 .unwrap_or((row, col));
576
577 if context.direction == LayoutDirection::Row && row != current_row
578 || context.direction == LayoutDirection::Column && col != current_col
579 {
580 flex.end();
581
582 flex = match context.direction {
583 LayoutDirection::Row => group::Flex::default_fill().row(),
584 LayoutDirection::Column => group::Flex::default_fill().column(),
585 };
586 }
587
588 row = current_row;
589 col = current_col;
590
591 if i == elements.len() - 1 {
592 draw_element(element, context, &mut flex, event_sender)?;
593 break;
594 }
595 draw_element(element, context.clone(), &mut flex, event_sender.clone())?;
596 }
597
598 flex.end();
599 if let Some(outer) = outer_flex {
600 outer.end();
601 return Ok(outer);
602 }
603 Ok(flex)
604}
605
606#[allow(clippy::too_many_lines)]
607#[allow(clippy::cognitive_complexity)]
608fn draw_element(
609 element: &Element,
610 mut context: Context,
611 flex: &mut group::Flex,
612 event_sender: Sender<AppEvent>,
613) -> Result<Option<Box<dyn WidgetExt>>, FltkError> {
614 log::debug!(
615 "draw_element: element={element:?} flex_width={} flex_height={} bounds={:?}",
616 flex.width(),
617 flex.height(),
618 flex.bounds()
619 );
620
621 let direction = context.direction;
622 let mut width = None;
623 let mut height = None;
624 let mut flex_element = None;
625 let mut other_element: Option<Box<dyn WidgetExt>> = None;
626
627 match element {
628 Element::Raw { value } => {
629 app::set_font_size(context.size);
630 let frame = frame::Frame::default()
631 .with_label(value)
632 .with_align(enums::Align::Inside | enums::Align::Left);
633
634 other_element = Some(Box::new(frame));
635 }
636 Element::Div { element } => {
637 context = context.with_container(element);
638 width = element.calculated_width;
639 height = element.calculated_height;
640 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
641 }
642 Element::Aside { element } => {
643 context = context.with_container(element);
644 width = element.calculated_width;
645 height = element.calculated_height;
646 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
647 }
648 Element::Header { element } => {
649 context = context.with_container(element);
650 width = element.calculated_width;
651 height = element.calculated_height;
652 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
653 }
654 Element::Footer { element } => {
655 context = context.with_container(element);
656 width = element.calculated_width;
657 height = element.calculated_height;
658 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
659 }
660 Element::Main { element } => {
661 context = context.with_container(element);
662 width = element.calculated_width;
663 height = element.calculated_height;
664 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
665 }
666 Element::Section { element } => {
667 context = context.with_container(element);
668 width = element.calculated_width;
669 height = element.calculated_height;
670 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
671 }
672 Element::Form { element } => {
673 context = context.with_container(element);
674 width = element.calculated_width;
675 height = element.calculated_height;
676 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
677 }
678 Element::Span { element } => {
679 context = context.with_container(element);
680 width = element.calculated_width;
681 height = element.calculated_height;
682 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
683 }
684 Element::Input(_) => {}
685 Element::Button { element } => {
686 context = context.with_container(element);
687 width = element.calculated_width;
688 height = element.calculated_height;
689 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
690 }
691 Element::Image { source, element } => {
692 context = context.with_container(element);
693 width = element.calculated_width;
694 height = element.calculated_height;
695 let mut frame = Frame::default_fill();
696
697 if let Some(source) = source {
698 if source.starts_with("http") {
699 if let Err(e) = event_sender.send(AppEvent::LoadImage {
700 source: source.to_owned(),
701 width: element.width.map(|_| width.unwrap()),
702 height: element.height.map(|_| height.unwrap()),
703 frame: frame.clone(),
704 }) {
705 log::error!("Failed to send LoadImage event with source={source}: {e:?}");
706 }
707 } else if let Ok(manifest_path) = std::env::var("CARGO_MANIFEST_DIR") {
708 if let Ok(path) = std::path::PathBuf::from_str(&manifest_path) {
709 let source = source
710 .chars()
711 .skip_while(|x| *x == '/' || *x == '\\')
712 .collect::<String>();
713
714 if let Some(path) = path
715 .parent()
716 .and_then(|x| x.parent())
717 .map(|x| x.join("app-website").join("public").join(source))
718 {
719 if let Ok(path) = path.canonicalize() {
720 if path.is_file() {
721 let image = SharedImage::load(path)?;
722
723 if width.is_some() || height.is_some() {
724 #[allow(clippy::cast_possible_truncation)]
725 let width = calc_number(
726 element.width.unwrap_or_default(),
727 context.width,
728 )
729 .round()
730 as i32;
731 #[allow(clippy::cast_possible_truncation)]
732 let height = calc_number(
733 element.height.unwrap_or_default(),
734 context.height,
735 )
736 .round()
737 as i32;
738
739 frame.set_size(width, height);
740 }
741
742 frame.set_image_scaled(Some(image));
743 }
744 }
745 }
746 }
747 }
748 }
749
750 other_element = Some(Box::new(frame));
751 }
752 Element::Anchor { element, href } => {
753 context = context.with_container(element);
754 width = element.calculated_width;
755 height = element.calculated_height;
756 let mut elements = draw_elements(&element.elements, context, event_sender.clone())?;
757 if let Some(href) = href.to_owned() {
758 elements.handle(move |_, ev| match ev {
759 Event::Push => true,
760 Event::Released => {
761 if let Err(e) = event_sender.send(AppEvent::Navigate { href: href.clone() })
762 {
763 log::error!("Failed to navigate to href={href}: {e:?}");
764 }
765 true
766 }
767 _ => false,
768 });
769 }
770 flex_element = Some(elements);
771 }
772 Element::Heading { element, size } => {
773 context = context.with_container(element);
774 context.size = match size {
775 HeaderSize::H1 => 36,
776 HeaderSize::H2 => 30,
777 HeaderSize::H3 => 24,
778 HeaderSize::H4 => 20,
779 HeaderSize::H5 => 16,
780 HeaderSize::H6 => 12,
781 };
782 width = element.calculated_width;
783 height = element.calculated_height;
784 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
785 }
786 Element::OrderedList { element } | Element::UnorderedList { element } => {
787 context = context.with_container(element);
788 width = element.calculated_width;
789 height = element.calculated_height;
790 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
791 }
792 Element::ListItem { element } => {
793 context = context.with_container(element);
794 width = element.calculated_width;
795 height = element.calculated_height;
796 flex_element = Some(draw_elements(&element.elements, context, event_sender)?);
797 }
798 }
799
800 if let Some(flex_element) = &flex_element {
801 match direction {
802 LayoutDirection::Row => {
803 if let Some(width) = width {
804 #[allow(clippy::cast_possible_truncation)]
805 flex.fixed(flex_element, width.round() as i32);
806 log::debug!("draw_element: setting fixed width={width}");
807 } else {
808 log::debug!(
809 "draw_element: not setting fixed width size width={width:?} height={height:?}"
810 );
811 }
812 }
813 LayoutDirection::Column => {
814 if let Some(height) = height {
815 #[allow(clippy::cast_possible_truncation)]
816 flex.fixed(flex_element, height.round() as i32);
817 log::debug!("draw_element: setting fixed height={height}");
818 } else {
819 log::debug!(
820 "draw_element: not setting fixed height size width={width:?} height={height:?}"
821 );
822 }
823 }
824 }
825 }
826
827 Ok(flex_element
828 .map(|x| Box::new(x) as Box<dyn WidgetExt>)
829 .or(other_element))
830}