Skip to main content

fret_chart/retained/
multi_grid.rs

1use std::cell::RefCell;
2use std::collections::BTreeSet;
3use std::rc::Rc;
4
5use delinea::data::DataTable;
6use delinea::engine::ChartEngine;
7use delinea::engine::model::ModelError;
8use delinea::ids::{DatasetId, GridId};
9use delinea::spec::ChartSpec;
10use fret_core::{NodeId, Px, Rect, Size};
11use fret_ui::layout_pass::LayoutPassKind;
12use fret_ui::retained_bridge::{LayoutCx, UiTreeRetainedExt as _, Widget};
13use fret_ui::{UiHost, UiTree};
14
15use super::ChartCanvas;
16
17#[derive(Debug, Clone, Copy)]
18pub struct UniformGrid {
19    pub columns: usize,
20    pub gap: Px,
21}
22
23impl UniformGrid {
24    pub fn new(columns: usize) -> Self {
25        Self {
26            columns: columns.max(1),
27            gap: Px(0.0),
28        }
29    }
30
31    pub fn with_gap(mut self, gap: Px) -> Self {
32        self.gap = gap;
33        self
34    }
35
36    pub fn create_node<H: UiHost>(ui: &mut UiTree<H>, grid: UniformGrid) -> NodeId {
37        ui.create_node_retained(grid)
38    }
39}
40
41impl<H: UiHost> Widget<H> for UniformGrid {
42    fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
43        let child_count = cx.children.len();
44        if child_count == 0 {
45            return cx.available;
46        }
47
48        let columns = self.columns.max(1).min(child_count);
49        let rows = child_count.div_ceil(columns);
50
51        let gap = self.gap.0.max(0.0);
52        let total_gap_x = gap * (columns.saturating_sub(1) as f32);
53        let total_gap_y = gap * (rows.saturating_sub(1) as f32);
54
55        let cell_w = ((cx.available.width.0 - total_gap_x) / columns.max(1) as f32).max(0.0);
56        let cell_h = ((cx.available.height.0 - total_gap_y) / rows.max(1) as f32).max(0.0);
57
58        let origin = cx.bounds.origin;
59        let is_final = cx.pass_kind == LayoutPassKind::Final;
60
61        for (i, child) in cx.children.iter().copied().enumerate() {
62            let col = i % columns;
63            let row = i / columns;
64
65            let x = origin.x.0 + (cell_w + gap) * (col as f32);
66            let y = origin.y.0 + (cell_h + gap) * (row as f32);
67
68            let rect = Rect::new(
69                fret_core::Point::new(Px(x), Px(y)),
70                Size::new(Px(cell_w), Px(cell_h)),
71            );
72
73            if is_final {
74                let _ = cx.layout_viewport_root(child, rect);
75            } else {
76                let _ = cx.layout_in(child, rect);
77            }
78        }
79
80        cx.available
81    }
82}
83
84/// A minimal retained container that lays out all children to fill the same bounds.
85///
86/// Child order defines paint/input stacking (last child is on top).
87#[derive(Debug, Default, Clone, Copy)]
88pub struct FillStack;
89
90impl FillStack {
91    pub fn create_node<H: UiHost>(ui: &mut UiTree<H>) -> NodeId {
92        ui.create_node_retained(Self)
93    }
94}
95
96impl<H: UiHost> Widget<H> for FillStack {
97    fn layout(&mut self, cx: &mut LayoutCx<'_, H>) -> Size {
98        let is_final = cx.pass_kind == LayoutPassKind::Final;
99        let rect = cx.bounds;
100        for child in cx.children.iter().copied() {
101            if is_final {
102                let _ = cx.layout_viewport_root(child, rect);
103            } else {
104                let _ = cx.layout_in(child, rect);
105            }
106        }
107        cx.available
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct MultiGridChartCanvasNodes {
113    pub root: NodeId,
114    pub canvases: Vec<(GridId, NodeId)>,
115}
116
117fn collect_grids(spec: &ChartSpec) -> Vec<GridId> {
118    if !spec.grids.is_empty() {
119        return spec.grids.iter().map(|g| g.id).collect();
120    }
121
122    let mut ids: BTreeSet<GridId> = spec.axes.iter().map(|a| a.grid).collect();
123    if ids.is_empty() {
124        ids.insert(GridId::new(1));
125    }
126    ids.into_iter().collect()
127}
128
129pub fn create_multi_grid_chart_canvas_nodes<H: UiHost>(
130    ui: &mut UiTree<H>,
131    spec: ChartSpec,
132    datasets: &[(DatasetId, DataTable)],
133    layout: UniformGrid,
134) -> Result<MultiGridChartCanvasNodes, ModelError> {
135    let grids = collect_grids(&spec);
136    if grids.len() <= 1 {
137        let mut canvas = ChartCanvas::new(spec)?;
138        for (dataset_id, table) in datasets {
139            canvas
140                .engine_mut()
141                .datasets_mut()
142                .insert(*dataset_id, table.clone());
143        }
144        let node = ChartCanvas::create_node(ui, canvas);
145        let root = UniformGrid::create_node(ui, layout);
146        ui.add_child(root, node);
147        return Ok(MultiGridChartCanvasNodes {
148            root,
149            canvases: vec![(grids.first().copied().unwrap_or(GridId::new(1)), node)],
150        });
151    }
152
153    let mut spec = spec;
154    spec.axis_pointer.get_or_insert_with(Default::default);
155    let engine = Rc::new(RefCell::new(ChartEngine::new(spec)?));
156    {
157        let mut engine = engine.borrow_mut();
158        for (dataset_id, table) in datasets {
159            engine.datasets_mut().insert(*dataset_id, table.clone());
160        }
161    }
162
163    let mut canvases: Vec<(GridId, NodeId)> = Vec::with_capacity(grids.len());
164    for grid in grids {
165        let canvas = ChartCanvas::new_grid_view(engine.clone(), grid);
166        let node = ChartCanvas::create_node(ui, canvas);
167        canvases.push((grid, node));
168    }
169
170    let grid_root = UniformGrid::create_node(ui, layout);
171    for (_, node) in &canvases {
172        ui.add_child(grid_root, *node);
173    }
174
175    let overlay = ChartCanvas::create_node(ui, ChartCanvas::new_overlay(engine.clone()));
176
177    let root = FillStack::create_node(ui);
178    ui.add_child(root, grid_root);
179    ui.add_child(root, overlay);
180
181    Ok(MultiGridChartCanvasNodes { root, canvases })
182}