Skip to main content

wiki_tui/components/
page.rs

1use std::{collections::HashMap, sync::Arc};
2
3use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::{
5    layout::{Constraint, Direction, Flex, Layout},
6    prelude::{Margin, Rect},
7    style::{Color, Modifier, Style, Stylize},
8    text::{Line, Span},
9    widgets::{Block, List, ListState, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
10};
11use tracing::{debug, info, warn};
12use wiki_api::{
13    document::{Data, Node},
14    page::{Link, Page, Section},
15};
16
17use crate::{
18    action::{Action, ActionPacket, ActionResult, PageAction},
19    components::Component,
20    config::{Config, Theme, TocConfigPosition, TocConfigTitle, ZenModeComponents},
21    has_modifier,
22    renderer::{default_renderer::render_document, RenderedDocument},
23    terminal::Frame,
24    ui::padded_rect,
25};
26
27#[cfg(debug_assertions)]
28use crate::renderer::test_renderer::{render_nodes_raw, render_tree_data, render_tree_raw};
29
30#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31#[repr(u8)]
32pub enum Renderer {
33    #[default]
34    Default,
35
36    #[cfg(debug_assertions)]
37    TestRendererTreeData,
38    #[cfg(debug_assertions)]
39    TestRendererTreeRaw,
40    #[cfg(debug_assertions)]
41    TestRendererNodeRaw,
42}
43
44impl Renderer {
45    pub fn next(&self) -> Self {
46        match self {
47            #[cfg(not(debug_assertions))]
48            &Renderer::Default => Renderer::Default,
49
50            #[cfg(debug_assertions)]
51            &Renderer::Default => Renderer::TestRendererTreeData,
52            #[cfg(debug_assertions)]
53            &Renderer::TestRendererTreeData => Renderer::TestRendererTreeRaw,
54            #[cfg(debug_assertions)]
55            &Renderer::TestRendererTreeRaw => Renderer::TestRendererNodeRaw,
56            #[cfg(debug_assertions)]
57            &Renderer::TestRendererNodeRaw => Renderer::Default,
58        }
59    }
60}
61
62#[derive(Default, Clone)]
63struct PageContentsState {
64    list_state: ListState,
65    max_idx_section: u8,
66}
67
68macro_rules! rendered_page {
69    ($self: ident, $width: expr) => {
70        match $self.rendered_page($width) {
71            Some(page) => page,
72            None => {
73                $self.render_page($width);
74                $self.rendered_page($width).unwrap()
75            }
76        }
77    };
78}
79
80#[derive(serde::Serialize, serde::Deserialize, Clone)]
81pub struct PageComponent {
82    pub page: Page,
83    renderer: Renderer,
84    render_cache: HashMap<u16, RenderedDocument>,
85    viewport: Rect,
86    selected: (usize, usize),
87
88    #[serde(skip)]
89    config: Arc<Config>,
90    #[serde(skip)]
91    theme: Arc<Theme>,
92
93    is_contents: bool,
94    is_zen_mode: bool,
95    #[serde(skip)]
96    contents_state: PageContentsState,
97}
98
99impl PageComponent {
100    pub fn new(page: Page, config: Arc<Config>, theme: Arc<Theme>) -> Self {
101        let contents_state = PageContentsState {
102            list_state: ListState::default().with_selected(Some(0)),
103            max_idx_section: page.sections().map(|x| x.len() as u8).unwrap_or_default(),
104        };
105
106        Self {
107            page,
108            renderer: Renderer::default(),
109            render_cache: HashMap::new(),
110            viewport: Rect::default(),
111            selected: (0, 0),
112
113            is_contents: false,
114            is_zen_mode: config.page.default_zen,
115            contents_state,
116
117            config,
118            theme,
119        }
120    }
121
122    pub fn is_zen_mode(&self) -> bool {
123        self.is_zen_mode
124    }
125
126    pub fn rebuild(&mut self, config: Arc<Config>, theme: Arc<Theme>) {
127        self.config = config;
128        self.theme = theme;
129        self.contents_state = PageContentsState {
130            list_state: ListState::default().with_selected(Some(0)),
131            max_idx_section: self
132                .page
133                .sections()
134                .map(|x| x.len() as u8)
135                .unwrap_or_default(),
136        };
137    }
138
139    fn render_page(&mut self, width: u16) {
140        info!("rendering page '{}' at width {}", self.page.title, width);
141        let page = match self.renderer {
142            Renderer::Default => render_document(&self.page.content, width),
143            #[cfg(debug_assertions)]
144            Renderer::TestRendererTreeData => render_tree_data(&self.page.content),
145            #[cfg(debug_assertions)]
146            Renderer::TestRendererTreeRaw => render_tree_raw(&self.page.content),
147            #[cfg(debug_assertions)]
148            Renderer::TestRendererNodeRaw => render_nodes_raw(&self.page.content),
149        };
150
151        self.render_cache.insert(width, page);
152        info!(
153            "cached render for page '{}' at width {} (total cached widths: {})",
154            self.page.title,
155            width,
156            self.render_cache.len()
157        );
158    }
159
160    fn rendered_page(&self, width: u16) -> Option<&RenderedDocument> {
161        self.render_cache.get(&width)
162    }
163
164    fn render_contents(&mut self, f: &mut Frame<'_>, area: Rect) {
165        let title = match self.config.page.toc.title {
166            TocConfigTitle::Default => "Contents".to_string(),
167            TocConfigTitle::Article => self.page.title.to_string(),
168            TocConfigTitle::Custom(ref title) => title.to_string(),
169        };
170
171        let sections = self.page.sections.as_ref();
172        let mut block = self.theme.default_block().title(title);
173        if self.is_contents {
174            block = block.border_style(
175                Style::default()
176                    .fg(self.theme.border_highlight_fg)
177                    .bg(self.theme.border_highlight_bg),
178            );
179        }
180
181        if sections.is_none() {
182            f.render_widget(
183                self.theme
184                    .default_paragraph("No Contents available")
185                    .block(block),
186                area,
187            );
188            return;
189        }
190
191        let sections = sections.unwrap();
192        let list = List::new(sections.iter().map(|x| {
193            let mut item = self.config.page.toc.formatted_item(&x.number, &x.text);
194
195            if item
196                .char_indices()
197                .nth(block.inner(area).width as usize)
198                .is_some()
199            {
200                let idx = item
201                    .char_indices()
202                    .nth(block.inner(area).width.saturating_sub(3) as usize)
203                    .map_or(0, |(idx, _)| idx);
204                let trunc = item.drain(0..idx).collect::<String>() + "...";
205                trunc.fg(self.theme.fg)
206            } else {
207                item.fg(self.theme.fg)
208            }
209        }))
210        .block(block)
211        .highlight_style(
212            Style::default()
213                .fg(self.theme.selected_fg)
214                .bg(self.theme.selected_bg)
215                .add_modifier(Modifier::ITALIC),
216        );
217        f.render_stateful_widget(list, area, &mut self.contents_state.list_state);
218    }
219
220    fn switch_renderer(&mut self, renderer: Renderer) {
221        self.renderer = renderer;
222
223        debug!("flushing '{}' cached renders", self.render_cache.len());
224        self.render_cache.clear();
225        self.selected = (0, 0);
226    }
227
228    fn select_header(&mut self, anchor: String) {
229        // HACK: do not hardcode this
230        if &anchor == "Content_Top" {
231            info!("special case: jumping to top");
232            self.viewport.y = 0;
233            return;
234        }
235
236        let header_node = self
237            .page
238            .content
239            .nth(0)
240            .unwrap()
241            .descendants()
242            .filter(|node| {
243                if let Data::Header { id, .. } = node.data() {
244                    id == &anchor
245                } else {
246                    false
247                }
248            })
249            .last();
250
251        if header_node.is_none() {
252            warn!("no header with the anchor '{}' could be found", anchor);
253            return;
254        }
255
256        let header_node = header_node.unwrap();
257        self.scroll_to_node(header_node.index());
258    }
259
260    fn selected_header(&self) -> Option<&Section> {
261        let sections = self.page.sections()?;
262        let section_idx = self.contents_state.list_state.selected()?;
263        assert!(section_idx < self.contents_state.max_idx_section as usize);
264
265        Some(&sections[section_idx])
266    }
267
268    /// Returns the y-Position of the selected element
269    fn selected_y(&self) -> usize {
270        let page = match self.render_cache.get(&self.viewport.width) {
271            Some(page) => page,
272            None => return 0,
273        };
274
275        for (y, line) in page.lines.iter().enumerate() {
276            if line
277                .iter()
278                .any(|word| self.selected.0 <= word.index && self.selected.1 >= word.index)
279            {
280                return y;
281            }
282        }
283
284        0
285    }
286
287    fn select_node(&mut self, idx: usize) {
288        let node = match Node::new(&self.page.content, idx) {
289            Some(node) => node,
290            None => return,
291        };
292
293        let first_index = node.index();
294        let last_index = node.last_child().map(|x| x.index()).unwrap_or(first_index);
295
296        self.selected = (first_index, last_index);
297    }
298
299    fn selected_node(&self) -> Option<Node<'_>> {
300        self.page.content.nth(self.selected.0)
301    }
302
303    fn select_first(&mut self) {
304        if self.page.content.nth(0).is_none() {
305            return;
306        }
307
308        let selectable_node = self
309            .page
310            .content
311            .nth(0)
312            .unwrap()
313            .descendants()
314            .find(|node| matches!(node.data(), &Data::Link(_)));
315
316        if let Some(node) = selectable_node {
317            self.select_node(node.index());
318            self.check_and_update_scrolling();
319        }
320    }
321
322    fn select_last(&mut self) {
323        if self.page.content.nth(0).is_none() {
324            return;
325        }
326
327        let selectable_node = self
328            .page
329            .content
330            .nth(0)
331            .unwrap()
332            .descendants()
333            .filter(|node| matches!(node.data(), &Data::Link(_)) && node.index() > self.selected.1)
334            .last();
335
336        if let Some(node) = selectable_node {
337            self.select_node(node.index());
338            self.check_and_update_scrolling();
339        }
340    }
341
342    fn select_next(&mut self) {
343        if self.page.content.nth(0).is_none() {
344            return;
345        }
346
347        let selectable_node = self
348            .page
349            .content
350            .nth(0)
351            .unwrap()
352            .descendants()
353            .find(|node| matches!(node.data(), &Data::Link(_)) && self.selected.1 < node.index());
354
355        if let Some(node) = selectable_node {
356            self.select_node(node.index());
357            self.check_and_update_scrolling();
358        }
359    }
360
361    fn select_prev(&mut self) {
362        if self.page.content.nth(0).is_none() {
363            return;
364        }
365
366        let selectable_node = self
367            .page
368            .content
369            .nth(0)
370            .unwrap()
371            .descendants()
372            .filter(|node| matches!(node.data(), &Data::Link(_)) && node.index() < self.selected.0)
373            .last();
374
375        if let Some(node) = selectable_node {
376            self.select_node(node.index());
377            self.check_and_update_scrolling();
378        }
379    }
380
381    /// Checks if the current link is out of the viewport and moves the selection accordingly. If
382    /// no links could be found in the current viewport, the selection stays as it was
383    fn check_and_update_selection(&mut self) {
384        let page = rendered_page!(self, self.viewport.width);
385
386        let selected_y = self.selected_y() as u16;
387        let selected_node = match self.selected_node() {
388            Some(node) => node,
389            None => return,
390        };
391
392        if self.viewport.contains((0_u16, selected_y).into()) {
393            return;
394        }
395
396        if selected_y < self.viewport.top() {
397            let (_, idx) = page
398                .links
399                .iter()
400                .find(|(y, _)| self.viewport.contains((0, *y as u16).into()))
401                .map(|x| x.to_owned())
402                .unwrap_or((selected_y as usize, selected_node.index()));
403
404            self.select_node(idx);
405            return;
406        }
407
408        if selected_y > self.viewport.bottom() {
409            let (_, idx) = page
410                .links
411                .iter()
412                .rev()
413                .find(|(y, _)| self.viewport.contains((0, *y as u16).into()))
414                .map(|x| x.to_owned())
415                .unwrap_or((selected_y as usize, selected_node.index()));
416
417            self.select_node(idx)
418        }
419    }
420
421    fn scroll_up(&mut self, amount: u16) {
422        if self.is_contents && self.config.page.toc.enable_scrolling {
423            let i = match self.contents_state.list_state.selected() {
424                Some(i) => {
425                    if i == 0 {
426                        self.contents_state.max_idx_section as usize - 1
427                    } else {
428                        i - 1
429                    }
430                }
431                None => 0,
432            };
433
434            self.contents_state.list_state.select(Some(i));
435            return;
436        }
437
438        self.scroll_to_y(self.viewport.y.saturating_sub(amount));
439    }
440
441    fn scroll_down(&mut self, amount: u16) {
442        if self.is_contents && self.config.page.toc.enable_scrolling {
443            let i = match self.contents_state.list_state.selected() {
444                Some(i) => {
445                    if i >= self.contents_state.max_idx_section as usize - 1 {
446                        0
447                    } else {
448                        i + 1
449                    }
450                }
451                None => 0,
452            };
453
454            self.contents_state.list_state.select(Some(i));
455            return;
456        }
457
458        self.scroll_to_y(self.viewport.y + amount);
459    }
460
461    fn scroll_to_bottom(&mut self) {
462        let page = rendered_page!(self, self.viewport.width);
463        self.scroll_to_y(page.lines.len() as u16);
464    }
465
466    fn scroll_to_y(&mut self, y: u16) {
467        let page = rendered_page!(self, self.viewport.width);
468        let n_lines = page.lines.len() as u16;
469        self.viewport.y = y;
470
471        if self.viewport.bottom() >= n_lines {
472            self.viewport.y = n_lines.saturating_sub(self.viewport.height);
473        }
474
475        self.check_and_update_selection();
476    }
477
478    fn scroll_to_node(&mut self, idx: usize) {
479        let page = rendered_page!(self, self.viewport.width);
480        let node = match Node::new(&self.page.content, idx) {
481            Some(node) => node,
482            None => return,
483        };
484        let first_index = idx;
485        let last_index = node.last_child().map(|x| x.index()).unwrap_or(first_index);
486        let y = page.lines.iter().enumerate().find_map(|(y, line)| {
487            line.iter()
488                .find(|word| {
489                    if let Some(node) = word.node(&self.page.content) {
490                        first_index <= node.index() && node.index() <= last_index
491                    } else {
492                        false
493                    }
494                })
495                .map(|_| y)
496        });
497
498        if let Some(y) = y {
499            self.scroll_to_y(y as u16);
500        }
501    }
502
503    /// Checks if the current viewport shows the selected link and if not, moves the viewport so
504    /// the link is visible
505    fn check_and_update_scrolling(&mut self) {
506        let selection_y = self.selected_y() as u16;
507
508        if selection_y < self.viewport.top() {
509            self.scroll_to_y(selection_y);
510            return;
511        }
512
513        if selection_y >= self.viewport.bottom() {
514            self.scroll_to_y(selection_y.saturating_sub(self.viewport.height) + 1);
515        }
516    }
517
518    fn open_link(&self) -> ActionResult {
519        let index = self.selected.0;
520        let node = Node::new(&self.page.content, index).unwrap();
521        let data = node.data().to_owned();
522
523        let link = match data {
524            Data::Link(link) => link,
525            _ => {
526                warn!("tried to open an element that is not a link");
527                return ActionResult::Ignored;
528            }
529        };
530
531        match link {
532            Link::Internal(_) | Link::Anchor(_) => (),
533            Link::External(link_data) => return Action::PopupMessage(
534                "Warning".to_string(), 
535                format!("This link doesn't point to another page. \nInstead, it leads to the following external webpage: \n\n{}", link_data.url.as_str())
536            ).into(),
537            Link::RedLink(link_data) => return Action::PopupMessage(
538                "Information".to_string(), 
539                format!("The page '{}' doesn't exist yet", link_data.title)
540            ).into(),
541            Link::MediaLink(_) | Link::ExternalToInternal(_) => {
542                info!("tried to open an unsupported link '{:?}'", link);
543                return Action::PopupMessage(
544                    "Information".to_string(), 
545                    "This type of link is not supported yet".to_string()
546                ).into()
547            }
548        }
549
550        Action::PopupDialog(
551            "Information".to_string(),
552            format!(
553                "Do you want to open the page '{}'",
554                link.title().unwrap_or("UNKNOWN")
555            ),
556            Box::<ActionPacket>::new(Action::LoadLink(link).into()),
557        )
558        .into()
559    }
560
561    fn resize(&mut self, width: u16, height: u16) {
562        self.viewport.width = width;
563        self.viewport.height = height;
564    }
565
566    fn render_status_bar(&self, f: &mut Frame<'_>, area: Rect) -> Rect {
567        let (area, status_area) = {
568            let splits = Layout::default()
569                .direction(Direction::Vertical)
570                .constraints([Constraint::Percentage(100), Constraint::Min(1)])
571                .split(padded_rect(area, 1, 1));
572            (splits[0], splits[1])
573        };
574
575        let status_msg = format!(
576            " wiki-tui | Page '{}' | Language '{}' | '{}' other languages available",
577            self.page.title,
578            self.page.language.name(),
579            self.page.available_languages().unwrap_or_default()
580        );
581        f.render_widget(
582            self.theme.default_paragraph(status_msg).style(
583                Style::default()
584                    .fg(self.theme.status_bar_fg)
585                    .bg(self.theme.status_bar_bg),
586            ),
587            status_area,
588        );
589
590        area
591    }
592
593    fn render_toc(&mut self, f: &mut Frame<'_>, area: Rect) -> Rect {
594        if self.config.page.toc.enabled {
595            let mut constraints = [
596                Constraint::Percentage(
597                    100_u16.saturating_sub(self.config.page.toc.width_percentage),
598                ),
599                Constraint::Percentage(self.config.page.toc.width_percentage),
600            ];
601
602            if self.config.page.toc.position == TocConfigPosition::Left {
603                constraints.reverse();
604            }
605
606            let splits = Layout::default()
607                .direction(Direction::Horizontal)
608                .constraints(constraints)
609                .split(area);
610
611            match self.config.page.toc.position {
612                TocConfigPosition::Left => {
613                    self.render_contents(f, splits[0]);
614                    splits[1]
615                }
616                TocConfigPosition::Right => {
617                    self.render_contents(f, splits[1]);
618                    splits[0]
619                }
620            }
621        } else {
622            area
623        }
624    }
625
626    fn render_scrollbar(&mut self, f: &mut Frame<'_>, area: Rect, content_length: usize) {
627        let scrollbar = Scrollbar::default()
628            .begin_symbol(None)
629            .end_symbol(None)
630            .track_symbol(Some(" "))
631            .track_style(
632                Style::new()
633                    .fg(self.theme.scrollbar_track_fg)
634                    .bg(self.theme.scrollbar_track_fg),
635            )
636            .thumb_style(Style::new().fg(self.theme.scrollbar_thumb_fg))
637            .orientation(ScrollbarOrientation::VerticalRight);
638        let mut scrollbar_state =
639            ScrollbarState::new(content_length).position(self.viewport.top() as usize);
640        f.render_stateful_widget(scrollbar, area, &mut scrollbar_state);
641    }
642}
643
644impl Component for PageComponent {
645    fn handle_key_events(&mut self, key: KeyEvent) -> ActionResult {
646        let page_bindings = &self.config.bindings.page;
647        macro_rules! matches_binding {
648            ($binding:ident, $action:expr) => {
649                if page_bindings.$binding.matches_event(key) {
650                    return $action.into();
651                }
652            };
653        }
654        matches_binding!(toggle_toc, Action::Page(PageAction::ToggleContents));
655
656        if self.is_contents {
657            matches_binding!(jump_to_header, {
658                let header = self.selected_header();
659                if header.is_none() {
660                    info!("no header selected");
661                    return ActionResult::Ignored;
662                }
663                ActionPacket::single(Action::Page(PageAction::GoToHeader(
664                    header.unwrap().anchor.to_string(),
665                )))
666                .action(Action::Page(PageAction::ToggleContents))
667            });
668            return ActionResult::Ignored;
669        }
670
671        matches_binding!(select_first_link, Action::Page(PageAction::SelectFirstLink));
672        matches_binding!(select_last_link, Action::Page(PageAction::SelectLastLink));
673
674        matches_binding!(select_prev_link, Action::Page(PageAction::SelectPrevLink));
675        matches_binding!(select_next_link, Action::Page(PageAction::SelectNextLink));
676
677        matches_binding!(open_link, self.open_link());
678        matches_binding!(toggle_zen_mode, {
679            self.is_zen_mode = !self.is_zen_mode;
680            ActionResult::Ignored
681        });
682
683        match key.code {
684            KeyCode::Char('r') if has_modifier!(key, Modifier::CONTROL) => {
685                Action::Page(PageAction::SwitchRenderer(self.renderer.next())).into()
686            }
687            _ => ActionResult::Ignored,
688        }
689    }
690
691    fn update(&mut self, action: Action) -> ActionResult {
692        match action {
693            Action::Page(page_action) => match page_action {
694                PageAction::SwitchRenderer(renderer) => self.switch_renderer(renderer),
695                PageAction::ToggleContents => self.is_contents = !self.is_contents,
696
697                PageAction::SelectFirstLink => self.select_first(),
698                PageAction::SelectLastLink => self.select_last(),
699
700                PageAction::SelectTopLink | PageAction::SelectBottomLink => todo!(),
701
702                PageAction::SelectPrevLink => self.select_prev(),
703                PageAction::SelectNextLink => self.select_next(),
704
705                PageAction::GoToHeader(anchor) => self.select_header(anchor),
706            },
707            Action::ScrollUp(amount) => self.scroll_up(amount),
708            Action::ScrollDown(amount) => self.scroll_down(amount),
709
710            Action::ScrollHalfUp => self.scroll_up(self.viewport.height / 2),
711            Action::ScrollHalfDown => self.scroll_down(self.viewport.height / 2),
712
713            Action::ScrollToTop => self.scroll_to_y(0),
714            Action::ScrollToBottom => self.scroll_to_bottom(),
715
716            Action::Resize(width, heigth) => self.resize(width, heigth),
717            _ => return ActionResult::Ignored,
718        }
719        ActionResult::consumed()
720    }
721
722    fn render(&mut self, f: &mut Frame, mut area: Rect) {
723        let zen_mode = self.config.page.zen_mode.clone();
724
725        // when in zen mode, use the constraints for the zen mode, otherwise the regular padding
726        area = if self.is_zen_mode() {
727            let [area] = Layout::horizontal([self.config.page.zen_horizontal])
728                .flex(Flex::Center)
729                .areas(area);
730            Layout::vertical([self.config.page.zen_vertical])
731                .flex(Flex::Center)
732                .split(area)[0]
733        } else {
734            Block::new().padding(self.config.page.padding).inner(area)
735        };
736
737        if !self.is_zen_mode || zen_mode.contains(ZenModeComponents::STATUS_BAR) {
738            area = self.render_status_bar(f, area);
739        }
740
741        if !self.is_zen_mode || zen_mode.contains(ZenModeComponents::TOC) {
742            area = self.render_toc(f, area);
743        }
744
745        let page_area = if !self.is_zen_mode || zen_mode.contains(ZenModeComponents::SCROLLBAR) {
746            area.inner(Margin {
747                vertical: 0,
748                horizontal: 2, // for the scrollbar
749            })
750        } else {
751            area
752        };
753
754        self.viewport.width = page_area.width;
755        self.viewport.height = page_area.height;
756
757        let rendered_page = rendered_page!(self, page_area.width);
758        let mut lines: Vec<Line> = rendered_page
759            .lines
760            .iter()
761            .skip(self.viewport.top() as usize)
762            .take(self.viewport.bottom() as usize)
763            .map(|line| {
764                let mut spans: Vec<Span> = Vec::new();
765                line.iter()
766                    .map(|word| {
767                        let mut span = Span::styled(
768                            format!(
769                                "{}{}",
770                                word.content,
771                                " ".repeat(word.whitespace_width as usize)
772                            ),
773                            word.style,
774                        );
775
776                        if let Some(node) = word.node(&self.page.content) {
777                            let index = node.index();
778                            if self.selected.0 <= index && index <= self.selected.1 {
779                                span = span
780                                    .patch_style(Style::new().add_modifier(Modifier::UNDERLINED))
781                            }
782                        }
783
784                        spans.push(span);
785                    })
786                    .count();
787                Line {
788                    spans,
789                    ..Default::default()
790                }
791            })
792            .collect();
793
794        if self.viewport.y == 0 {
795            let title_line =
796                Line::raw(&self.page.title).patch_style(Style::default().fg(Color::Red).bold());
797
798            lines.insert(0, title_line);
799            lines.pop();
800        }
801
802        f.render_widget(Paragraph::new(lines), page_area);
803
804        if !self.is_zen_mode || zen_mode.contains(ZenModeComponents::SCROLLBAR) {
805            self.render_scrollbar(
806                f,
807                area,
808                rendered_page
809                    .lines
810                    .len()
811                    .saturating_sub(self.viewport.height as usize),
812            )
813        }
814    }
815}