rat_dialog/window_control/
window_frame.rs

1//!
2//! Widget for a moveable window.
3//!
4use crate::_private::NonExhaustive;
5use crate::WindowFrameOutcome;
6use rat_event::util::MouseFlags;
7use rat_event::{ConsumedEvent, Dialog, HandleEvent, ct_event};
8use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
9use ratatui::buffer::Buffer;
10use ratatui::layout::{Position, Rect};
11use ratatui::style::Style;
12use ratatui::text::Span;
13use ratatui::widgets::{Block, StatefulWidget, Widget};
14use std::cmp::max;
15
16/// Widget for a moveable window.
17///
18/// This widget ignores the area given to render,
19/// and uses the area stored in the state instead.
20/// The area given to render is used as the outer limit for
21/// the window instead.
22///
23/// Render this widget and then use WindowState::widget_area to
24/// render your content.
25///
26/// It can handle events for move/resize/close.
27#[derive(Debug, Default)]
28pub struct WindowFrame<'a> {
29    block: Block<'a>,
30    style: Style,
31    top_style: Option<Style>,
32    focus_style: Option<Style>,
33    hover_style: Style,
34    drag_style: Style,
35    limit: Option<Rect>,
36    can_move: Option<bool>,
37    can_resize: Option<bool>,
38    can_close: Option<bool>,
39}
40
41#[derive(Debug)]
42pub struct WindowFrameStyle {
43    pub style: Style,
44    pub top: Option<Style>,
45    pub focus: Option<Style>,
46    pub block: Block<'static>,
47    pub hover: Option<Style>,
48    pub drag: Option<Style>,
49    pub can_move: Option<bool>,
50    pub can_resize: Option<bool>,
51    pub can_close: Option<bool>,
52    pub non_exhaustive: NonExhaustive,
53}
54
55/// Window state.
56#[derive(Debug)]
57pub struct WindowFrameState {
58    /// Outer limit for the window.
59    /// This will be set by the widget during render.
60    /// __read only__
61    pub limit: Rect,
62    /// the rendered window-area.
63    /// change this area to move the window.
64    /// __read+write__
65    pub area: Rect,
66    /// archived area. used when switching between
67    /// maximized and normal size.
68    pub arc_area: Rect,
69    /// area for window content.
70    /// __read only__ renewed with each render.
71    pub widget_area: Rect,
72    /// is this the top window?
73    /// __read+write__
74    pub top: bool,
75
76    /// Window can be moved.
77    /// __read+write__ May be overwritten by the widget.
78    pub can_move: bool,
79    /// Window can be resized.
80    /// __read+write__ May be overwritten by the widget.
81    pub can_resize: bool,
82    /// Window can be closed.
83    /// __read+write__ May be overwritten by the widget.
84    pub can_close: bool,
85
86    /// move area
87    pub move_area: Rect,
88    /// resize area
89    pub resize_area: Rect,
90    /// close area
91    pub close_area: Rect,
92
93    /// mouse flags for close area
94    pub mouse_close: MouseFlags,
95    /// mouse flags for resize area
96    pub mouse_resize: MouseFlags,
97
98    /// window and mouse position at the start of move
99    pub start_move: (Rect, Position),
100    /// mouse flags for move area
101    pub mouse_move: MouseFlags,
102
103    /// Focus for move/resize
104    pub focus: FocusFlag,
105
106    pub non_exhaustive: NonExhaustive,
107}
108
109impl Default for WindowFrameStyle {
110    fn default() -> Self {
111        Self {
112            style: Default::default(),
113            top: Default::default(),
114            focus: Default::default(),
115            block: Block::bordered(),
116            hover: Default::default(),
117            drag: Default::default(),
118            can_move: Default::default(),
119            can_resize: Default::default(),
120            can_close: Default::default(),
121            non_exhaustive: NonExhaustive,
122        }
123    }
124}
125
126impl<'a> WindowFrame<'a> {
127    pub fn new() -> Self {
128        Self {
129            block: Default::default(),
130            style: Default::default(),
131            top_style: Default::default(),
132            focus_style: Default::default(),
133            hover_style: Default::default(),
134            drag_style: Default::default(),
135            limit: Default::default(),
136            can_move: Default::default(),
137            can_resize: Default::default(),
138            can_close: Default::default(),
139        }
140    }
141
142    /// Limits for the window.
143    ///
144    /// If this is not set, the area given to render will be used.
145    pub fn limit(mut self, area: Rect) -> Self {
146        self.limit = Some(area);
147        self
148    }
149
150    /// Window can be moved?
151    pub fn can_move(mut self, v: bool) -> Self {
152        self.can_move = Some(v);
153        self
154    }
155
156    /// Window can be resized?
157    pub fn can_resize(mut self, v: bool) -> Self {
158        self.can_resize = Some(v);
159        self
160    }
161
162    /// Window can be closed?
163    pub fn can_close(mut self, v: bool) -> Self {
164        self.can_close = Some(v);
165        self
166    }
167
168    /// Window block
169    pub fn block(mut self, block: Block<'a>) -> Self {
170        self.block = block.style(self.style);
171        self
172    }
173
174    pub fn styles(mut self, styles: WindowFrameStyle) -> Self {
175        self.style = styles.style;
176        self.block = styles.block;
177        if styles.top.is_some() {
178            self.top_style = styles.top;
179        }
180        if styles.focus.is_some() {
181            self.focus_style = styles.focus;
182        }
183        if let Some(hover) = styles.hover {
184            self.hover_style = hover;
185        }
186        if let Some(drag) = styles.drag {
187            self.drag_style = drag;
188        }
189        if let Some(can_move) = styles.can_move {
190            self.can_move = Some(can_move);
191        }
192        if let Some(can_resize) = styles.can_resize {
193            self.can_resize = Some(can_resize);
194        }
195        if let Some(can_close) = styles.can_close {
196            self.can_move = Some(can_close);
197        }
198        self
199    }
200
201    /// Window base style
202    pub fn style(mut self, style: Style) -> Self {
203        self.style = style;
204        self.block = self.block.style(style);
205        self
206    }
207
208    /// Window title style
209    pub fn title_style(mut self, style: Style) -> Self {
210        self.top_style = Some(style);
211        self
212    }
213
214    /// Window focus style
215    pub fn focus_style(mut self, style: Style) -> Self {
216        self.top_style = Some(style);
217        self
218    }
219
220    /// Hover style
221    pub fn hover_style(mut self, hover: Style) -> Self {
222        self.hover_style = hover;
223        self
224    }
225
226    /// Drag style
227    pub fn drag_style(mut self, drag: Style) -> Self {
228        self.drag_style = drag;
229        self
230    }
231}
232
233impl<'a> StatefulWidget for WindowFrame<'a> {
234    type State = WindowFrameState;
235
236    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
237        if let Some(limit) = self.limit {
238            state.limit = limit;
239        } else {
240            state.limit = area;
241        }
242        state.area = state.area.intersection(state.limit);
243        state.widget_area = self.block.inner(state.area);
244
245        if let Some(v) = self.can_move {
246            state.can_move = v;
247        }
248        if let Some(v) = self.can_resize {
249            state.can_resize = v;
250        }
251        if let Some(v) = self.can_close {
252            state.can_close = v;
253        }
254
255        if state.can_resize {
256            state.resize_area = Rect::new(
257                state.area.right().saturating_sub(2),
258                state.area.bottom().saturating_sub(1),
259                2,
260                1,
261            );
262        } else {
263            state.resize_area = Default::default();
264        }
265        if state.can_close {
266            state.close_area =
267                Rect::new(state.area.right().saturating_sub(4), state.area.top(), 3, 1);
268        } else {
269            state.close_area = Default::default();
270        }
271        if state.can_move {
272            if state.can_close {
273                state.move_area = Rect::new(
274                    state.area.x + 1,
275                    state.area.y,
276                    state.area.width.saturating_sub(6),
277                    1,
278                );
279            } else {
280                state.move_area = Rect::new(
281                    state.area.x + 1,
282                    state.area.y,
283                    state.area.width.saturating_sub(2),
284                    1,
285                );
286            }
287        } else {
288            state.move_area = Default::default();
289        }
290
291        for y in state.area.top()..state.area.bottom() {
292            for x in state.area.left()..state.area.right() {
293                if let Some(cell) = buf.cell_mut((x, y)) {
294                    cell.reset();
295                }
296            }
297        }
298
299        let block = if state.top {
300            if state.is_focused() {
301                if let Some(top_style) = self.focus_style.or(self.top_style) {
302                    self.block.title_style(top_style)
303                } else {
304                    self.block
305                }
306            } else {
307                if let Some(top_style) = self.top_style {
308                    self.block.title_style(top_style)
309                } else {
310                    self.block
311                }
312            }
313        } else {
314            self.block
315        };
316
317        block.render(state.area, buf);
318
319        if state.can_move {
320            Span::from("[x]")
321                .style(self.style)
322                .render(state.close_area, buf);
323        }
324
325        if state.mouse_close.hover.get() {
326            buf.set_style(state.close_area, self.hover_style);
327        }
328
329        if state.mouse_move.drag.get() {
330            buf.set_style(state.move_area, self.drag_style);
331        } else if state.mouse_move.hover.get() {
332            buf.set_style(state.move_area, self.hover_style);
333        }
334
335        if state.mouse_resize.drag.get() {
336            buf.set_style(state.resize_area, self.drag_style);
337        } else if state.mouse_resize.hover.get() {
338            buf.set_style(state.resize_area, self.hover_style);
339        }
340    }
341}
342
343impl Default for WindowFrameState {
344    fn default() -> Self {
345        Self {
346            limit: Default::default(),
347            area: Default::default(),
348            arc_area: Default::default(),
349            widget_area: Default::default(),
350            top: Default::default(),
351            can_move: true,
352            can_resize: true,
353            can_close: true,
354            move_area: Default::default(),
355            resize_area: Default::default(),
356            close_area: Default::default(),
357            mouse_close: Default::default(),
358            mouse_resize: Default::default(),
359            start_move: Default::default(),
360            mouse_move: Default::default(),
361            focus: Default::default(),
362            non_exhaustive: NonExhaustive,
363        }
364    }
365}
366
367impl HasFocus for WindowFrameState {
368    fn build(&self, builder: &mut FocusBuilder) {
369        builder.leaf_widget(self);
370    }
371
372    fn focus(&self) -> FocusFlag {
373        self.focus.clone()
374    }
375
376    fn area(&self) -> Rect {
377        Rect::default()
378    }
379
380    fn navigable(&self) -> Navigation {
381        Navigation::Leave
382    }
383}
384
385impl WindowFrameState {
386    pub fn new() -> Self {
387        Self::default()
388    }
389
390    /// Switch between maximized and normal size.
391    pub fn flip_maximize(&mut self) {
392        if self.area == self.limit && !self.arc_area.is_empty() {
393            self.area = self.arc_area;
394        } else {
395            self.area = self.limit;
396        }
397    }
398
399    /// Set the window area and check the limits.
400    ///
401    /// It always resizes the area to keep it within the limits.
402    ///
403    /// Return
404    ///
405    /// Returns WindowFrameOutcome::Resized if the area is changed.
406    pub fn set_resized_area(&mut self, mut new_area: Rect, arc: bool) -> WindowFrameOutcome {
407        if new_area.x < self.limit.x {
408            new_area.width -= self.limit.x - new_area.x;
409            new_area.x = self.limit.x;
410        }
411        if new_area.y < self.limit.y {
412            new_area.height -= self.limit.y - new_area.y;
413            new_area.y = self.limit.y;
414        }
415        if new_area.right() > self.limit.right() {
416            new_area.width -= new_area.right() - self.limit.right();
417        }
418        if new_area.bottom() > self.limit.bottom() {
419            new_area.height -= new_area.bottom() - self.limit.bottom();
420        }
421
422        if new_area != self.area {
423            if arc {
424                self.arc_area = new_area;
425            }
426            self.area = new_area;
427            WindowFrameOutcome::Resized
428        } else {
429            WindowFrameOutcome::Continue
430        }
431    }
432
433    /// Set the window area and check the limits.
434    ///
435    /// If possible it moves the area to stay within the limits.
436    /// If the given area is bigger than the limit it is clipped.
437    ///
438    /// Return
439    ///
440    /// Returns WindowFrameOutcome::Moved if the area is changed.
441    pub fn set_moved_area(&mut self, mut new_area: Rect, arc: bool) -> WindowFrameOutcome {
442        if new_area.x < self.limit.x {
443            new_area.x = self.limit.x;
444        }
445        if new_area.y < self.limit.y {
446            new_area.y = self.limit.y;
447        }
448        if new_area.right() > self.limit.right() {
449            let delta = new_area.right() - self.limit.right();
450            new_area.x -= delta;
451        }
452        if new_area.bottom() > self.limit.bottom() {
453            let delta = new_area.bottom() - self.limit.bottom();
454            new_area.y -= delta;
455        }
456
457        // need clip
458        if new_area.x < self.limit.x {
459            new_area.x = self.limit.x;
460            new_area.width = self.limit.width;
461        }
462        if new_area.y < self.limit.y {
463            new_area.y = self.limit.y;
464            new_area.height = self.limit.height;
465        }
466
467        if new_area != self.area {
468            if arc {
469                self.arc_area = new_area;
470            }
471            self.area = new_area;
472            WindowFrameOutcome::Moved
473        } else {
474            WindowFrameOutcome::Continue
475        }
476    }
477}
478
479impl HandleEvent<crossterm::event::Event, Dialog, WindowFrameOutcome> for WindowFrameState {
480    fn handle(
481        &mut self,
482        event: &crossterm::event::Event,
483        _qualifier: Dialog,
484    ) -> WindowFrameOutcome {
485        let r = if self.is_focused() {
486            match event {
487                ct_event!(keycode press Up) => {
488                    let mut new_area = self.area;
489                    if new_area.y > 0 {
490                        new_area.y -= 1;
491                    }
492                    self.set_moved_area(new_area, true)
493                }
494                ct_event!(keycode press Down) => {
495                    let mut new_area = self.area;
496                    new_area.y += 1;
497                    self.set_moved_area(new_area, true)
498                }
499                ct_event!(keycode press Left) => {
500                    let mut new_area = self.area;
501                    if new_area.x > 0 {
502                        new_area.x -= 1;
503                    }
504                    self.set_moved_area(new_area, true)
505                }
506                ct_event!(keycode press Right) => {
507                    let mut new_area = self.area;
508                    new_area.x += 1;
509                    self.set_moved_area(new_area, true)
510                }
511
512                ct_event!(keycode press Home) => {
513                    let mut new_area = self.area;
514                    new_area.x = self.limit.left();
515                    self.set_moved_area(new_area, true)
516                }
517                ct_event!(keycode press End) => {
518                    let mut new_area = self.area;
519                    new_area.x = self.limit.right().saturating_sub(new_area.width);
520                    self.set_moved_area(new_area, true)
521                }
522                ct_event!(keycode press CONTROL-Home) => {
523                    let mut new_area = self.area;
524                    new_area.y = self.limit.top();
525                    self.set_moved_area(new_area, true)
526                }
527                ct_event!(keycode press CONTROL-End) => {
528                    let mut new_area = self.area;
529                    new_area.y = self.limit.bottom().saturating_sub(new_area.height);
530                    self.set_moved_area(new_area, true)
531                }
532
533                ct_event!(keycode press ALT-Up) => {
534                    let mut new_area = self.area;
535                    if new_area.height > 1 {
536                        new_area.height -= 1;
537                    }
538                    self.set_resized_area(new_area, true)
539                }
540                ct_event!(keycode press ALT-Down) => {
541                    let mut new_area = self.area;
542                    new_area.height += 1;
543                    self.set_resized_area(new_area, true)
544                }
545                ct_event!(keycode press ALT-Left) => {
546                    let mut new_area = self.area;
547                    if new_area.width > 1 {
548                        new_area.width -= 1;
549                    }
550                    self.set_resized_area(new_area, true)
551                }
552                ct_event!(keycode press ALT-Right) => {
553                    let mut new_area = self.area;
554                    new_area.width += 1;
555                    self.set_resized_area(new_area, true)
556                }
557
558                ct_event!(keycode press CONTROL_ALT-Down) => {
559                    let mut new_area = self.area;
560                    if new_area.height > 1 {
561                        new_area.y += 1;
562                        new_area.height -= 1;
563                    }
564                    self.set_resized_area(new_area, true)
565                }
566                ct_event!(keycode press CONTROL_ALT-Up) => {
567                    let mut new_area = self.area;
568                    if new_area.y > 0 {
569                        new_area.y -= 1;
570                        new_area.height += 1;
571                    }
572                    self.set_resized_area(new_area, true)
573                }
574                ct_event!(keycode press CONTROL_ALT-Right) => {
575                    let mut new_area = self.area;
576                    if new_area.width > 1 {
577                        new_area.x += 1;
578                        new_area.width -= 1;
579                    }
580                    self.set_resized_area(new_area, true)
581                }
582                ct_event!(keycode press CONTROL_ALT-Left) => {
583                    let mut new_area = self.area;
584                    if new_area.x > 0 {
585                        new_area.x -= 1;
586                        new_area.width += 1;
587                    }
588                    self.set_resized_area(new_area, true)
589                }
590
591                ct_event!(keycode press CONTROL-Up) => {
592                    let mut new_area = self.area;
593                    if self.area.y != self.limit.y || self.area.height != self.limit.height {
594                        new_area.y = self.limit.y;
595                        new_area.height = self.limit.height;
596                    }
597                    self.set_resized_area(new_area, false)
598                }
599                ct_event!(keycode press CONTROL-Down) => {
600                    let mut new_area = self.area;
601                    if !self.arc_area.is_empty() {
602                        new_area.y = self.arc_area.y;
603                        new_area.height = self.arc_area.height;
604                    }
605                    self.set_resized_area(new_area, false)
606                }
607                ct_event!(keycode press CONTROL-Right) => {
608                    let mut new_area = self.area;
609                    if self.area.x != self.limit.x || self.area.width != self.limit.width {
610                        new_area.x = self.limit.x;
611                        new_area.width = self.limit.width;
612                    }
613                    self.set_resized_area(new_area, false)
614                }
615                ct_event!(keycode press CONTROL-Left) => {
616                    let mut new_area = self.area;
617                    if !self.arc_area.is_empty() {
618                        new_area.x = self.arc_area.x;
619                        new_area.width = self.arc_area.width;
620                    }
621                    self.set_resized_area(new_area, false)
622                }
623
624                _ => WindowFrameOutcome::Continue,
625            }
626        } else {
627            WindowFrameOutcome::Continue
628        };
629
630        r.or_else(|| match event {
631            ct_event!(mouse any for m) if self.mouse_close.hover(self.close_area, m) => {
632                WindowFrameOutcome::Changed
633            }
634            ct_event!(mouse down Left for x,y) if self.close_area.contains((*x, *y).into()) => {
635                WindowFrameOutcome::ShouldClose
636            }
637
638            ct_event!(mouse any for m) if self.mouse_resize.hover(self.resize_area, m) => {
639                WindowFrameOutcome::Changed
640            }
641            ct_event!(mouse any for m) if self.mouse_resize.drag(self.resize_area, m) => {
642                let mut new_area = self.area;
643                new_area.width = max(10, m.column.saturating_sub(self.area.x));
644                new_area.height = max(3, m.row.saturating_sub(self.area.y));
645                self.set_resized_area(new_area, true)
646            }
647
648            ct_event!(mouse any for m) if self.mouse_move.hover(self.move_area, m) => {
649                WindowFrameOutcome::Changed
650            }
651            ct_event!(mouse any for m) if self.mouse_move.doubleclick(self.move_area, m) => {
652                self.flip_maximize();
653                WindowFrameOutcome::Resized
654            }
655            ct_event!(mouse any for m) if self.mouse_move.drag(self.move_area, m) => {
656                let delta_x = m.column as i16 - self.start_move.1.x as i16;
657                let delta_y = m.row as i16 - self.start_move.1.y as i16;
658                self.set_moved_area(
659                    Rect::new(
660                        self.start_move.0.x.saturating_add_signed(delta_x),
661                        self.start_move.0.y.saturating_add_signed(delta_y),
662                        self.start_move.0.width,
663                        self.start_move.0.height,
664                    ),
665                    true,
666                )
667            }
668            ct_event!(mouse down Left for x,y) if self.move_area.contains((*x, *y).into()) => {
669                self.start_move = (self.area, Position::new(*x, *y));
670                WindowFrameOutcome::Changed
671            }
672            _ => WindowFrameOutcome::Continue,
673        })
674    }
675}