Skip to main content

agg_gui/widgets/table/
mod.rs

1//! `Table` — a virtualised data-table widget with header, scrolling body,
2//! striping, overlines, row-selection, sort-toggle hooks and
3//! `scroll_to_row`.
4//!
5//! Designed to mirror the shape of `egui_extras::TableBuilder` so the same
6//! mental model carries over: configure columns (`auto` / `exact` /
7//! `remainder.at_least(...).clip(...)`), describe the row set
8//! (`Homogeneous { count, height }` or `Heterogeneous { heights }`), and
9//! provide cell + header painters.  Cells are produced lazily — only rows
10//! within the visible viewport are painted, so 100 000-row demos remain
11//! cheap.
12//!
13//! The widget intentionally does *not* know about your data: it owns the
14//! chrome (backgrounds, separators, scroll, selection bookkeeping) and
15//! invokes user-supplied painters for cell content.  Cell painters get a
16//! `CellInfo` (rect, row, column, selected, font, visuals) and may use any
17//! of the project's draw primitives.
18//!
19//! Y-up note: agg-gui's coordinate system has its origin at the bottom-
20//! left.  Rows are stored top-down in the public API (row 0 visually
21//! topmost) and the widget converts to Y-up internally.
22//!
23//! Resizable columns are intentionally not implemented in this first
24//! revision; the configuration is preserved so the API doesn't break when
25//! it lands later.
26
27mod body;
28mod config;
29mod state;
30
31pub use config::{
32    distribute_widths, CellInfo, CellPainter, ColumnSize, HeaderClick, HeaderInfo, HeaderPainter,
33    RowPredicate, RowsProvider, TableColumn, TableRows, MIN_COL_W, RESIZE_HIT_HALF,
34};
35
36use std::cell::{Cell, RefCell};
37use std::collections::HashSet;
38use std::rc::Rc;
39use std::sync::Arc;
40
41use crate::color::Color;
42use crate::cursor::{set_cursor_icon, CursorIcon};
43use crate::draw_ctx::DrawCtx;
44use crate::event::{Event, EventResult, MouseButton};
45use crate::geometry::{Rect, Size};
46use crate::layout_props::WidgetBase;
47use crate::text::Font;
48use crate::widget::Widget;
49use crate::widgets::scroll_view::ScrollView;
50
51use body::TableBody;
52use state::TableState;
53
54// ── Builder ────────────────────────────────────────────────────────────────
55
56pub struct TableBuilder {
57    state: TableState,
58    columns: Vec<TableColumn>,
59    header_height: f64,
60    header_painter: Option<HeaderPainter>,
61    header_click: Option<HeaderClick>,
62    /// Forwarded to the inner `ScrollView::with_fade_color` so the
63    /// scrollbar's edge-fade gradient blends into the table's actual
64    /// ancestor background instead of the default `window_fill`.  When
65    /// the table sits in a `FlexColumn::with_panel_bg`, pass the same
66    /// panel fill colour here.
67    fade_color: Option<Color>,
68}
69
70impl Default for TableBuilder {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl TableBuilder {
77    pub fn new() -> Self {
78        Self {
79            state: TableState::defaults(),
80            columns: Vec::new(),
81            header_height: 22.0,
82            header_painter: None,
83            header_click: None,
84            fade_color: None,
85        }
86    }
87
88    /// Set the scroll-fade gradient colour on the body's inner
89    /// `ScrollView`.  Pass the visible ancestor background colour
90    /// (e.g. `Visuals::panel_fill` when the table sits on a panel) so
91    /// the fade dissolves invisibly instead of painting a bright halo
92    /// of the default `window_fill`.  See [`ScrollView::with_fade_color`].
93    pub fn fade_color(mut self, c: Color) -> Self {
94        self.fade_color = Some(c);
95        self
96    }
97
98    pub fn columns(mut self, cols: Vec<TableColumn>) -> Self {
99        self.columns = cols;
100        self
101    }
102
103    pub fn striped(self, on: bool) -> Self {
104        self.state.striped.set(on);
105        self
106    }
107    pub fn striped_cell(self, cell: Rc<Cell<bool>>) -> Self {
108        Self {
109            state: TableState {
110                striped: cell,
111                ..self.state
112            },
113            ..self
114        }
115    }
116
117    pub fn sense_click(self, on: bool) -> Self {
118        self.state.sense_click.set(on);
119        self
120    }
121    pub fn sense_click_cell(self, cell: Rc<Cell<bool>>) -> Self {
122        Self {
123            state: TableState {
124                sense_click: cell,
125                ..self.state
126            },
127            ..self
128        }
129    }
130
131    pub fn rows(self, spec: TableRows) -> Self {
132        *self.state.rows.borrow_mut() = spec;
133        self
134    }
135    pub fn rows_cell(self, cell: Rc<RefCell<TableRows>>) -> Self {
136        Self {
137            state: TableState {
138                rows: cell,
139                ..self.state
140            },
141            ..self
142        }
143    }
144    /// Install a closure that produces the current row spec.  The widget
145    /// invokes it during each layout pass and writes the result into the
146    /// internal `rows` cell, so external state changes flow in without
147    /// the caller having to manage an observer widget.
148    pub fn rows_provider(self, p: RowsProvider) -> Self {
149        *self.state.rows_provider.borrow_mut() = Some(p);
150        self
151    }
152
153    pub fn overline_pred(self, pred: RowPredicate) -> Self {
154        *self.state.overline_pred.borrow_mut() = Some(pred);
155        self
156    }
157
158    /// Use a `HashSet<usize>` as the selection mask (highlights any row
159    /// whose internal index is in the set).  Convenience over
160    /// [`Self::selection_pred`] for the common case where rows are not
161    /// reversed/transformed.
162    pub fn selection(self, sel: Rc<RefCell<HashSet<usize>>>) -> Self {
163        let pred: RowPredicate = Box::new(move |i| sel.borrow().contains(&i));
164        *self.state.selection_pred.borrow_mut() = Some(pred);
165        self
166    }
167
168    /// Pass an arbitrary predicate to compute "is row N selected?".  Useful
169    /// when display indices differ from internal indices (e.g. reversed
170    /// order) and the caller wants the highlight to track the display
171    /// index rather than the internal one.
172    pub fn selection_pred(self, pred: RowPredicate) -> Self {
173        *self.state.selection_pred.borrow_mut() = Some(pred);
174        self
175    }
176
177    pub fn resizable(self, on: bool) -> Self {
178        self.state.resizable.set(on);
179        self
180    }
181    pub fn resizable_cell(self, cell: Rc<Cell<bool>>) -> Self {
182        Self {
183            state: TableState {
184                resizable: cell,
185                ..self.state
186            },
187            ..self
188        }
189    }
190
191    /// Adopt an external `Rc<RefCell<Vec<Option<f64>>>>` as the column
192    /// overrides cell.  External code can clear it (e.g. on a Reset
193    /// button) to restore the configured widths.
194    pub fn column_overrides_cell(self, cell: Rc<RefCell<Vec<Option<f64>>>>) -> Self {
195        Self {
196            state: TableState {
197                column_overrides: cell,
198                ..self.state
199            },
200            ..self
201        }
202    }
203
204    pub fn scroll_to_row_cell(self, cell: Rc<Cell<Option<usize>>>) -> Self {
205        Self {
206            state: TableState {
207                scroll_to_row: cell,
208                ..self.state
209            },
210            ..self
211        }
212    }
213
214    pub fn scroll_offset_cell(self, cell: Rc<Cell<f64>>) -> Self {
215        Self {
216            state: TableState {
217                scroll_offset: cell,
218                ..self.state
219            },
220            ..self
221        }
222    }
223
224    pub fn header_height(mut self, h: f64) -> Self {
225        self.header_height = h;
226        self
227    }
228    pub fn header_painter(mut self, p: HeaderPainter) -> Self {
229        self.header_painter = Some(p);
230        self
231    }
232    pub fn header_click(mut self, p: HeaderClick) -> Self {
233        self.header_click = Some(p);
234        self
235    }
236
237    pub fn cell_painter(self, p: CellPainter) -> Self {
238        *self.state.cell_painter.borrow_mut() = Some(p);
239        self
240    }
241
242    pub fn on_row_click(self, f: Box<dyn FnMut(usize, usize)>) -> Self {
243        *self.state.on_row_click.borrow_mut() = Some(f);
244        self
245    }
246
247    /// Materialise the configured table into a widget.
248    pub fn build(self, font: Arc<Font>) -> Table {
249        let body = TableBody {
250            bounds: Rect::default(),
251            children: Vec::new(),
252            font: Arc::clone(&font),
253            state: self.state.clone(),
254        };
255        let mut scroll = ScrollView::new(Box::new(body))
256            .vertical(true)
257            .horizontal(true)
258            .with_offset_cell(Rc::clone(&self.state.scroll_offset))
259            .with_h_offset_cell(Rc::clone(&self.state.h_offset))
260            .with_viewport_cell(Rc::clone(&self.state.viewport_cell));
261        if let Some(c) = self.fade_color {
262            scroll = scroll.with_fade_color(c);
263        }
264
265        let n = self.columns.len();
266        self.state.column_overrides.borrow_mut().resize(n, None);
267        Table {
268            bounds: Rect::default(),
269            children: vec![Box::new(scroll)],
270            base: WidgetBase::new(),
271            font,
272            columns: self.columns,
273            state: self.state,
274            header_height: self.header_height,
275            header_painter: RefCell::new(self.header_painter),
276            header_click: RefCell::new(self.header_click),
277            drag_resize: Cell::new(None),
278        }
279    }
280}
281
282// ── Table widget ────────────────────────────────────────────────────────────
283
284pub struct Table {
285    bounds: Rect,
286    children: Vec<Box<dyn Widget>>, // [0] = ScrollView wrapping the body widget.
287    base: WidgetBase,
288    font: Arc<Font>,
289    columns: Vec<TableColumn>,
290    state: TableState,
291    header_height: f64,
292    header_painter: RefCell<Option<HeaderPainter>>,
293    header_click: RefCell<Option<HeaderClick>>,
294    /// Active column resize drag: (column_index, pointer_x_at_down, original_width).
295    drag_resize: Cell<Option<(usize, f64, f64)>>,
296}
297
298impl Table {
299    /// Begin configuring a new table.  Convenience for [`TableBuilder::new`].
300    pub fn builder() -> TableBuilder {
301        TableBuilder::new()
302    }
303
304    /// Reset every column's user-resized override back to its configured
305    /// `auto`/`exact`/`remainder` width.
306    pub fn reset_column_widths(&self) {
307        self.state.column_overrides.borrow_mut().clear();
308    }
309
310    /// Read-only handle to the column overrides — `None` for the
311    /// configured default, `Some(w)` for user-resized columns.  Useful for
312    /// persistence layers that want to save and restore the layout.
313    pub fn column_overrides(&self) -> Rc<RefCell<Vec<Option<f64>>>> {
314        Rc::clone(&self.state.column_overrides)
315    }
316
317    /// Replace the row set in place.  Useful for switching between
318    /// homogeneous/heterogeneous modes without rebuilding the widget.
319    pub fn set_rows(&self, rows: TableRows) {
320        *self.state.rows.borrow_mut() = rows;
321    }
322
323    /// Read-only access to the current top-down row set spec.
324    pub fn rows_handle(&self) -> Rc<RefCell<TableRows>> {
325        Rc::clone(&self.state.rows)
326    }
327
328    pub fn margin(mut self, m: crate::layout_props::Insets) -> Self {
329        self.base.margin = m;
330        self
331    }
332}
333
334impl Widget for Table {
335    fn type_name(&self) -> &'static str {
336        "Table"
337    }
338    fn bounds(&self) -> Rect {
339        self.bounds
340    }
341    fn set_bounds(&mut self, b: Rect) {
342        self.bounds = b;
343    }
344    fn children(&self) -> &[Box<dyn Widget>] {
345        &self.children
346    }
347    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
348        &mut self.children
349    }
350
351    fn margin(&self) -> crate::layout_props::Insets {
352        self.base.margin
353    }
354
355    fn layout(&mut self, available: Size) -> Size {
356        let w = available.width.max(40.0);
357        let h = available.height.max(40.0);
358        self.bounds = Rect::new(0.0, 0.0, w, h);
359
360        // Pull live row spec from the provider closure (if any) before any
361        // measurement that depends on row count or heights.
362        if let Some(p) = self.state.rows_provider.borrow().as_ref() {
363            let new_rows = p();
364            *self.state.rows.borrow_mut() = new_rows;
365        }
366
367        // Apply any pending scroll-to-row before laying out the scroll view.
368        if let Some(target) = self.state.scroll_to_row.get() {
369            self.state.scroll_to_row.set(None);
370            let rows = self.state.rows.borrow();
371            let n = rows.count();
372            if n > 0 {
373                let target = target.min(n - 1);
374                self.state.scroll_offset.set(rows.top_down_y_at(target));
375            }
376        }
377
378        // Compute column widths & publish.  The natural content width
379        // (sum of column widths) may exceed the table's own width when
380        // the user has resized columns wider than the viewport — the
381        // body's ScrollView handles horizontal panning in that case.
382        let overrides = self.state.column_overrides.borrow().clone();
383        let widths = distribute_widths(&self.columns, w, &overrides);
384        let content_w: f64 = widths.iter().sum();
385        *self.state.widths.borrow_mut() = widths;
386        self.state.content_w.set(content_w);
387
388        // Body / scroll view fills the area below the header.
389        let body_h = (h - self.header_height).max(0.0);
390        let scroll = &mut self.children[0];
391        scroll.layout(Size::new(w, body_h));
392        scroll.set_bounds(Rect::new(0.0, 0.0, w, body_h));
393
394        Size::new(w, h)
395    }
396
397    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
398        let v = ctx.visuals();
399        let widths = self.state.widths.borrow().clone();
400        let header_y = self.bounds.height - self.header_height;
401        let h = self.header_height;
402        let viewport_w = self.bounds.width;
403        let h_offset = self.state.h_offset.get();
404
405        // Header background spans the visible viewport width — the
406        // backdrop is always opaque even when the table content is
407        // wider than the viewport.
408        ctx.set_fill_color(Color::rgba(0.5, 0.5, 0.5, 0.10));
409        ctx.begin_path();
410        ctx.rect(0.0, header_y, viewport_w, h);
411        ctx.fill();
412
413        // Header bottom border.
414        ctx.set_stroke_color(v.separator);
415        ctx.set_line_width(1.0);
416        ctx.begin_path();
417        ctx.move_to(0.0, header_y);
418        ctx.line_to(viewport_w, header_y);
419        ctx.stroke();
420
421        // Clip and translate so column-cell painters and separators draw
422        // in CONTENT space (origin = leftmost column edge) but render
423        // only within the visible header strip.
424        ctx.save();
425        ctx.clip_rect(0.0, header_y, viewport_w, h);
426        ctx.translate(-h_offset, 0.0);
427
428        // Per-column header painters.
429        if let Some(painter) = self.header_painter.borrow_mut().as_mut() {
430            let mut x = 0.0;
431            for (col, &cw) in widths.iter().enumerate() {
432                let info = HeaderInfo {
433                    col,
434                    rect: Rect::new(x, header_y, cw, h),
435                    visuals: &v,
436                    font: &self.font,
437                };
438                ctx.save();
439                ctx.clip_rect(x, header_y, cw, h);
440                painter(&info, ctx);
441                ctx.restore();
442                x += cw;
443            }
444        }
445
446        // Vertical column separators across the header.  Resizable column
447        // edges get a slightly thicker line so users can see where to grab.
448        let mut sx = 0.0;
449        let dragging = self.drag_resize.get().map(|(c, _, _)| c);
450        for (i, &cw) in widths.iter().enumerate() {
451            sx += cw;
452            if i + 1 < widths.len() {
453                let is_resizable = self.columns.get(i).map(|c| c.resizable).unwrap_or(false)
454                    && self.state.resizable.get();
455                let is_active = dragging == Some(i);
456                let color = if is_active {
457                    v.accent
458                } else if is_resizable {
459                    Color::rgba(v.separator.r, v.separator.g, v.separator.b, 0.9)
460                } else {
461                    v.separator
462                };
463                ctx.set_stroke_color(color);
464                ctx.set_line_width(if is_active { 2.0 } else { 1.0 });
465                ctx.begin_path();
466                ctx.move_to(sx, header_y);
467                ctx.line_to(sx, header_y + h);
468                ctx.stroke();
469            }
470        }
471        ctx.restore();
472    }
473
474    fn on_event(&mut self, event: &Event) -> EventResult {
475        let header_y = self.bounds.height - self.header_height;
476        let in_header = |y: f64| y >= header_y && y <= header_y + self.header_height;
477        let h_offset = self.state.h_offset.get();
478
479        // Helper: which (resizable) column edge — if any — is the pointer
480        // (in CONTENT space, i.e. with the horizontal scroll offset
481        // already added back) within `RESIZE_HIT_HALF` of?  Used both
482        // for cursor change on hover and for starting a drag.
483        let resize_target_at = |content_x: f64, y: f64| -> Option<(usize, f64)> {
484            if !in_header(y) || !self.state.resizable.get() {
485                return None;
486            }
487            let widths = self.state.widths.borrow().clone();
488            let mut acc = 0.0;
489            for (col, &cw) in widths.iter().enumerate() {
490                let edge = acc + cw;
491                let last = col + 1 == widths.len();
492                let resizable = self.columns.get(col).map(|c| c.resizable).unwrap_or(false);
493                if !last && resizable && (content_x - edge).abs() <= RESIZE_HIT_HALF {
494                    return Some((col, cw));
495                }
496                acc += cw;
497            }
498            None
499        };
500
501        // Active resize drag — must be checked first so MouseMove stays
502        // routed to us via the framework's mouse-capture path.
503        if let Some((col, content_x0, w0)) = self.drag_resize.get() {
504            match event {
505                Event::MouseMove { pos } => {
506                    set_cursor_icon(CursorIcon::ResizeHorizontal);
507                    let content_x = pos.x + h_offset;
508                    let dx = content_x - content_x0;
509                    let new_w = (w0 + dx).max(MIN_COL_W);
510                    let mut overs = self.state.column_overrides.borrow_mut();
511                    if overs.len() <= col {
512                        overs.resize(col + 1, None);
513                    }
514                    overs[col] = Some(new_w);
515                    crate::animation::request_draw();
516                    return EventResult::Consumed;
517                }
518                Event::MouseUp {
519                    button: MouseButton::Left,
520                    ..
521                } => {
522                    self.drag_resize.set(None);
523                    crate::animation::request_draw();
524                    return EventResult::Consumed;
525                }
526                _ => {}
527            }
528        }
529
530        // Hover affordance for resize handles — only meaningful when not
531        // already dragging (handled above).
532        if let Event::MouseMove { pos } = event {
533            let content_x = pos.x + h_offset;
534            if resize_target_at(content_x, pos.y).is_some() {
535                set_cursor_icon(CursorIcon::ResizeHorizontal);
536                // Quiet: this only sets the OS cursor, which is applied by the
537                // host outside the widget's backbuffer — nothing repaints, so
538                // consuming every hover move here must NOT schedule a frame.
539                return EventResult::ConsumedQuiet;
540            }
541        }
542
543        if let Event::MouseDown {
544            pos,
545            button: MouseButton::Left,
546            ..
547        } = event
548        {
549            let content_x = pos.x + h_offset;
550            if let Some((col, cw)) = resize_target_at(content_x, pos.y) {
551                // SNAPSHOT every column's current width into overrides at
552                // drag-start.  Without this, dragging one Remainder column
553                // would leak space taken/given into the other Remainder
554                // columns via re-distribution; the user expects only the
555                // dragged edge to move.  Sized columns already had their
556                // override (or are fixed) — pinning everything ensures
557                // distribute_widths returns the same vector after the
558                // drag for non-target columns.
559                {
560                    let widths = self.state.widths.borrow().clone();
561                    let mut overs = self.state.column_overrides.borrow_mut();
562                    overs.resize(widths.len(), None);
563                    for (j, &w) in widths.iter().enumerate() {
564                        if overs[j].is_none() {
565                            overs[j] = Some(w);
566                        }
567                    }
568                }
569                self.drag_resize.set(Some((col, content_x, cw)));
570                set_cursor_icon(CursorIcon::ResizeHorizontal);
571                crate::animation::request_draw();
572                return EventResult::Consumed;
573            }
574        }
575
576        if let Event::MouseUp {
577            pos,
578            button: MouseButton::Left,
579            ..
580        } = event
581        {
582            if in_header(pos.y) {
583                let widths = self.state.widths.borrow().clone();
584                let content_x = pos.x + h_offset;
585                let mut x = 0.0;
586                for (col, cw) in widths.iter().enumerate() {
587                    if content_x >= x && content_x < x + cw {
588                        let local_x = content_x - x;
589                        let local_y = pos.y - header_y;
590                        if let Some(cb) = self.header_click.borrow_mut().as_mut() {
591                            let r = cb(col, local_x, local_y);
592                            if r.is_consumed() {
593                                crate::animation::request_draw();
594                            }
595                            return r;
596                        }
597                        return EventResult::Ignored;
598                    }
599                    x += cw;
600                }
601            }
602        }
603        EventResult::Ignored
604    }
605}
606
607// ── Helpers exposed to users for cell painters ──────────────────────────────
608
609/// Trim `text` from the end with an ellipsis until it fits `max_w`, using
610/// the current font/size on `ctx`.
611pub fn clip_text_to_width(ctx: &dyn DrawCtx, text: &str, max_w: f64) -> String {
612    if let Some(m) = ctx.measure_text(text) {
613        if m.width <= max_w {
614            return text.to_string();
615        }
616    }
617    let mut out = text.to_string();
618    let ell = "…";
619    while !out.is_empty() {
620        out.pop();
621        let candidate = format!("{out}{ell}");
622        if let Some(m) = ctx.measure_text(&candidate) {
623            if m.width <= max_w {
624                return candidate;
625            }
626        }
627    }
628    String::new()
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn distribute_widths_splits_remainders_equally() {
637        let cols = vec![
638            TableColumn::auto(50.0),
639            TableColumn::remainder().at_least(40.0),
640            TableColumn::auto(60.0),
641            TableColumn::remainder(),
642            TableColumn::remainder(),
643        ];
644        let widths = distribute_widths(&cols, 410.0, &[]);
645        assert_eq!(widths[0], 50.0);
646        assert_eq!(widths[2], 60.0);
647        assert!((widths[1] - 100.0).abs() < 0.001);
648        assert!((widths[3] - 100.0).abs() < 0.001);
649        assert!((widths[4] - 100.0).abs() < 0.001);
650    }
651
652    #[test]
653    fn distribute_widths_respects_at_least() {
654        let cols = vec![
655            TableColumn::auto(200.0),
656            TableColumn::remainder().at_least(40.0),
657        ];
658        let widths = distribute_widths(&cols, 100.0, &[]);
659        assert!(widths[1] >= 40.0);
660    }
661
662    #[test]
663    fn distribute_widths_pins_overrides_and_redistributes_remainders() {
664        let cols = vec![
665            TableColumn::auto(50.0),
666            TableColumn::remainder().at_least(20.0),
667            TableColumn::remainder().at_least(20.0),
668            TableColumn::remainder().at_least(20.0),
669        ];
670        // User dragged column 1 to 200 px wide.
671        let overrides = vec![None, Some(200.0), None, None];
672        let widths = distribute_widths(&cols, 500.0, &overrides);
673        assert_eq!(widths[0], 50.0);
674        assert!((widths[1] - 200.0).abs() < 0.001);
675        // Remaining 250 split between cols 2 and 3 = 125 each.
676        assert!((widths[2] - 125.0).abs() < 0.001);
677        assert!((widths[3] - 125.0).abs() < 0.001);
678    }
679
680    #[test]
681    fn distribute_widths_clamps_override_min() {
682        let cols = vec![
683            TableColumn::auto(100.0),
684            TableColumn::remainder().at_least(20.0),
685        ];
686        let widths = distribute_widths(&cols, 200.0, &[Some(2.0), None]);
687        assert!(widths[0] >= MIN_COL_W);
688    }
689
690    #[test]
691    fn rows_homogeneous_total() {
692        let r = TableRows::Homogeneous {
693            count: 5,
694            height: 10.0,
695        };
696        assert_eq!(r.total_height(), 50.0);
697        assert_eq!(r.height_at(3), 10.0);
698        assert_eq!(r.top_down_y_at(2), 20.0);
699    }
700
701    #[test]
702    fn rows_heterogeneous_total() {
703        let r = TableRows::Heterogeneous {
704            heights: vec![10.0, 20.0, 30.0],
705        };
706        assert_eq!(r.total_height(), 60.0);
707        assert_eq!(r.top_down_y_at(2), 30.0);
708    }
709}