bubbletea_widgets/textinput/
view.rs

1//! View rendering methods for the textinput component.
2
3use super::model::Model;
4use super::types::EchoMode;
5
6impl Model {
7    /// View renders the textinput in its current state.
8    /// Matches Go's View method exactly.
9    pub fn view(&self) -> String {
10        // Placeholder text
11        if self.value.is_empty() && !self.placeholder.is_empty() {
12            return self.placeholder_view();
13        }
14
15        let value_slice = if self.offset_right <= self.value.len() {
16            &self.value[self.offset..self.offset_right]
17        } else {
18            &self.value[self.offset..]
19        };
20
21        let pos = self.pos.saturating_sub(self.offset);
22        let value_str: String = value_slice.iter().collect();
23        let display_value = self.echo_transform(&value_str);
24
25        let mut v = String::new();
26
27        // Text before cursor
28        if pos < display_value.len() {
29            v.push_str(&self.text_style.render(&display_value[..pos]));
30        } else {
31            v.push_str(&self.text_style.render(&display_value));
32        }
33
34        // Cursor and text under it
35        if pos < display_value.len() {
36            let char_at_pos = display_value.chars().nth(pos).unwrap_or(' ');
37            let mut cur = self.cursor.clone();
38            cur.set_char(&char_at_pos.to_string());
39            v.push_str(&cur.view());
40
41            // Text after cursor
42            if pos + 1 < display_value.len() {
43                v.push_str(&self.text_style.render(&display_value[pos + 1..]));
44            }
45
46            v.push_str(&self.completion_view(0));
47        } else {
48            // Cursor at end
49            if self.focus && self.can_accept_suggestion() {
50                let suggestion = &self.matched_suggestions[self.current_suggestion_index];
51                if self.value.len() < suggestion.len() {
52                    let next_char = suggestion[pos];
53                    let mut cur = self.cursor.clone();
54                    cur.set_char(&next_char.to_string());
55                    v.push_str(&cur.view());
56                    v.push_str(&self.completion_view(1));
57                } else {
58                    let mut cur = self.cursor.clone();
59                    cur.set_char(" ");
60                    v.push_str(&cur.view());
61                }
62            } else {
63                let mut cur = self.cursor.clone();
64                cur.set_char(" ");
65                v.push_str(&cur.view());
66            }
67        }
68
69        // Fill remaining width with background
70        let val_width = display_value.chars().count();
71        if self.width > 0 && val_width <= self.width as usize {
72            let padding = (self.width as usize).saturating_sub(val_width);
73            if val_width + padding <= self.width as usize && pos < display_value.len() {
74                // padding += 1; // Adjust for cursor
75            }
76            v.push_str(&self.text_style.render(&" ".repeat(padding)));
77        }
78
79        format!("{}{}", self.prompt_style.render(&self.prompt), v)
80    }
81
82    /// Internal placeholder view rendering
83    pub(super) fn placeholder_view(&self) -> String {
84        let mut v = String::new();
85
86        let placeholder_chars: Vec<char> = self.placeholder.chars().collect();
87        let p = if self.width > 0 {
88            let mut p_vec = vec![' '; self.width as usize + 1];
89            for (i, &ch) in placeholder_chars.iter().enumerate() {
90                if i < p_vec.len() {
91                    p_vec[i] = ch;
92                }
93            }
94            p_vec
95        } else {
96            placeholder_chars
97        };
98
99        if !p.is_empty() {
100            let mut cur = self.cursor.clone();
101            cur.set_char(&p[0].to_string());
102            v.push_str(&cur.view());
103        }
104
105        if self.width < 1 && p.len() <= 1 {
106            return format!("{}{}", self.prompt_style.render(&self.prompt), v);
107        }
108
109        if self.width > 0 {
110            let min_width = self.placeholder.chars().count();
111            let avail_width = (self.width as usize).saturating_sub(min_width) + 1;
112
113            if p.len() > 1 {
114                let end_idx = std::cmp::min(p.len(), min_width);
115                let text: String = p[1..end_idx].iter().collect();
116                v.push_str(&self.placeholder_style.render(&text));
117            }
118            v.push_str(&self.placeholder_style.render(&" ".repeat(avail_width)));
119        } else if p.len() > 1 {
120            // Render remaining placeholder text after the cursor character
121            let text: String = p[1..].iter().collect();
122            v.push_str(&self.placeholder_style.render(&text));
123        }
124
125        format!("{}{}", self.prompt_style.render(&self.prompt), v)
126    }
127
128    /// Internal echo transformation
129    pub(super) fn echo_transform(&self, v: &str) -> String {
130        match self.echo_mode {
131            EchoMode::EchoPassword => {
132                let width = v.chars().count();
133                self.echo_character.to_string().repeat(width)
134            }
135            EchoMode::EchoNone => String::new(),
136            EchoMode::EchoNormal => v.to_string(),
137        }
138    }
139
140    /// Internal completion view rendering
141    pub(super) fn completion_view(&self, offset: usize) -> String {
142        if self.can_accept_suggestion() {
143            let suggestion = &self.matched_suggestions[self.current_suggestion_index];
144            if self.value.len() + offset < suggestion.len() {
145                let remaining: String = suggestion[self.value.len() + offset..].iter().collect();
146                return self.completion_style.render(&remaining);
147            }
148        }
149        String::new()
150    }
151}