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
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
use crate::{
config::CONFIG,
ui::article::content::ArticleContent,
ui::{article::on_link_submit, utils::display_message},
wiki::article::{Article, ElementType},
};
use cursive::{
direction::Absolute,
event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent},
view::CannotFocus,
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 {
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 {
debug!("scrolling '{:?}' with an amount of '{}'", direction, amount);
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_or_default();
let viewport_top = self.viewport_offset.get();
debug!("link pos is {:?}", link_pos);
// 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);
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);
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_section(&mut self, index: usize) {
if !CONFIG.features.toc {
return;
}
info!("selecting the header '{}'", 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();
debug!(
"header_pos: '{}' viewport_top: '{}'",
header_pos, viewport_top
);
// 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);
debug!("scrolled '{}' 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
let move_amount = header_pos.saturating_sub(viewport_top);
self.scroll(Absolute::Down, move_amount);
debug!("scrolled '{}' down", move_amount);
}
/// Check if the link can safely be opened and open it
fn check_and_open_link(&self) -> EventResult {
// get current link and retrieve the ArticleElement linked to it
let current_link = self.content.current_link();
debug!("current link is '{:?}'", current_link);
if let Some(element) = self.content.element_by_id(current_link) {
debug!("found the element of the link");
// get target link from the article element
let target = match element.attr("target") {
Some(t) => t.to_string(),
None => {
warn!("missing attribute 'target' from element '{}'", element.id());
warn!("the link '{}' is not valid", element.id());
return EventResult::Ignored;
}
};
debug!("target link is '{}'", target);
// check whether this link pointing to another wikipedia article
if element.attr("external").is_some() {
warn!("element '{}' contains attribute 'external'", element.id());
warn!("the link '{}' is external", element.id());
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
let title = "Information";
let message = format!("This link doesn't point to another article. \nInstead, it leads to the following external webpage and therefore, cannot be opened: \n\n> {}", target);
display_message(s, title, &message);
})));
}
debug!("target link is pointing to another wikipedia article");
// return the callback
debug!("returning the callback to open the link");
info!(
"opening the link '{}' with the target '{}'",
element.id(),
target
);
return EventResult::Consumed(Some(Callback::from_fn(move |s| {
on_link_submit(s, target.clone())
})));
}
EventResult::Ignored
}
}
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;
}
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);
debug!("current link id: {:?}", self.content.current_link());
debug!("current link pos: {:?}", self.content.current_link_pos());
}
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
// calculate and return the required size
self.content.required_size(constraint)
}
fn take_focus(&mut self, _: cursive::direction::Direction) -> Result<EventResult, CannotFocus> {
// this view is always focusable
Ok(EventResult::Consumed(None))
}
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_or_else(|| (0, 0).into());
// 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 => {
debug!("moving to the right");
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_or_else(|| (0, 0).into());
// 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 {
debug!(
"link is below the viewport, current_link_pos: {:?} viewport_bottom: {}",
current_link_pos, 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);
}
debug!(
"link pos after selection: {:?}",
self.content.current_link_pos()
);
EventResult::Consumed(None)
}
Event::Key(Key::Enter) if CONFIG.features.links => self.check_and_open_link(),
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} => {
// get what element was clicked
if let Some(element) = self
.content
.get_element_at_position(position.saturating_sub(offset))
{
return match element.kind() {
// if it's a link, check if it's valid and then open it
ElementType::Link if CONFIG.features.links => {
// select this link
self.content.set_current_link(element.id());
debug!("selected the clicked link");
self.check_and_open_link()
}
// this element doesn't support mouse clicking
_ => EventResult::Ignored,
};
}
// if there isn't an element at the event position, ignore the event altogether
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
}