ratatui_toolkit/widgets/code_diff/extensions/file_tree/traits/
widget.rs

1//! Widget trait implementations for DiffFileTree.
2//!
3//! Uses TreeViewRef to avoid cloning nodes on every render.
4
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use ratatui::style::{Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::Widget;
10
11use crate::primitives::tree_view::{matches_filter, TreeViewRef};
12use crate::services::theme::AppTheme;
13use crate::widgets::code_diff::diff_file_tree::{DiffFileEntry, DiffFileTree};
14
15use super::render_entry::render_entry;
16
17/// Filter function for DiffFileEntry nodes.
18///
19/// Returns true if the entry's name matches the filter (case-insensitive contains).
20fn entry_matches_filter(entry: &DiffFileEntry, filter: &Option<String>) -> bool {
21    matches_filter(&entry.name, filter)
22}
23
24/// Renders the filter input line at the bottom of the tree.
25fn render_filter_line(
26    filter_text: Option<&str>,
27    filter_mode: bool,
28    area: Rect,
29    buf: &mut Buffer,
30    theme: &AppTheme,
31) {
32    if area.height == 0 {
33        return;
34    }
35
36    let y = area.y + area.height - 1;
37
38    // Build the filter line
39    let filter_str = filter_text.unwrap_or("");
40    let cursor = if filter_mode { "_" } else { "" };
41
42    let line = Line::from(vec![
43        Span::styled(
44            "/ ",
45            Style::default()
46                .fg(theme.warning)
47                .add_modifier(Modifier::BOLD),
48        ),
49        Span::styled(filter_str, Style::default().fg(theme.text)),
50        Span::styled(
51            cursor,
52            Style::default()
53                .fg(theme.warning)
54                .add_modifier(Modifier::SLOW_BLINK),
55        ),
56    ]);
57
58    // Fill background for the filter line
59    let bg_style = Style::default().bg(theme.background_panel);
60    for x in area.x..(area.x + area.width) {
61        buf[(x, y)].set_style(bg_style);
62    }
63
64    buf.set_line(area.x, y, &line, area.width);
65}
66
67impl Widget for DiffFileTree {
68    /// Renders the diff file tree widget to the given buffer.
69    ///
70    /// Uses TreeViewRef internally to render the tree with custom styling.
71    /// When filter mode is active, shows a filter input at the bottom.
72    fn render(mut self, area: Rect, buf: &mut Buffer) {
73        if area.width == 0 || area.height == 0 {
74            return;
75        }
76
77        let focused = self.focused;
78        let theme = self.theme.clone();
79        let filter_mode = self.state.filter_mode;
80        let has_filter = self.state.filter.as_ref().map_or(false, |f| !f.is_empty());
81        let show_filter_line = filter_mode || has_filter;
82
83        // Calculate tree area (leave room for filter line if needed)
84        let tree_area = if show_filter_line && area.height > 1 {
85            Rect {
86                height: area.height - 1,
87                ..area
88            }
89        } else {
90            area
91        };
92
93        let highlight_bg = theme.background_element;
94        let icon_style = Style::default().fg(theme.info);
95        let theme_for_render = theme.clone();
96
97        let tree_view = TreeViewRef::new(&self.nodes)
98            .icons("\u{F07B}", "\u{F07C}") // Nerd font folder icons (closed, open)
99            .icon_style(icon_style)
100            .render_fn(move |entry, node_state| {
101                render_entry(entry, node_state, focused, &theme_for_render)
102            })
103            .filter_fn(entry_matches_filter)
104            .highlight_style(Style::default().bg(highlight_bg));
105
106        ratatui::widgets::StatefulWidget::render(tree_view, tree_area, buf, &mut self.state);
107
108        // Render filter line if needed
109        if show_filter_line && area.height > 1 {
110            render_filter_line(self.state.filter.as_deref(), filter_mode, area, buf, &theme);
111        }
112    }
113}
114
115impl Widget for &DiffFileTree {
116    /// Renders the diff file tree widget from a reference.
117    ///
118    /// Note: This creates a clone of the state for rendering.
119    /// When filter mode is active, shows a filter input at the bottom.
120    fn render(self, area: Rect, buf: &mut Buffer) {
121        if area.width == 0 || area.height == 0 {
122            return;
123        }
124
125        let focused = self.focused;
126        let theme = self.theme.clone();
127        let filter_mode = self.state.filter_mode;
128        let has_filter = self.state.filter.as_ref().map_or(false, |f| !f.is_empty());
129        let show_filter_line = filter_mode || has_filter;
130        let mut state = self.state.clone();
131
132        // Calculate tree area (leave room for filter line if needed)
133        let tree_area = if show_filter_line && area.height > 1 {
134            Rect {
135                height: area.height - 1,
136                ..area
137            }
138        } else {
139            area
140        };
141
142        let highlight_bg = theme.background_element;
143        let icon_style = Style::default().fg(theme.info);
144        let theme_for_render = theme.clone();
145
146        let tree_view = TreeViewRef::new(&self.nodes)
147            .icons("\u{F07B}", "\u{F07C}") // Nerd font folder icons (closed, open)
148            .icon_style(icon_style)
149            .render_fn(move |entry, node_state| {
150                render_entry(entry, node_state, focused, &theme_for_render)
151            })
152            .filter_fn(entry_matches_filter)
153            .highlight_style(Style::default().bg(highlight_bg));
154
155        ratatui::widgets::StatefulWidget::render(tree_view, tree_area, buf, &mut state);
156
157        // Render filter line if needed
158        if show_filter_line && area.height > 1 {
159            render_filter_line(self.state.filter.as_deref(), filter_mode, area, buf, &theme);
160        }
161    }
162}