1use std::ops::ControlFlow;
2use std::str::FromStr;
3
4use anathema_geometry::{LocalPos, Pos, Region, Size};
5use anathema_value_resolver::{AttributeStorage, Attributes, ValueKind};
6use anathema_widgets::error::Result;
7use anathema_widgets::layout::{Constraints, LayoutCtx, PositionCtx};
8use anathema_widgets::paint::{Glyph, Glyphs, PaintCtx, SizePos};
9use anathema_widgets::{AnyWidget, LayoutForEach, PaintChildren, PositionChildren, Widget, WidgetId};
10
11use crate::layout::Axis;
12use crate::layout::border::BorderLayout;
13use crate::{HEIGHT, MAX_HEIGHT, MAX_WIDTH, MIN_HEIGHT, MIN_WIDTH, WIDTH};
14
15pub const BORDER_STYLE: &str = "border_style";
16
17pub const BORDER_EDGE_TOP_LEFT: usize = 0;
22pub const BORDER_EDGE_TOP: usize = 1;
23pub const BORDER_EDGE_TOP_RIGHT: usize = 2;
24pub const BORDER_EDGE_RIGHT: usize = 3;
25pub const BORDER_EDGE_BOTTOM_RIGHT: usize = 4;
26pub const BORDER_EDGE_BOTTOM: usize = 5;
27pub const BORDER_EDGE_BOTTOM_LEFT: usize = 6;
28pub const BORDER_EDGE_LEFT: usize = 7;
29
30bitflags::bitflags! {
34 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
39 pub struct Sides: u8 {
40 const EMPTY = 0x0;
42 const TOP = 0b0001;
44 const RIGHT = 0b0010;
46 const BOTTOM = 0b0100;
48 const LEFT = 0b1000;
50 const ALL = Self::TOP.bits() | Self::RIGHT.bits() | Self::BOTTOM.bits() | Self::LEFT.bits();
52 }
53}
54
55impl Default for Sides {
56 fn default() -> Self {
57 Self::ALL
58 }
59}
60
61impl TryFrom<&ValueKind<'_>> for Sides {
62 type Error = ();
63
64 fn try_from(value: &ValueKind<'_>) -> Result<Self, Self::Error> {
65 let mut sides = Sides::EMPTY;
66 match value {
67 ValueKind::Str(cow) => Sides::from_str(cow),
68 ValueKind::List(list) => {
69 for x in list {
70 sides |= Sides::try_from(x)?;
71 }
72 Ok(sides)
73 }
74 ValueKind::DynList(value) => {
75 let Some(state) = value.as_state() else { return Err(()) };
76 let Some(list) = state.as_any_list() else { return Err(()) };
77 for i in 0..list.len() {
78 if sides == Sides::ALL {
79 break;
80 }
81 let value = list.lookup(i).ok_or(())?;
82 let value = value.as_state().ok_or(())?;
83 let s = value.as_str().ok_or(())?;
84 sides |= Sides::from_str(s)?;
85 }
86 Ok(sides)
87 }
88 _ => Err(()),
89 }
90 }
91}
92
93impl FromStr for Sides {
94 type Err = ();
95
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 let sides = match s {
98 "all" => Sides::ALL,
99 "top" => Sides::TOP,
100 "left" => Sides::LEFT,
101 "right" => Sides::RIGHT,
102 "bottom" => Sides::BOTTOM,
103 _ => Sides::EMPTY,
104 };
105 Ok(sides)
106 }
107}
108
109pub const DEFAULT_SLIM_EDGES: [Glyph; 8] = [
113 Glyph::from_char('┌', 1),
114 Glyph::from_char('─', 1),
115 Glyph::from_char('┐', 1),
116 Glyph::from_char('│', 1),
117 Glyph::from_char('┘', 1),
118 Glyph::from_char('─', 1),
119 Glyph::from_char('└', 1),
120 Glyph::from_char('│', 1),
121];
122
123pub const DEFAULT_THICK_EDGES: [Glyph; 8] = [
124 Glyph::from_char('╔', 1),
125 Glyph::from_char('═', 1),
126 Glyph::from_char('╗', 1),
127 Glyph::from_char('║', 1),
128 Glyph::from_char('╝', 1),
129 Glyph::from_char('═', 1),
130 Glyph::from_char('╚', 1),
131 Glyph::from_char('║', 1),
132];
133
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
136pub enum BorderStyle {
137 #[default]
143 Thin,
144 Thick,
150 Custom([Glyph; 8]),
156}
157
158impl BorderStyle {
159 pub fn edges(&self) -> [Glyph; 8] {
160 match self {
161 BorderStyle::Thin => DEFAULT_SLIM_EDGES,
162 BorderStyle::Thick => DEFAULT_THICK_EDGES,
163 BorderStyle::Custom(edges) => *edges,
164 }
165 }
166}
167
168struct Brush {
169 glyph: Glyph,
170 width: u8,
171}
172
173impl Brush {
174 pub fn new(glyph: Glyph, width: u8) -> Self {
175 Self { width, glyph }
176 }
177}
178
179struct BorderPainter {
180 top: Line,
181 bottom: Line,
182 left: Line,
183 right: Line,
184}
185
186struct Line {
187 start_cap: Option<Brush>,
188 middle: Option<Brush>,
189 end_cap: Option<Brush>,
190 start: LocalPos,
191 end: u16,
192 axis: Axis,
193}
194
195impl Line {
196 fn will_draw(&self) -> bool {
197 self.start_cap.is_some() || self.end_cap.is_some() || self.middle.is_some()
198 }
199
200 fn draw<F>(&self, f: &mut F)
201 where
202 F: FnMut(LocalPos, Glyph),
203 {
204 let mut pos = self.start;
205 let mut end = self.end;
206
207 if let Some(brush) = &self.start_cap {
208 f(pos, brush.glyph);
209 match self.axis {
210 Axis::Horizontal => pos.x += brush.width as u16,
211 Axis::Vertical => pos.y += 1,
212 }
213 }
214
215 if let Some(brush) = &self.end_cap {
216 let pos = match self.axis {
217 Axis::Horizontal => {
218 end -= brush.width as u16;
219 LocalPos::new(end, pos.y)
220 }
221 Axis::Vertical => {
222 end -= 1;
223 LocalPos::new(pos.x, end)
224 }
225 };
226 f(pos, brush.glyph);
227 }
228
229 if let Some(brush) = &self.middle {
230 loop {
231 match self.axis {
232 Axis::Horizontal => {
233 if pos.x + brush.width as u16 > end {
234 break;
235 }
236 f(pos, brush.glyph);
237 pos.x += brush.width as u16;
238 }
239 Axis::Vertical => {
240 if pos.y + 1 > end {
241 break;
242 }
243 f(pos, brush.glyph);
244 pos.y += 1;
245 }
246 }
247 }
248 }
249 }
250}
251
252impl BorderPainter {
253 fn new(glyphs: &[Glyph; 8], border_size: BorderSize, size: Size) -> Self {
254 let mut height = size.height;
255
256 let top = Line {
257 start_cap: (border_size.top_left > 0).then(|| Brush::new(glyphs[0], border_size.top_left)),
258 middle: (border_size.top > 0).then(|| Brush::new(glyphs[1], border_size.top)),
259 end_cap: (border_size.top_right > 0).then(|| Brush::new(glyphs[2], border_size.top_right)),
260 start: LocalPos::ZERO,
261 axis: Axis::Horizontal,
262 end: size.width,
263 };
264
265 let bottom = Line {
266 start_cap: (border_size.bottom_left > 0).then(|| Brush::new(glyphs[6], border_size.bottom_left)),
267 middle: (border_size.bottom > 0).then(|| Brush::new(glyphs[5], border_size.bottom)),
268 end_cap: (border_size.bottom_right > 0).then(|| Brush::new(glyphs[4], border_size.bottom_right)),
269 start: LocalPos::new(0, height - 1),
270 axis: Axis::Horizontal,
271 end: size.width,
272 };
273
274 if bottom.will_draw() {
275 height -= 1;
276 }
277
278 let mut offset = 0;
279 if top.will_draw() {
280 offset += 1;
281 }
282
283 let left = Line {
284 start_cap: None,
285 middle: (border_size.left > 0).then(|| Brush::new(glyphs[7], border_size.left)),
286 end_cap: None,
287 start: LocalPos::new(0, offset),
288 axis: Axis::Vertical,
289 end: height,
290 };
291
292 let right = Line {
293 start_cap: None,
294 middle: (border_size.right > 0).then(|| Brush::new(glyphs[3], border_size.right)),
295 end_cap: None,
296 start: LocalPos::new(size.width - border_size.right as u16, offset),
297 axis: Axis::Vertical,
298 end: height,
299 };
300
301 Self {
302 top,
303 bottom,
304 left,
305 right,
306 }
307 }
308
309 fn paint<F>(&mut self, mut f: F)
310 where
311 F: FnMut(LocalPos, Glyph),
312 {
313 self.top.draw(&mut f);
314 self.bottom.draw(&mut f);
315 self.left.draw(&mut f);
316 self.right.draw(&mut f);
317 }
318}
319
320#[derive(Debug, Default, Copy, Clone)]
322pub(crate) struct BorderSize {
323 pub top_left: u8,
324 pub top: u8,
325 pub top_right: u8,
326 pub right: u8,
327 pub bottom_right: u8,
328 pub bottom: u8,
329 pub bottom_left: u8,
330 pub left: u8,
331}
332
333impl BorderSize {
334 pub(crate) fn as_size(&self) -> Size {
335 let left_width = self.left.max(self.top_left).max(self.bottom_left) as u16;
336 let right_width = self.right.max(self.top_right).max(self.bottom_right) as u16;
337
338 Size::new(left_width + right_width, (self.top + self.bottom) as u16)
339 }
340}
341
342#[derive(Debug)]
353pub struct Border {
354 border_style: BorderStyle,
357 sides: Sides,
359 edges: [Glyph; 8],
363}
364
365impl Border {
366 fn border_size(&self, sides: Sides) -> BorderSize {
369 let mut border_size = BorderSize::default();
373
374 if sides.contains(Sides::LEFT | Sides::TOP) {
375 border_size.top_left = self.edges[BORDER_EDGE_TOP_LEFT].width() as u8;
376 }
377
378 if sides.contains(Sides::LEFT | Sides::BOTTOM) {
379 border_size.bottom_left = self.edges[BORDER_EDGE_BOTTOM_LEFT].width() as u8;
380 }
381
382 if sides.contains(Sides::RIGHT | Sides::BOTTOM) {
383 border_size.bottom_right = self.edges[BORDER_EDGE_BOTTOM_RIGHT].width() as u8;
384 }
385
386 if sides.contains(Sides::RIGHT | Sides::TOP) {
387 border_size.top_right = self.edges[BORDER_EDGE_TOP_RIGHT].width() as u8;
388 }
389
390 if sides.contains(Sides::LEFT) {
391 border_size.left = self.edges[BORDER_EDGE_LEFT].width() as u8;
392 }
393
394 if sides.contains(Sides::RIGHT) {
395 border_size.right = self.edges[BORDER_EDGE_RIGHT].width() as u8;
396 }
397
398 if sides.contains(Sides::TOP) {
399 border_size.top = self.edges[BORDER_EDGE_TOP].width() as u8;
400 }
401
402 if sides.contains(Sides::BOTTOM) {
403 border_size.bottom = self.edges[BORDER_EDGE_BOTTOM].width() as u8;
404 }
405
406 border_size
407 }
408}
409
410impl Widget for Border {
411 fn layout<'bp>(
412 &mut self,
413 children: LayoutForEach<'_, 'bp>,
414 constraints: Constraints,
415 id: WidgetId,
416 ctx: &mut LayoutCtx<'_, 'bp>,
417 ) -> Result<Size> {
418 let attributes = ctx.attribute_storage.get_mut(id);
419
420 self.sides = attributes.get_as::<Sides>("sides").unwrap_or_default();
421
422 self.border_style = match attributes.get(BORDER_STYLE) {
423 None => BorderStyle::Thin,
424 Some(val) => {
425 let s = val.as_str();
426 let mut edges = DEFAULT_SLIM_EDGES;
427 let mut index = 0;
428
429 match s {
430 Some("thin") | None => BorderStyle::default(),
431 Some("thick") => BorderStyle::Thick,
432 Some(s) => {
433 let mut glyphs = Glyphs::new(s);
434 while let Some(g) = glyphs.next(ctx.glyph_map) {
435 edges[index] = g;
436 index += 1;
437 if index >= DEFAULT_SLIM_EDGES.len() {
438 break;
439 };
440 }
441 BorderStyle::Custom(edges)
442 }
443 }
444 }
445 };
446 self.edges = self.border_style.edges();
447
448 let mut layout = BorderLayout {
449 min_width: attributes.get_as::<u16>(MIN_WIDTH),
450 min_height: attributes.get_as::<u16>(MIN_HEIGHT),
451 max_width: attributes.get_as::<u16>(MAX_WIDTH),
452 max_height: attributes.get_as::<u16>(MAX_HEIGHT),
453 height: attributes.get_as::<u16>(HEIGHT),
454 width: attributes.get_as::<u16>(WIDTH),
455 border_size: self.border_size(self.sides),
456 };
457
458 layout.layout(children, constraints, ctx)
459 }
460
461 fn position<'bp>(
462 &mut self,
463 mut children: PositionChildren<'_, 'bp>,
464 _: WidgetId,
465 attribute_storage: &AttributeStorage<'bp>,
466 mut ctx: PositionCtx,
467 ) {
468 _ = children.each(|child, children| {
469 if self.sides.contains(Sides::TOP) {
470 ctx.pos.y += 1;
471 }
472
473 if self.sides.contains(Sides::LEFT) {
474 ctx.pos.x += self.edges[BORDER_EDGE_LEFT].width() as i32;
475 }
476
477 child.position(children, ctx.pos, attribute_storage, ctx.viewport);
478 ControlFlow::Break(())
479 });
480 }
481
482 fn paint<'bp>(
483 &mut self,
484 mut children: PaintChildren<'_, 'bp>,
485 _id: WidgetId,
486 attribute_storage: &AttributeStorage<'bp>,
487 mut ctx: PaintCtx<'_, SizePos>,
488 ) {
489 let border_size = self.border_size(self.sides);
490
491 _ = children.each(|child, children| {
492 let ctx = ctx.to_unsized();
493 child.paint(children, ctx, attribute_storage);
494 ControlFlow::Break(())
495 });
496
497 if ctx.local_size.width == 0 || ctx.local_size.height == 0 {
505 return;
506 }
507
508 let mut painter = BorderPainter::new(&self.edges, border_size, ctx.local_size);
509 let paint = |pos, glyph| {
510 ctx.place_glyph(glyph, pos);
511 };
512
513 painter.paint(paint);
514 }
515
516 fn inner_bounds(&self, mut pos: Pos, mut size: Size) -> Region {
517 let bs = self.border_size(self.sides);
518 pos.x += bs.top_left.max(bs.bottom_left).max(bs.left) as i32;
519 pos.y += bs.top as i32;
520 size.width = size
521 .width
522 .saturating_sub(bs.top_right.max(bs.bottom_right).max(bs.right) as u16);
523 size.height = size.height.saturating_sub(bs.bottom as u16);
524 Region::from((pos, size))
525 }
526}
527
528pub(crate) fn make(attributes: &Attributes<'_>) -> Box<dyn AnyWidget> {
529 let sides = attributes.get_as::<Sides>("sides").unwrap_or_default();
530
531 let text = Border {
532 sides,
533 edges: DEFAULT_SLIM_EDGES,
534 border_style: BorderStyle::Thin,
535 };
536
537 Box::new(text)
538}
539#[cfg(test)]
542mod test {
543 use crate::testing::TestRunner;
544
545 #[test]
546 fn thin_border() {
547 let tpl = "border [width: 6, height: 4]";
548
549 let expected = "
550 ╔════════╗
551 ║┌────┐ ║
552 ║│ │ ║
553 ║│ │ ║
554 ║└────┘ ║
555 ║ ║
556 ║ ║
557 ╚════════╝
558 ";
559
560 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
561 }
562
563 #[test]
564 fn thick_border() {
565 let tpl = "border [width: 6, height: 4, border_style: 'thick']";
566
567 let expected = "
568 ╔════════╗
569 ║╔════╗ ║
570 ║║ ║ ║
571 ║║ ║ ║
572 ║╚════╝ ║
573 ║ ║
574 ║ ║
575 ╚════════╝
576 ";
577
578 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
579 }
580
581 #[test]
582 fn custom_border() {
583 let tpl = "border [width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
584
585 let expected = "
586 ╔════════╗
587 ║╔────╗ ║
588 ║│ │ ║
589 ║│ │ ║
590 ║╚────╝ ║
591 ║ ║
592 ║ ║
593 ╚════════╝
594 ";
595
596 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
597 }
598
599 #[test]
600 fn border_top() {
601 let tpl = "border [sides: 'top', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
602
603 let expected = "
604 ╔════════╗
605 ║────── ║
606 ║ ║
607 ║ ║
608 ║ ║
609 ║ ║
610 ║ ║
611 ╚════════╝
612 ";
613
614 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
615 }
616
617 #[test]
618 fn border_top_bottom() {
619 let tpl = "border [sides: 'bottom', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
620
621 let expected = "
622 ╔════════╗
623 ║ ║
624 ║ ║
625 ║ ║
626 ║────── ║
627 ║ ║
628 ║ ║
629 ╚════════╝
630 ";
631
632 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
633 }
634
635 #[test]
636 fn border_left() {
637 let tpl = "border [sides: 'left', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
638
639 let expected = "
640 ╔════════╗
641 ║│ ║
642 ║│ ║
643 ║│ ║
644 ║│ ║
645 ║ ║
646 ║ ║
647 ╚════════╝
648 ";
649
650 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
651 }
652
653 #[test]
654 fn border_right() {
655 let tpl = "border [sides: 'right', width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
656
657 let expected = "
658 ╔════════╗
659 ║ │ ║
660 ║ │ ║
661 ║ │ ║
662 ║ │ ║
663 ║ ║
664 ║ ║
665 ╚════════╝
666 ";
667
668 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
669 }
670
671 #[test]
672 fn border_top_left() {
673 let tpl = "border [sides: ['top', 'left'], width: 6, height: 4, border_style: '╔─╗│╝─╚│']";
674
675 let expected = "
676 ╔════════╗
677 ║╔───── ║
678 ║│ ║
679 ║│ ║
680 ║│ ║
681 ║ ║
682 ║ ║
683 ╚════════╝
684 ";
685
686 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
687 }
688
689 #[test]
690 fn border_bottom_right() {
691 let tpl = "border [sides: ['bottom', 'right'], width: 6, height: 4]";
692
693 let expected = "
694 ╔════════╗
695 ║ │ ║
696 ║ │ ║
697 ║ │ ║
698 ║─────┘ ║
699 ║ ║
700 ║ ║
701 ╚════════╝
702 ";
703
704 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
705 }
706
707 #[test]
708 fn unsized_empty_border() {
709 let tpl = "
710 border [sides: '']
711 text 'hi'
712 ";
713
714 let expected = "
715 ╔════════╗
716 ║hi ║
717 ║ ║
718 ║ ║
719 ║ ║
720 ╚════════╝
721 ";
722
723 TestRunner::new(tpl, (8, 4)).instance().render_assert(expected);
724 }
725
726 #[test]
727 fn sized_by_child() {
728 let tpl = "
729 border
730 text 'hello world'
731 ";
732
733 let expected = "
734 ╔════════╗
735 ║┌──────┐║
736 ║│hello │║
737 ║│world │║
738 ║└──────┘║
739 ║ ║
740 ║ ║
741 ╚════════╝
742 ";
743
744 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
745 }
746
747 #[test]
748 fn fixed_size() {
749 let tpl = "
750 border [width: 3 + 2, height: 2 + 2]
751 text 'hello world'
752 ";
753
754 let expected = "
755 ╔════════╗
756 ║┌───┐ ║
757 ║│hel│ ║
758 ║│lo │ ║
759 ║└───┘ ║
760 ║ ║
761 ║ ║
762 ╚════════╝
763 ";
764
765 TestRunner::new(tpl, (8, 6)).instance().render_assert(expected);
766 }
767}