rat_dialog/decorations/
window_frame.rs

1//!
2//! Widget for a moveable window.
3//!
4use crate::decorations::frame_state::{WindowFrameState, WindowFrameStyle};
5use rat_focus::HasFocus;
6use ratatui::buffer::Buffer;
7use ratatui::layout::Rect;
8use ratatui::prelude::BlockExt;
9use ratatui::style::Style;
10use ratatui::text::Span;
11use ratatui::widgets::{Block, StatefulWidget, Widget};
12
13/// Widget for a moveable window.
14///
15/// This widget ignores the area given to render,
16/// and uses the area stored in the state instead.
17/// The area given to render is used as the outer limit for
18/// the window instead.
19///
20/// Render this widget and then use WindowState::widget_area to
21/// render your content.
22///
23/// It can handle events for move/resize/close.
24#[derive(Debug, Default)]
25pub struct WindowFrame<'a> {
26    no_fill: bool,
27    block: Option<Block<'a>>,
28
29    style: Style,
30    top_style: Option<Style>,
31    focus_style: Option<Style>,
32    hover_style: Style,
33    drag_style: Style,
34    close_style: Option<Style>,
35    min_style: Option<Style>,
36    max_style: Option<Style>,
37
38    limit: Option<Rect>,
39
40    can_move: Option<bool>,
41    can_resize: Option<bool>,
42    can_close: Option<bool>,
43    can_min: Option<bool>,
44    can_max: Option<bool>,
45}
46
47impl<'a> WindowFrame<'a> {
48    pub fn new() -> Self {
49        Self {
50            no_fill: Default::default(),
51            block: Default::default(),
52            style: Default::default(),
53            top_style: Default::default(),
54            focus_style: Default::default(),
55            hover_style: Default::default(),
56            drag_style: Default::default(),
57            close_style: Default::default(),
58            min_style: Default::default(),
59            max_style: Default::default(),
60            limit: Default::default(),
61            can_move: Default::default(),
62            can_resize: Default::default(),
63            can_close: Default::default(),
64            can_min: Default::default(),
65            can_max: Default::default(),
66        }
67    }
68
69    /// Don't fill the area.
70    pub fn no_fill(mut self) -> Self {
71        self.no_fill = true;
72        self
73    }
74
75    /// Limits for the window.
76    ///
77    /// If this is not set, the area given to render will be used.
78    pub fn limit(mut self, area: Rect) -> Self {
79        self.limit = Some(area);
80        self
81    }
82
83    /// Window can be moved?
84    pub fn can_move(mut self, v: bool) -> Self {
85        self.can_move = Some(v);
86        self
87    }
88
89    /// Window can be resized?
90    pub fn can_resize(mut self, v: bool) -> Self {
91        self.can_resize = Some(v);
92        self
93    }
94
95    /// Window can be closed?
96    pub fn can_close(mut self, v: bool) -> Self {
97        self.can_close = Some(v);
98        self
99    }
100
101    /// Window can be minimized?
102    pub fn can_min(mut self, v: bool) -> Self {
103        self.can_min = Some(v);
104        self
105    }
106
107    /// Window can be maximized?
108    pub fn can_max(mut self, v: bool) -> Self {
109        self.can_max = Some(v);
110        self
111    }
112
113    /// Window block
114    pub fn block(mut self, block: Block<'a>) -> Self {
115        self.block = Some(block.style(self.style));
116        self
117    }
118
119    pub fn styles(mut self, styles: WindowFrameStyle) -> Self {
120        self.style = styles.style;
121        self.block = styles.block;
122        if styles.top.is_some() {
123            self.top_style = styles.top;
124        }
125        if styles.focus.is_some() {
126            self.focus_style = styles.focus;
127        }
128        if let Some(hover) = styles.hover {
129            self.hover_style = hover;
130        }
131        if let Some(drag) = styles.drag {
132            self.drag_style = drag;
133        }
134        if let Some(drag) = styles.drag {
135            self.drag_style = drag;
136        }
137        if let Some(close) = styles.close {
138            self.close_style = Some(close);
139        }
140        if let Some(min) = styles.min {
141            self.min_style = Some(min);
142        }
143        if let Some(max) = styles.max {
144            self.max_style = Some(max);
145        }
146        if let Some(can_move) = styles.can_move {
147            self.can_move = Some(can_move);
148        }
149        if let Some(can_resize) = styles.can_resize {
150            self.can_resize = Some(can_resize);
151        }
152        if let Some(can_close) = styles.can_close {
153            self.can_move = Some(can_close);
154        }
155        if let Some(can_min) = styles.can_min {
156            self.can_min = Some(can_min);
157        }
158        if let Some(can_max) = styles.can_max {
159            self.can_max = Some(can_max);
160        }
161        self
162    }
163
164    /// Window base style
165    pub fn style(mut self, style: Style) -> Self {
166        self.style = style;
167        self.block = self.block.map(|v| v.style(style));
168        self
169    }
170
171    /// Window title style
172    pub fn title_style(mut self, style: Style) -> Self {
173        self.top_style = Some(style);
174        self
175    }
176
177    /// Window focus style
178    pub fn focus_style(mut self, style: Style) -> Self {
179        self.top_style = Some(style);
180        self
181    }
182
183    /// Hover style
184    pub fn hover_style(mut self, hover: Style) -> Self {
185        self.hover_style = hover;
186        self
187    }
188
189    /// Drag style
190    pub fn drag_style(mut self, drag: Style) -> Self {
191        self.drag_style = drag;
192        self
193    }
194}
195
196impl<'a> StatefulWidget for WindowFrame<'a> {
197    type State = WindowFrameState;
198
199    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
200        if let Some(limit) = self.limit {
201            state.limit = limit;
202        } else {
203            state.limit = area;
204        }
205        state.area = state.area.intersection(state.limit);
206        state.widget_area = self.block.inner_if_some(state.area);
207
208        if let Some(v) = self.can_move {
209            state.can_move = v;
210        }
211        if let Some(v) = self.can_resize {
212            state.can_resize = v;
213        }
214        if let Some(v) = self.can_close {
215            state.can_close = v;
216        }
217
218        if state.can_resize {
219            state.resize_area = Rect::new(
220                state.area.right().saturating_sub(2),
221                state.area.bottom().saturating_sub(1),
222                2,
223                1,
224            );
225        } else {
226            state.resize_area = Default::default();
227        }
228        if state.can_min && !state.area.is_empty() {
229            state.min_area = Rect::new(state.area.right().saturating_sub(10), state.area.y, 3, 1);
230        } else {
231            state.min_area = Default::default();
232        }
233        if state.can_max && !state.area.is_empty() {
234            state.max_area = Rect::new(state.area.right().saturating_sub(7), state.area.y, 3, 1);
235        } else {
236            state.max_area = Default::default();
237        }
238        if state.can_close && !state.area.is_empty() {
239            state.close_area =
240                Rect::new(state.area.right().saturating_sub(4), state.area.top(), 3, 1);
241        } else {
242            state.close_area = Default::default();
243        }
244
245        if state.can_move {
246            if state.can_close || state.can_min || state.can_max {
247                state.move_area = Rect::new(
248                    state.area.x + 1,
249                    state.area.y,
250                    state.area.width.saturating_sub(11),
251                    1,
252                );
253            } else {
254                state.move_area = Rect::new(
255                    state.area.x + 1,
256                    state.area.y,
257                    state.area.width.saturating_sub(2),
258                    1,
259                );
260            }
261        } else {
262            state.move_area = Default::default();
263        }
264
265        if !self.no_fill {
266            for y in state.area.top()..state.area.bottom() {
267                for x in state.area.left()..state.area.right() {
268                    if let Some(cell) = buf.cell_mut((x, y)) {
269                        cell.reset();
270                    }
271                }
272            }
273        }
274
275        let block = if state.top {
276            if state.is_focused() {
277                if let Some(top_style) = self.focus_style.or(self.top_style) {
278                    self.block.map(|v| v.title_style(top_style))
279                } else {
280                    self.block
281                }
282            } else {
283                if let Some(top_style) = self.top_style {
284                    self.block.map(|v| v.title_style(top_style))
285                } else {
286                    self.block
287                }
288            }
289        } else {
290            self.block
291        };
292        let block = if self.no_fill {
293            block.map(|v| v.style(Style::new()))
294        } else {
295            block
296        };
297
298        block.render(state.area, buf);
299
300        if state.can_min && !state.area.is_empty() {
301            Span::from("[↓]")
302                .style(self.style)
303                .render(state.min_area, buf);
304        }
305        if state.can_max && !state.area.is_empty() {
306            if state.area == state.limit {
307                Span::from("[⇵]")
308                    .style(self.style)
309                    .render(state.max_area, buf);
310            } else {
311                Span::from("[↑]")
312                    .style(self.style)
313                    .render(state.max_area, buf);
314            }
315        }
316        if state.can_close && !state.area.is_empty() {
317            Span::from("[x]")
318                .style(self.style)
319                .render(state.close_area, buf);
320        }
321
322        if state.mouse_min.hover.get() {
323            buf.set_style(state.min_area, self.hover_style);
324        }
325        if state.mouse_max.hover.get() {
326            buf.set_style(state.max_area, self.hover_style);
327        }
328        if state.mouse_close.hover.get() {
329            buf.set_style(state.close_area, self.hover_style);
330        }
331
332        if state.mouse_move.drag.get() {
333            buf.set_style(state.move_area, self.drag_style);
334        } else if state.mouse_move.hover.get() {
335            buf.set_style(state.move_area, self.hover_style);
336        }
337
338        if state.mouse_resize.drag.get() {
339            buf.set_style(state.resize_area, self.drag_style);
340        } else if state.mouse_resize.hover.get() {
341            buf.set_style(state.resize_area, self.hover_style);
342        }
343    }
344}