Skip to main content

inset_rendering/
table_border.rs

1//! Flutter counterpart: `rendering/table_border.dart`.
2
3use std::fmt;
4
5use inset_embedder::{
6    Canvas, Color, FillRule, Offset, Paint, PaintStyle, PathBuilder, Rect, Stroke,
7};
8use inset_painting::{
9    BorderRadius, BorderSide, BorderStyle, EdgeInsets, draw_drrect, paint_border,
10};
11
12/// Border specification for `Table` widgets.
13///
14/// This is like `Border`, with the addition of two sides: the inner horizontal borders between
15/// rows and the inner vertical borders between columns.
16///
17/// The sides are represented by [`BorderSide`] objects.
18#[derive(Clone, Copy, PartialEq)]
19pub struct TableBorder {
20    /// The top side of this border.
21    pub top: BorderSide,
22    /// The right side of this border.
23    pub right: BorderSide,
24    /// The bottom side of this border.
25    pub bottom: BorderSide,
26    /// The left side of this border.
27    pub left: BorderSide,
28    /// The horizontal interior sides of this border.
29    pub horizontal_inside: BorderSide,
30    /// The vertical interior sides of this border.
31    pub vertical_inside: BorderSide,
32    /// The [`BorderRadius`] to use when painting the corners of this border.
33    ///
34    /// It is also applied to `DataTable`'s `Material`.
35    pub border_radius: BorderRadius,
36}
37
38impl Default for TableBorder {
39    fn default() -> TableBorder {
40        TableBorder::new()
41    }
42}
43
44impl TableBorder {
45    /// Creates a border for a table.
46    ///
47    /// All the sides of the border default to [`BorderSide::NONE`]; Dart's named arguments are
48    /// the setters.
49    pub const fn new() -> TableBorder {
50        TableBorder {
51            top: BorderSide::NONE,
52            right: BorderSide::NONE,
53            bottom: BorderSide::NONE,
54            left: BorderSide::NONE,
55            horizontal_inside: BorderSide::NONE,
56            vertical_inside: BorderSide::NONE,
57            border_radius: BorderRadius::ZERO,
58        }
59    }
60
61    /// Dart `TableBorder(top:)`.
62    pub fn top(mut self, top: BorderSide) -> TableBorder {
63        self.top = top;
64        self
65    }
66
67    /// Dart `TableBorder(right:)`.
68    pub fn right(mut self, right: BorderSide) -> TableBorder {
69        self.right = right;
70        self
71    }
72
73    /// Dart `TableBorder(bottom:)`.
74    pub fn bottom(mut self, bottom: BorderSide) -> TableBorder {
75        self.bottom = bottom;
76        self
77    }
78
79    /// Dart `TableBorder(left:)`.
80    pub fn left(mut self, left: BorderSide) -> TableBorder {
81        self.left = left;
82        self
83    }
84
85    /// Dart `TableBorder(horizontalInside:)`.
86    pub fn horizontal_inside(mut self, horizontal_inside: BorderSide) -> TableBorder {
87        self.horizontal_inside = horizontal_inside;
88        self
89    }
90
91    /// Dart `TableBorder(verticalInside:)`.
92    pub fn vertical_inside(mut self, vertical_inside: BorderSide) -> TableBorder {
93        self.vertical_inside = vertical_inside;
94        self
95    }
96
97    /// Dart `TableBorder(borderRadius:)`.
98    pub fn border_radius(mut self, border_radius: BorderRadius) -> TableBorder {
99        self.border_radius = border_radius;
100        self
101    }
102
103    /// A uniform border with all sides the same color and width.
104    ///
105    /// Dart's `TableBorder.all` defaults the sides to black solid borders, one logical pixel
106    /// wide, with no radius.
107    pub fn all(
108        color: Color,
109        width: f64,
110        style: BorderStyle,
111        border_radius: BorderRadius,
112    ) -> TableBorder {
113        let side = BorderSide::new(color, width, style, BorderSide::STROKE_ALIGN_INSIDE);
114        TableBorder {
115            top: side,
116            right: side,
117            bottom: side,
118            left: side,
119            horizontal_inside: side,
120            vertical_inside: side,
121            border_radius,
122        }
123    }
124
125    /// Creates a border for a table where all the interior sides use the same styling and all
126    /// the exterior sides use the same styling.
127    pub fn symmetric(
128        inside: BorderSide,
129        outside: BorderSide,
130        border_radius: BorderRadius,
131    ) -> TableBorder {
132        TableBorder {
133            top: outside,
134            right: outside,
135            bottom: outside,
136            left: outside,
137            horizontal_inside: inside,
138            vertical_inside: inside,
139            border_radius,
140        }
141    }
142
143    /// The widths of the sides of this border represented as an [`EdgeInsets`].
144    ///
145    /// This can be used, for example, with a `Padding` widget to inset a box by the size of
146    /// these borders.
147    pub fn dimensions(&self) -> EdgeInsets {
148        EdgeInsets::from_ltrb(
149            self.left.width,
150            self.top.width,
151            self.right.width,
152            self.bottom.width,
153        )
154    }
155
156    /// Whether all the sides of the border (outside and inside) are identical. Uniform borders
157    /// are typically more efficient to paint.
158    pub fn is_uniform(&self) -> bool {
159        self.all_sides_match(|side| side.color)
160            && self.all_sides_match(|side| side.width)
161            && self.all_sides_match(|side| side.style)
162    }
163
164    /// Whether all the outer sides of the border are identical.
165    fn outer_border_is_uniform(&self) -> bool {
166        self.outer_sides_match(|side| side.color)
167            && self.outer_sides_match(|side| side.width)
168            && self.outer_sides_match(|side| side.style)
169    }
170
171    fn all_sides_match<T: PartialEq>(&self, selector: impl Fn(&BorderSide) -> T) -> bool {
172        let top_value = selector(&self.top);
173
174        selector(&self.right) == top_value
175            && selector(&self.bottom) == top_value
176            && selector(&self.left) == top_value
177            && selector(&self.horizontal_inside) == top_value
178            && selector(&self.vertical_inside) == top_value
179    }
180
181    fn outer_sides_match<T: PartialEq>(&self, selector: impl Fn(&BorderSide) -> T) -> bool {
182        let top_value = selector(&self.top);
183
184        selector(&self.right) == top_value
185            && selector(&self.bottom) == top_value
186            && selector(&self.left) == top_value
187    }
188
189    /// Returns the set of distinct visible colors from the outer border sides.
190    ///
191    /// Only includes colors from border sides that are not [`BorderStyle::None`].
192    fn distinct_visible_outer_colors(&self) -> Vec<Color> {
193        let mut colors: Vec<Color> = Vec::new();
194        for side in [self.top, self.right, self.bottom, self.left] {
195            if side.style != BorderStyle::None && !colors.contains(&side.color) {
196                colors.push(side.color);
197            }
198        }
199        colors
200    }
201
202    fn paint_table_border(&self, canvas: &mut Canvas, rect: Rect) {
203        if self.outer_border_is_uniform() && self.border_radius != BorderRadius::ZERO {
204            let outer = self.border_radius.to_rrect(rect);
205            let inner = outer.deflate(self.top.width);
206            let paint = Paint {
207                color: self.top.color.into(),
208                ..Paint::default()
209            };
210            draw_drrect(canvas, outer, inner, &paint);
211            return;
212        }
213
214        let visible_colors = self.distinct_visible_outer_colors();
215        if visible_colors.len() == 1 && self.border_radius != BorderRadius::ZERO {
216            let visible = |side: BorderSide| match side.style {
217                BorderStyle::None => BorderSide::NONE,
218                BorderStyle::Solid => side,
219            };
220            TableBorder::paint_non_uniform_border_with_radius(
221                canvas,
222                rect,
223                self.border_radius,
224                visible_colors[0],
225                visible(self.top),
226                visible(self.right),
227                visible(self.bottom),
228                visible(self.left),
229            );
230            return;
231        }
232
233        paint_border(canvas, rect, self.top, self.right, self.bottom, self.left);
234    }
235
236    /// Paints a non-uniform border with border radius support.
237    ///
238    /// This is similar to `BoxBorder.paintNonUniformBorder` but adapted for table borders.
239    #[allow(clippy::too_many_arguments)]
240    fn paint_non_uniform_border_with_radius(
241        canvas: &mut Canvas,
242        rect: Rect,
243        border_radius: BorderRadius,
244        color: Color,
245        top: BorderSide,
246        right: BorderSide,
247        bottom: BorderSide,
248        left: BorderSide,
249    ) {
250        let border_rect = border_radius.to_rrect(rect);
251        let paint = Paint {
252            color: color.into(),
253            ..Paint::default()
254        };
255
256        let inner = EdgeInsets::from_ltrb(
257            left.stroke_inset(),
258            top.stroke_inset(),
259            right.stroke_inset(),
260            bottom.stroke_inset(),
261        )
262        .deflate_rrect(border_rect);
263
264        let outer = EdgeInsets::from_ltrb(
265            left.stroke_outset(),
266            top.stroke_outset(),
267            right.stroke_outset(),
268            bottom.stroke_outset(),
269        )
270        .inflate_rrect(border_rect);
271
272        draw_drrect(canvas, outer, inner, &paint);
273    }
274
275    /// Creates a copy of this border but with the widths scaled by the factor `t`.
276    ///
277    /// The `t` argument represents the multiplicand, or the position on the timeline for an
278    /// interpolation from nothing to `self`, with 0.0 meaning that the object returned should
279    /// be the nil variant of this object, 1.0 meaning that no change should be applied,
280    /// returning `self` (or something equivalent to `self`), and other values meaning that the
281    /// object should be multiplied by `t`. Negative values are treated like zero.
282    ///
283    /// Values for `t` are usually obtained from an `Animation<f64>`, such as an
284    /// `AnimationController`.
285    ///
286    /// See also:
287    ///
288    ///  * [`BorderSide::scale`], which is used to implement this method.
289    pub fn scale(&self, t: f64) -> TableBorder {
290        TableBorder::new()
291            .top(self.top.scale(t))
292            .right(self.right.scale(t))
293            .bottom(self.bottom.scale(t))
294            .left(self.left.scale(t))
295            .horizontal_inside(self.horizontal_inside.scale(t))
296            .vertical_inside(self.vertical_inside.scale(t))
297    }
298
299    /// Linearly interpolate between two table borders.
300    ///
301    /// If a border is `None`, it is treated as having only [`BorderSide::NONE`] borders.
302    ///
303    /// The `t` argument represents position on the timeline, with 0.0 meaning that the
304    /// interpolation has not started, returning `a` (or something equivalent to `a`), 1.0
305    /// meaning that the interpolation has finished, returning `b` (or something equivalent to
306    /// `b`), and values in between meaning that the interpolation is at the relevant point on
307    /// the timeline between `a` and `b`.
308    pub fn lerp(a: Option<TableBorder>, b: Option<TableBorder>, t: f64) -> Option<TableBorder> {
309        if a == b {
310            return a;
311        }
312        match (a, b) {
313            (None, None) => None,
314            (None, Some(b)) => Some(b.scale(t)),
315            (Some(a), None) => Some(a.scale(1.0 - t)),
316            (Some(a), Some(b)) => Some(
317                TableBorder::new()
318                    .top(BorderSide::lerp(a.top, b.top, t))
319                    .right(BorderSide::lerp(a.right, b.right, t))
320                    .bottom(BorderSide::lerp(a.bottom, b.bottom, t))
321                    .left(BorderSide::lerp(a.left, b.left, t))
322                    .horizontal_inside(BorderSide::lerp(
323                        a.horizontal_inside,
324                        b.horizontal_inside,
325                        t,
326                    ))
327                    .vertical_inside(BorderSide::lerp(a.vertical_inside, b.vertical_inside, t)),
328            ),
329        }
330    }
331
332    /// Paints the border around the given [`Rect`] on the given [`Canvas`], with the given
333    /// rows and columns.
334    ///
335    /// Uniform borders are more efficient to paint than more complex borders.
336    ///
337    /// The `rows` argument specifies the vertical positions between the rows, relative to the
338    /// given rectangle. For example, if the table contained two rows of height 100.0 each,
339    /// then `rows` would contain a single value, 100.0, which is the vertical position between
340    /// the two rows (relative to the top edge of `rect`).
341    ///
342    /// The `columns` argument specifies the horizontal positions between the columns, relative
343    /// to the given rectangle. For example, if the table contained two columns of height 100.0
344    /// each, then `columns` would contain a single value, 100.0, which is the vertical position
345    /// between the two columns (relative to the left edge of `rect`).
346    ///
347    /// The [`vertical_inside`](Self::vertical_inside) border is only drawn if there are at
348    /// least two columns. The [`horizontal_inside`](Self::horizontal_inside) border is only
349    /// drawn if there are at least two rows. The horizontal borders are drawn after the
350    /// vertical borders.
351    ///
352    /// The outer borders (in the order [`top`](Self::top), [`right`](Self::right),
353    /// [`bottom`](Self::bottom), [`left`](Self::left), with [`left`](Self::left) above the
354    /// others) are painted after the inner borders.
355    ///
356    /// The paint order is particularly notable in the case of partially-transparent borders.
357    pub fn paint(&self, canvas: &mut Canvas, rect: Rect, rows: &[f64], columns: &[f64]) {
358        // properties can't be null
359
360        // arguments can't be null
361        debug_assert!(rows.is_empty() || (rows[0] >= 0.0 && rows[rows.len() - 1] <= rect.height()));
362        debug_assert!(
363            columns.is_empty() || (columns[0] >= 0.0 && columns[columns.len() - 1] <= rect.width())
364        );
365
366        if !columns.is_empty() || !rows.is_empty() {
367            let mut paint = Paint::default();
368
369            if !columns.is_empty() {
370                match self.vertical_inside.style {
371                    BorderStyle::Solid => {
372                        paint.color = self.vertical_inside.color.into();
373                        paint.style =
374                            PaintStyle::Stroke(Stroke::new(self.vertical_inside.width as f32));
375                        let mut path = PathBuilder::new();
376                        for &x in columns {
377                            path.move_to(Offset::new(rect.left + x, rect.top));
378                            path.line_to(Offset::new(rect.left + x, rect.bottom));
379                        }
380                        canvas.draw_path(&path.build(), FillRule::NonZero, &paint);
381                    }
382                    BorderStyle::None => {}
383                }
384            }
385
386            if !rows.is_empty() {
387                match self.horizontal_inside.style {
388                    BorderStyle::Solid => {
389                        paint.color = self.horizontal_inside.color.into();
390                        paint.style =
391                            PaintStyle::Stroke(Stroke::new(self.horizontal_inside.width as f32));
392                        let mut path = PathBuilder::new();
393                        for &y in rows {
394                            path.move_to(Offset::new(rect.left, rect.top + y));
395                            path.line_to(Offset::new(rect.right, rect.top + y));
396                        }
397                        canvas.draw_path(&path.build(), FillRule::NonZero, &paint);
398                    }
399                    BorderStyle::None => {}
400                }
401            }
402        }
403
404        self.paint_table_border(canvas, rect);
405    }
406}
407
408impl fmt::Debug for TableBorder {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        write!(
411            f,
412            "TableBorder({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?})",
413            self.top,
414            self.right,
415            self.bottom,
416            self.left,
417            self.horizontal_inside,
418            self.vertical_inside,
419            self.border_radius
420        )
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use inset_embedder::Radius;
427
428    use super::*;
429
430    fn side(width: f64, color: u32) -> BorderSide {
431        BorderSide::new(
432            Color::new(color),
433            width,
434            BorderStyle::Solid,
435            BorderSide::STROKE_ALIGN_INSIDE,
436        )
437    }
438
439    /// `table_border_test.dart`: "TableBorder constructor".
440    #[test]
441    fn the_sides_not_given_are_none() {
442        let border = TableBorder::new()
443            .left(BorderSide::default())
444            .right(side(1.0, 0xFF00FF00))
445            .vertical_inside(BorderSide::default());
446        assert!(border.top == BorderSide::NONE);
447        assert!(border.right == side(1.0, 0xFF00FF00));
448        assert!(border.bottom == BorderSide::NONE);
449        assert!(border.left == BorderSide::default());
450        assert!(border.horizontal_inside == BorderSide::NONE);
451        assert!(border.vertical_inside == BorderSide::default());
452        assert!(border.dimensions() == EdgeInsets::symmetric(0.0, 1.0));
453        assert!(!border.is_uniform());
454        assert!(
455            border.scale(2.0)
456                == TableBorder::new()
457                    .left(side(2.0, 0xFF000000))
458                    .right(side(2.0, 0xFF00FF00))
459                    .vertical_inside(side(2.0, 0xFF000000))
460        );
461    }
462
463    /// `table_border_test.dart`: "TableBorder.all constructor" and "TableBorder.symmetric
464    /// constructor".
465    #[test]
466    fn all_and_symmetric_fill_the_sides() {
467        let all = TableBorder::all(
468            Color::new(0xFF00FFFF),
469            2.0,
470            BorderStyle::Solid,
471            BorderRadius::ZERO,
472        );
473        for each in [
474            all.top,
475            all.right,
476            all.bottom,
477            all.left,
478            all.horizontal_inside,
479            all.vertical_inside,
480        ] {
481            assert!(each == side(2.0, 0xFF00FFFF));
482        }
483        assert!(all.dimensions() == EdgeInsets::symmetric(2.0, 2.0));
484        assert!(all.is_uniform());
485        assert!(
486            all.scale(0.5)
487                == TableBorder::all(
488                    Color::new(0xFF00FFFF),
489                    1.0,
490                    BorderStyle::Solid,
491                    BorderRadius::ZERO
492                )
493        );
494
495        let symmetric = TableBorder::symmetric(
496            side(3.0, 0xFF000000),
497            side(1.0, 0xFFFF0000),
498            BorderRadius::ZERO,
499        );
500        assert!(symmetric.top == side(1.0, 0xFFFF0000));
501        assert!(symmetric.horizontal_inside == side(3.0, 0xFF000000));
502        assert!(symmetric.dimensions() == EdgeInsets::symmetric(1.0, 1.0));
503        assert!(!symmetric.is_uniform());
504        let gone = BorderSide::new(
505            Color::new(0xFFFF0000),
506            0.0,
507            BorderStyle::None,
508            BorderSide::STROKE_ALIGN_INSIDE,
509        );
510        assert!(
511            symmetric.scale(0.0)
512                == TableBorder::symmetric(BorderSide::NONE.scale(0.0), gone, BorderRadius::ZERO)
513        );
514    }
515
516    /// `table_border_test.dart`: "TableBorder.lerp" and "TableBorder.lerp with nulls".
517    #[test]
518    fn lerp_scales_every_side() {
519        let sides = [
520            side(1.0, 0x00000001),
521            side(2.0, 0x00000002),
522            side(3.0, 0x00000003),
523            side(4.0, 0x00000004),
524            side(5.0, 0x00000005),
525            side(6.0, 0x00000006),
526        ];
527        let scaled = |t: f64| {
528            TableBorder::new()
529                .top(sides[0].scale(t))
530                .right(sides[1].scale(t))
531                .bottom(sides[2].scale(t))
532                .left(sides[3].scale(t))
533                .horizontal_inside(sides[4].scale(t))
534                .vertical_inside(sides[5].scale(t))
535        };
536        let (a, b, c) = (scaled(1.0), scaled(2.0), scaled(3.0));
537        assert!(a.dimensions() == EdgeInsets::from_ltrb(4.0, 1.0, 2.0, 3.0));
538        assert!(TableBorder::lerp(Some(a), Some(c), 0.5) == Some(b));
539        assert!(TableBorder::lerp(Some(a), Some(b), 2.0) == Some(c));
540        assert!(TableBorder::lerp(Some(b), Some(c), -1.0) == Some(a));
541
542        let two = TableBorder::all(
543            Color::new(0xFF000000),
544            2.0,
545            BorderStyle::Solid,
546            BorderRadius::ZERO,
547        );
548        let one = TableBorder::all(
549            Color::new(0xFF000000),
550            1.0,
551            BorderStyle::Solid,
552            BorderRadius::ZERO,
553        );
554        assert!(TableBorder::lerp(Some(two), None, 0.5) == Some(one));
555        assert!(TableBorder::lerp(None, Some(two), 0.5) == Some(one));
556        assert!(TableBorder::lerp(None, None, 0.5).is_none());
557    }
558
559    /// `table_border_test.dart`: "TableBorder.all with a borderRadius".
560    #[test]
561    fn all_keeps_its_radius() {
562        let radius = BorderRadius::all(Radius::circular(8.0));
563        let border = TableBorder::all(Color::new(0xFF000000), 1.0, BorderStyle::Solid, radius);
564        assert!(border.border_radius == radius);
565    }
566}