1use 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#[derive(Clone, Copy, PartialEq)]
19pub struct TableBorder {
20 pub top: BorderSide,
22 pub right: BorderSide,
24 pub bottom: BorderSide,
26 pub left: BorderSide,
28 pub horizontal_inside: BorderSide,
30 pub vertical_inside: BorderSide,
32 pub border_radius: BorderRadius,
36}
37
38impl Default for TableBorder {
39 fn default() -> TableBorder {
40 TableBorder::new()
41 }
42}
43
44impl TableBorder {
45 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 pub fn top(mut self, top: BorderSide) -> TableBorder {
63 self.top = top;
64 self
65 }
66
67 pub fn right(mut self, right: BorderSide) -> TableBorder {
69 self.right = right;
70 self
71 }
72
73 pub fn bottom(mut self, bottom: BorderSide) -> TableBorder {
75 self.bottom = bottom;
76 self
77 }
78
79 pub fn left(mut self, left: BorderSide) -> TableBorder {
81 self.left = left;
82 self
83 }
84
85 pub fn horizontal_inside(mut self, horizontal_inside: BorderSide) -> TableBorder {
87 self.horizontal_inside = horizontal_inside;
88 self
89 }
90
91 pub fn vertical_inside(mut self, vertical_inside: BorderSide) -> TableBorder {
93 self.vertical_inside = vertical_inside;
94 self
95 }
96
97 pub fn border_radius(mut self, border_radius: BorderRadius) -> TableBorder {
99 self.border_radius = border_radius;
100 self
101 }
102
103 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 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 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 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 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 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 #[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 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 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 pub fn paint(&self, canvas: &mut Canvas, rect: Rect, rows: &[f64], columns: &[f64]) {
358 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 #[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 #[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 #[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 #[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}