1use 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#[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#[derive(Debug)]
57pub struct WindowFrameState {
58 pub limit: Rect,
62 pub area: Rect,
66 pub arc_area: Rect,
69 pub widget_area: Rect,
72 pub top: bool,
75
76 pub can_move: bool,
79 pub can_resize: bool,
82 pub can_close: bool,
85
86 pub move_area: Rect,
88 pub resize_area: Rect,
90 pub close_area: Rect,
92
93 pub mouse_close: MouseFlags,
95 pub mouse_resize: MouseFlags,
97
98 pub start_move: (Rect, Position),
100 pub mouse_move: MouseFlags,
102
103 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 pub fn limit(mut self, area: Rect) -> Self {
146 self.limit = Some(area);
147 self
148 }
149
150 pub fn can_move(mut self, v: bool) -> Self {
152 self.can_move = Some(v);
153 self
154 }
155
156 pub fn can_resize(mut self, v: bool) -> Self {
158 self.can_resize = Some(v);
159 self
160 }
161
162 pub fn can_close(mut self, v: bool) -> Self {
164 self.can_close = Some(v);
165 self
166 }
167
168 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 pub fn style(mut self, style: Style) -> Self {
203 self.style = style;
204 self.block = self.block.style(style);
205 self
206 }
207
208 pub fn title_style(mut self, style: Style) -> Self {
210 self.top_style = Some(style);
211 self
212 }
213
214 pub fn focus_style(mut self, style: Style) -> Self {
216 self.top_style = Some(style);
217 self
218 }
219
220 pub fn hover_style(mut self, hover: Style) -> Self {
222 self.hover_style = hover;
223 self
224 }
225
226 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 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 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 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 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}