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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
use crate::{
config::CONFIG, ui::article::content::ArticleContent, ui::article::on_link_submit,
wiki::article::Article,
};
use cursive::{
direction::Absolute,
event::{Callback, Event, EventResult, Key},
Rect, Vec2, View,
};
use std::cell::Cell;
/// A view displaying an article
pub struct ArticleView {
/// The content of the view
content: ArticleContent,
/// The last size the view had
last_size: Vec2,
/// The offset of the viewport
viewport_offset: Cell<usize>,
/// The size of the viewport
viewport_size: Cell<Vec2>,
}
impl ArticleView {
/// Creates a new ArticleView with a given article as its content
pub fn new(article: Article) -> Self {
log::debug!("creating a new instance of ArticleView");
ArticleView {
content: ArticleContent::new(article),
last_size: Vec2::zero(),
viewport_offset: Cell::new(0),
viewport_size: Cell::new(Vec2::zero()),
}
}
/// Moves the viewport by a given amount in a given direction
fn scroll(&mut self, direction: Absolute, amount: usize) -> EventResult {
match direction {
Absolute::Up => self
.viewport_offset
.set(self.viewport_offset.get().saturating_sub(amount)),
Absolute::Down => self
.viewport_offset
.set(self.viewport_offset.get().saturating_add(amount)),
_ => return EventResult::Ignored,
}
// if the links are enabled, check if the current link is out of the viewport
if !CONFIG.features.links {
return EventResult::Consumed(None);
}
// get the position of the current link and the top of the viewport
let link_pos = self.content.current_link_pos().unwrap(); // we can use unwrap here because the feature is enabled
let viewport_top = self.viewport_offset.get();
// if the link is above the viewport (aka its y-pos is smaller than the viewport offset),
// then increase the links position by the difference between the viewport offset and its
// y-position
if link_pos.y <= viewport_top {
let move_amount = viewport_top.saturating_sub(link_pos.y);
log::debug!("moving the link down by '{}'", move_amount);
self.content.move_selected_link(Absolute::Down, move_amount);
return EventResult::Consumed(None);
}
// if the link is below the viewport (aka its y-pos is bigger than the viewport offset plus
// its size),
// then decrease the links position by the difference between its y-position and the
// viewport offset
let viewport_bottom = viewport_top.saturating_add(self.viewport_size.get().y);
if link_pos.y >= viewport_bottom {
let move_amount = link_pos.y.saturating_sub(viewport_bottom);
log::debug!("moving the link up by '{}'", move_amount);
self.content.move_selected_link(Absolute::Up, move_amount);
return EventResult::Consumed(None);
}
EventResult::Consumed(None)
}
/// Select a header by moving the viewport to its coordinates
pub fn select_header(&mut self, index: usize) {
if !CONFIG.features.headers {
return;
}
log::info!("selecting the header number '{}'", index);
// get the position of the header and the viewport top and bottom
let header_pos = self
.content
.header_y_pos(index)
.unwrap_or_else(|| self.viewport_offset.get());
let viewport_top = self.viewport_offset.get();
let viewport_bottom = viewport_top.saturating_mul(self.viewport_size.get().y);
// if the header is above the viewport, then get the difference between the header and the
// viewport and scroll up by that amount
if header_pos < viewport_top {
let move_amount = viewport_top.saturating_sub(header_pos);
self.scroll(Absolute::Up, move_amount);
return;
}
// if the header is below the viewport, then get the difference between the header and the
// viewport and scroll down by that amount
if header_pos > viewport_bottom {
let move_amount = header_pos.saturating_sub(viewport_bottom);
self.scroll(Absolute::Down, move_amount);
}
}
}
impl View for ArticleView {
fn draw(&self, printer: &cursive::Printer) {
// get the start and end y coordinates so that we only draw the lines visible
let miny = printer.content_offset.y;
let maxy = printer.content_offset.y + printer.output_size.y;
// update the viewport
self.viewport_offset.set(miny);
self.viewport_size.set(printer.output_size);
// go through every line and print it to the screen
for (y, line) in self
.content
.get_rendered_lines()
.enumerate()
.filter(|(y, _)| &miny <= y && y <= &maxy)
{
// go through every element in the line and print it with its style
let mut x = 0;
for element in line {
let mut style = element.style;
if Some(element.id) == self.content.current_link() {
style = style.combine(CONFIG.theme.highlight);
}
printer.with_style(style, |printer| {
printer.print((x, y), &element.content);
x += element.width;
});
}
}
}
fn layout(&mut self, size: Vec2) {
// is this the same size as before? stop recalculating things!
if self.last_size == size {
return;
}
log::debug!("final size for the view is '({},{})'", size.x, size.y);
// save the new size and compute the lines
self.last_size = size;
self.content.compute_lines(size);
}
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
// calculate and return the required size
log::debug!(
"calculating the required size for '({},{})'",
constraint.x,
constraint.y
);
self.content.required_size(constraint)
}
fn take_focus(&mut self, _: cursive::direction::Direction) -> bool {
// this view is always focusable
true
}
fn important_area(&self, _: Vec2) -> cursive::Rect {
// return the viewport
Rect::from_size(
Vec2::new(0, self.viewport_offset.get()),
self.viewport_size.get(),
)
}
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Up) => self.scroll(Absolute::Up, 1),
Event::Key(Key::Down) => self.scroll(Absolute::Down, 1),
Event::Key(Key::Left) if CONFIG.features.links => {
self.content.move_selected_link(Absolute::Left, 1);
// if the current link is outside of the viewport, then scroll
// get the current links position
let current_link_pos = self.content.current_link_pos().unwrap(); // we can use unwrap here
// we've moved the link to the left, so we only need to check if the link is above
// the viewport
let viewport_top = self.viewport_offset.get();
if current_link_pos.y <= viewport_top {
// so the link is below the viewport... great...
// calculate how much below the viewport the link is
let move_amount = viewport_top.saturating_sub(current_link_pos.y);
// then scroll that amount
self.scroll(Absolute::Up, move_amount);
}
EventResult::Consumed(None)
}
Event::Key(Key::Right) if CONFIG.features.links => {
self.content.move_selected_link(Absolute::Right, 1);
// if the current link is outside of the viewport, then scroll
// get the current links position
let current_link_pos = self.content.current_link_pos().unwrap(); // we can use unwrap here
// we've moved the link to the right, so we only need to check if the link is below
// the viewport
let viewport_bottom = self
.viewport_offset
.get()
.saturating_add(self.viewport_size.get().y);
if current_link_pos.y >= viewport_bottom {
// so the link is below the viewport... great...
// calculate how much below the viewport the link is
let move_amount = current_link_pos.y.saturating_sub(viewport_bottom);
// then scroll that amount
self.scroll(Absolute::Down, move_amount);
}
EventResult::Consumed(None)
}
Event::Key(Key::Enter) if CONFIG.features.links => {
log::info!("opening the link");
// get current link and retrieve the ArticleElement linked to it
let current_link = self.content.current_link();
log::debug!("current link is '{:?}'", current_link);
if let Some(element) = self.content.element_by_id(current_link) {
log::debug!("found the element");
// get target link from the article element
let target = match element.get_attribute("target") {
Some(t) => t.to_string(),
None => return EventResult::Ignored,
};
log::info!("target article is '{}'", target);
// return the callback
log::debug!("returning the callback to open the link");
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
on_link_submit(s, target.clone())
})));
}
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
}