1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! A module containing the `Scroll` component.

use std::cmp;

use tui::{
  buffer::Buffer,
  text::Spans as TuiSpans,
  widgets::{Block, Borders, Paragraph, Widget},
};

use crate::{
  components::Component,
  element::{Any as AnyElement, Element},
  event::{KeyEvent, KeyHandler, MouseEvent, MouseEventKind},
  state::{use_state, State},
  style::Style,
  terminal::{Frame, Rect},
  text::{Lines, Spans},
};

/// A component that displays text along with a scrollbar.
///
/// `Scroll` renders the `Spans` passed into it along with a scrollbar.
///
/// [`Section`]: ../../struct.Section.html
#[derive(Default)]
pub struct Scroll {
  pub title: Spans,
  pub border: Style,
  pub text: Spans,

  pub on_key: KeyHandler,
}

impl Component for Scroll {
  fn render(&self) -> AnyElement {
    let buffer_offset = use_state(|| 0);

    AnyElement::new(Frozen {
      title: self.title.clone(),
      border: self.border,
      lines: self.text.clone().into(),
      on_key: self.on_key.clone(),

      buffer_offset,
    })
  }
}

struct Frozen {
  title: Spans,
  lines: Lines,
  border: Style,
  on_key: KeyHandler,

  buffer_offset: State<u16>,
}

impl Frozen {
  fn scroll_height(&self, rect: Rect) -> u16 {
    let num_lines = self.lines.0.len();
    let height = rect.height.saturating_sub(2) as usize;

    cmp::min(height, height * height / num_lines) as u16
  }

  fn max_buffer_offset(&self, rect: Rect) -> u16 {
    self.lines.0.len().saturating_sub(rect.height.saturating_sub(2) as usize) as u16
  }

  fn max_scroll_offset(&self, rect: Rect) -> u16 {
    rect.height.saturating_sub(2) - self.scroll_height(rect)
  }
}

impl Element for Frozen {
  fn on_key(&self, event: KeyEvent) {
    self.on_key.handle(event);
  }

  fn on_mouse(&self, rect: Rect, event: MouseEvent) {
    match event.kind {
      MouseEventKind::ScrollDown => self
        .buffer_offset
        .update(|offset| cmp::min(self.max_buffer_offset(rect), offset + 1)),
      MouseEventKind::ScrollUp => self.buffer_offset.update(|offset| offset.saturating_sub(1)),

      _ => (),
    }
  }

  fn draw(&self, rect: Rect, frame: &mut Frame) {
    frame.render_widget(self, rect);
  }
}

impl Widget for &Frozen {
  fn render(self, rect: Rect, buf: &mut Buffer) {
    let block = Block::default()
      .title::<TuiSpans>((&self.title).into())
      .borders(Borders::ALL)
      .border_style(self.border.into());

    let buffer_offset = self.buffer_offset.get();

    let lines = self
      .lines
      .0
      .iter()
      .skip(buffer_offset as usize)
      .cloned()
      .map(TuiSpans::from)
      .collect();

    // render text
    let paragraph = Paragraph::new::<Vec<TuiSpans>>(lines).block(block);
    Widget::render(paragraph, rect, buf);

    // render border & scroll-bar
    buf.set_string(rect.right() - 1, rect.top(), "▲", self.border.into());
    buf.set_string(rect.right() - 1, rect.bottom() - 1, "▼", self.border.into());

    let scroll_offset = self.max_scroll_offset(rect) * buffer_offset / self.max_buffer_offset(rect);

    for y in 1..=self.scroll_height(rect) {
      buf.set_string(rect.x + rect.width - 1, rect.y + y + scroll_offset, "█", self.border.into());
    }
  }
}