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 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 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 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 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 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 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 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 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}