rat_widget/clipper/
clipper.rs

1use crate::_private::NonExhaustive;
2use crate::caption::{Caption, CaptionState, CaptionStyle};
3use crate::clipper::ClipperStyle;
4use crate::layout::GenericLayout;
5use rat_event::{ct_event, ConsumedEvent, HandleEvent, MouseOnly, Outcome, Regular};
6use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
7use rat_reloc::RelocatableState;
8use rat_scrolled::event::ScrollOutcome;
9use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState};
10use ratatui::buffer::Buffer;
11use ratatui::layout::{Alignment, Position, Rect, Size};
12use ratatui::style::Style;
13use ratatui::text::Line;
14use ratatui::widgets::Widget;
15use ratatui::widgets::{Block, StatefulWidget};
16use std::borrow::Cow;
17use std::cell::{Ref, RefCell};
18use std::cmp::{max, min};
19use std::hash::Hash;
20use std::marker::PhantomData;
21use std::rc::Rc;
22
23/// This widget allows rendering to a temp-buffer and clips
24/// it to size for the final rendering.
25#[derive(Debug)]
26pub struct Clipper<'a, W>
27where
28    W: Eq + Clone + Hash,
29{
30    layout: Option<GenericLayout<W>>,
31    style: Style,
32    block: Option<Block<'a>>,
33    hscroll: Option<Scroll<'a>>,
34    vscroll: Option<Scroll<'a>>,
35    label_style: Option<Style>,
36    label_alignment: Option<Alignment>,
37    caption_style: Option<CaptionStyle>,
38    auto_label: bool,
39}
40
41/// Intermediate stage for rendering. Uses a temp-buffer.
42#[derive(Debug)]
43pub struct ClipperBuffer<'a, W>
44where
45    W: Eq + Clone + Hash,
46{
47    layout: Rc<RefCell<GenericLayout<W>>>,
48    auto_label: bool,
49
50    // offset from buffer to scroll area
51    offset: Position,
52    buffer: Buffer,
53
54    // inner area that will finally be rendered.
55    widget_area: Rect,
56
57    style: Style,
58    block: Option<Block<'a>>,
59    hscroll: Option<Scroll<'a>>,
60    vscroll: Option<Scroll<'a>>,
61    label_style: Option<Style>,
62    label_alignment: Option<Alignment>,
63    caption_style: Option<CaptionStyle>,
64}
65
66#[derive(Debug)]
67pub struct ClipperWidget<'a, W>
68where
69    W: Eq + Clone + Hash,
70{
71    offset: Position,
72    buffer: Buffer,
73
74    style: Style,
75    block: Option<Block<'a>>,
76    hscroll: Option<Scroll<'a>>,
77    vscroll: Option<Scroll<'a>>,
78    phantom: PhantomData<W>,
79}
80
81#[derive(Debug)]
82pub struct ClipperState<W>
83where
84    W: Eq + Clone + Hash,
85{
86    // Full area for the widget.
87    /// __read only__ renewed for each render.
88    pub area: Rect,
89    /// Area inside the border.
90    /// __read only__ renewed for each render.
91    pub widget_area: Rect,
92
93    /// Page layout.
94    /// __read only__ renewed for each render.
95    pub layout: Rc<RefCell<GenericLayout<W>>>,
96
97    /// Horizontal scroll
98    /// __read+write__
99    pub hscroll: ScrollState,
100    /// Vertical scroll
101    /// __read+write__
102    pub vscroll: ScrollState,
103
104    /// This widget has no focus of its own, but this flag
105    /// can be used to set a container state.
106    pub container: FocusFlag,
107
108    /// For the buffer to survive render()
109    buffer: Option<Buffer>,
110
111    /// Only construct with `..Default::default()`.
112    pub non_exhaustive: NonExhaustive,
113}
114
115impl<W> Clone for Clipper<'_, W>
116where
117    W: Eq + Clone + Hash,
118{
119    fn clone(&self) -> Self {
120        Self {
121            style: Default::default(),
122            block: self.block.clone(),
123            layout: self.layout.clone(),
124            hscroll: self.hscroll.clone(),
125            vscroll: self.vscroll.clone(),
126            label_style: self.label_style.clone(),
127            label_alignment: self.label_alignment.clone(),
128            caption_style: self.caption_style.clone(),
129            auto_label: self.auto_label,
130        }
131    }
132}
133
134impl<W> Default for Clipper<'_, W>
135where
136    W: Eq + Clone + Hash,
137{
138    fn default() -> Self {
139        Self {
140            style: Default::default(),
141            block: Default::default(),
142            layout: Default::default(),
143            hscroll: Default::default(),
144            vscroll: Default::default(),
145            label_style: Default::default(),
146            label_alignment: Default::default(),
147            caption_style: Default::default(),
148            auto_label: true,
149        }
150    }
151}
152
153impl<'a, W> Clipper<'a, W>
154where
155    W: Eq + Clone + Hash,
156{
157    /// New Clipper.
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Set the layout. If no layout is set here the layout is
163    /// taken from the state.
164    pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
165        self.layout = Some(layout);
166        self
167    }
168
169    /// Base style.
170    pub fn style(mut self, style: Style) -> Self {
171        self.style = style;
172        self.block = self.block.map(|v| v.style(style));
173        self
174    }
175
176    /// Render the label automatically when rendering the widget.
177    ///
178    /// Default: true
179    pub fn auto_label(mut self, auto: bool) -> Self {
180        self.auto_label = auto;
181        self
182    }
183
184    /// Widget labels.
185    pub fn label_style(mut self, style: Style) -> Self {
186        self.label_style = Some(style);
187        self
188    }
189
190    /// Widget labels.
191    pub fn label_alignment(mut self, alignment: Alignment) -> Self {
192        self.label_alignment = Some(alignment);
193        self
194    }
195
196    /// Styles for caption labels.
197    pub fn caption_style(mut self, style: CaptionStyle) -> Self {
198        self.caption_style = Some(style);
199        self
200    }
201
202    /// Block for border
203    pub fn block(mut self, block: Block<'a>) -> Self {
204        self.block = Some(block);
205        self
206    }
207
208    /// Scroll support.
209    pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
210        self.hscroll = Some(scroll.clone().override_horizontal());
211        self.vscroll = Some(scroll.override_vertical());
212        self
213    }
214
215    /// Horizontal scroll support.
216    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
217        self.hscroll = Some(scroll.override_horizontal());
218        self
219    }
220
221    /// Vertical scroll support.
222    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
223        self.vscroll = Some(scroll.override_vertical());
224        self
225    }
226
227    /// Combined style.
228    pub fn styles(mut self, styles: ClipperStyle) -> Self {
229        self.style = styles.style;
230        if styles.label_style.is_some() {
231            self.label_style = styles.label_style;
232        }
233        if styles.label_alignment.is_some() {
234            self.label_alignment = styles.label_alignment;
235        }
236        if styles.block.is_some() {
237            self.block = styles.block;
238        }
239        if let Some(styles) = styles.scroll {
240            self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
241            self.vscroll = self.vscroll.map(|v| v.styles(styles.clone()));
242        }
243        self.block = self.block.map(|v| v.style(styles.style));
244        self
245    }
246
247    /// Calculate the layout width.
248    pub fn layout_size(&self, area: Rect, state: &ClipperState<W>) -> Size {
249        let width = self.inner(area, state).width;
250        Size::new(width, u16::MAX)
251    }
252
253    /// Calculate the view area.
254    fn inner(&self, area: Rect, state: &ClipperState<W>) -> Rect {
255        let sa = ScrollArea::new()
256            .block(self.block.as_ref())
257            .h_scroll(self.hscroll.as_ref())
258            .v_scroll(self.vscroll.as_ref());
259        sa.inner(area, Some(&state.hscroll), Some(&state.vscroll))
260    }
261
262    fn calc_layout(&self, area: Rect, state: &mut ClipperState<W>) -> (Rect, Position) {
263        let layout = state.layout.borrow();
264
265        let view = Rect::new(
266            state.hscroll.offset() as u16,
267            state.vscroll.offset() as u16,
268            area.width,
269            area.height,
270        );
271
272        // maxima for scroll bar max
273        let mut max_pos = Position::default();
274
275        // find the bounding box for the buffer.
276        // convex hull of all visible widgets/labels/blocks.
277        let mut ext_view: Option<Rect> = None;
278        for idx in 0..layout.widget_len() {
279            let area = layout.widget(idx);
280            let label_area = layout.label(idx);
281
282            if view.intersects(area) || view.intersects(label_area) {
283                if !area.is_empty() {
284                    ext_view = ext_view //
285                        .map(|v| v.union(area))
286                        .or(Some(area));
287                }
288                if !label_area.is_empty() {
289                    ext_view = ext_view //
290                        .map(|v| v.union(label_area))
291                        .or(Some(label_area));
292                }
293            }
294
295            max_pos.x = max(max_pos.x, area.right());
296            max_pos.y = max(max_pos.y, area.bottom());
297            max_pos.x = max(max_pos.x, label_area.right());
298            max_pos.y = max(max_pos.y, label_area.bottom());
299        }
300        for idx in 0..layout.block_len() {
301            let block_area = layout.block_area(idx);
302            if view.intersects(block_area) {
303                ext_view = ext_view //
304                    .map(|v| v.union(block_area))
305                    .or(Some(block_area));
306            }
307
308            max_pos.x = max(max_pos.x, block_area.right());
309            max_pos.y = max(max_pos.y, block_area.bottom());
310        }
311
312        let ext_view = ext_view.unwrap_or(view);
313
314        (ext_view, max_pos)
315    }
316
317    /// Calculates the layout and creates a temporary buffer.
318    pub fn into_buffer(mut self, area: Rect, state: &mut ClipperState<W>) -> ClipperBuffer<'a, W> {
319        state.area = area;
320        if let Some(layout) = self.layout.take() {
321            state.layout = Rc::new(RefCell::new(layout));
322        }
323
324        let sa = ScrollArea::new()
325            .block(self.block.as_ref())
326            .h_scroll(self.hscroll.as_ref())
327            .v_scroll(self.vscroll.as_ref());
328        state.widget_area = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
329
330        // run the layout
331        let (ext_area, max_pos) = self.calc_layout(area, state);
332
333        // adjust scroll
334        state
335            .vscroll
336            .set_page_len(state.widget_area.height as usize);
337        state
338            .vscroll
339            .set_max_offset(max_pos.y.saturating_sub(state.widget_area.height) as usize);
340        state.hscroll.set_page_len(state.widget_area.width as usize);
341        state
342            .hscroll
343            .set_max_offset(max_pos.x.saturating_sub(state.widget_area.width) as usize);
344
345        let offset = Position::new(state.hscroll.offset as u16, state.vscroll.offset as u16);
346
347        // resize buffer to fit all visible widgets.
348        let buffer_area = ext_area;
349        // resize buffer to fit the layout.
350        let mut buffer = if let Some(mut buffer) = state.buffer.take() {
351            buffer.reset();
352            buffer.resize(buffer_area);
353            buffer
354        } else {
355            Buffer::empty(buffer_area)
356        };
357        buffer.set_style(buffer_area, self.style);
358
359        ClipperBuffer {
360            layout: state.layout.clone(),
361            auto_label: self.auto_label,
362            offset,
363            buffer,
364            widget_area: state.widget_area,
365            style: self.style,
366            block: self.block,
367            hscroll: self.hscroll,
368            vscroll: self.vscroll,
369            label_style: self.label_style,
370            label_alignment: self.label_alignment,
371            caption_style: self.caption_style,
372        }
373    }
374}
375
376impl<'a, W> ClipperBuffer<'a, W>
377where
378    W: Eq + Hash + Clone,
379{
380    /// Is the widget visible.
381    pub fn is_visible(&self, widget: W) -> bool {
382        let layout = self.layout.borrow();
383        let Some(idx) = layout.try_index_of(widget) else {
384            return false;
385        };
386        let area = layout.widget(idx);
387        self.buffer.area.intersects(area)
388    }
389
390    /// Render the label with the set style and alignment.
391    #[inline(always)]
392    fn render_auto_label(&mut self, idx: usize) -> bool {
393        let layout = self.layout.borrow();
394        let Some(label_area) = self.locate_area(layout.label(idx)) else {
395            return false;
396        };
397        let Some(label_str) = layout.try_label_str(idx) else {
398            return false;
399        };
400
401        let style = self.label_style.unwrap_or_default();
402        let align = self.label_alignment.unwrap_or_default();
403        Line::from(label_str.as_ref())
404            .style(style)
405            .alignment(align)
406            .render(label_area, &mut self.buffer);
407
408        true
409    }
410
411    /// Render all visible blocks.
412    pub fn render_block(&mut self) {
413        let layout = self.layout.borrow();
414        for (idx, block_area) in layout.block_area_iter().enumerate() {
415            if let Some(block_area) = self.locate_area(*block_area) {
416                if let Some(block) = layout.block(idx) {
417                    block.render(block_area, &mut self.buffer);
418                }
419            }
420        }
421    }
422
423    /// Render the label for the given widget.
424    #[inline(always)]
425    pub fn render_label<FN>(&mut self, widget: W, render_fn: FN) -> bool
426    where
427        FN: FnOnce(&Option<Cow<'static, str>>, Rect, &mut Buffer),
428    {
429        let layout = self.layout.borrow();
430        let Some(idx) = layout.try_index_of(widget) else {
431            return false;
432        };
433        let Some(label_area) = self.locate_area(layout.label(idx)) else {
434            return false;
435        };
436        let label_str = layout.try_label_str(idx);
437        render_fn(label_str, label_area, &mut self.buffer);
438        true
439    }
440
441    /// Render the label as a caption with the set style.
442    #[inline(always)]
443    pub fn render_caption(
444        &mut self,
445        widget: W,
446        link: &FocusFlag,
447        state: &mut CaptionState,
448    ) -> bool {
449        let layout = self.layout.borrow();
450        let Some(idx) = layout.try_index_of(widget) else {
451            return false;
452        };
453        let Some(label_area) = self.locate_area(layout.label(idx)) else {
454            return false;
455        };
456        let Some(label_str) = layout.try_label_str(idx) else {
457            return false;
458        };
459
460        let mut label = Caption::parse(label_str.as_ref()).link(link);
461        if let Some(style) = &self.caption_style {
462            label = label.styles(style.clone())
463        }
464        label.render(label_area, &mut self.buffer, state);
465
466        true
467    }
468
469    /// Render a stateless widget and its label.
470    #[inline(always)]
471    pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
472    where
473        FN: FnOnce() -> WW,
474        WW: Widget,
475    {
476        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
477            return false;
478        };
479        if self.auto_label {
480            self.render_auto_label(idx);
481        }
482        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
483            return false;
484        };
485        render_fn().render(widget_area, &mut self.buffer);
486        true
487    }
488
489    /// Render a stateful widget and its label.
490    #[inline(always)]
491    pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
492    where
493        FN: FnOnce() -> WW,
494        WW: StatefulWidget<State = SS>,
495        SS: RelocatableState,
496    {
497        let Some(idx) = self.layout.borrow().try_index_of(widget) else {
498            return false;
499        };
500        if self.auto_label {
501            self.render_auto_label(idx);
502        }
503        let Some(widget_area) = self.locate_area(self.layout.borrow().widget(idx)) else {
504            self.hidden(state);
505            return false;
506        };
507        render_fn().render(widget_area, &mut self.buffer, state);
508        self.relocate(state);
509        true
510    }
511
512    /// Get the buffer coordinates for the given widget.
513    #[inline]
514    pub fn locate_widget(&self, widget: W) -> Option<Rect> {
515        let layout = self.layout.borrow();
516        let Some(idx) = layout.try_index_of(widget) else {
517            return None;
518        };
519        self.locate_area(layout.widget(idx))
520    }
521
522    /// Get the buffer coordinates for the label of the given widget.
523    #[inline]
524    #[allow(clippy::question_mark)]
525    pub fn locate_label(&self, widget: W) -> Option<Rect> {
526        let layout = self.layout.borrow();
527        let Some(idx) = layout.try_index_of(widget) else {
528            return None;
529        };
530        self.locate_area(layout.label(idx))
531    }
532
533    /// Relocate the area from layout coordinates to buffer coordinates,
534    /// which is a noop as those are aligned.
535    ///
536    /// But this will return None if the given area is outside the buffer.
537    #[inline]
538    pub fn locate_area(&self, area: Rect) -> Option<Rect> {
539        let area = self.buffer.area.intersection(area);
540        if area.is_empty() {
541            None
542        } else {
543            Some(area)
544        }
545    }
546
547    /// Calculate the necessary shift from layout to screen.
548    pub fn shift(&self) -> (i16, i16) {
549        (
550            self.widget_area.x as i16 - self.offset.x as i16,
551            self.widget_area.y as i16 - self.offset.y as i16,
552        )
553    }
554
555    /// After rendering the widget to the buffer it may have
556    /// stored areas in its state. These will be in buffer
557    /// coordinates instead of screen coordinates.
558    ///
559    /// Call this function to correct this after rendering.
560    pub fn relocate<S>(&self, state: &mut S)
561    where
562        S: RelocatableState,
563    {
564        state.relocate(self.shift(), self.widget_area);
565    }
566
567    /// If a widget is not rendered because it is out of
568    /// the buffer area, it may still have left over areas
569    /// in its state.
570    ///
571    /// This uses the mechanism for [relocate](Self::relocate) to zero them out.
572    pub fn hidden<S>(&self, state: &mut S)
573    where
574        S: RelocatableState,
575    {
576        state.relocate((0, 0), Rect::default())
577    }
578
579    /// Return a reference to the buffer.
580    #[inline]
581    pub fn buffer(&mut self) -> &mut Buffer {
582        &mut self.buffer
583    }
584
585    /// Rendering the content is finished.
586    ///
587    /// Convert to the output widget that can be rendered in the target area.
588    pub fn into_widget(self) -> ClipperWidget<'a, W> {
589        ClipperWidget {
590            block: self.block,
591            hscroll: self.hscroll,
592            vscroll: self.vscroll,
593            offset: self.offset,
594            buffer: self.buffer,
595            phantom: Default::default(),
596            style: self.style,
597        }
598    }
599}
600
601impl<W> StatefulWidget for ClipperWidget<'_, W>
602where
603    W: Eq + Clone + Hash,
604{
605    type State = ClipperState<W>;
606
607    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
608        assert_eq!(area, state.area);
609
610        ScrollArea::new()
611            .style(self.style)
612            .block(self.block.as_ref())
613            .h_scroll(self.hscroll.as_ref())
614            .v_scroll(self.vscroll.as_ref())
615            .render(
616                area,
617                buf,
618                &mut ScrollAreaState::new()
619                    .h_scroll(&mut state.hscroll)
620                    .v_scroll(&mut state.vscroll),
621            );
622
623        let src_area = self.buffer.area;
624        let tgt_area = state.widget_area;
625        let offset = self.offset;
626
627        // extra offset due to buffer starts right of offset.
628        let off_x0 = src_area.x.saturating_sub(offset.x);
629        let off_y0 = src_area.y.saturating_sub(offset.y);
630        // cut source buffer due to start left of offset.
631        let cut_x0 = offset.x.saturating_sub(src_area.x);
632        let cut_y0 = offset.y.saturating_sub(src_area.y);
633
634        // length to copy
635        let len_src = src_area.width.saturating_sub(cut_x0);
636        let len_tgt = tgt_area.width.saturating_sub(off_x0);
637        let len = min(len_src, len_tgt);
638
639        // area height to copy
640        let height_src = src_area.height.saturating_sub(cut_y0);
641        let height_tgt = tgt_area.height.saturating_sub(off_y0);
642        let height = min(height_src, height_tgt);
643
644        // ** slow version **
645        // for y in 0..height {
646        //     for x in 0..len {
647        //         let src_pos = Position::new(src_area.x + cut_x0 + x, src_area.y + cut_y0 + y);
648        //         let src_cell = self.buffer.cell(src_pos).expect("src-cell");
649        //
650        //         let tgt_pos = Position::new(tgt_area.x + off_x0 + x, tgt_area.y + off_y0 + y);
651        //         let tgt_cell = buf.cell_mut(tgt_pos).expect("tgt_cell");
652        //
653        //         *tgt_cell = src_cell.clone();
654        //     }
655        // }
656
657        for y in 0..height {
658            let src_0 = self
659                .buffer
660                .index_of(src_area.x + cut_x0, src_area.y + cut_y0 + y);
661            let tgt_0 = buf.index_of(tgt_area.x + off_x0, tgt_area.y + off_y0 + y);
662
663            let src = &self.buffer.content[src_0..src_0 + len as usize];
664            let tgt = &mut buf.content[tgt_0..tgt_0 + len as usize];
665            tgt.clone_from_slice(src);
666        }
667
668        // keep buffer
669        state.buffer = Some(self.buffer);
670    }
671}
672
673impl<W> Default for ClipperState<W>
674where
675    W: Eq + Hash + Clone,
676{
677    fn default() -> Self {
678        Self {
679            area: Default::default(),
680            widget_area: Default::default(),
681            layout: Default::default(),
682            hscroll: Default::default(),
683            vscroll: Default::default(),
684            container: Default::default(),
685            buffer: None,
686            non_exhaustive: NonExhaustive,
687        }
688    }
689}
690
691impl<W> Clone for ClipperState<W>
692where
693    W: Eq + Hash + Clone,
694{
695    fn clone(&self) -> Self {
696        Self {
697            area: self.area,
698            widget_area: self.widget_area,
699            layout: self.layout.clone(),
700            hscroll: self.hscroll.clone(),
701            vscroll: self.vscroll.clone(),
702            container: FocusFlag::named(self.container.name()),
703            buffer: None,
704            non_exhaustive: NonExhaustive,
705        }
706    }
707}
708
709impl<W> HasFocus for ClipperState<W>
710where
711    W: Eq + Clone + Hash,
712{
713    fn build(&self, _builder: &mut FocusBuilder) {
714        // not an autonomous widget
715    }
716
717    fn focus(&self) -> FocusFlag {
718        self.container.clone()
719    }
720
721    fn area(&self) -> Rect {
722        self.area
723    }
724}
725
726impl<W> ClipperState<W>
727where
728    W: Eq + Clone + Hash,
729{
730    pub fn new() -> Self {
731        Self::default()
732    }
733
734    /// Clear the layout data and reset any scroll
735    pub fn clear(&mut self) {
736        self.layout.borrow_mut().clear();
737        self.hscroll.clear();
738        self.vscroll.clear();
739    }
740
741    /// Layout needs to change?
742    pub fn valid_layout(&self, size: Size) -> bool {
743        let layout = self.layout.borrow();
744        !layout.size_changed(size) && !layout.is_empty()
745    }
746
747    /// Set the layout.
748    pub fn set_layout(&mut self, layout: GenericLayout<W>) {
749        self.layout = Rc::new(RefCell::new(layout));
750    }
751
752    /// Layout.
753    pub fn layout(&self) -> Ref<'_, GenericLayout<W>> {
754        self.layout.borrow()
755    }
756
757    /// Show the area for the given handle.
758    pub fn show(&mut self, widget: W) {
759        let layout = self.layout.borrow();
760        let Some(idx) = layout.try_index_of(widget) else {
761            return;
762        };
763        let widget_area = layout.widget(idx);
764        let label_area = layout.label(idx);
765
766        let area = if !widget_area.is_empty() {
767            if !label_area.is_empty() {
768                Some(widget_area.union(label_area))
769            } else {
770                Some(widget_area)
771            }
772        } else {
773            if !label_area.is_empty() {
774                Some(label_area)
775            } else {
776                None
777            }
778        };
779
780        if let Some(area) = area {
781            self.hscroll
782                .scroll_to_range(area.left() as usize..area.right() as usize);
783            self.vscroll
784                .scroll_to_range(area.top() as usize..area.bottom() as usize);
785        }
786    }
787
788    /// Returns the first visible widget.
789    /// This uses insertion order of the widgets, not
790    /// any graphical ordering.
791    pub fn first(&self) -> Option<W> {
792        let layout = self.layout.borrow();
793
794        let area = Rect::new(
795            self.hscroll.offset() as u16,
796            self.vscroll.offset() as u16,
797            self.widget_area.width,
798            self.widget_area.height,
799        );
800
801        for idx in 0..layout.widget_len() {
802            if layout.widget(idx).intersects(area) {
803                return Some(layout.widget_key(idx).clone());
804            }
805        }
806
807        None
808    }
809}
810
811impl<W> ClipperState<W>
812where
813    W: Eq + Clone + Hash,
814{
815    pub fn vertical_offset(&self) -> usize {
816        self.vscroll.offset()
817    }
818
819    pub fn set_vertical_offset(&mut self, offset: usize) -> bool {
820        let old = self.vscroll.offset();
821        self.vscroll.set_offset(offset);
822        old != self.vscroll.offset()
823    }
824
825    pub fn vertical_page_len(&self) -> usize {
826        self.vscroll.page_len()
827    }
828
829    pub fn horizontal_offset(&self) -> usize {
830        self.hscroll.offset()
831    }
832
833    pub fn set_horizontal_offset(&mut self, offset: usize) -> bool {
834        let old = self.hscroll.offset();
835        self.hscroll.set_offset(offset);
836        old != self.hscroll.offset()
837    }
838
839    pub fn horizontal_page_len(&self) -> usize {
840        self.hscroll.page_len()
841    }
842
843    pub fn horizontal_scroll_to(&mut self, pos: usize) -> bool {
844        self.hscroll.scroll_to_pos(pos)
845    }
846
847    pub fn vertical_scroll_to(&mut self, pos: usize) -> bool {
848        self.vscroll.scroll_to_pos(pos)
849    }
850
851    /// Scroll the widget to visible.
852    pub fn scroll_to(&mut self, widget: W) -> bool {
853        let area = self.layout.borrow().widget_for(widget);
854        let r0 = self
855            .vscroll
856            .scroll_to_range(area.top() as usize..area.bottom() as usize);
857        let r1 = self
858            .hscroll
859            .scroll_to_range(area.left() as usize..area.right() as usize);
860        r0 || r1
861    }
862
863    pub fn scroll_up(&mut self, delta: usize) -> bool {
864        self.vscroll.scroll_up(delta)
865    }
866
867    pub fn scroll_down(&mut self, delta: usize) -> bool {
868        self.vscroll.scroll_down(delta)
869    }
870
871    pub fn scroll_left(&mut self, delta: usize) -> bool {
872        self.hscroll.scroll_left(delta)
873    }
874
875    pub fn scroll_right(&mut self, delta: usize) -> bool {
876        self.hscroll.scroll_right(delta)
877    }
878}
879
880impl<W> HandleEvent<crossterm::event::Event, Regular, Outcome> for ClipperState<W>
881where
882    W: Eq + Clone + Hash,
883{
884    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> Outcome {
885        let r = if self.container.is_focused() {
886            match event {
887                ct_event!(keycode press PageUp) => self.scroll_up(self.vscroll.page_len()).into(),
888                ct_event!(keycode press PageDown) => {
889                    self.scroll_down(self.vscroll.page_len()).into()
890                }
891                ct_event!(keycode press Home) => self.vertical_scroll_to(0).into(),
892                ct_event!(keycode press End) => {
893                    self.vertical_scroll_to(self.vscroll.max_offset()).into()
894                }
895                _ => Outcome::Continue,
896            }
897        } else {
898            Outcome::Continue
899        };
900
901        r.or_else(|| self.handle(event, MouseOnly))
902    }
903}
904
905impl<W> HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ClipperState<W>
906where
907    W: Eq + Clone + Hash,
908{
909    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> Outcome {
910        let mut sas = ScrollAreaState::new()
911            .area(self.widget_area)
912            .h_scroll(&mut self.hscroll)
913            .v_scroll(&mut self.vscroll);
914        match sas.handle(event, MouseOnly) {
915            ScrollOutcome::Up(v) => self.scroll_up(v).into(),
916            ScrollOutcome::Down(v) => self.scroll_down(v).into(),
917            ScrollOutcome::VPos(v) => self.set_vertical_offset(v).into(),
918            ScrollOutcome::Left(v) => self.scroll_left(v).into(),
919            ScrollOutcome::Right(v) => self.scroll_right(v).into(),
920            ScrollOutcome::HPos(v) => self.set_horizontal_offset(v).into(),
921            r => r.into(),
922        }
923    }
924}