rat_widget/pager/
pager_nav.rs

1use crate::_private::NonExhaustive;
2use crate::event::PagerOutcome;
3use crate::pager::PagerStyle;
4use crate::util::revert_style;
5use rat_event::util::MouseFlagsN;
6use rat_event::{ct_event, ConsumedEvent, HandleEvent, MouseOnly, Regular};
7use rat_focus::{FocusFlag, HasFocus};
8use ratatui::buffer::Buffer;
9use ratatui::layout::{Alignment, Rect, Size};
10use ratatui::style::Style;
11use ratatui::text::Span;
12use ratatui::widgets::{Block, StatefulWidget, Widget};
13use std::cmp::min;
14use unicode_display_width::width as unicode_width;
15
16/// Render the navigation for one or more [Pager](crate::pager::Pager) widgets.
17#[derive(Debug, Clone)]
18pub struct PageNavigation<'a> {
19    pages: u8,
20    block: Option<Block<'a>>,
21    style: Style,
22    nav_style: Option<Style>,
23    title_style: Option<Style>,
24    next_page: &'a str,
25    prev_page: &'a str,
26    first_page: &'a str,
27    last_page: &'a str,
28}
29
30/// Widget state.
31#[derive(Debug, Clone)]
32pub struct PageNavigationState {
33    /// Full area for the widget.
34    /// __read only__ renewed for each render.
35    pub area: Rect,
36    /// Area for each page.
37    /// __read only__ renewed for each render.
38    pub widget_areas: Vec<Rect>,
39    /// Area for prev-page indicator.
40    /// __read only__ renewed with each render.
41    pub prev_area: Rect,
42    /// Area for next-page indicator.
43    /// __read only__ renewed with each render.
44    pub next_area: Rect,
45
46    /// Current, left-most page.
47    /// __read+write__
48    pub page: usize,
49    /// Increment by n when navigating.
50    /// __read only__ renewed with each render.
51    pub page_inc: usize,
52
53    /// Page count.
54    /// __read+write__
55    pub page_count: usize,
56
57    /// This widget has no focus of its own, but this flag
58    /// can be used to set a container state.
59    pub container: FocusFlag,
60
61    /// Mouse
62    pub mouse: MouseFlagsN,
63
64    /// Only construct with `..Default::default()`.
65    pub non_exhaustive: NonExhaustive,
66}
67
68impl Default for PageNavigation<'_> {
69    fn default() -> Self {
70        Self {
71            pages: 1,
72            block: Default::default(),
73            style: Default::default(),
74            nav_style: Default::default(),
75            title_style: Default::default(),
76            next_page: ">>>",
77            prev_page: "<<<",
78            first_page: "[·]",
79            last_page: "[·]",
80        }
81    }
82}
83
84impl<'a> PageNavigation<'a> {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Number of pages displayed.
90    ///
91    /// Splits the inner area into n equally sized areas.
92    /// Any rounding areas are not given to any of these
93    /// areas but area added as padding on the right side.
94    pub fn pages(mut self, pages: u8) -> Self {
95        self.pages = pages;
96        self
97    }
98
99    /// Base style.
100    pub fn style(mut self, style: Style) -> Self {
101        self.style = style;
102        self.block = self.block.map(|v| v.style(style));
103        self
104    }
105
106    /// Style for navigation.
107    pub fn nav_style(mut self, nav_style: Style) -> Self {
108        self.nav_style = Some(nav_style);
109        self
110    }
111
112    /// Style for the title.
113    pub fn title_style(mut self, title_style: Style) -> Self {
114        self.title_style = Some(title_style);
115        self
116    }
117
118    /// Block for border
119    pub fn block(mut self, block: Block<'a>) -> Self {
120        self.block = Some(block.style(self.style));
121        self
122    }
123
124    pub fn next_page_mark(mut self, txt: &'a str) -> Self {
125        self.next_page = txt;
126        self
127    }
128
129    pub fn prev_page_mark(mut self, txt: &'a str) -> Self {
130        self.prev_page = txt;
131        self
132    }
133
134    pub fn first_page_mark(mut self, txt: &'a str) -> Self {
135        self.first_page = txt;
136        self
137    }
138
139    pub fn last_page_mark(mut self, txt: &'a str) -> Self {
140        self.last_page = txt;
141        self
142    }
143
144    /// Set all styles.
145    pub fn styles(mut self, styles: PagerStyle) -> Self {
146        self.style = styles.style;
147        if let Some(nav) = styles.navigation {
148            self.nav_style = Some(nav);
149        }
150        if let Some(title) = styles.title {
151            self.title_style = Some(title);
152        }
153        if let Some(block) = styles.block {
154            self.block = Some(block);
155        }
156        if let Some(txt) = styles.next_page_mark {
157            self.next_page = txt;
158        }
159        if let Some(txt) = styles.prev_page_mark {
160            self.prev_page = txt;
161        }
162        if let Some(txt) = styles.first_page_mark {
163            self.first_page = txt;
164        }
165        if let Some(txt) = styles.last_page_mark {
166            self.last_page = txt;
167        }
168        self.block = self.block.map(|v| v.style(styles.style));
169        self
170    }
171
172    /// Calculate the layout size for one column.
173    pub fn layout_size(&self, area: Rect) -> Size {
174        let inner = self.inner(area);
175        Size::new(inner.width / self.pages as u16, inner.height)
176    }
177
178    // Calculate the view area for all columns.
179    pub fn inner(&self, area: Rect) -> Rect {
180        if let Some(block) = &self.block {
181            block.inner(area)
182        } else {
183            area
184        }
185    }
186}
187
188impl StatefulWidget for PageNavigation<'_> {
189    type State = PageNavigationState;
190
191    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
192        state.area = area;
193        state.page_inc = self.pages as usize;
194
195        let widget_area = self.inner(area);
196
197        let width = widget_area.width / self.pages as u16;
198        let mut column_area = Rect::new(widget_area.x, widget_area.y, width, widget_area.height);
199        state.widget_areas.clear();
200        for _ in 0..self.pages {
201            state.widget_areas.push(column_area);
202            column_area.x += column_area.width;
203        }
204
205        if state.page > 0 {
206            state.prev_area = Rect::new(
207                widget_area.x,
208                area.y,
209                unicode_width(self.prev_page) as u16,
210                1,
211            );
212        } else {
213            state.prev_area = Rect::new(
214                widget_area.x,
215                area.y,
216                unicode_width(self.first_page) as u16,
217                1,
218            );
219        }
220        if (state.page + self.pages as usize) < state.page_count {
221            let p = unicode_width(self.next_page) as u16;
222            state.next_area = Rect::new(
223                widget_area.x + widget_area.width.saturating_sub(p),
224                area.y,
225                p,
226                1,
227            );
228        } else {
229            let p = unicode_width(self.last_page) as u16;
230            state.next_area = Rect::new(
231                widget_area.x + widget_area.width.saturating_sub(p),
232                area.y,
233                p,
234                1,
235            );
236        }
237
238        // render
239        let block = if state.page_count > 1 {
240            let title = format!(" {}/{} ", state.page + 1, state.page_count);
241            let block = self
242                .block
243                .unwrap_or_else(|| Block::new().style(self.style))
244                .title_bottom(title)
245                .title_alignment(Alignment::Right);
246            if let Some(title_style) = self.title_style {
247                block.title_style(title_style)
248            } else {
249                block
250            }
251        } else {
252            self.block.unwrap_or_else(|| Block::new().style(self.style))
253        };
254        block.render(area, buf);
255
256        // active areas
257        let nav_style = self.nav_style.unwrap_or(self.style);
258        if matches!(state.mouse.hover.get(), Some(0)) {
259            buf.set_style(state.prev_area, revert_style(nav_style));
260        } else {
261            buf.set_style(state.prev_area, nav_style);
262        }
263        if state.page > 0 {
264            Span::from(self.prev_page).render(state.prev_area, buf);
265        } else {
266            Span::from(self.first_page).render(state.prev_area, buf);
267        }
268        if matches!(state.mouse.hover.get(), Some(1)) {
269            buf.set_style(state.next_area, revert_style(nav_style));
270        } else {
271            buf.set_style(state.next_area, nav_style);
272        }
273        if (state.page + self.pages as usize) < state.page_count {
274            Span::from(self.next_page).render(state.next_area, buf);
275        } else {
276            Span::from(self.last_page).render(state.next_area, buf);
277        }
278    }
279}
280
281impl Default for PageNavigationState {
282    fn default() -> Self {
283        Self {
284            area: Default::default(),
285            widget_areas: Default::default(),
286            prev_area: Default::default(),
287            next_area: Default::default(),
288            page: Default::default(),
289            page_inc: 1,
290            page_count: Default::default(),
291            container: Default::default(),
292            mouse: Default::default(),
293            non_exhaustive: NonExhaustive,
294        }
295    }
296}
297
298impl PageNavigationState {
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// Reset page and page-count.
304    pub fn clear(&mut self) {
305        self.page = 0;
306        self.page_count = 0;
307    }
308
309    /// Show the page.
310    pub fn set_page(&mut self, page: usize) -> bool {
311        let old_page = self.page;
312        self.page = min(page, self.page_count.saturating_sub(1));
313        old_page != self.page
314    }
315
316    /// Current page.
317    pub fn page(&self) -> usize {
318        self.page
319    }
320
321    /// Set the page count.
322    pub fn set_page_count(&mut self, count: usize) {
323        self.page_count = count;
324        self.page = min(self.page, count.saturating_sub(1));
325    }
326
327    /// Page count.
328    pub fn page_count(&self) -> usize {
329        self.page_count
330    }
331
332    /// Select next page. Keeps the page in bounds.
333    pub fn next_page(&mut self) -> bool {
334        let old_page = self.page;
335
336        if self.page + self.page_inc == self.page_count {
337            // don't change
338        } else if self.page + self.page_inc > self.page_count {
339            self.page = self.page_count.saturating_sub(1);
340        } else {
341            self.page += self.page_inc;
342        }
343
344        old_page != self.page
345    }
346
347    /// Select prev page.
348    pub fn prev_page(&mut self) -> bool {
349        if self.page >= self.page_inc {
350            self.page -= self.page_inc;
351            true
352        } else if self.page > 0 {
353            self.page = 0;
354            true
355        } else {
356            false
357        }
358    }
359}
360
361impl HandleEvent<crossterm::event::Event, Regular, PagerOutcome> for PageNavigationState {
362    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> PagerOutcome {
363        let r = if self.container.is_focused() {
364            match event {
365                ct_event!(keycode press ALT-PageUp) => {
366                    if self.prev_page() {
367                        PagerOutcome::Page(self.page())
368                    } else {
369                        PagerOutcome::Continue
370                    }
371                }
372                ct_event!(keycode press ALT-PageDown) => {
373                    if self.next_page() {
374                        PagerOutcome::Page(self.page())
375                    } else {
376                        PagerOutcome::Continue
377                    }
378                }
379                _ => PagerOutcome::Continue,
380            }
381        } else {
382            PagerOutcome::Continue
383        };
384
385        r.or_else(|| self.handle(event, MouseOnly))
386    }
387}
388
389impl HandleEvent<crossterm::event::Event, MouseOnly, PagerOutcome> for PageNavigationState {
390    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> PagerOutcome {
391        match event {
392            ct_event!(mouse down Left for x,y) if self.prev_area.contains((*x, *y).into()) => {
393                if self.prev_page() {
394                    PagerOutcome::Page(self.page)
395                } else {
396                    PagerOutcome::Unchanged
397                }
398            }
399            ct_event!(mouse down Left for x,y) if self.next_area.contains((*x, *y).into()) => {
400                if self.next_page() {
401                    PagerOutcome::Page(self.page)
402                } else {
403                    PagerOutcome::Unchanged
404                }
405            }
406            ct_event!(scroll down for x,y) => {
407                if self.area.contains((*x, *y).into()) {
408                    if self.next_page() {
409                        PagerOutcome::Page(self.page)
410                    } else {
411                        PagerOutcome::Continue
412                    }
413                } else {
414                    PagerOutcome::Continue
415                }
416            }
417            ct_event!(scroll up for x,y) => {
418                if self.area.contains((*x, *y).into()) {
419                    if self.prev_page() {
420                        PagerOutcome::Page(self.page)
421                    } else {
422                        PagerOutcome::Continue
423                    }
424                } else {
425                    PagerOutcome::Continue
426                }
427            }
428            ct_event!(mouse any for m)
429                if self.mouse.hover(&[self.prev_area, self.next_area], m) =>
430            {
431                PagerOutcome::Changed
432            }
433            _ => PagerOutcome::Continue,
434        }
435    }
436}