1use ::ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
11
12use super::RatatuiStyle;
13use urushi::{AnchoredRect, Available, BlockStyle, ResolvedView, StyledGrapheme, View, resolve};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct RatatuiAnchor {
23 logical: AnchoredRect,
24 destination: Rect,
25 source_column: usize,
26 source_row: usize,
27}
28
29impl RatatuiAnchor {
30 pub const fn logical(self) -> AnchoredRect {
31 self.logical
32 }
33
34 pub const fn destination(self) -> Rect {
35 self.destination
36 }
37
38 pub const fn source_column(self) -> usize {
39 self.source_column
40 }
41
42 pub const fn source_row(self) -> usize {
43 self.source_row
44 }
45}
46
47#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
49pub enum CellWriteMode {
50 #[default]
52 Merge,
53 Replace,
55}
56
57#[derive(Debug, Clone, Copy)]
76pub struct ViewWidget<'a> {
77 view: &'a View,
78 cell_write_mode: CellWriteMode,
79}
80
81impl<'a> ViewWidget<'a> {
82 pub const fn new(view: &'a View) -> Self {
84 Self {
85 view,
86 cell_write_mode: CellWriteMode::Merge,
87 }
88 }
89
90 pub const fn cell_write_mode(mut self, mode: CellWriteMode) -> Self {
92 self.cell_write_mode = mode;
93 self
94 }
95}
96
97impl Widget for ViewWidget<'_> {
98 fn render(self, area: Rect, buffer: &mut Buffer) {
99 draw(self.view, area, buffer, self.cell_write_mode);
100 }
101}
102
103impl Widget for &ViewWidget<'_> {
104 fn render(self, area: Rect, buffer: &mut Buffer) {
105 draw(self.view, area, buffer, self.cell_write_mode);
106 }
107}
108
109#[derive(Debug, Clone, Copy)]
115pub struct RatatuiWidget<'a> {
116 content: &'a str,
117 style: &'a BlockStyle,
118 cell_write_mode: CellWriteMode,
119}
120
121impl<'a> RatatuiWidget<'a> {
122 pub const fn new(content: &'a str, style: &'a BlockStyle) -> Self {
124 Self {
125 content,
126 style,
127 cell_write_mode: CellWriteMode::Merge,
128 }
129 }
130
131 pub const fn cell_write_mode(mut self, mode: CellWriteMode) -> Self {
133 self.cell_write_mode = mode;
134 self
135 }
136
137 fn view(&self) -> View {
139 View::block(
140 self.style.clone(),
141 View::text(self.content, self.style.text_style().clone()),
142 )
143 }
144}
145
146pub trait RatatuiStyleExt {
148 fn widget<'a>(&'a self, content: &'a str) -> RatatuiWidget<'a>;
150}
151
152impl RatatuiStyleExt for BlockStyle {
153 fn widget<'a>(&'a self, content: &'a str) -> RatatuiWidget<'a> {
154 RatatuiWidget::new(content, self)
155 }
156}
157
158impl Widget for RatatuiWidget<'_> {
159 fn render(self, area: Rect, buffer: &mut Buffer) {
160 draw(&self.view(), area, buffer, self.cell_write_mode);
161 }
162}
163
164impl Widget for &RatatuiWidget<'_> {
165 fn render(self, area: Rect, buffer: &mut Buffer) {
166 draw(&self.view(), area, buffer, self.cell_write_mode);
167 }
168}
169
170fn draw(view: &View, area: Rect, buffer: &mut Buffer, mode: CellWriteMode) {
172 draw_resolved_with_mode(
173 &resolve(view, available(area)).expect("a ratatui area supplies finite view geometry"),
174 area,
175 buffer,
176 mode,
177 );
178}
179
180pub fn draw_resolved(resolved: &ResolvedView, area: Rect, buffer: &mut Buffer) {
191 draw_resolved_with_mode(resolved, area, buffer, CellWriteMode::Merge);
192}
193
194pub fn draw_resolved_with_mode(
200 resolved: &ResolvedView,
201 area: Rect,
202 buffer: &mut Buffer,
203 mode: CellWriteMode,
204) {
205 let clip = area.intersection(buffer.area);
206 if clip.is_empty() {
207 return;
208 }
209 write_cells(resolved, area, clip, buffer, mode);
210}
211
212pub fn available(area: Rect) -> Available {
214 Available::size(usize::from(area.width), usize::from(area.height))
215}
216
217pub fn anchor_placement(anchor: &AnchoredRect, area: Rect) -> Option<RatatuiAnchor> {
223 let visible = anchor.visible()?;
224 let visible_x = usize::try_from(visible.x()).ok()?;
225 let visible_y = usize::try_from(visible.y()).ok()?;
226 let x = offset(area.x, visible_x)?;
227 let y = offset(area.y, visible_y)?;
228 let width = u16::try_from(visible.width())
229 .ok()?
230 .min(area.right().saturating_sub(x));
231 let height = u16::try_from(visible.height())
232 .ok()?
233 .min(area.bottom().saturating_sub(y));
234 let source_column = usize::try_from(visible.x().checked_sub(anchor.x())?).ok()?;
235 let source_row = usize::try_from(visible.y().checked_sub(anchor.y())?).ok()?;
236
237 Some(RatatuiAnchor {
238 logical: *anchor,
239 destination: Rect::new(x, y, width, height),
240 source_column,
241 source_row,
242 })
243}
244
245fn write_cells(
247 resolved: &ResolvedView,
248 area: Rect,
249 clip: Rect,
250 buffer: &mut Buffer,
251 mode: CellWriteMode,
252) {
253 visit_resolved(resolved, |column, row, grapheme| {
254 let Some(y) = offset(area.y, row) else {
255 return;
256 };
257 if y < clip.top() {
258 return;
259 }
260 if y >= clip.bottom() {
261 return;
262 }
263 let Some(x) = offset(area.x, column) else {
264 return;
265 };
266 write_grapheme(grapheme, x, y, clip, buffer, mode);
267 });
268}
269
270pub(crate) fn write_grapheme(
272 grapheme: &StyledGrapheme,
273 x: u16,
274 y: u16,
275 clip: Rect,
276 buffer: &mut Buffer,
277 mode: CellWriteMode,
278) {
279 let Ok(width) = u16::try_from(grapheme.width()) else {
282 return;
283 };
284 if width == 0 {
285 return;
286 }
287 let end = x.saturating_add(width);
288 if x < clip.left() || end > clip.right() {
289 return;
290 }
291
292 let style = RatatuiStyle::from(grapheme.style()).into_inner();
293 if let Some(cell) = buffer.cell_mut((x, y)) {
294 if mode == CellWriteMode::Replace {
295 cell.reset();
296 }
297 cell.set_symbol(grapheme.symbol()).set_style(style);
298 }
299 for hidden in x.saturating_add(1)..end {
300 if let Some(cell) = buffer.cell_mut((hidden, y)) {
301 cell.reset();
302 }
303 }
304}
305
306fn offset(origin: u16, cells: usize) -> Option<u16> {
307 u16::try_from(cells)
308 .ok()
309 .and_then(|cells| origin.checked_add(cells))
310}
311
312fn visit_resolved(resolved: &ResolvedView, mut visit: impl FnMut(usize, usize, &StyledGrapheme)) {
314 for (row, graphemes) in resolved.rows().iter().enumerate() {
315 let mut column = 0;
316 for grapheme in graphemes {
317 visit(column, row, grapheme);
318 column = column.saturating_add(grapheme.width());
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use ::ratatui::style::{Color as RatatuiColor, Modifier};
326 use urushi::{
327 Align, Border, Color, Length, Projection, ProjectionBoundary, TextStyle, VerticalAlign,
328 Viewport,
329 };
330
331 use super::*;
332
333 #[test]
334 fn converts_colors_and_every_supported_attribute() {
335 let converted = RatatuiStyle::from(
336 &TextStyle::new()
337 .foreground(Color::Rgb(1, 2, 3))
338 .background(Color::Ansi256(212))
339 .bold()
340 .dim()
341 .italic()
342 .underlined()
343 .blink()
344 .reverse()
345 .hide()
346 .strikethrough(),
347 )
348 .into_inner();
349
350 assert_eq!(converted.fg, Some(RatatuiColor::Rgb(1, 2, 3)));
351 assert_eq!(converted.bg, Some(RatatuiColor::Indexed(212)));
352 assert_eq!(
353 converted.add_modifier,
354 Modifier::BOLD
355 | Modifier::DIM
356 | Modifier::ITALIC
357 | Modifier::UNDERLINED
358 | Modifier::SLOW_BLINK
359 | Modifier::REVERSED
360 | Modifier::HIDDEN
361 | Modifier::CROSSED_OUT
362 );
363 }
364
365 #[test]
366 fn maps_sixteen_color_palette_to_named_colors() {
367 let expected = [
368 RatatuiColor::Black,
369 RatatuiColor::Red,
370 RatatuiColor::Green,
371 RatatuiColor::Yellow,
372 RatatuiColor::Blue,
373 RatatuiColor::Magenta,
374 RatatuiColor::Cyan,
375 RatatuiColor::Gray,
376 RatatuiColor::DarkGray,
377 RatatuiColor::LightRed,
378 RatatuiColor::LightGreen,
379 RatatuiColor::LightYellow,
380 RatatuiColor::LightBlue,
381 RatatuiColor::LightMagenta,
382 RatatuiColor::LightCyan,
383 RatatuiColor::White,
384 ];
385
386 for (index, expected) in expected.into_iter().enumerate() {
387 let converted =
388 RatatuiStyle::from(&TextStyle::new().foreground(Color::Ansi(index as u8)))
389 .into_inner();
390 assert_eq!(converted.fg, Some(expected));
391 }
392 }
393
394 #[test]
397 fn border_colors_arrive_as_the_border_graphemes_text_style() {
398 let style = BlockStyle::new()
399 .border(Border::ROUNDED)
400 .border_foreground(Color::Rgb(10, 20, 30))
401 .border_background(Color::Ansi256(236));
402 let area = Rect::new(0, 0, 3, 3);
403 let mut buffer = Buffer::empty(area);
404
405 style.widget("x").render(area, &mut buffer);
406
407 let corner = buffer.cell((0, 0)).expect("border cell");
408 assert_eq!(corner.symbol(), "╭");
409 assert_eq!(corner.fg, RatatuiColor::Rgb(10, 20, 30));
410 assert_eq!(corner.bg, RatatuiColor::Indexed(236));
411
412 let content = buffer.cell((1, 1)).expect("content cell");
413 assert_eq!(content.fg, RatatuiColor::Reset);
414 assert_eq!(content.bg, RatatuiColor::Reset);
415 }
416
417 #[test]
418 fn widget_preserves_box_model_alignment_and_cjk_width() {
419 let style = BlockStyle::new()
420 .foreground(Color::GREEN)
421 .background(Color::BLUE)
422 .padding((0, 1))
423 .margin((1, 2))
424 .border(Border::ROUNDED)
425 .border_foreground(Color::RED)
426 .width(10)
429 .align(Align::Center);
430 let area = Rect::new(0, 0, 14, 6);
431 let mut buffer = Buffer::empty(area);
432
433 style.widget("日本").render(area, &mut buffer);
434
435 assert_eq!(buffer_line(&buffer, 0), " ");
436 assert_eq!(buffer_line(&buffer, 1), " ╭────────╮ ");
437 assert_eq!(buffer_line(&buffer, 2), " │ 日本 │ ");
438 assert_eq!(buffer_line(&buffer, 3), " ╰────────╯ ");
439 let content = buffer.cell((5, 2)).expect("content cell");
440 assert_eq!(content.fg, RatatuiColor::Green);
441 assert_eq!(content.bg, RatatuiColor::Blue);
442 let border = buffer.cell((2, 1)).expect("border cell");
443 assert_eq!(border.fg, RatatuiColor::Red);
444 }
445
446 #[test]
450 fn a_narrow_rect_refits_wide_content_and_the_frame_stays_closed() {
451 let area = Rect::new(0, 0, 5, 3);
452 let mut buffer = Buffer::empty(area);
453
454 BlockStyle::new()
455 .border(Border::NORMAL)
456 .widget("日本語")
457 .render(area, &mut buffer);
458
459 assert_eq!(buffer_line(&buffer, 0), "┌───┐");
460 assert_eq!(buffer_line(&buffer, 1), "│日 │");
461 assert_eq!(buffer_line(&buffer, 2), "└───┘");
462 }
463
464 #[test]
465 fn widget_is_safe_for_zero_and_narrow_areas() {
466 let style = BlockStyle::new().border(Border::NORMAL).padding(2);
467 let mut empty = Buffer::empty(Rect::new(0, 0, 0, 0));
468 style
469 .widget("content")
470 .render(Rect::new(0, 0, 0, 0), &mut empty);
471
472 let area = Rect::new(0, 0, 1, 1);
473 let mut narrow = Buffer::empty(area);
474 style.widget("content").render(area, &mut narrow);
475 assert_eq!(buffer_line(&narrow, 0), "┌");
476 }
477
478 #[test]
479 fn widget_styles_only_enabled_border_edges() {
480 let style = BlockStyle::new()
481 .foreground(Color::GREEN)
482 .border(Border::NORMAL)
483 .border_top(false)
484 .border_right(false)
485 .border_bottom(false)
486 .border_foreground(Color::RED);
487 let area = Rect::new(0, 0, 2, 1);
488 let mut buffer = Buffer::empty(area);
489
490 style.widget("x").render(area, &mut buffer);
491
492 assert_eq!(buffer_line(&buffer, 0), "│x");
493 assert_eq!(
494 buffer.cell((0, 0)).expect("border cell").fg,
495 RatatuiColor::Red
496 );
497 assert_eq!(
498 buffer.cell((1, 0)).expect("content cell").fg,
499 RatatuiColor::Green
500 );
501 }
502
503 #[test]
509 fn a_one_cell_area_degrades_to_the_frame_or_the_leading_cell() {
510 let cases = [
512 ("top edge", (true, false, false, false), "-"),
513 ("left edge", (false, false, false, true), "|"),
514 ("bottom edge", (false, false, true, false), "-"),
515 ("right edge", (false, true, false, false), "x"),
516 ];
517
518 for (case, (top, right, bottom, left), expected) in cases {
519 let style = BlockStyle::new()
520 .border(Border::ASCII)
521 .border_top(top)
522 .border_right(right)
523 .border_bottom(bottom)
524 .border_left(left);
525 let area = Rect::new(0, 0, 1, 1);
526 let mut buffer = Buffer::empty(area);
527
528 style.widget("x").render(area, &mut buffer);
529
530 assert_eq!(buffer_line(&buffer, 0), expected, "{case}");
531 }
532 }
533
534 #[test]
537 fn widget_matches_direct_rendering_for_border_side_combinations() {
538 let styles = [
539 BlockStyle::new().border(Border::ASCII),
540 BlockStyle::new()
541 .border(Border::ASCII)
542 .border_top(false)
543 .border_right(false)
544 .border_bottom(false)
545 .border_left(false),
546 BlockStyle::new()
547 .border(Border::ASCII)
548 .border_right(false)
549 .border_bottom(false)
550 .border_left(false),
551 BlockStyle::new()
552 .border(Border::ASCII)
553 .border_top(false)
554 .border_right(false)
555 .border_bottom(false),
556 BlockStyle::new()
557 .border(Border::ASCII)
558 .border_right(false)
559 .border_bottom(false),
560 BlockStyle::new()
561 .border(Border::ASCII)
562 .border_right(false)
563 .border_left(false),
564 BlockStyle::new()
565 .border(Border::ASCII)
566 .border_top(false)
567 .border_bottom(false),
568 ];
569
570 for (index, style) in styles.into_iter().enumerate() {
571 assert_widget_matches_direct(&style, "x", &format!("border combination {index}"));
572 }
573 }
574
575 #[test]
576 fn widget_matches_direct_rendering_for_fixed_height_content_cases() {
577 let cases = [
578 ("empty", ""),
579 ("single line", "x"),
580 ("multiple lines", "x\ny"),
581 ("CJK", "日本"),
582 ("overflow", "a\nb\nc\nd\ne"),
583 ];
584
585 for (case, content) in cases {
586 let style = BlockStyle::new()
587 .width(6)
588 .height(4)
589 .padding((1, 1))
590 .border(Border::ASCII)
591 .border_top(false);
592 assert_widget_matches_direct(&style, content, case);
593 }
594 }
595
596 #[test]
597 fn widget_matches_direct_vertical_alignment_with_horizontal_alignment_and_cjk() {
598 for horizontal in [Align::Left, Align::Center, Align::Right] {
599 for vertical in [
600 VerticalAlign::Top,
601 VerticalAlign::Center,
602 VerticalAlign::Bottom,
603 ] {
604 let style = BlockStyle::new()
605 .width(6)
606 .height(7)
607 .padding((1, 1))
608 .align(horizontal)
609 .vertical_align(vertical);
610 assert_widget_matches_direct(
611 &style,
612 "日\nx",
613 &format!("{horizontal:?} {vertical:?}"),
614 );
615 }
616 }
617 }
618
619 #[test]
620 fn widget_matches_direct_rendering_for_maximum_dimensions() {
621 let style = BlockStyle::new()
622 .width(6)
623 .height(4)
624 .padding((1, 1))
625 .border(Border::ASCII)
626 .margin(1)
627 .max_width(6)
628 .max_height(4);
629
630 assert_widget_matches_direct(&style, "ab", "maximum dimensions");
631 }
632
633 #[test]
634 fn widget_expands_narrow_fixed_width_for_cjk_parity() {
635 assert_widget_matches_direct(&BlockStyle::new().width(1).height(2), "日本", "wrapped");
638 assert_widget_matches_direct(&BlockStyle::new().width(1), "日", "single grapheme");
639 }
640
641 #[test]
642 fn widget_matches_direct_maximum_width_cropping_of_wide_graphemes() {
643 assert_widget_matches_direct(&BlockStyle::new().max_width(3), "日本語", "CJK");
646 assert_widget_matches_direct(&BlockStyle::new().max_width(3), "👩💻x", "ZWJ emoji");
647 assert_widget_matches_direct(
648 &BlockStyle::new().margin((0, 0, 0, 1)).max_width(2),
649 "日",
650 "margin before a wide cell",
651 );
652 }
653
654 #[test]
658 fn a_rect_smaller_than_the_block_resizes_it() {
659 let style = BlockStyle::new().width(4).height(6);
660 let area = Rect::new(0, 0, 4, 4);
661
662 for (align, expected_row) in [
663 (VerticalAlign::Center, Some(1)),
664 (VerticalAlign::Bottom, Some(3)),
665 ] {
666 let style = style.clone().vertical_align(align);
667 let mut buffer = Buffer::empty(area);
668
669 style.widget("x").render(area, &mut buffer);
670
671 for y in 0..area.height {
672 let expected = if Some(y) == expected_row {
673 "x "
674 } else {
675 " "
676 };
677 assert_eq!(buffer_line(&buffer, y), expected, "{align:?} row {y}");
678 }
679 }
680 }
681
682 #[test]
683 fn a_shorter_area_closes_the_frame_instead_of_dropping_its_bottom_edge() {
684 let style = BlockStyle::new().width(4).height(6).border(Border::ASCII);
685 let area = Rect::new(0, 0, 6, 4);
686 let mut buffer = Buffer::empty(area);
687
688 style.widget("x").render(area, &mut buffer);
689
690 assert_eq!(buffer_line(&buffer, 0), "+--+ ");
693 assert_eq!(buffer_line(&buffer, 1), "|x | ");
694 assert_eq!(buffer_line(&buffer, 2), "| | ");
695 assert_eq!(buffer_line(&buffer, 3), "+--+ ");
696 }
697
698 #[test]
701 fn a_partial_buffer_masks_cells_without_moving_the_layout() {
702 let style = BlockStyle::new().width(10);
703 let mut buffer = Buffer::empty(Rect::new(5, 0, 5, 1));
704
705 style
706 .widget("abcdefghij")
707 .render(Rect::new(0, 0, 10, 1), &mut buffer);
708
709 assert_eq!(buffer_line(&buffer, 0), "fghij");
710
711 let style = BlockStyle::new().width(1).height(4);
712 let mut buffer = Buffer::empty(Rect::new(0, 2, 1, 2));
713
714 style
715 .widget("a\nb\nc\nd")
716 .render(Rect::new(0, 0, 1, 4), &mut buffer);
717
718 assert_eq!(buffer.cell((0, 2)).expect("third row").symbol(), "c");
719 assert_eq!(buffer.cell((0, 3)).expect("fourth row").symbol(), "d");
720 }
721
722 #[test]
723 fn view_widget_draws_a_bordered_block_inside_a_row() {
724 let view = View::row(
725 VerticalAlign::Center,
726 [
727 View::text("status: ", TextStyle::new()),
728 View::block(
729 BlockStyle::new()
730 .border(Border::ROUNDED)
731 .border_foreground(Color::GREEN),
732 View::text("ok", TextStyle::new().foreground(Color::GREEN)),
733 ),
734 ],
735 );
736 let area = Rect::new(0, 0, 12, 3);
737 let mut buffer = Buffer::empty(area);
738
739 ViewWidget::new(&view).render(area, &mut buffer);
740
741 assert_eq!(buffer_line(&buffer, 0), " ╭──╮");
742 assert_eq!(buffer_line(&buffer, 1), "status: │ok│");
743 assert_eq!(buffer_line(&buffer, 2), " ╰──╯");
744 assert_eq!(
745 buffer.cell((8, 0)).expect("border cell").fg,
746 RatatuiColor::Green
747 );
748 }
749
750 #[test]
754 fn resolving_first_and_drawing_reaches_the_same_cells_as_the_widget() {
755 let view = View::row(
756 VerticalAlign::Center,
757 [
758 View::text("status: ", TextStyle::new()),
759 View::block(
760 BlockStyle::new()
761 .border(Border::ROUNDED)
762 .border_foreground(Color::GREEN),
763 View::text("日本", TextStyle::new().foreground(Color::GREEN)),
764 ),
765 ],
766 );
767 let area = Rect::new(1, 0, 14, 3);
770 let mut through_widget = Buffer::empty(Rect::new(0, 0, 10, 3));
771 let mut through_resolved = Buffer::empty(Rect::new(0, 0, 10, 3));
772
773 ViewWidget::new(&view).render(area, &mut through_widget);
774 draw_resolved(
775 &resolve(&view, available(area)).expect("a ratatui area supplies finite view geometry"),
776 area,
777 &mut through_resolved,
778 );
779
780 assert_eq!(through_resolved, through_widget);
781 assert_eq!(buffer_line(&through_widget, 1), " status: │");
784 }
785
786 #[test]
787 fn view_widget_matches_one_shot_viewport_resolution() {
788 let view = View::viewport(
789 Viewport::both(
790 Projection::new(1, ProjectionBoundary::Preserve),
791 Projection::new(1, ProjectionBoundary::Preserve),
792 ),
793 View::text("abcd\nefgh\nijkl", TextStyle::new()),
794 );
795 let area = Rect::new(2, 1, 2, 2);
796 let mut through_widget = Buffer::empty(Rect::new(0, 0, 6, 4));
797 let mut through_resolved = Buffer::empty(Rect::new(0, 0, 6, 4));
798
799 ViewWidget::new(&view).render(area, &mut through_widget);
800 draw_resolved(
801 &resolve(&view, available(area)).expect("finite viewport geometry"),
802 area,
803 &mut through_resolved,
804 );
805
806 assert_eq!(through_widget, through_resolved);
807 assert_eq!(buffer_line(&through_widget, 1), " fg ");
808 assert_eq!(buffer_line(&through_widget, 2), " jk ");
809 }
810
811 #[test]
812 fn partially_visible_anchor_maps_destination_and_source_offset() {
813 let view = View::viewport(
814 Viewport::horizontal(Projection::new(2, ProjectionBoundary::Preserve)),
815 View::anchor_block(
816 "foreign",
817 BlockStyle::new()
818 .width(Length::Cells(4))
819 .height(Length::Cells(1)),
820 View::empty(),
821 ),
822 );
823 let area = Rect::new(10, 5, 3, 1);
824 let resolved = resolve(&view, available(area)).expect("finite viewport geometry");
825 let anchor = resolved.anchors().first().expect("foreign anchor");
826
827 let placement = anchor_placement(anchor, area).expect("partially visible placement");
828
829 assert_eq!(placement.logical().x(), -2);
830 assert_eq!(placement.logical().width(), 4);
831 assert_eq!(placement.destination(), Rect::new(10, 5, 2, 1));
832 assert_eq!(placement.source_column(), 2);
833 assert_eq!(placement.source_row(), 0);
834 }
835
836 fn assert_widget_matches_direct(style: &BlockStyle, content: &str, case: &str) {
838 let view = View::block(
839 style.clone(),
840 View::text(content, style.text_style().clone()),
841 );
842 let direct = resolve(&view, Available::NONE).unwrap();
843 let expected: Vec<String> = direct
844 .rows()
845 .iter()
846 .map(|row| row.iter().map(StyledGrapheme::symbol).collect())
847 .collect();
848 let area = Rect::new(
849 0,
850 0,
851 u16::try_from(direct.size().width()).expect("block width"),
852 u16::try_from(direct.size().height()).expect("block height"),
853 );
854 let mut buffer = Buffer::empty(area);
855
856 style.widget(content).render(area, &mut buffer);
857
858 for (y, expected_line) in expected.into_iter().enumerate() {
859 assert_eq!(buffer_line(&buffer, y as u16), expected_line, "{case}");
860 }
861 }
862
863 fn buffer_line(buffer: &Buffer, y: u16) -> String {
864 let mut line = String::new();
865 let mut x = buffer.area.left();
866 while x < buffer.area.right() {
867 let symbol = buffer.cell((x, y)).expect("cell").symbol();
868 line.push_str(symbol);
869 let width = urushi::PrintableText::new(symbol)
870 .width()
871 .max(1)
872 .min(usize::from(u16::MAX)) as u16;
873 x = x.saturating_add(width);
874 }
875 line
876 }
877}