1use std::fmt::Debug;
2
3use planus_buffer_inspection::{InspectableFlatbuffer, Object};
4use planus_types::intermediate::Declarations;
5use ratatui::{
6 buffer::Buffer,
7 layout::{Alignment, Constraint, Direction, Layout, Rect},
8 style::{Color, Modifier, Style},
9 text::{Line, Span},
10 widgets::{Block, BorderType, Borders, Clear, Paragraph, Widget, Wrap},
11 Frame,
12};
13
14use crate::{
15 vec_with_index::VecWithIndex, ActiveWindow, HexViewState, InfoViewData, Inspector, ModalState,
16 RangeMatch, ViewState,
17};
18
19const DARK_BLUE: Color = Color::Rgb(62, 103, 113);
20const DARK_GREEN: Color = Color::Rgb(100, 88, 55);
21const DARK_MAGENTA: Color = Color::Rgb(114, 77, 106);
22const WHITE: Color = Color::Rgb(241, 242, 216);
23const GREY: Color = Color::Rgb(145, 145, 133);
24const BLACK: Color = Color::Rgb(0, 0, 0);
25const RED: Color = Color::Red;
26
27const CURSOR_STYLE: Style = Style {
28 fg: Some(WHITE),
29 bg: None,
30 add_modifier: Modifier::UNDERLINED,
31 sub_modifier: Modifier::DIM,
32 underline_color: None,
33};
34
35const INNER_AREA_STYLE: Style = Style {
36 fg: Some(WHITE),
37 bg: Some(DARK_MAGENTA),
38 add_modifier: Modifier::empty(),
39 sub_modifier: Modifier::empty(),
40 underline_color: None,
41};
42
43const OUTER_AREA_STYLE: Style = Style {
44 fg: Some(WHITE),
45 bg: Some(DARK_BLUE),
46 add_modifier: Modifier::empty(),
47 sub_modifier: Modifier::empty(),
48 underline_color: None,
49};
50
51const ADDRESS_STYLE: Style = Style {
52 fg: Some(DARK_GREEN),
53 bg: None,
54 add_modifier: Modifier::empty(),
55 sub_modifier: Modifier::empty(),
56 underline_color: None,
57};
58
59const DEFAULT_STYLE: Style = Style {
60 fg: Some(GREY),
61 bg: Some(BLACK),
62 add_modifier: Modifier::DIM,
63 sub_modifier: Modifier::empty(),
64 underline_color: None,
65};
66
67const OFFSET_STYLE: Style = Style {
68 fg: Some(GREY),
69 bg: None,
70 add_modifier: Modifier::DIM,
71 sub_modifier: Modifier::empty(),
72 underline_color: None,
73};
74
75const EMPTY_STYLE: Style = Style {
76 fg: None,
77 bg: None,
78 add_modifier: Modifier::empty(),
79 sub_modifier: Modifier::empty(),
80 underline_color: None,
81};
82
83const ACTIVE_STYLE: Style = Style {
84 fg: Some(WHITE),
85 bg: None,
86 add_modifier: Modifier::empty(),
87 sub_modifier: Modifier::empty(),
88 underline_color: None,
89};
90
91const ALERT_STYLE: Style = Style {
92 fg: Some(RED),
93 bg: None,
94 add_modifier: Modifier::BOLD,
95 sub_modifier: Modifier::DIM,
96 underline_color: None,
97};
98
99pub fn draw(f: &mut Frame, inspector: &mut Inspector) {
100 inspector.view_state.draw_main_ui(
101 f,
102 inspector.active_window,
103 inspector.modal.is_some(),
104 &inspector.buffer,
105 &mut inspector.hex_view_state,
106 );
107
108 if let Some(modal_state) = inspector.modal.as_ref() {
109 let modal_area = if let ModalState::GoToByte { .. } = modal_state {
110 let mut area = centered_rect(20, 20, f.area());
111 area.height = 3;
112 area.width = 21;
113 area
114 } else {
115 centered_rect(60, 60, f.area())
116 };
117 inspector.view_state.draw_modal_view(
118 f,
119 modal_area,
120 modal_state,
121 &inspector.hex_view_state,
122 &inspector.view_stack,
123 inspector.buffer.declarations,
124 );
125 }
126}
127
128impl<'a> ViewState<'a> {
129 fn draw_main_ui(
130 &mut self,
131 f: &mut Frame,
132 active_window: ActiveWindow,
133 modal_is_active: bool,
134 buffer: &InspectableFlatbuffer<'a>,
135 hex_view_state: &mut HexViewState,
136 ) {
137 use Constraint::*;
138
139 let areas = Layout::default()
140 .direction(Direction::Vertical)
141 .constraints([Length(f.area().height.saturating_sub(1)), Min(0)].as_ref())
142 .split(f.area());
143
144 let main_area = areas[0];
145 let legend_area = areas[1];
146
147 let areas = Layout::default()
148 .direction(Direction::Vertical)
149 .constraints([Percentage(60), Percentage(40)].as_ref())
150 .split(main_area);
151
152 let top_area = areas[0];
153 let hex_area = areas[1];
154
155 let areas = Layout::default()
156 .direction(Direction::Horizontal)
157 .constraints([Length(top_area.width.saturating_sub(45)), Min(0)].as_ref())
158 .split(top_area);
159
160 let object_area = areas[0];
161 let info_area = areas[1];
162
163 self.draw_object_view(
164 f,
165 object_area,
166 matches!(active_window, ActiveWindow::ObjectView) && !modal_is_active,
167 );
168 self.draw_hex_view(
169 f,
170 hex_area,
171 matches!(active_window, ActiveWindow::HexView) && !modal_is_active,
172 buffer,
173 hex_view_state,
174 );
175 self.draw_info_view(f, info_area, hex_view_state, buffer.declarations);
176 self.draw_legend_view(f, legend_area, active_window);
177 }
178
179 pub fn draw_hex_view(
180 &self,
181 f: &mut Frame,
182 area: Rect,
183 is_active: bool,
184 buffer: &InspectableFlatbuffer<'a>,
185 hex_view_state: &mut HexViewState,
186 ) {
187 let block = block(is_active, " Hex view ", DEFAULT_STYLE);
188 let inner_area = block.inner(area);
189
190 hex_view_state.update_for_area(buffer.buffer.len(), self.byte_index, inner_area);
191
192 let paragraph = self
193 .hex_view(buffer, inner_area, is_active, hex_view_state)
194 .block(block);
195 f.render_widget(paragraph, area);
196 }
197
198 fn hex_view(
199 &self,
200 buffer: &InspectableFlatbuffer<'a>,
201 area: Rect,
202 is_active: bool,
203 hex_view_state: &HexViewState,
204 ) -> Paragraph<'_> {
205 let mut view = Vec::new();
206 if area.height != 0 && hex_view_state.line_size != 0 {
207 let first_line = hex_view_state.line_pos / hex_view_state.line_size;
208 let ranges = self.hex_ranges();
209
210 for (line_no, chunk) in buffer
211 .buffer
212 .chunks(hex_view_state.line_size)
213 .skip(first_line)
214 .take(area.height as usize)
215 .enumerate()
216 {
217 let mut line = vec![
218 Span::styled(
219 format!(
220 "{:0width$x}",
221 (first_line + line_no) * hex_view_state.line_size,
222 width = hex_view_state.max_offset_hex_digits
223 ),
224 ADDRESS_STYLE,
225 ),
226 Span::styled(" ", EMPTY_STYLE),
227 ];
228 for (col_no, b) in chunk.iter().enumerate() {
229 let pos = (line_no + first_line) * hex_view_state.line_size + col_no;
230
231 let mut style = EMPTY_STYLE;
232
233 if is_active && pos == self.byte_index {
234 style = style.patch(CURSOR_STYLE);
235 }
236
237 match ranges.best_match(pos) {
238 Some(RangeMatch::Outer) => {
239 style = style.patch(OUTER_AREA_STYLE);
240 }
241 Some(RangeMatch::Inner) => {
242 style = style.patch(INNER_AREA_STYLE);
243 }
244 None => (),
245 }
246
247 line.push(Span::styled(format!("{b:02x}"), style));
248 if col_no + 1 < chunk.len() {
249 let style = match (ranges.best_match(pos), ranges.best_match(pos + 1)) {
250 (None, _) | (_, None) => EMPTY_STYLE,
251 (Some(RangeMatch::Outer), Some(_))
252 | (Some(_), Some(RangeMatch::Outer)) => OUTER_AREA_STYLE,
253 (Some(RangeMatch::Inner), Some(RangeMatch::Inner)) => INNER_AREA_STYLE,
254 };
255 if col_no % 8 != 7 {
256 line.push(Span::styled(" ", style));
257 } else {
258 line.push(Span::styled(" ", style));
259 }
260 }
261 }
262 view.push(Line::from(line));
263 }
264 }
265
266 Paragraph::new(view)
267 }
268
269 pub fn draw_object_view(&mut self, f: &mut Frame, area: Rect, is_active: bool) {
270 let block = block(is_active, " Object view ", OUTER_AREA_STYLE);
271 let inner_area = block.inner(area);
272
273 if let Some(info_view_data) = &mut self.info_view_data {
274 if area.height <= 4 {
275 info_view_data.first_line_shown = info_view_data.first_line_shown.clamp(
276 (info_view_data.lines.index() + 1).saturating_sub(inner_area.height as usize),
277 info_view_data.lines.index(),
278 );
279 } else {
280 info_view_data.first_line_shown = info_view_data.first_line_shown.clamp(
281 (info_view_data.lines.index() + 2).saturating_sub(inner_area.height as usize),
282 info_view_data.lines.index().saturating_sub(1),
283 );
284 }
285 }
286
287 let widget = ObjectViewWidget::new(self.info_view_data.as_ref(), is_active).block(block);
288 f.render_widget(widget, area);
289 }
290
291 fn legend_view(&self, _active_window: ActiveWindow) -> Paragraph<'_> {
292 let text = "?: help menu arrow keys: move cursor enter: follow pointer tab: cycle view focus";
293 Paragraph::new(Span::styled(text, DEFAULT_STYLE))
294 }
295
296 fn draw_legend_view(&self, f: &mut Frame, area: Rect, active_window: ActiveWindow) {
297 let paragraph = self.legend_view(active_window);
298 f.render_widget(paragraph, area);
299 }
300
301 pub fn draw_info_view(
302 &self,
303 f: &mut Frame,
304 area: Rect,
305 hex_view_state: &HexViewState,
306 declarations: &Declarations,
307 ) {
308 let block = block(false, " Info view ", DEFAULT_STYLE);
309
310 let paragraph = self
311 .info_view(self.info_view_data.as_ref(), hex_view_state, declarations)
312 .block(block);
313 f.render_widget(paragraph, area);
314 }
315
316 fn info_view(
317 &self,
318 info_view_data: Option<&InfoViewData>,
319 hex_view_state: &HexViewState,
320 declarations: &Declarations,
321 ) -> Paragraph<'_> {
322 let mut text = Vec::new();
323
324 text.push(Line::from(vec![
325 Span::styled("Cursor", CURSOR_STYLE),
326 Span::raw(format!(
327 " {:0width$x}",
328 self.byte_index,
329 width = hex_view_state.max_offset_hex_digits
330 )),
331 ]));
332 if let Some(info_view_area) = &info_view_data {
333 let line = info_view_area.lines.cur();
334 text.push(Line::from(vec![
335 Span::styled("Inner", INNER_AREA_STYLE),
336 Span::raw(format!(
337 " {:0width$x}-{:0width$x}",
338 line.start,
339 line.end - 1,
340 width = hex_view_state.max_offset_hex_digits
341 )),
342 ]));
343
344 text.push(Line::from(Span::raw(format!(
345 " {}",
346 line.object.type_name(declarations)
347 ))));
348 } else {
349 text.push(Line::from(Span::styled("Inner", INNER_AREA_STYLE)));
350 text.push(Line::from(Span::raw(" -")));
351 }
352 if let Some(info_view_area) = &info_view_data {
353 let line = &info_view_area.lines[0];
354 text.push(Line::from(vec![
355 Span::styled("Outer", OUTER_AREA_STYLE),
356 Span::raw(format!(
357 " {:0width$x}-{:0width$x}",
358 line.start,
359 line.end - 1,
360 width = hex_view_state.max_offset_hex_digits
361 )),
362 ]));
363 text.push(Line::from(Span::raw(format!(
364 " {}",
365 line.object.type_name(declarations)
366 ))));
367 } else {
368 text.push(Line::from(Span::styled("Outer", OUTER_AREA_STYLE)));
369 text.push(Line::from(Span::raw(" -")));
370 }
371
372 if let Some(info_view_data) = &info_view_data {
373 if info_view_data.interpretations.len() > 1 {
374 text.push(Line::from(Span::raw("")));
375 text.push(Line::from(Span::styled(
376 format!(
377 "Interpretation: {}/{}",
378 info_view_data.interpretations.index() + 1,
379 info_view_data.interpretations.len()
380 ),
381 ALERT_STYLE,
382 )));
383 text.push(Line::from(Span::raw("[c]: Cycle interpretations")));
384 text.push(Line::from(Span::raw("[i]: Pick interpretation")));
385 }
386 }
387
388 Paragraph::new(text).wrap(Wrap { trim: false })
389 }
390
391 fn draw_modal_view(
392 &self,
393 f: &mut Frame,
394 area: Rect,
395 modal_state: &ModalState,
396 hex_view_state: &HexViewState,
397 view_stack: &[ViewState<'a>],
398 declarations: &Declarations,
399 ) {
400 f.render_widget(Clear, area);
401 let subareas = Layout::default()
402 .direction(Direction::Horizontal)
403 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
404 .split(area);
405 let left = subareas[0];
406 let right = subareas[1];
407
408 let mut info_view_data = None;
409
410 match modal_state {
411 ModalState::GoToByte { input } => {
412 let text = vec![
413 Line::from(vec![
414 Span::styled("0x", DEFAULT_STYLE),
415 Span::styled(input, ACTIVE_STYLE),
416 ]),
417 Line::default(),
418 ];
419 let block = block(true, " Go to offset ", DEFAULT_STYLE);
420 f.set_cursor_position((
421 area.x + input.len() as u16 + 3,
423 area.y + 1,
425 ));
426 f.render_widget(
427 Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
428 area,
429 );
430 }
431 ModalState::XRefs { .. } => {
432 let text = vec![
433 Line::from(Span::styled("0x", DEFAULT_STYLE)),
434 Line::default(),
435 ];
436 let block = block(true, " XRefs ", DEFAULT_STYLE);
437 f.render_widget(
438 Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
439 left,
440 );
441 }
442 ModalState::ViewHistory { index } => {
443 let mut text = Vec::new();
444
445 for (line_no, view) in view_stack.iter().chain(std::iter::once(self)).enumerate() {
446 let byte_index = view.byte_index;
447 let name = if let Some(info) = &view.info_view_data {
448 &info.lines.cur().name
449 } else {
450 "no object"
451 };
452
453 let style = if *index == line_no {
454 CURSOR_STYLE
455 } else {
456 DEFAULT_STYLE
457 };
458 text.push(Line::from(Span::styled(
459 format!(
460 "{byte_index:0width$x} {name}",
461 width = hex_view_state.max_offset_hex_digits
462 ),
463 style,
464 )));
465 }
466 if *index < view_stack.len() {
467 info_view_data = view_stack.get(*index);
468 } else {
469 info_view_data = Some(self);
470 }
471
472 let block = block(true, " History ", DEFAULT_STYLE);
473 f.render_widget(
474 Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
475 left,
476 );
477 }
478 ModalState::HelpMenu => {
479 let text = vec![
480 Line::from(Span::styled(
481 "Tab: cycle Object/Hex view focus",
482 DEFAULT_STYLE,
483 )),
484 Line::from(Span::styled("Arrow keys: move cursor", DEFAULT_STYLE)),
485 Line::from(Span::styled(
486 "Enter: go to entry / follow pointer",
487 DEFAULT_STYLE,
488 )),
489 Line::from(Span::styled(
490 "Backspace: return from entry / pointer",
491 DEFAULT_STYLE,
492 )),
493 Line::from(Span::styled("H: open history modal", DEFAULT_STYLE)),
494 Line::from(Span::styled("G: go to offset", DEFAULT_STYLE)),
495 Line::from(Span::styled("I: open interpretations modal", DEFAULT_STYLE)),
496 Line::from(Span::styled(
497 "C: cycle between interpretations",
498 DEFAULT_STYLE,
499 )),
500 Line::from(Span::styled("Q: quit", DEFAULT_STYLE)),
501 ];
502
503 let block = block(true, " Help ", DEFAULT_STYLE);
504 f.render_widget(
505 Paragraph::new(text).block(block).wrap(Wrap { trim: false }),
506 area,
507 );
508 }
509 ModalState::TreeView { state, header } => {
510 f.render_widget(
511 TreeStateWidget {
512 tree_state: state,
513 block: Some(block(true, header, DEFAULT_STYLE)),
514 },
515 left,
516 );
517
518 f.render_widget(
519 ObjectViewWidget {
520 info_view_data: state
521 .lines
522 .cur()
523 .node
524 .view_state
525 .as_ref()
526 .and_then(|v| v.info_view_data.as_ref()),
527 block: Some(block(true, " Preview ", OUTER_AREA_STYLE)),
528 is_active: false,
529 },
530 right,
531 );
532 }
533 };
534
535 if let Some(info_view_data) = info_view_data {
536 let info_view = self.info_view(
537 info_view_data.info_view_data.as_ref(),
538 hex_view_state,
539 declarations,
540 );
541 f.render_widget(info_view, right);
542 }
543 }
544}
545
546fn block(is_active: bool, title: &'static str, inner_style: Style) -> Block<'static> {
547 let res = Block::default()
548 .borders(Borders::ALL)
549 .border_type(BorderType::Rounded)
550 .title_alignment(Alignment::Center)
551 .title(title)
552 .style(inner_style);
553 if is_active {
554 res.border_style(ACTIVE_STYLE)
555 } else {
556 let mut style = DEFAULT_STYLE;
557 if let Some(bg) = inner_style.bg {
558 style = style.bg(bg);
559 }
560 res.border_style(style)
561 }
562}
563
564fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
565 let popup_layout = Layout::default()
566 .direction(Direction::Vertical)
567 .constraints(
568 [
569 Constraint::Percentage((100 - percent_y) / 2),
570 Constraint::Percentage(percent_y),
571 Constraint::Percentage((100 - percent_y) / 2),
572 ]
573 .as_ref(),
574 )
575 .split(r);
576
577 Layout::default()
578 .direction(Direction::Horizontal)
579 .constraints(
580 [
581 Constraint::Percentage((100 - percent_x) / 2),
582 Constraint::Percentage(percent_x),
583 Constraint::Percentage((100 - percent_x) / 2),
584 ]
585 .as_ref(),
586 )
587 .split(popup_layout[1])[1]
588}
589
590struct ObjectViewWidget<'a> {
591 info_view_data: Option<&'a InfoViewData<'a>>,
592 block: Option<Block<'a>>,
593 is_active: bool,
594}
595
596impl<'a> ObjectViewWidget<'a> {
597 pub fn new(info_view_data: Option<&'a InfoViewData<'a>>, is_active: bool) -> Self {
598 Self {
599 info_view_data,
600 block: None,
601 is_active,
602 }
603 }
604
605 pub fn block(mut self, block: Block<'a>) -> Self {
606 self.block = Some(block);
607 self
608 }
609}
610
611impl Widget for ObjectViewWidget<'_> {
612 fn render(mut self, area: Rect, buf: &mut Buffer) {
613 let area = match self.block.take() {
614 Some(b) => {
615 let inner_area = b.inner(area);
616 b.render(area, buf);
617 inner_area
618 }
619 None => area,
620 };
621 if area.height == 0 {
622 return;
623 }
624
625 if let Some(info_view_data) = &self.info_view_data {
626 let selected_line = info_view_data.lines.cur();
627 let mut inner_area = area;
628
629 inner_area.x += selected_line.indentation as u16;
630 inner_area.width =
631 (inner_area.width as usize).saturating_sub(selected_line.indentation) as u16;
632
633 let first_selected_line = selected_line
634 .start_line_index
635 .max(info_view_data.first_line_shown)
636 - info_view_data.first_line_shown;
637 let last_selected_line = selected_line
638 .end_line_index
639 .min(info_view_data.first_line_shown + area.height as usize - 1)
640 - info_view_data.first_line_shown;
641 inner_area.y += first_selected_line as u16;
642 inner_area.height =
643 (inner_area.height as usize).saturating_sub(first_selected_line) as u16;
644
645 inner_area.width = (inner_area.width as usize).min(selected_line.object_width) as u16;
646 inner_area.height = (inner_area.height as usize)
647 .min(last_selected_line - first_selected_line + 1)
648 as u16;
649
650 buf.set_style(inner_area, INNER_AREA_STYLE);
651
652 for (line_index, (i, line)) in info_view_data
653 .lines
654 .iter()
655 .enumerate()
656 .skip(info_view_data.first_line_shown)
657 .enumerate()
658 .take(area.height as usize)
659 {
660 let style = if i == info_view_data.lines.index() && self.is_active {
661 CURSOR_STYLE
662 } else {
663 EMPTY_STYLE
664 };
665
666 buf.set_stringn(
667 area.left() + line.indentation as u16,
668 area.top() + line_index as u16,
669 &line.line,
670 area.width as usize - line.indentation,
671 style,
672 );
673
674 if matches!(line.object, Object::Offset(_)) {
675 buf.set_string(
676 area.left(),
677 area.top() + line_index as u16,
678 "*",
679 OFFSET_STYLE,
680 );
681 }
682 }
683 }
684 }
685}
686
687pub struct Node<'a> {
688 pub text: String,
689 pub view_state: Option<ViewState<'a>>,
690 pub children: Option<Box<dyn Fn() -> Vec<Node<'a>>>>,
691}
692
693impl Debug for Node<'_> {
694 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695 f.debug_struct("Node")
696 .field("text", &self.text)
697 .field("view_state", &self.view_state)
698 .finish_non_exhaustive()
699 }
700}
701
702#[derive(Debug)]
703pub struct TreeState<'a> {
704 pub lines: VecWithIndex<TreeStateLine<'a>>,
705}
706
707impl TreeState<'_> {
708 pub fn toggle_fold(&mut self) {
709 let line = self.lines.cur_mut();
710 match line.fold_state {
711 FoldState::NoChildren => (),
712 FoldState::Folded => {
713 if let Some(children_gen) = &line.node.children {
714 let nodes = children_gen();
715 let new_lines = nodes
716 .into_iter()
717 .map(|node| TreeStateLine {
718 indent_level: line.indent_level + 2,
719 fold_state: if node.children.is_some() {
720 FoldState::Folded
721 } else {
722 FoldState::NoChildren
723 },
724 node,
725 })
726 .collect::<Vec<_>>();
727 line.fold_state = FoldState::Unfolded;
728 self.lines.insert(new_lines);
729 } else {
730 line.fold_state = FoldState::NoChildren;
732 }
733 }
734 FoldState::Unfolded => {
735 line.fold_state = FoldState::Folded;
736 let indent_level = line.indent_level;
737 self.lines.remove_while(|l| indent_level < l.indent_level);
738 }
739 }
740 }
741}
742
743#[derive(Debug)]
744pub struct TreeStateLine<'a> {
745 pub indent_level: usize,
746 pub node: Node<'a>,
747 pub fold_state: FoldState,
748}
749
750#[derive(Copy, Clone, Debug)]
751pub enum FoldState {
752 NoChildren,
753 Folded,
754 Unfolded,
755}
756
757struct TreeStateWidget<'a, 'b> {
758 pub tree_state: &'b TreeState<'a>,
759 pub block: Option<Block<'a>>,
760}
761
762impl Widget for TreeStateWidget<'_, '_> {
763 fn render(mut self, area: Rect, buf: &mut Buffer) {
764 let area = match self.block.take() {
765 Some(b) => {
766 let inner_area = b.inner(area);
767 b.render(area, buf);
768 inner_area
769 }
770 None => area,
771 };
772 if area.height == 0 {
773 return;
774 }
775
776 for (i, line) in self.tree_state.lines.iter().enumerate() {
777 let style = if i == self.tree_state.lines.index() {
778 CURSOR_STYLE
779 } else {
780 EMPTY_STYLE
781 };
782
783 let suffix = match line.fold_state {
784 FoldState::NoChildren => "",
785 FoldState::Folded => " [+]",
786 FoldState::Unfolded => " [-]",
787 };
788
789 buf.set_stringn(
790 area.left() + 2 * line.indent_level as u16,
791 area.top() + i as u16,
792 format!("{}{suffix}", line.node.text),
793 area.width as usize - line.indent_level,
794 style,
795 );
796 }
797 }
798}