1use crate::_private::NonExhaustive;
2use crate::clipper::ClipperStyle;
3use crate::layout::GenericLayout;
4use rat_event::{ct_event, ConsumedEvent, HandleEvent, MouseOnly, Outcome, Regular};
5use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
6use rat_reloc::RelocatableState;
7use rat_scrolled::event::ScrollOutcome;
8use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState};
9use ratatui::buffer::Buffer;
10use ratatui::layout::{Alignment, Position, Rect, Size};
11use ratatui::prelude::{Style, Widget};
12use ratatui::text::Line;
13use ratatui::widgets::{Block, StatefulWidget};
14use std::borrow::Cow;
15use std::cell::{Ref, RefCell};
16use std::cmp::{max, min};
17use std::hash::Hash;
18use std::marker::PhantomData;
19use std::rc::Rc;
20
21#[derive(Debug)]
22pub struct Clipper<'a, W>
23where
24 W: Eq + Clone + Hash,
25{
26 style: Style,
27 block: Option<Block<'a>>,
28 layout: Option<GenericLayout<W>>,
29 hscroll: Option<Scroll<'a>>,
30 vscroll: Option<Scroll<'a>>,
31 label_style: Option<Style>,
32 label_alignment: Option<Alignment>,
33 phantom: PhantomData<W>,
34}
35
36#[derive(Debug)]
37pub struct ClipperBuffer<'a, W>
38where
39 W: Eq + Clone + Hash,
40{
41 layout: Rc<RefCell<GenericLayout<W>>>,
42
43 offset: Position,
45 buffer: Buffer,
46
47 widget_area: Rect,
49
50 style: Style,
51 block: Option<Block<'a>>,
52 hscroll: Option<Scroll<'a>>,
53 vscroll: Option<Scroll<'a>>,
54 label_style: Option<Style>,
55 label_alignment: Option<Alignment>,
56}
57
58#[derive(Debug)]
59pub struct ClipperWidget<'a, W>
60where
61 W: Eq + Clone + Hash,
62{
63 offset: Position,
64 buffer: Buffer,
65
66 style: Style,
67 block: Option<Block<'a>>,
68 hscroll: Option<Scroll<'a>>,
69 vscroll: Option<Scroll<'a>>,
70 phantom: PhantomData<W>,
71}
72
73#[derive(Debug)]
74pub struct ClipperState<W>
75where
76 W: Eq + Clone + Hash,
77{
78 pub area: Rect,
81 pub widget_area: Rect,
84
85 pub layout: Rc<RefCell<GenericLayout<W>>>,
88
89 pub hscroll: ScrollState,
92 pub vscroll: ScrollState,
95
96 pub container: FocusFlag,
99
100 buffer: Option<Buffer>,
102
103 pub non_exhaustive: NonExhaustive,
105}
106
107impl<W> Clone for Clipper<'_, W>
108where
109 W: Eq + Clone + Hash,
110{
111 fn clone(&self) -> Self {
112 Self {
113 style: Default::default(),
114 block: self.block.clone(),
115 layout: self.layout.clone(),
116 hscroll: self.hscroll.clone(),
117 vscroll: self.vscroll.clone(),
118 label_style: self.label_style.clone(),
119 label_alignment: self.label_alignment.clone(),
120 phantom: Default::default(),
121 }
122 }
123}
124
125impl<W> Default for Clipper<'_, W>
126where
127 W: Eq + Clone + Hash,
128{
129 fn default() -> Self {
130 Self {
131 style: Default::default(),
132 block: Default::default(),
133 layout: Default::default(),
134 hscroll: Default::default(),
135 vscroll: Default::default(),
136 label_style: Default::default(),
137 label_alignment: Default::default(),
138 phantom: Default::default(),
139 }
140 }
141}
142
143impl<'a, W> Clipper<'a, W>
144where
145 W: Eq + Clone + Hash,
146{
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
155 self.layout = Some(layout);
156 self
157 }
158
159 pub fn style(mut self, style: Style) -> Self {
161 self.style = style;
162 self.block = self.block.map(|v| v.style(style));
163 self
164 }
165
166 pub fn label_style(mut self, style: Style) -> Self {
168 self.label_style = Some(style);
169 self
170 }
171
172 pub fn label_alignment(mut self, alignment: Alignment) -> Self {
174 self.label_alignment = Some(alignment);
175 self
176 }
177
178 pub fn block(mut self, block: Block<'a>) -> Self {
180 self.block = Some(block);
181 self
182 }
183
184 pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
186 self.hscroll = Some(scroll.clone().override_horizontal());
187 self.vscroll = Some(scroll.override_vertical());
188 self
189 }
190
191 pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
193 self.hscroll = Some(scroll.override_horizontal());
194 self
195 }
196
197 pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
199 self.vscroll = Some(scroll.override_vertical());
200 self
201 }
202
203 pub fn styles(mut self, styles: ClipperStyle) -> Self {
205 self.style = styles.style;
206 if styles.label_style.is_some() {
207 self.label_style = styles.label_style;
208 }
209 if styles.label_alignment.is_some() {
210 self.label_alignment = styles.label_alignment;
211 }
212 if styles.block.is_some() {
213 self.block = styles.block;
214 }
215 if let Some(styles) = styles.scroll {
216 self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
217 self.vscroll = self.vscroll.map(|v| v.styles(styles.clone()));
218 }
219 self.block = self.block.map(|v| v.style(styles.style));
220 self
221 }
222
223 pub fn layout_size(&self, area: Rect, state: &ClipperState<W>) -> Size {
225 let width = self.inner(area, state).width;
226 Size::new(width, u16::MAX)
227 }
228
229 fn inner(&self, area: Rect, state: &ClipperState<W>) -> Rect {
231 let sa = ScrollArea::new()
232 .block(self.block.as_ref())
233 .h_scroll(self.hscroll.as_ref())
234 .v_scroll(self.vscroll.as_ref());
235 sa.inner(area, Some(&state.hscroll), Some(&state.vscroll))
236 }
237
238 fn calc_layout(&self, area: Rect, state: &mut ClipperState<W>) -> (Rect, Position) {
239 let layout = state.layout.borrow();
240
241 let view = Rect::new(
242 state.hscroll.offset() as u16,
243 state.vscroll.offset() as u16,
244 area.width,
245 area.height,
246 );
247
248 let mut max_pos = Position::default();
250
251 let mut ext_view: Option<Rect> = None;
254 for idx in 0..layout.widget_len() {
255 let area = layout.widget(idx);
256 let label_area = layout.label(idx);
257
258 if view.intersects(area) || view.intersects(label_area) {
259 if !area.is_empty() {
260 ext_view = ext_view .map(|v| v.union(area))
262 .or(Some(area));
263 }
264 if !label_area.is_empty() {
265 ext_view = ext_view .map(|v| v.union(label_area))
267 .or(Some(label_area));
268 }
269 }
270
271 max_pos.x = max(max_pos.x, area.right());
272 max_pos.y = max(max_pos.y, area.bottom());
273 max_pos.x = max(max_pos.x, label_area.right());
274 max_pos.y = max(max_pos.y, label_area.bottom());
275 }
276 for idx in 0..layout.block_len() {
277 let block_area = layout.block_area(idx);
278 if view.intersects(block_area) {
279 ext_view = ext_view .map(|v| v.union(block_area))
281 .or(Some(block_area));
282 }
283
284 max_pos.x = max(max_pos.x, block_area.right());
285 max_pos.y = max(max_pos.y, block_area.bottom());
286 }
287
288 let ext_view = ext_view.unwrap_or(view);
289
290 (ext_view, max_pos)
291 }
292
293 pub fn into_buffer(mut self, area: Rect, state: &mut ClipperState<W>) -> ClipperBuffer<'a, W> {
295 state.area = area;
296 if let Some(layout) = self.layout.take() {
297 state.layout = Rc::new(RefCell::new(layout));
298 }
299
300 let sa = ScrollArea::new()
301 .block(self.block.as_ref())
302 .h_scroll(self.hscroll.as_ref())
303 .v_scroll(self.vscroll.as_ref());
304 state.widget_area = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
305
306 let (ext_area, max_pos) = self.calc_layout(area, state);
308
309 state
311 .vscroll
312 .set_page_len(state.widget_area.height as usize);
313 state
314 .vscroll
315 .set_max_offset(max_pos.y.saturating_sub(state.widget_area.height) as usize);
316 state.hscroll.set_page_len(state.widget_area.width as usize);
317 state
318 .hscroll
319 .set_max_offset(max_pos.x.saturating_sub(state.widget_area.width) as usize);
320
321 let offset = Position::new(state.hscroll.offset as u16, state.vscroll.offset as u16);
322
323 let buffer_area = ext_area;
325 let mut buffer = if let Some(mut buffer) = state.buffer.take() {
327 buffer.reset();
328 buffer.resize(buffer_area);
329 buffer
330 } else {
331 Buffer::empty(buffer_area)
332 };
333 buffer.set_style(buffer_area, self.style);
334
335 ClipperBuffer {
336 layout: state.layout.clone(),
337 offset,
338 buffer,
339 widget_area: state.widget_area,
340 style: self.style,
341 block: self.block,
342 hscroll: self.hscroll,
343 vscroll: self.vscroll,
344 label_style: self.label_style,
345 label_alignment: self.label_alignment,
346 }
347 }
348}
349
350impl<'a, W> ClipperBuffer<'a, W>
351where
352 W: Eq + Hash + Clone,
353{
354 pub fn is_visible(&self, widget: W) -> bool {
356 let layout = self.layout.borrow();
357 let Some(idx) = layout.try_index_of(widget) else {
358 return false;
359 };
360 let area = layout.widget(idx);
361 self.buffer.area.intersects(area)
362 }
363
364 #[inline(always)]
366 fn render_auto_label(&mut self, idx: usize) -> bool {
367 let layout = self.layout.borrow();
368 let Some(label_area) = self.locate_area(layout.label(idx)) else {
369 return false;
370 };
371 let Some(label_str) = layout.try_label_str(idx) else {
372 return false;
373 };
374
375 let style = self.label_style.unwrap_or_default();
376 let align = self.label_alignment.unwrap_or_default();
377 Line::from(label_str.as_ref())
378 .style(style)
379 .alignment(align)
380 .render(label_area, &mut self.buffer);
381
382 true
383 }
384
385 #[inline(always)]
387 pub fn render_label<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
388 where
389 FN: FnOnce(&Option<Cow<'static, str>>) -> WW,
390 WW: Widget,
391 {
392 let layout = self.layout.borrow();
393 let Some(idx) = layout.try_index_of(widget) else {
394 return false;
395 };
396 let Some(label_area) = self.locate_area(layout.label(idx)) else {
397 return false;
398 };
399 let label_str = layout.try_label_str(idx);
400
401 render_fn(label_str).render(label_area, &mut self.buffer);
402
403 true
404 }
405
406 #[inline(always)]
408 pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
409 where
410 FN: FnOnce() -> WW,
411 WW: Widget,
412 {
413 let Some(idx) = self.layout.borrow().try_index_of(widget) else {
414 return false;
415 };
416
417 self.render_auto_label(idx);
418
419 let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
420 return false;
421 };
422 render_fn().render(widget_area, &mut self.buffer);
423
424 true
425 }
426
427 #[inline(always)]
429 pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
430 where
431 FN: FnOnce() -> WW,
432 WW: StatefulWidget<State = SS>,
433 SS: RelocatableState,
434 {
435 let Some(idx) = self.layout.borrow().try_index_of(widget) else {
436 return false;
437 };
438
439 self.render_auto_label(idx);
440
441 let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
442 self.hidden(state);
443 return false;
444 };
445 render_fn().render(widget_area, &mut self.buffer, state);
446 self.relocate(state);
447
448 true
449 }
450
451 pub fn render_block(&mut self) {
453 let layout = self.layout.borrow();
454 for (idx, block_area) in layout.block_area_iter().enumerate() {
455 if let Some(block_area) = self.locate_area(*block_area) {
456 layout.block(idx).render(block_area, &mut self.buffer);
457 }
458 }
459 }
460
461 #[inline]
463 #[allow(clippy::question_mark)]
464 pub fn locate_widget(&self, widget: W) -> Option<Rect> {
465 let layout = self.layout.borrow();
466 let Some(idx) = layout.try_index_of(widget) else {
467 return None;
468 };
469 self.locate_area(layout.widget(idx))
470 }
471
472 #[inline]
474 #[allow(clippy::question_mark)]
475 pub fn locate_label(&self, widget: W) -> Option<Rect> {
476 let layout = self.layout.borrow();
477 let Some(idx) = layout.try_index_of(widget) else {
478 return None;
479 };
480 self.locate_area(layout.label(idx))
481 }
482
483 #[inline]
488 pub fn locate_area(&self, area: Rect) -> Option<Rect> {
489 let area = self.buffer.area.intersection(area);
490 if area.is_empty() {
491 None
492 } else {
493 Some(area)
494 }
495 }
496
497 pub fn shift(&self) -> (i16, i16) {
499 (
500 self.widget_area.x as i16 - self.offset.x as i16,
501 self.widget_area.y as i16 - self.offset.y as i16,
502 )
503 }
504
505 pub fn relocate<S>(&self, state: &mut S)
511 where
512 S: RelocatableState,
513 {
514 state.relocate(self.shift(), self.widget_area);
515 }
516
517 pub fn hidden<S>(&self, state: &mut S)
523 where
524 S: RelocatableState,
525 {
526 state.relocate((0, 0), Rect::default())
527 }
528
529 #[inline]
531 pub fn buffer(&mut self) -> &mut Buffer {
532 &mut self.buffer
533 }
534
535 pub fn into_widget(self) -> ClipperWidget<'a, W> {
539 ClipperWidget {
540 block: self.block,
541 hscroll: self.hscroll,
542 vscroll: self.vscroll,
543 offset: self.offset,
544 buffer: self.buffer,
545 phantom: Default::default(),
546 style: self.style,
547 }
548 }
549}
550
551impl<W> StatefulWidget for ClipperWidget<'_, W>
552where
553 W: Eq + Clone + Hash,
554{
555 type State = ClipperState<W>;
556
557 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
558 assert_eq!(area, state.area);
559
560 ScrollArea::new()
561 .style(self.style)
562 .block(self.block.as_ref())
563 .h_scroll(self.hscroll.as_ref())
564 .v_scroll(self.vscroll.as_ref())
565 .render(
566 area,
567 buf,
568 &mut ScrollAreaState::new()
569 .h_scroll(&mut state.hscroll)
570 .v_scroll(&mut state.vscroll),
571 );
572
573 let src_area = self.buffer.area;
574 let tgt_area = state.widget_area;
575 let offset = self.offset;
576
577 let off_x0 = src_area.x.saturating_sub(offset.x);
579 let off_y0 = src_area.y.saturating_sub(offset.y);
580 let cut_x0 = offset.x.saturating_sub(src_area.x);
582 let cut_y0 = offset.y.saturating_sub(src_area.y);
583
584 let len_src = src_area.width.saturating_sub(cut_x0);
586 let len_tgt = tgt_area.width.saturating_sub(off_x0);
587 let len = min(len_src, len_tgt);
588
589 let height_src = src_area.height.saturating_sub(cut_y0);
591 let height_tgt = tgt_area.height.saturating_sub(off_y0);
592 let height = min(height_src, height_tgt);
593
594 for y in 0..height {
608 let src_0 = self
609 .buffer
610 .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
611 let tgt_0 = buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
612
613 let src = &self.buffer.content[src_0..src_0 + len as usize];
614 let tgt = &mut buf.content[tgt_0..tgt_0 + len as usize];
615 tgt.clone_from_slice(src);
616 }
617
618 state.buffer = Some(self.buffer);
620 }
621}
622
623impl<W> Default for ClipperState<W>
624where
625 W: Eq + Hash + Clone,
626{
627 fn default() -> Self {
628 Self {
629 area: Default::default(),
630 widget_area: Default::default(),
631 layout: Default::default(),
632 hscroll: Default::default(),
633 vscroll: Default::default(),
634 container: Default::default(),
635 buffer: None,
636 non_exhaustive: NonExhaustive,
637 }
638 }
639}
640
641impl<W> Clone for ClipperState<W>
642where
643 W: Eq + Hash + Clone,
644{
645 fn clone(&self) -> Self {
646 Self {
647 area: self.area,
648 widget_area: self.widget_area,
649 layout: self.layout.clone(),
650 hscroll: self.hscroll.clone(),
651 vscroll: self.vscroll.clone(),
652 container: FocusFlag::named(self.container.name()),
653 buffer: None,
654 non_exhaustive: NonExhaustive,
655 }
656 }
657}
658
659impl<W> HasFocus for ClipperState<W>
660where
661 W: Eq + Clone + Hash,
662{
663 fn build(&self, _builder: &mut FocusBuilder) {
664 }
666
667 fn focus(&self) -> FocusFlag {
668 self.container.clone()
669 }
670
671 fn area(&self) -> Rect {
672 self.area
673 }
674}
675
676impl<W> ClipperState<W>
677where
678 W: Eq + Clone + Hash,
679{
680 pub fn new() -> Self {
681 Self::default()
682 }
683
684 pub fn clear(&mut self) {
686 self.layout.borrow_mut().clear();
687 self.hscroll.clear();
688 self.vscroll.clear();
689 }
690
691 pub fn valid_layout(&self, size: Size) -> bool {
693 let layout = self.layout.borrow();
694 !layout.size_changed(size) && !layout.is_empty()
695 }
696
697 pub fn set_layout(&mut self, layout: GenericLayout<W>) {
699 self.layout = Rc::new(RefCell::new(layout));
700 }
701
702 pub fn layout(&self) -> Ref<'_, GenericLayout<W>> {
704 self.layout.borrow()
705 }
706
707 pub fn show(&mut self, widget: W) {
709 let layout = self.layout.borrow();
710 let Some(idx) = layout.try_index_of(widget) else {
711 return;
712 };
713 let widget_area = layout.widget(idx);
714 let label_area = layout.label(idx);
715
716 let area = if !widget_area.is_empty() {
717 if !label_area.is_empty() {
718 Some(widget_area.union(label_area))
719 } else {
720 Some(widget_area)
721 }
722 } else {
723 if !label_area.is_empty() {
724 Some(label_area)
725 } else {
726 None
727 }
728 };
729
730 if let Some(area) = area {
731 self.hscroll
732 .scroll_to_range(area.left() as usize..area.right() as usize);
733 self.vscroll
734 .scroll_to_range(area.top() as usize..area.bottom() as usize);
735 }
736 }
737
738 pub fn first(&self) -> Option<W> {
742 let layout = self.layout.borrow();
743
744 let area = Rect::new(
745 self.hscroll.offset() as u16,
746 self.vscroll.offset() as u16,
747 self.widget_area.width,
748 self.widget_area.height,
749 );
750
751 for idx in 0..layout.widget_len() {
752 if layout.widget(idx).intersects(area) {
753 return Some(layout.widget_key(idx).clone());
754 }
755 }
756
757 None
758 }
759}
760
761impl<W> ClipperState<W>
762where
763 W: Eq + Clone + Hash,
764{
765 pub fn vertical_offset(&self) -> usize {
766 self.vscroll.offset()
767 }
768
769 pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
770 let old = self.vscroll.offset();
771 self.vscroll.set_offset(offset);
772 old != self.vscroll.offset()
773 }
774
775 pub fn vertical_page_len(&self) -> usize {
776 self.vscroll.page_len()
777 }
778
779 pub fn horizontal_offset(&self) -> usize {
780 self.hscroll.offset()
781 }
782
783 pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
784 let old = self.hscroll.offset();
785 self.hscroll.set_offset(offset);
786 old != self.hscroll.offset()
787 }
788
789 pub fn horizontal_page_len(&self) -> usize {
790 self.hscroll.page_len()
791 }
792
793 pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
794 self.hscroll.scroll_to_pos(pos)
795 }
796
797 pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
798 self.vscroll.scroll_to_pos(pos)
799 }
800
801 pub fn scroll_to(&mut self, widget: W) -> bool {
803 let area = self.layout.borrow().widget_for(widget);
804 let r0 = self
805 .vscroll
806 .scroll_to_range(area.top() as usize..area.bottom() as usize);
807 let r1 = self
808 .hscroll
809 .scroll_to_range(area.left() as usize..area.right() as usize);
810 r0 || r1
811 }
812
813 pub fn scroll_up(&mut self, delta: usize) -> bool {
814 self.vscroll.scroll_up(delta)
815 }
816
817 pub fn scroll_down(&mut self, delta: usize) -> bool {
818 self.vscroll.scroll_down(delta)
819 }
820
821 pub fn scroll_left(&mut self, delta: usize) -> bool {
822 self.hscroll.scroll_left(delta)
823 }
824
825 pub fn scroll_right(&mut self, delta: usize) -> bool {
826 self.hscroll.scroll_right(delta)
827 }
828}
829
830impl<W> HandleEvent<crossterm::event::Event, Regular, Outcome> for ClipperState<W>
831where
832 W: Eq + Clone + Hash,
833{
834 fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> Outcome {
835 let r = if self.container.is_focused() {
836 match event {
837 ct_event!(keycode press PageUp) => self.scroll_up(self.vscroll.page_len()).into(),
838 ct_event!(keycode press PageDown) => {
839 self.scroll_down(self.vscroll.page_len()).into()
840 }
841 ct_event!(keycode press Home) => self.vertical_scroll_to(0).into(),
842 ct_event!(keycode press End) => {
843 self.vertical_scroll_to(self.vscroll.max_offset()).into()
844 }
845 _ => Outcome::Continue,
846 }
847 } else {
848 Outcome::Continue
849 };
850
851 r.or_else(|| self.handle(event, MouseOnly))
852 }
853}
854
855impl<W> HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ClipperState<W>
856where
857 W: Eq + Clone + Hash,
858{
859 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> Outcome {
860 let mut sas = ScrollAreaState::new()
861 .area(self.widget_area)
862 .h_scroll(&mut self.hscroll)
863 .v_scroll(&mut self.vscroll);
864 match sas.handle(event, MouseOnly) {
865 ScrollOutcome::Up(v) => self.scroll_up(v).into(),
866 ScrollOutcome::Down(v) => self.scroll_down(v).into(),
867 ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
868 ScrollOutcome::Left(v) => self.scroll_left(v).into(),
869 ScrollOutcome::Right(v) => self.scroll_right(v).into(),
870 ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
871 r => r.into(),
872 }
873 }
874}