rat_widget/
clipper.rs

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