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