Skip to main content

planus_inspector/
ui.rs

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                    // Put cursor past the end of the input text
422                    area.x + input.len() as u16 + 3,
423                    // Move one line down, from the border to the input line
424                    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                    // This should never happen, but we just ignore it
731                    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}