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