Skip to main content

hefesto_widgets/popups/text_input_popup/
mod.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Constraint, Layout, Rect},
4    style::{Color, Style},
5    widgets::{Borders, StatefulWidget},
6};
7
8use crate::popup::{Popup, PopupSize};
9use crate::popups::AutoSized;
10use crate::text_input::{TextInput, TextInputState};
11use crate::{BadgeStack, BorderType, DotPattern};
12
13/// Popup que envuelve un `TextInput` para capturar texto del usuario.
14///
15/// Compone un `Popup` (chrome) con un `TextInput` (campo de texto centrado).
16/// Hereda la configuración del `TextInput` a través de builders delegados.
17///
18/// El popup se auto-dimensiona según el alto requerido por el `TextInput`,
19/// y centra verticalmente el campo dentro del área disponible.
20#[derive(Clone)]
21pub struct TextInputPopup<'a> {
22    popup: Popup<'a>,
23    text_input: TextInput<'a>,
24}
25
26impl<'a> TextInputPopup<'a> {
27    /// Crea un `TextInputPopup` con valores predeterminados.
28    ///
29    /// Por defecto: `Popup` con borde redondeado y padding 0,
30    /// `TextInput` con borders invisibles (`Borders::NONE`)
31    /// y `BorderType::None` (la entrada se renderiza sin borde propio).
32    pub fn new() -> Self {
33        Self {
34            popup: Popup::new(Color::White)
35                .padding(0)
36                .border_type(BorderType::Rounded),
37            text_input: TextInput::new(),
38        }
39    }
40
41    pub fn title(mut self, title: &'a str) -> Self {
42        self.popup = self.popup.title(title);
43        self
44    }
45
46    pub fn cursor_style(mut self, style: Style) -> Self {
47        self.text_input = self.text_input.cursor_style(style);
48        self
49    }
50
51    pub fn text_style(mut self, style: Style) -> Self {
52        self.text_input = self.text_input.text_style(style);
53        self
54    }
55
56    pub fn border_style(mut self, style: Style) -> Self {
57        self.text_input = self.text_input.border_style(style);
58        self
59    }
60
61    pub fn borders(mut self, borders: Borders) -> Self {
62        self.text_input = self.text_input.borders(borders);
63        self
64    }
65
66    /// Background color for the inner `TextInput` content area.
67    /// Use `bg_color` for the popup's background instead.
68    pub fn input_bg_color(mut self, color: Color) -> Self {
69        self.text_input = self.text_input.bg_color(color);
70        self
71    }
72
73    /// Background color for the entire popup area (including border).
74    /// Delegates to `Popup::bg_color`. See `input_bg_color` for the
75    /// inner text input background.
76    pub fn bg_color(mut self, color: Color) -> Self {
77        self.popup = self.popup.bg_color(color);
78        self
79    }
80
81    pub fn scroll_padding(mut self, padding: u16) -> Self {
82        self.text_input = self.text_input.scroll_padding(padding);
83        self
84    }
85
86    pub fn scroll_reserve(mut self, reserve: u16) -> Self {
87        self.text_input = self.text_input.scroll_reserve(reserve);
88        self
89    }
90
91    pub fn rows(mut self, rows: u16) -> Self {
92        self.text_input = self.text_input.rows(rows);
93        self
94    }
95
96    pub fn placeholder(mut self, placeholder: &'a str) -> Self {
97        self.text_input = self.text_input.placeholder(placeholder);
98        self
99    }
100
101    pub fn border_color(mut self, color: Color) -> Self {
102        self.popup = self.popup.border_color(color);
103        self
104    }
105
106    pub fn border_type(mut self, bt: BorderType) -> Self {
107        self.popup = self.popup.border_type(bt);
108        self
109    }
110
111    pub fn width(mut self, w: PopupSize) -> Self {
112        self.popup = self.popup.width(w);
113        self
114    }
115
116    pub fn height(mut self, h: PopupSize) -> Self {
117        self.popup = self.popup.height(h);
118        self
119    }
120
121    pub fn position(mut self, x: u16, y: u16) -> Self {
122        self.popup = self.popup.position(x, y);
123        self
124    }
125
126    pub fn origin(mut self, x: u16, y: u16) -> Self {
127        self.popup = self.popup.origin(x, y);
128        self
129    }
130
131    pub fn header(mut self) -> Self {
132        self.popup = self.popup.header();
133        self
134    }
135
136    pub fn no_background(mut self) -> Self {
137        self.popup = self.popup.no_background();
138        self
139    }
140
141    pub fn background_dots(mut self, color: Color, symbol: &str, density: u16) -> Self {
142        self.popup = self.popup.background_dots(color, symbol, density);
143        self
144    }
145
146    pub fn background_pattern(mut self, pattern: DotPattern) -> Self {
147        self.popup = self.popup.background_pattern(pattern);
148        self
149    }
150
151    pub fn background_spacing(mut self, density_x: u16, density_y: u16) -> Self {
152        self.popup = self.popup.background_spacing(density_x, density_y);
153        self
154    }
155
156    pub fn badges(mut self, badges: BadgeStack<'a>) -> Self {
157        self.popup = self.popup.badges(badges);
158        self
159    }
160}
161
162impl AutoSized for TextInputPopup<'_> {
163    type State = TextInputState;
164
165    fn auto_height(&self, _state: &Self::State, _area: Rect) -> u16 {
166        let header_extra = if self.popup.has_header() { 2 } else { 0 };
167        self.text_input.required_height() + 2 + header_extra
168    }
169}
170
171impl<'a> TextInputPopup<'a> {
172    /// Resolves the final `Rect` for this popup within the given `area`,
173    /// using the text input state for Auto-height resolution.
174    ///
175    /// See [`AutoSized::auto_height`].
176    pub fn resolve_rect(&self, area: Rect, state: &TextInputState) -> Rect {
177        if self.popup.get_height() == PopupSize::Auto {
178            self.popup
179                .clone()
180                .height(PopupSize::Fixed(self.auto_height(state, area)))
181                .resolve_rect(area)
182        } else {
183            self.popup.resolve_rect(area)
184        }
185    }
186}
187
188impl StatefulWidget for TextInputPopup<'_> {
189    type State = TextInputState;
190
191    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
192        let input_height = self.text_input.required_height();
193
194        let auto_h = if self.popup.get_height() == PopupSize::Auto {
195            Some(self.auto_height(state, area))
196        } else {
197            None
198        };
199        let mut popup = self.popup;
200        if let Some(h) = auto_h {
201            popup = popup.height(PopupSize::Fixed(h));
202        }
203        let inner = popup.render_inner(area, buf);
204
205        // Split inner area to place text_input in center
206        let chunks = Layout::vertical([
207            Constraint::Min(0),
208            Constraint::Length(input_height),
209            Constraint::Min(0),
210        ]).split(inner);
211
212        self.text_input.render(chunks[1], buf, state);
213    }
214}
215
216#[cfg(test)]
217mod tests;