hyperchad_renderer_fltk/
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
5use std::{
6    borrow::Cow,
7    collections::HashMap,
8    fmt::Write,
9    ops::Deref,
10    str::FromStr as _,
11    sync::{
12        Arc, LazyLock, Mutex, RwLock,
13        atomic::{AtomicBool, AtomicI32},
14    },
15};
16
17use async_trait::async_trait;
18use bytes::Bytes;
19use canvas::CanvasUpdate;
20use fltk::{
21    app::{self, App},
22    enums::{self, Event},
23    frame::{self, Frame},
24    group,
25    image::{RgbImage, SharedImage},
26    prelude::*,
27    widget,
28    window::{DoubleWindow, Window},
29};
30use flume::{Receiver, Sender};
31use hyperchad_actions::logic::Value;
32use hyperchad_renderer::viewport::retained::{
33    Viewport, ViewportListener, ViewportPosition, WidgetPosition,
34};
35use hyperchad_transformer::{
36    Container, Element, HeaderSize, ResponsiveTrigger,
37    layout::{
38        Calc as _,
39        calc::{Calculator, CalculatorDefaults},
40    },
41    models::{LayoutDirection, LayoutOverflow, LayoutPosition},
42};
43use thiserror::Error;
44use tokio::task::JoinHandle;
45
46pub use hyperchad_renderer::*;
47
48mod font_metrics;
49
50static CLIENT: LazyLock<gimbal_http::Client> = LazyLock::new(gimbal_http::Client::new);
51
52#[cfg(feature = "debug")]
53static DEBUG: LazyLock<RwLock<bool>> = LazyLock::new(|| {
54    RwLock::new(
55        std::env::var("DEBUG_RENDERER")
56            .is_ok_and(|x| ["1", "true"].contains(&x.to_lowercase().as_str())),
57    )
58});
59
60const DELTA: f32 = 14.0f32 / 16.0;
61static FLTK_CALCULATOR: Calculator<font_metrics::FltkFontMetrics> = Calculator::new(
62    font_metrics::FltkFontMetrics,
63    CalculatorDefaults {
64        font_size: 16.0 * DELTA,
65        font_margin_top: 0.0 * DELTA,
66        font_margin_bottom: 0.0 * DELTA,
67        h1_font_size: 32.0 * DELTA,
68        h1_font_margin_top: 21.44 * DELTA,
69        h1_font_margin_bottom: 21.44 * DELTA,
70        h2_font_size: 24.0 * DELTA,
71        h2_font_margin_top: 19.92 * DELTA,
72        h2_font_margin_bottom: 19.92 * DELTA,
73        h3_font_size: 18.72 * DELTA,
74        h3_font_margin_top: 18.72 * DELTA,
75        h3_font_margin_bottom: 18.72 * DELTA,
76        h4_font_size: 16.0 * DELTA,
77        h4_font_margin_top: 21.28 * DELTA,
78        h4_font_margin_bottom: 21.28 * DELTA,
79        h5_font_size: 13.28 * DELTA,
80        h5_font_margin_top: 22.1776 * DELTA,
81        h5_font_margin_bottom: 22.1776 * DELTA,
82        h6_font_size: 10.72 * DELTA,
83        h6_font_margin_top: 24.9776 * DELTA,
84        h6_font_margin_bottom: 24.9776 * DELTA,
85    },
86);
87
88#[derive(Debug, Error)]
89pub enum LoadImageError {
90    #[error(transparent)]
91    Reqwest(#[from] gimbal_http::Error),
92    #[error(transparent)]
93    Image(#[from] image::ImageError),
94    #[error(transparent)]
95    Fltk(#[from] FltkError),
96}
97
98#[derive(Debug, Clone)]
99pub enum ImageSource {
100    Bytes { bytes: Arc<Bytes>, source: String },
101    Url(String),
102}
103
104#[derive(Debug, Clone)]
105pub enum AppEvent {
106    Resize {},
107    MouseWheel {},
108    Navigate {
109        href: String,
110    },
111    RegisterImage {
112        viewport: Option<Viewport>,
113        source: ImageSource,
114        width: Option<f32>,
115        height: Option<f32>,
116        frame: Frame,
117    },
118    LoadImage {
119        source: ImageSource,
120        width: Option<f32>,
121        height: Option<f32>,
122        frame: Frame,
123    },
124    UnloadImage {
125        frame: Frame,
126    },
127}
128
129#[derive(Debug, Clone)]
130pub struct RegisteredImage {
131    source: ImageSource,
132    width: Option<f32>,
133    height: Option<f32>,
134    frame: Frame,
135}
136
137type JoinHandleAndCancelled = (JoinHandle<()>, Arc<AtomicBool>);
138
139#[derive(Clone)]
140pub struct FltkRenderer {
141    app: Option<App>,
142    window: Option<DoubleWindow>,
143    elements: Arc<RwLock<Container>>,
144    root: Arc<RwLock<Option<group::Flex>>>,
145    images: Arc<RwLock<Vec<RegisteredImage>>>,
146    viewport_listeners: Arc<RwLock<Vec<ViewportListener>>>,
147    width: Arc<AtomicI32>,
148    height: Arc<AtomicI32>,
149    event_sender: Option<Sender<AppEvent>>,
150    event_receiver: Option<Receiver<AppEvent>>,
151    viewport_listener_join_handle: Arc<Mutex<Option<JoinHandleAndCancelled>>>,
152    sender: Sender<String>,
153    receiver: Receiver<String>,
154    #[allow(unused)]
155    request_action: Sender<(String, Option<Value>)>,
156}
157
158impl FltkRenderer {
159    #[must_use]
160    pub fn new(request_action: Sender<(String, Option<Value>)>) -> Self {
161        let (tx, rx) = flume::unbounded();
162        Self {
163            app: None,
164            window: None,
165            elements: Arc::new(RwLock::new(Container::default())),
166            root: Arc::new(RwLock::new(None)),
167            images: Arc::new(RwLock::new(vec![])),
168            viewport_listeners: Arc::new(RwLock::new(vec![])),
169            width: Arc::new(AtomicI32::new(0)),
170            height: Arc::new(AtomicI32::new(0)),
171            event_sender: None,
172            event_receiver: None,
173            viewport_listener_join_handle: Arc::new(Mutex::new(None)),
174            sender: tx,
175            receiver: rx,
176            request_action,
177        }
178    }
179
180    fn handle_resize(&self, window: &Window) {
181        let width = self.width.load(std::sync::atomic::Ordering::SeqCst);
182        let height = self.height.load(std::sync::atomic::Ordering::SeqCst);
183
184        if width != window.width() || height != window.height() {
185            self.width
186                .store(window.width(), std::sync::atomic::Ordering::SeqCst);
187            self.height
188                .store(window.height(), std::sync::atomic::Ordering::SeqCst);
189            log::debug!(
190                "event resize: width={width}->{} height={height}->{}",
191                window.width(),
192                window.height()
193            );
194
195            if let Err(e) = self.perform_render() {
196                log::error!("Failed to draw elements: {e:?}");
197            }
198        }
199    }
200
201    fn check_viewports(&self, cancelled: &AtomicBool) {
202        for listener in self.viewport_listeners.write().unwrap().iter_mut() {
203            if cancelled.load(std::sync::atomic::Ordering::SeqCst) {
204                break;
205            }
206            listener.check();
207        }
208    }
209
210    fn trigger_load_image(&self, frame: &Frame) -> Result<(), flume::SendError<AppEvent>> {
211        let image = {
212            self.images
213                .write()
214                .unwrap()
215                .iter()
216                .find(|x| x.frame.is_same(frame))
217                .cloned()
218        };
219        log::debug!("trigger_load_image: image={image:?}");
220
221        if let Some(image) = image {
222            if let Some(sender) = &self.event_sender {
223                sender.send(AppEvent::LoadImage {
224                    source: image.source,
225                    width: image.width,
226                    height: image.height,
227                    frame: frame.to_owned(),
228                })?;
229            }
230        }
231
232        Ok(())
233    }
234
235    fn register_image(
236        &self,
237        viewport: Option<Viewport>,
238        source: ImageSource,
239        width: Option<f32>,
240        height: Option<f32>,
241        frame: &Frame,
242    ) {
243        self.images.write().unwrap().push(RegisteredImage {
244            source,
245            width,
246            height,
247            frame: frame.clone(),
248        });
249
250        let mut frame = frame.clone();
251        let renderer = self.clone();
252        self.viewport_listeners
253            .write()
254            .unwrap()
255            .push(ViewportListener::new(
256                WidgetWrapper(frame.as_base_widget()),
257                viewport,
258                move |_visible, dist| {
259                    if dist < 200 {
260                        if let Err(e) = renderer.trigger_load_image(&frame) {
261                            log::error!("Failed to trigger_load_image: {e:?}");
262                        }
263                    } else {
264                        Self::set_frame_image(&mut frame, None);
265                    }
266                },
267            ));
268    }
269
270    fn set_frame_image(frame: &mut Frame, image: Option<SharedImage>) {
271        frame.set_image_scaled(image);
272        frame.set_damage(true);
273        app::awake();
274    }
275
276    async fn load_image(
277        source: ImageSource,
278        width: Option<f32>,
279        height: Option<f32>,
280        mut frame: Frame,
281    ) -> Result<(), LoadImageError> {
282        type ImageCache = LazyLock<
283            Arc<tokio::sync::RwLock<HashMap<String, (Arc<Bytes>, u32, u32, enums::ColorDepth)>>>,
284        >;
285        static IMAGE_CACHE: ImageCache =
286            LazyLock::new(|| Arc::new(tokio::sync::RwLock::new(HashMap::new())));
287
288        let uri = match &source {
289            ImageSource::Bytes { source, .. } | ImageSource::Url(source) => source,
290        };
291
292        let key = format!("{uri}:{width:?}:{height:?}");
293
294        let cached_image = { IMAGE_CACHE.read().await.get(&key).cloned() };
295
296        let rgb_image = {
297            let (bytes, width, height, depth) =
298                if let Some((bytes, width, height, depth)) = cached_image {
299                    (bytes, width, height, depth)
300                } else {
301                    let image = match source {
302                        ImageSource::Bytes { bytes, .. } => image::load_from_memory(&bytes)?,
303                        ImageSource::Url(source) => image::load_from_memory(
304                            &CLIENT.get(&source).send().await?.bytes().await?,
305                        )?,
306                    };
307                    let width = image.width();
308                    let height = image.height();
309                    let depth = match image.color() {
310                        image::ColorType::Rgba8
311                        | image::ColorType::Rgba16
312                        | image::ColorType::Rgba32F => enums::ColorDepth::Rgba8,
313                        _ => enums::ColorDepth::Rgb8,
314                    };
315                    let bytes = Arc::new(Bytes::from(image.into_bytes()));
316                    IMAGE_CACHE
317                        .write()
318                        .await
319                        .insert(key, (bytes.clone(), width, height, depth));
320                    (bytes, width, height, depth)
321                };
322
323            RgbImage::new(
324                &bytes,
325                width.try_into().unwrap(),
326                height.try_into().unwrap(),
327                depth,
328            )?
329        };
330
331        let image = SharedImage::from_image(&rgb_image)?;
332
333        if width.is_some() || height.is_some() {
334            #[allow(clippy::cast_possible_truncation)]
335            #[allow(clippy::cast_precision_loss)]
336            let width = width.unwrap_or_else(|| image.width() as f32).round() as i32;
337            #[allow(clippy::cast_possible_truncation)]
338            #[allow(clippy::cast_precision_loss)]
339            let height = height.unwrap_or_else(|| image.height() as f32).round() as i32;
340
341            frame.set_size(width, height);
342        }
343
344        Self::set_frame_image(&mut frame, Some(image));
345
346        Ok(())
347    }
348
349    fn perform_render(&self) -> Result<(), FltkError> {
350        let (Some(mut window), Some(tx)) = (self.window.clone(), self.event_sender.clone()) else {
351            moosicbox_assert::die_or_panic!(
352                "perform_render: cannot perform_render before app is started"
353            );
354        };
355        log::debug!("perform_render: started");
356        {
357            let mut root = self.root.write().unwrap();
358            if let Some(root) = root.take() {
359                window.remove(&root);
360                log::debug!("perform_render: removed root");
361            }
362            window.begin();
363            log::debug!("perform_render: begin");
364            let container: &mut Container = &mut self.elements.write().unwrap();
365
366            #[allow(clippy::cast_precision_loss)]
367            let window_width = self.width.load(std::sync::atomic::Ordering::SeqCst) as f32;
368            #[allow(clippy::cast_precision_loss)]
369            let window_height = self.height.load(std::sync::atomic::Ordering::SeqCst) as f32;
370
371            let recalc = if let (Some(width), Some(height)) =
372                (container.calculated_width, container.calculated_height)
373            {
374                let diff_width = (width - window_width).abs();
375                let diff_height = (height - window_height).abs();
376                log::trace!("perform_render: diff_width={diff_width} diff_height={diff_height}");
377                diff_width > 0.01 || diff_height > 0.01
378            } else {
379                true
380            };
381
382            if recalc {
383                container.calculated_width.replace(window_width);
384                container.calculated_height.replace(window_height);
385
386                FLTK_CALCULATOR.calc(container);
387            } else {
388                log::debug!("perform_render: Container had same size, not recalculating");
389            }
390
391            log::trace!(
392                "perform_render: initialized Container for rendering {container:?} window_width={window_width} window_height={window_height}"
393            );
394
395            {
396                log::debug!("perform_render: aborting any existing viewport_listener_join_handle");
397                let handle = self.viewport_listener_join_handle.lock().unwrap().take();
398                if let Some((handle, cancel)) = handle {
399                    cancel.store(true, std::sync::atomic::Ordering::SeqCst);
400                    handle.abort();
401                }
402                log::debug!("perform_render: clearing images");
403                self.images.write().unwrap().clear();
404                log::debug!("perform_render: clearing viewport_listeners");
405                self.viewport_listeners.write().unwrap().clear();
406            }
407
408            root.replace(self.draw_elements(
409                Cow::Owned(None),
410                container,
411                0,
412                #[allow(clippy::cast_precision_loss)]
413                Context::new(
414                    window_width,
415                    window_height,
416                    self.width.load(std::sync::atomic::Ordering::SeqCst) as f32,
417                    self.height.load(std::sync::atomic::Ordering::SeqCst) as f32,
418                ),
419                tx,
420            )?);
421        }
422        window.end();
423        window.flush();
424        app::awake();
425        log::debug!("perform_render: finished");
426        Ok(())
427    }
428
429    #[allow(clippy::too_many_lines)]
430    #[allow(clippy::cognitive_complexity)]
431    fn draw_elements(
432        &self,
433        mut viewport: Cow<'_, Option<Viewport>>,
434        element: &Container,
435        depth: usize,
436        context: Context,
437        event_sender: Sender<AppEvent>,
438    ) -> Result<group::Flex, FltkError> {
439        static SCROLL_LINESIZE: i32 = 40;
440        static SCROLLBAR_SIZE: i32 = 16;
441
442        log::debug!("draw_elements: element={element:?} depth={depth} viewport={viewport:?}");
443
444        let (Some(calculated_width), Some(calculated_height)) =
445            (element.calculated_width, element.calculated_height)
446        else {
447            moosicbox_assert::die_or_panic!(
448                "draw_elements: missing calculated_width and/or calculated_height value"
449            );
450        };
451
452        moosicbox_assert::assert!(
453            calculated_width > 0.0 && calculated_height > 0.0
454                || calculated_width <= 0.0 && calculated_height <= 0.0,
455            "Invalid calculated_width/calculated_height: calculated_width={calculated_width} calculated_height={calculated_height}"
456        );
457
458        log::debug!(
459            "draw_elements: calculated_width={calculated_width} calculated_height={calculated_height}"
460        );
461        let direction = context.direction;
462
463        #[allow(clippy::cast_possible_truncation)]
464        let mut container = group::Flex::default_fill().with_size(
465            calculated_width.round() as i32,
466            calculated_height.round() as i32,
467        );
468        container.set_clip_children(false);
469        container.set_pad(0);
470
471        #[allow(clippy::cast_possible_truncation)]
472        let container_scroll_y: Option<Box<dyn Group>> = match context.overflow_y {
473            LayoutOverflow::Auto => Some({
474                let mut scroll = group::Scroll::default_fill()
475                    .with_size(
476                        calculated_width.round() as i32,
477                        calculated_height.round() as i32,
478                    )
479                    .with_type(group::ScrollType::Vertical);
480                scroll.set_scrollbar_size(SCROLLBAR_SIZE);
481                scroll.scrollbar().set_linesize(SCROLL_LINESIZE);
482                let parent = viewport.deref().clone();
483                viewport
484                    .to_mut()
485                    .replace(Viewport::new(parent, ScrollWrapper(scroll.clone())));
486                scroll.into()
487            }),
488            LayoutOverflow::Scroll => Some({
489                let mut scroll = group::Scroll::default_fill()
490                    .with_size(
491                        calculated_width.round() as i32,
492                        calculated_height.round() as i32,
493                    )
494                    .with_type(group::ScrollType::VerticalAlways);
495                scroll.set_scrollbar_size(SCROLLBAR_SIZE);
496                scroll.scrollbar().set_linesize(SCROLL_LINESIZE);
497                let parent = viewport.deref().clone();
498                viewport
499                    .to_mut()
500                    .replace(Viewport::new(parent, ScrollWrapper(scroll.clone())));
501                scroll.into()
502            }),
503            LayoutOverflow::Squash
504            | LayoutOverflow::Expand
505            | LayoutOverflow::Wrap { .. }
506            | LayoutOverflow::Hidden => None,
507        };
508        #[allow(clippy::cast_possible_truncation)]
509        let container_scroll_x: Option<Box<dyn Group>> = match context.overflow_x {
510            LayoutOverflow::Auto => Some({
511                let mut scroll = group::Scroll::default_fill()
512                    .with_size(
513                        calculated_width.round() as i32,
514                        calculated_height.round() as i32,
515                    )
516                    .with_type(group::ScrollType::Horizontal);
517                scroll.set_scrollbar_size(SCROLLBAR_SIZE);
518                scroll.hscrollbar().set_linesize(SCROLL_LINESIZE);
519                let parent = viewport.deref().clone();
520                viewport
521                    .to_mut()
522                    .replace(Viewport::new(parent, ScrollWrapper(scroll.clone())));
523                scroll.into()
524            }),
525            LayoutOverflow::Scroll => Some({
526                let mut scroll = group::Scroll::default_fill()
527                    .with_size(
528                        calculated_width.round() as i32,
529                        calculated_height.round() as i32,
530                    )
531                    .with_type(group::ScrollType::HorizontalAlways);
532                scroll.set_scrollbar_size(SCROLLBAR_SIZE);
533                scroll.hscrollbar().set_linesize(SCROLL_LINESIZE);
534                let parent = viewport.deref().clone();
535                viewport
536                    .to_mut()
537                    .replace(Viewport::new(parent, ScrollWrapper(scroll.clone())));
538                scroll.into()
539            }),
540            LayoutOverflow::Squash
541            | LayoutOverflow::Expand
542            | LayoutOverflow::Wrap { .. }
543            | LayoutOverflow::Hidden => None,
544        };
545
546        #[allow(clippy::cast_possible_truncation)]
547        let container_wrap_y: Option<Box<dyn Group>> =
548            if matches!(context.overflow_y, LayoutOverflow::Wrap { .. }) {
549                Some({
550                    let mut flex = match context.direction {
551                        LayoutDirection::Row => group::Flex::default_fill().column(),
552                        LayoutDirection::Column => group::Flex::default_fill().row(),
553                    }
554                    .with_size(
555                        calculated_width.round() as i32,
556                        calculated_height.round() as i32,
557                    );
558                    flex.set_pad(0);
559                    flex.set_clip_children(false);
560                    flex.into()
561                })
562            } else {
563                None
564            };
565        #[allow(clippy::cast_possible_truncation)]
566        let container_wrap_x: Option<Box<dyn Group>> =
567            if matches!(context.overflow_x, LayoutOverflow::Wrap { .. }) {
568                Some({
569                    let mut flex = match context.direction {
570                        LayoutDirection::Row => group::Flex::default_fill().column(),
571                        LayoutDirection::Column => group::Flex::default_fill().row(),
572                    }
573                    .with_size(
574                        calculated_width.round() as i32,
575                        calculated_height.round() as i32,
576                    );
577                    flex.set_pad(0);
578                    flex.set_clip_children(false);
579                    flex.into()
580                })
581            } else {
582                None
583            };
584
585        let contained_width = element.calculated_width.unwrap();
586        let contained_height = element.calculated_height.unwrap();
587
588        moosicbox_assert::assert!(
589            contained_width > 0.0 && contained_height > 0.0
590                || contained_width <= 0.0 && contained_height <= 0.0,
591            "Invalid contained_width/contained_height: contained_width={contained_width} contained_height={contained_height}"
592        );
593
594        log::debug!(
595            "draw_elements: contained_width={contained_width} contained_height={contained_height}"
596        );
597        #[allow(clippy::cast_possible_truncation)]
598        let contained_width = contained_width.round() as i32;
599        #[allow(clippy::cast_possible_truncation)]
600        let contained_height = contained_height.round() as i32;
601        log::debug!(
602            "draw_elements: rounded contained_width={contained_width} contained_height={contained_height}"
603        );
604
605        let inner_container = if contained_width > 0 && contained_height > 0 {
606            Some(
607                group::Flex::default()
608                    .with_size(contained_width, contained_height)
609                    .column(),
610            )
611        } else {
612            None
613        };
614        let flex = group::Flex::default_fill();
615        let mut flex = match context.direction {
616            LayoutDirection::Row => flex.row(),
617            LayoutDirection::Column => flex.column(),
618        };
619
620        flex.set_clip_children(false);
621        flex.set_pad(0);
622
623        #[cfg(feature = "debug")]
624        {
625            if *DEBUG.read().unwrap() {
626                flex.draw(|w| {
627                    fltk::draw::set_draw_color(enums::Color::White);
628                    fltk::draw::draw_rect(w.x(), w.y(), w.w(), w.h());
629                });
630            }
631        }
632
633        let (mut row, mut col) = element
634            .calculated_position
635            .as_ref()
636            .and_then(|x| match x {
637                LayoutPosition::Wrap { row, col } => Some((*row, *col)),
638                LayoutPosition::Default => None,
639            })
640            .unwrap_or((0, 0));
641
642        let len = element.children.len();
643        for (i, element) in element.children.iter().enumerate() {
644            let (current_row, current_col) = element
645                .calculated_position
646                .as_ref()
647                .and_then(|x| match x {
648                    LayoutPosition::Wrap { row, col } => {
649                        log::debug!("draw_elements: drawing row={row} col={col}");
650                        Some((*row, *col))
651                    }
652                    LayoutPosition::Default => None,
653                })
654                .unwrap_or((row, col));
655
656            if context.direction == LayoutDirection::Row && row != current_row
657                || context.direction == LayoutDirection::Column && col != current_col
658            {
659                log::debug!(
660                    "draw_elements: finished row/col current_row={current_row} current_col={current_col} flex_width={} flex_height={}",
661                    flex.w(),
662                    flex.h()
663                );
664                flex.end();
665
666                #[allow(clippy::cast_possible_truncation)]
667                {
668                    flex = match context.direction {
669                        LayoutDirection::Row => group::Flex::default_fill().row(),
670                        LayoutDirection::Column => group::Flex::default_fill().column(),
671                    };
672                    flex.set_clip_children(false);
673                    flex.set_pad(0);
674                }
675
676                #[cfg(feature = "debug")]
677                {
678                    if *DEBUG.read().unwrap() {
679                        flex.draw(|w| {
680                            fltk::draw::set_draw_color(enums::Color::White);
681                            fltk::draw::draw_rect(w.x(), w.y(), w.w(), w.h());
682                        });
683                    }
684                }
685            }
686
687            row = current_row;
688            col = current_col;
689
690            if i == len - 1 {
691                if let Some(widget) = self.draw_element(
692                    Cow::Borrowed(&viewport),
693                    element,
694                    i,
695                    depth + 1,
696                    context,
697                    event_sender,
698                )? {
699                    fixed_size(
700                        direction,
701                        element.calculated_width,
702                        element.calculated_height,
703                        &mut flex,
704                        &widget,
705                    );
706                }
707                break;
708            }
709            if let Some(widget) = self.draw_element(
710                Cow::Borrowed(&viewport),
711                element,
712                i,
713                depth + 1,
714                context.clone(),
715                event_sender.clone(),
716            )? {
717                fixed_size(
718                    direction,
719                    element.calculated_width,
720                    element.calculated_height,
721                    &mut flex,
722                    &widget,
723                );
724            }
725        }
726
727        log::debug!(
728            "draw_elements: finished draw: container_wrap_x={:?} container_wrap_y={:?} container_scroll_x={:?} container_scroll_y={:?} container=({}, {})",
729            container_wrap_x
730                .as_ref()
731                .map(|x| format!("({}, {})", x.wid(), x.hei())),
732            container_wrap_y
733                .as_ref()
734                .map(|x| format!("({}, {})", x.wid(), x.hei())),
735            container_scroll_x
736                .as_ref()
737                .map(|x| format!("({}, {})", x.wid(), x.hei())),
738            container_scroll_y
739                .as_ref()
740                .map(|x| format!("({}, {})", x.wid(), x.hei())),
741            container.w(),
742            container.h(),
743        );
744        flex.end();
745
746        if let Some(container) = inner_container {
747            container.end();
748        }
749
750        if let Some(mut container) = container_wrap_x {
751            log::debug!(
752                "draw_elements: ending container_wrap_x {} ({}, {})",
753                container.type_str(),
754                container.wid(),
755                container.hei(),
756            );
757            container.end();
758        }
759        if let Some(mut container) = container_wrap_y {
760            log::debug!(
761                "draw_elements: ending container_wrap_y {} ({}, {})",
762                container.type_str(),
763                container.wid(),
764                container.hei(),
765            );
766            container.end();
767        }
768        if let Some(mut container) = container_scroll_x {
769            log::debug!(
770                "draw_elements: ending container_scroll_x {} ({}, {})",
771                container.type_str(),
772                container.wid(),
773                container.hei(),
774            );
775            container.end();
776        }
777        if let Some(mut container) = container_scroll_y {
778            log::debug!(
779                "draw_elements: ending container_scroll_y {} ({}, {})",
780                container.type_str(),
781                container.wid(),
782                container.hei(),
783            );
784            container.end();
785        }
786        log::debug!(
787            "draw_elements: ending container {} ({}, {})",
788            container.type_str(),
789            container.wid(),
790            container.hei(),
791        );
792        container.end();
793
794        if log::log_enabled!(log::Level::Trace) {
795            let mut hierarchy = String::new();
796
797            let mut current = Some(flex.as_base_widget());
798            while let Some(widget) = current.take() {
799                write!(
800                    hierarchy,
801                    "\n\t({}, {}, {}, {})",
802                    widget.x(),
803                    widget.y(),
804                    widget.w(),
805                    widget.h()
806                )
807                .unwrap();
808                current = widget.parent().map(|x| x.as_base_widget());
809            }
810
811            log::trace!("draw_elements: hierarchy:{hierarchy}");
812        }
813
814        Ok(container)
815    }
816
817    #[allow(clippy::too_many_lines)]
818    #[allow(clippy::cognitive_complexity)]
819    fn draw_element(
820        &self,
821        viewport: Cow<'_, Option<Viewport>>,
822        container: &Container,
823        index: usize,
824        depth: usize,
825        mut context: Context,
826        event_sender: Sender<AppEvent>,
827    ) -> Result<Option<widget::Widget>, FltkError> {
828        log::debug!("draw_element: container={container:?} index={index} depth={depth}");
829
830        let mut flex_element = None;
831        let mut other_element: Option<widget::Widget> = None;
832
833        match &container.element {
834            Element::Raw { value } => {
835                app::set_font_size(context.size);
836                #[allow(unused_mut)]
837                let mut frame = frame::Frame::default()
838                    .with_label(value)
839                    .with_align(enums::Align::Inside | enums::Align::Left);
840
841                #[cfg(feature = "debug")]
842                {
843                    if *DEBUG.read().unwrap() {
844                        frame.draw(|w| {
845                            fltk::draw::set_draw_color(enums::Color::White);
846                            fltk::draw::draw_rect(w.x(), w.y(), w.w(), w.h());
847                        });
848                    }
849                }
850
851                other_element = Some(frame.as_base_widget());
852            }
853            Element::Div => {
854                context = context.with_container(container);
855                flex_element =
856                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
857            }
858            Element::Aside => {
859                context = context.with_container(container);
860                flex_element =
861                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
862            }
863            Element::Header => {
864                context = context.with_container(container);
865                flex_element =
866                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
867            }
868            Element::Footer => {
869                context = context.with_container(container);
870                flex_element =
871                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
872            }
873            Element::Main => {
874                context = context.with_container(container);
875                flex_element =
876                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
877            }
878            Element::Section => {
879                context = context.with_container(container);
880                flex_element =
881                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
882            }
883            Element::Form => {
884                context = context.with_container(container);
885                flex_element =
886                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
887            }
888            Element::Span => {
889                context = context.with_container(container);
890                flex_element =
891                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
892            }
893            Element::Table => {
894                context = context.with_container(container);
895                flex_element =
896                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
897            }
898            Element::THead => {
899                context = context.with_container(container);
900                flex_element =
901                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
902            }
903            Element::TH => {
904                context = context.with_container(container);
905                flex_element =
906                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
907            }
908            Element::TBody => {
909                context = context.with_container(container);
910                flex_element =
911                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
912            }
913            Element::TR => {
914                context = context.with_container(container);
915                flex_element =
916                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
917            }
918            Element::TD => {
919                context = context.with_container(container);
920                flex_element =
921                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
922            }
923            Element::Canvas | Element::Input { .. } => {}
924            Element::Button => {
925                context = context.with_container(container);
926                flex_element =
927                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
928            }
929            Element::Image { source, .. } => {
930                context = context.with_container(container);
931                let width = container.calculated_width;
932                let height = container.calculated_height;
933                let mut frame = Frame::default_fill();
934
935                #[cfg(feature = "debug")]
936                {
937                    if *DEBUG.read().unwrap() {
938                        frame.draw(|w| {
939                            fltk::draw::set_draw_color(enums::Color::White);
940                            fltk::draw::draw_rect(w.x(), w.y(), w.w(), w.h());
941                        });
942                    }
943                }
944
945                if let Some(source) = source {
946                    if let Some(bytes) = moosicbox_app_native_image::get_image(source) {
947                        if let Err(e) = event_sender.send(AppEvent::RegisterImage {
948                            viewport: viewport.deref().clone(),
949                            source: ImageSource::Bytes {
950                                bytes,
951                                source: source.to_owned(),
952                            },
953                            width: container.width.as_ref().map(|_| width.unwrap()),
954                            height: container.height.as_ref().map(|_| height.unwrap()),
955                            frame: frame.clone(),
956                        }) {
957                            log::error!(
958                                "Failed to send LoadImage event with source={source}: {e:?}"
959                            );
960                        }
961                    } else if source.starts_with("http") {
962                        if let Err(e) = event_sender.send(AppEvent::RegisterImage {
963                            viewport: viewport.deref().clone(),
964                            source: ImageSource::Url(source.to_owned()),
965                            width: container.width.as_ref().map(|_| width.unwrap()),
966                            height: container.height.as_ref().map(|_| height.unwrap()),
967                            frame: frame.clone(),
968                        }) {
969                            log::error!(
970                                "Failed to send LoadImage event with source={source}: {e:?}"
971                            );
972                        }
973                    } else if let Ok(manifest_path) = std::env::var("CARGO_MANIFEST_DIR") {
974                        #[allow(irrefutable_let_patterns)]
975                        if let Ok(path) = std::path::PathBuf::from_str(&manifest_path) {
976                            let source = source
977                                .chars()
978                                .skip_while(|x| *x == '/' || *x == '\\')
979                                .collect::<String>();
980
981                            if let Some(path) = path
982                                .parent()
983                                .and_then(|x| x.parent())
984                                .map(|x| x.join("app-website").join("public").join(source))
985                            {
986                                if let Ok(path) = path.canonicalize() {
987                                    if path.is_file() {
988                                        let image = SharedImage::load(path)?;
989
990                                        // FIXME: Need to handle aspect ratio if either width or
991                                        // height is missing
992                                        if width.is_some() || height.is_some() {
993                                            #[allow(
994                                                clippy::cast_possible_truncation,
995                                                clippy::cast_precision_loss
996                                            )]
997                                            let width = container
998                                                .width
999                                                .as_ref()
1000                                                .unwrap()
1001                                                .calc(
1002                                                    context.width,
1003                                                    self.width
1004                                                        .load(std::sync::atomic::Ordering::SeqCst)
1005                                                        as f32,
1006                                                    self.height
1007                                                        .load(std::sync::atomic::Ordering::SeqCst)
1008                                                        as f32,
1009                                                )
1010                                                .round()
1011                                                as i32;
1012                                            #[allow(
1013                                                clippy::cast_possible_truncation,
1014                                                clippy::cast_precision_loss
1015                                            )]
1016                                            let height = container
1017                                                .height
1018                                                .as_ref()
1019                                                .unwrap()
1020                                                .calc(
1021                                                    context.height,
1022                                                    self.width
1023                                                        .load(std::sync::atomic::Ordering::SeqCst)
1024                                                        as f32,
1025                                                    self.height
1026                                                        .load(std::sync::atomic::Ordering::SeqCst)
1027                                                        as f32,
1028                                                )
1029                                                .round()
1030                                                as i32;
1031
1032                                            frame.set_size(width, height);
1033                                        }
1034
1035                                        frame.set_image_scaled(Some(image));
1036                                    }
1037                                }
1038                            }
1039                        }
1040                    }
1041                }
1042
1043                other_element = Some(frame.as_base_widget());
1044            }
1045            Element::Anchor { href, .. } => {
1046                context = context.with_container(container);
1047                let mut elements =
1048                    self.draw_elements(viewport, container, depth, context, event_sender.clone())?;
1049                if let Some(href) = href.to_owned() {
1050                    elements.handle(move |_, ev| match ev {
1051                        Event::Push => true,
1052                        Event::Released => {
1053                            if let Err(e) =
1054                                event_sender.send(AppEvent::Navigate { href: href.clone() })
1055                            {
1056                                log::error!("Failed to navigate to href={href}: {e:?}");
1057                            }
1058                            true
1059                        }
1060                        _ => false,
1061                    });
1062                }
1063                flex_element = Some(elements);
1064            }
1065            Element::Heading { size } => {
1066                context = context.with_container(container);
1067                context.size = match size {
1068                    HeaderSize::H1 => 36,
1069                    HeaderSize::H2 => 30,
1070                    HeaderSize::H3 => 24,
1071                    HeaderSize::H4 => 20,
1072                    HeaderSize::H5 => 16,
1073                    HeaderSize::H6 => 12,
1074                };
1075                flex_element =
1076                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
1077            }
1078            Element::OrderedList | Element::UnorderedList => {
1079                context = context.with_container(container);
1080                flex_element =
1081                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
1082            }
1083            Element::ListItem => {
1084                context = context.with_container(container);
1085                flex_element =
1086                    Some(self.draw_elements(viewport, container, depth, context, event_sender)?);
1087            }
1088        }
1089
1090        #[cfg(feature = "debug")]
1091        if let Some(flex_element) = &mut flex_element {
1092            if *DEBUG.read().unwrap() && (depth == 1 || index > 0) {
1093                let mut element_info = vec![];
1094
1095                let mut child = Some(container);
1096
1097                while let Some(container) = child.take() {
1098                    let element_name = container.element.tag_display_str();
1099                    let text = element_name.to_string();
1100                    let first = element_info.is_empty();
1101
1102                    element_info.push(text);
1103
1104                    let text = format!(
1105                        "    ({}, {}, {}, {})",
1106                        container.calculated_x.unwrap_or(0.0),
1107                        container.calculated_y.unwrap_or(0.0),
1108                        container.calculated_width.unwrap_or(0.0),
1109                        container.calculated_height.unwrap_or(0.0),
1110                    );
1111
1112                    element_info.push(text);
1113
1114                    if first {
1115                        let text = format!(
1116                            "    ({}, {}, {}, {})",
1117                            flex_element.x(),
1118                            flex_element.y(),
1119                            flex_element.w(),
1120                            flex_element.h(),
1121                        );
1122
1123                        element_info.push(text);
1124                    }
1125
1126                    child = container.children.first();
1127                }
1128
1129                flex_element.draw({
1130                    move |w| {
1131                        use fltk::draw;
1132
1133                        draw::set_draw_color(enums::Color::Red);
1134                        draw::draw_rect(w.x(), w.y(), w.w(), w.h());
1135                        draw::set_font(fltk::draw::font(), 8);
1136
1137                        let mut y_offset = 0;
1138
1139                        for text in &element_info {
1140                            let (_t_x, _t_y, _t_w, t_h) = draw::text_extents(text);
1141                            y_offset += t_h;
1142                            draw::draw_text(text, w.x(), w.y() + y_offset);
1143                        }
1144                    }
1145                });
1146            }
1147        }
1148
1149        Ok(flex_element.map(|x| x.as_base_widget()).or(other_element))
1150    }
1151
1152    async fn listen(&self) {
1153        let Some(rx) = self.event_receiver.clone() else {
1154            moosicbox_assert::die_or_panic!("Cannot listen before app is started");
1155        };
1156        let renderer = self.clone();
1157        while let Ok(event) = rx.recv_async().await {
1158            log::debug!("received event {event:?}");
1159            match event {
1160                AppEvent::Navigate { href } => {
1161                    if let Err(e) = self.sender.send(href) {
1162                        log::error!("Failed to send navigation href: {e:?}");
1163                    }
1164                }
1165                AppEvent::Resize {} => {}
1166                AppEvent::MouseWheel {} => {
1167                    {
1168                        let values = {
1169                            let value = renderer
1170                                .viewport_listener_join_handle
1171                                .lock()
1172                                .unwrap_or_else(std::sync::PoisonError::into_inner)
1173                                .take();
1174                            if let Some((handle, cancel)) = value {
1175                                Some((handle, cancel))
1176                            } else {
1177                                None
1178                            }
1179                        };
1180                        if let Some((handle, cancel)) = values {
1181                            cancel.store(true, std::sync::atomic::Ordering::SeqCst);
1182                            let _ = handle.await;
1183                        }
1184                    }
1185
1186                    let cancel = Arc::new(AtomicBool::new(false));
1187                    let handle = moosicbox_task::spawn("check_viewports", {
1188                        let renderer = renderer.clone();
1189                        let cancel = cancel.clone();
1190                        async move {
1191                            renderer.check_viewports(&cancel);
1192                        }
1193                    });
1194
1195                    renderer
1196                        .viewport_listener_join_handle
1197                        .lock()
1198                        .unwrap_or_else(std::sync::PoisonError::into_inner)
1199                        .replace((handle, cancel));
1200                }
1201                AppEvent::RegisterImage {
1202                    viewport,
1203                    source,
1204                    width,
1205                    height,
1206                    frame,
1207                } => {
1208                    renderer.register_image(viewport, source, width, height, &frame);
1209                }
1210                AppEvent::LoadImage {
1211                    source,
1212                    width,
1213                    height,
1214                    frame,
1215                } => {
1216                    moosicbox_task::spawn("renderer: load_image", async move {
1217                        Self::load_image(source, width, height, frame).await
1218                    });
1219                }
1220                AppEvent::UnloadImage { mut frame } => {
1221                    Self::set_frame_image(&mut frame, None);
1222                }
1223            }
1224        }
1225    }
1226
1227    #[must_use]
1228    pub async fn wait_for_navigation(&self) -> Option<String> {
1229        self.receiver.recv_async().await.ok()
1230    }
1231}
1232
1233pub struct FltkRenderRunner {
1234    app: App,
1235}
1236
1237impl RenderRunner for FltkRenderRunner {
1238    /// # Errors
1239    ///
1240    /// Will error if FLTK fails to run the event loop.
1241    fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Send>> {
1242        let app = self.app;
1243        log::debug!("run: starting");
1244        app.run()
1245            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send>)?;
1246        log::debug!("run: finished");
1247        Ok(())
1248    }
1249}
1250
1251impl ToRenderRunner for FltkRenderer {
1252    /// # Errors
1253    ///
1254    /// Will error if FLTK fails to run the event loop.
1255    fn to_runner(
1256        self,
1257        _handle: Handle,
1258    ) -> Result<Box<dyn RenderRunner>, Box<dyn std::error::Error + Send>> {
1259        let Some(app) = self.app else {
1260            moosicbox_assert::die_or_panic!("Cannot listen before app is started");
1261        };
1262
1263        Ok(Box::new(FltkRenderRunner { app }))
1264    }
1265}
1266
1267#[async_trait]
1268impl Renderer for FltkRenderer {
1269    fn add_responsive_trigger(&mut self, _name: String, _trigger: ResponsiveTrigger) {}
1270
1271    /// # Panics
1272    ///
1273    /// Will panic if elements `Mutex` is poisoned.
1274    ///
1275    /// # Errors
1276    ///
1277    /// Will error if FLTK app fails to start
1278    async fn init(
1279        &mut self,
1280        width: f32,
1281        height: f32,
1282        x: Option<i32>,
1283        y: Option<i32>,
1284        background: Option<Color>,
1285        title: Option<&str>,
1286        _description: Option<&str>,
1287        _viewport: Option<&str>,
1288    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
1289        let app = app::App::default();
1290        self.app.replace(app);
1291
1292        #[allow(clippy::cast_possible_truncation)]
1293        let mut window = Window::default()
1294            .with_size(width.round() as i32, height.round() as i32)
1295            .with_label(title.unwrap_or("MoosicBox"));
1296
1297        self.window.replace(window.clone());
1298        #[allow(clippy::cast_possible_truncation)]
1299        self.width
1300            .store(width.round() as i32, std::sync::atomic::Ordering::SeqCst);
1301        #[allow(clippy::cast_possible_truncation)]
1302        self.height
1303            .store(height.round() as i32, std::sync::atomic::Ordering::SeqCst);
1304
1305        if let Some(background) = background {
1306            app::set_background_color(background.r, background.g, background.b);
1307        } else {
1308            app::set_background_color(24, 26, 27);
1309        }
1310
1311        app::set_foreground_color(255, 255, 255);
1312
1313        app::set_frame_type(enums::FrameType::NoBox);
1314        fltk::image::Image::set_scaling_algorithm(fltk::image::RgbScaling::Bilinear);
1315        RgbImage::set_scaling_algorithm(fltk::image::RgbScaling::Bilinear);
1316
1317        let (tx, rx) = flume::unbounded();
1318        self.event_sender.replace(tx);
1319        self.event_receiver.replace(rx);
1320
1321        window.handle({
1322            let renderer = self.clone();
1323            move |window, ev| {
1324                log::trace!("Received event: {ev}");
1325                match ev {
1326                    Event::Resize => {
1327                        renderer.handle_resize(window);
1328                        if let Some(sender) = &renderer.event_sender {
1329                            let _ = sender.send(AppEvent::Resize {});
1330                        }
1331                        true
1332                    }
1333                    Event::MouseWheel => {
1334                        if let Some(sender) = &renderer.event_sender {
1335                            let _ = sender.send(AppEvent::MouseWheel {});
1336                        }
1337                        false
1338                    }
1339                    #[cfg(feature = "debug")]
1340                    Event::KeyUp => {
1341                        let key = app::event_key();
1342                        log::debug!("Received key press {key:?}");
1343                        if key == enums::Key::F3 {
1344                            let value = {
1345                                let mut handle = DEBUG.write().unwrap();
1346                                let value = *handle;
1347                                let value = !value;
1348                                *handle = value;
1349                                value
1350                            };
1351                            log::debug!("Set DEBUG to {value}");
1352                            if let Err(e) = renderer.perform_render() {
1353                                log::error!("Failed to draw elements: {e:?}");
1354                            }
1355                            true
1356                        } else {
1357                            false
1358                        }
1359                    }
1360                    _ => false,
1361                }
1362            }
1363        });
1364
1365        window.set_callback(|_| {
1366            if fltk::app::event() == fltk::enums::Event::Close {
1367                app::quit();
1368            }
1369        });
1370
1371        if let (Some(x), Some(y)) = (x, y) {
1372            log::debug!("start: positioning window x={x} y={y}");
1373            window = window.with_pos(x, y);
1374        } else {
1375            log::debug!("start: centering window");
1376            window = window.center_screen();
1377        }
1378        window.end();
1379        window.make_resizable(true);
1380        window.show();
1381        log::debug!("start: started");
1382
1383        log::debug!("start: spawning listen thread");
1384        moosicbox_task::spawn("renderer_fltk::start: listen", {
1385            let renderer = self.clone();
1386            async move {
1387                log::debug!("start: listening");
1388                renderer.listen().await;
1389                Ok::<_, Box<dyn std::error::Error + Send + 'static>>(())
1390            }
1391        });
1392
1393        Ok(())
1394    }
1395
1396    /// # Errors
1397    ///
1398    /// Will error if FLTK app fails to emit the event.
1399    async fn emit_event(
1400        &self,
1401        event_name: String,
1402        event_value: Option<String>,
1403    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
1404        log::trace!("emit_event: event_name={event_name} event_value={event_value:?}");
1405
1406        Ok(())
1407    }
1408
1409    /// # Errors
1410    ///
1411    /// Will error if FLTK fails to render the elements.
1412    ///
1413    /// # Panics
1414    ///
1415    /// Will panic if elements `Mutex` is poisoned.
1416    async fn render(
1417        &self,
1418        elements: View,
1419    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
1420        log::debug!("render: {:?}", elements.immediate);
1421
1422        *self.elements.write().unwrap() = elements.immediate;
1423
1424        let renderer = self.clone();
1425
1426        moosicbox_task::spawn_blocking("fltk render", move || renderer.perform_render())
1427            .await
1428            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + 'static>)?
1429            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + 'static>)?;
1430
1431        Ok(())
1432    }
1433
1434    /// # Errors
1435    ///
1436    /// Will error if FLTK fails to render the partial view.
1437    ///
1438    /// # Panics
1439    ///
1440    /// Will panic if elements `Mutex` is poisoned.
1441    async fn render_partial(
1442        &self,
1443        view: PartialView,
1444    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
1445        moosicbox_logging::debug_or_trace!(
1446            ("render_partial: start"),
1447            ("render_partial: start {:?}", view)
1448        );
1449
1450        Ok(())
1451    }
1452
1453    /// # Errors
1454    ///
1455    /// Will error if FLTK fails to render the canvas update.
1456    ///
1457    /// # Panics
1458    ///
1459    /// Will panic if elements `Mutex` is poisoned.
1460    async fn render_canvas(
1461        &self,
1462        _update: CanvasUpdate,
1463    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
1464        log::trace!("render_canvas");
1465
1466        Ok(())
1467    }
1468}
1469
1470#[derive(Clone)]
1471struct Context {
1472    size: u16,
1473    direction: LayoutDirection,
1474    overflow_x: LayoutOverflow,
1475    overflow_y: LayoutOverflow,
1476    width: f32,
1477    height: f32,
1478    root_width: f32,
1479    root_height: f32,
1480}
1481
1482impl Context {
1483    fn new(width: f32, height: f32, root_width: f32, root_height: f32) -> Self {
1484        Self {
1485            size: 12,
1486            direction: LayoutDirection::default(),
1487            overflow_x: LayoutOverflow::default(),
1488            overflow_y: LayoutOverflow::default(),
1489            width,
1490            height,
1491            root_width,
1492            root_height,
1493        }
1494    }
1495
1496    fn with_container(mut self, container: &Container) -> Self {
1497        self.direction = container.direction;
1498        self.overflow_x = container.overflow_x;
1499        self.overflow_y = container.overflow_y;
1500        self.width = container
1501            .calculated_width
1502            .or_else(|| {
1503                container
1504                    .width
1505                    .as_ref()
1506                    .map(|x| x.calc(self.width, self.root_width, self.root_height))
1507            })
1508            .unwrap_or(self.width);
1509        self.height = container
1510            .calculated_height
1511            .or_else(|| {
1512                container
1513                    .height
1514                    .as_ref()
1515                    .map(|x| x.calc(self.height, self.root_width, self.root_height))
1516            })
1517            .unwrap_or(self.height);
1518        self
1519    }
1520}
1521
1522#[derive(Clone)]
1523struct WidgetWrapper(widget::Widget);
1524
1525impl From<widget::Widget> for WidgetWrapper {
1526    fn from(value: widget::Widget) -> Self {
1527        Self(value)
1528    }
1529}
1530
1531impl WidgetPosition for WidgetWrapper {
1532    fn widget_x(&self) -> i32 {
1533        self.0.x()
1534    }
1535
1536    fn widget_y(&self) -> i32 {
1537        self.0.y()
1538    }
1539
1540    fn widget_w(&self) -> i32 {
1541        self.0.w()
1542    }
1543
1544    fn widget_h(&self) -> i32 {
1545        self.0.h()
1546    }
1547}
1548
1549#[derive(Clone)]
1550struct ScrollWrapper(group::Scroll);
1551
1552impl From<group::Scroll> for ScrollWrapper {
1553    fn from(value: group::Scroll) -> Self {
1554        Self(value)
1555    }
1556}
1557
1558impl WidgetPosition for ScrollWrapper {
1559    fn widget_x(&self) -> i32 {
1560        self.0.x()
1561    }
1562
1563    fn widget_y(&self) -> i32 {
1564        self.0.y()
1565    }
1566
1567    fn widget_w(&self) -> i32 {
1568        self.0.w()
1569    }
1570
1571    fn widget_h(&self) -> i32 {
1572        self.0.h()
1573    }
1574}
1575
1576impl ViewportPosition for ScrollWrapper {
1577    fn viewport_x(&self) -> i32 {
1578        self.0.xposition()
1579    }
1580
1581    fn viewport_y(&self) -> i32 {
1582        self.0.yposition()
1583    }
1584
1585    fn viewport_w(&self) -> i32 {
1586        self.0.w()
1587    }
1588
1589    fn viewport_h(&self) -> i32 {
1590        self.0.h()
1591    }
1592
1593    fn as_widget_position(&self) -> Box<dyn WidgetPosition> {
1594        Box::new(self.clone())
1595    }
1596}
1597
1598impl From<ScrollWrapper> for Box<dyn ViewportPosition + Send + Sync> {
1599    fn from(value: ScrollWrapper) -> Self {
1600        Box::new(value)
1601    }
1602}
1603
1604trait Group {
1605    fn end(&mut self);
1606    fn type_str(&self) -> &'static str;
1607    fn wid(&self) -> i32;
1608    fn hei(&self) -> i32;
1609}
1610
1611impl Group for group::Flex {
1612    fn end(&mut self) {
1613        <Self as GroupExt>::end(self);
1614    }
1615
1616    fn type_str(&self) -> &'static str {
1617        "Flex"
1618    }
1619
1620    fn wid(&self) -> i32 {
1621        <Self as fltk::prelude::WidgetExt>::w(self)
1622    }
1623
1624    fn hei(&self) -> i32 {
1625        <Self as fltk::prelude::WidgetExt>::h(self)
1626    }
1627}
1628
1629impl From<group::Flex> for Box<dyn Group> {
1630    fn from(value: group::Flex) -> Self {
1631        Box::new(value)
1632    }
1633}
1634
1635impl Group for group::Scroll {
1636    fn end(&mut self) {
1637        <Self as GroupExt>::end(self);
1638    }
1639
1640    fn type_str(&self) -> &'static str {
1641        "Scroll"
1642    }
1643
1644    fn wid(&self) -> i32 {
1645        <Self as fltk::prelude::WidgetExt>::w(self)
1646    }
1647
1648    fn hei(&self) -> i32 {
1649        <Self as fltk::prelude::WidgetExt>::h(self)
1650    }
1651}
1652
1653impl From<group::Scroll> for Box<dyn Group> {
1654    fn from(value: group::Scroll) -> Self {
1655        Box::new(value)
1656    }
1657}
1658
1659fn fixed_size<W: WidgetExt>(
1660    direction: LayoutDirection,
1661    width: Option<f32>,
1662    height: Option<f32>,
1663    container: &mut group::Flex,
1664    element: &W,
1665) {
1666    call_fixed_size(direction, width, height, move |size| {
1667        container.fixed(element, size);
1668    });
1669}
1670
1671#[inline]
1672fn call_fixed_size<F: FnMut(i32)>(
1673    direction: LayoutDirection,
1674    width: Option<f32>,
1675    height: Option<f32>,
1676    mut f: F,
1677) {
1678    match direction {
1679        LayoutDirection::Row => {
1680            if let Some(width) = width {
1681                #[allow(clippy::cast_possible_truncation)]
1682                f(width.round() as i32);
1683                log::debug!("call_fixed_size: setting fixed width={width}");
1684            } else {
1685                log::debug!(
1686                    "call_fixed_size: not setting fixed width size width={width:?} height={height:?}"
1687                );
1688            }
1689        }
1690        LayoutDirection::Column => {
1691            if let Some(height) = height {
1692                #[allow(clippy::cast_possible_truncation)]
1693                f(height.round() as i32);
1694                log::debug!("call_fixed_size: setting fixed height={height})");
1695            } else {
1696                log::debug!(
1697                    "call_fixed_size: not setting fixed height size width={width:?} height={height:?}"
1698                );
1699            }
1700        }
1701    }
1702}