use crate::{
config::CONFIG,
ui::{
article::{content::ArticleContent, open_link},
language_selector::article_language_selection_popup,
scroll,
utils::display_message,
},
wiki::article::{Article, ElementType},
};
use cursive::{
event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent},
view::CannotFocus,
Vec2, View,
};
pub struct ArticleView {
content: ArticleContent,
scroll: scroll::Core,
last_size: Vec2,
}
impl ArticleView {
pub fn new(article: Article) -> Self {
debug!("creating a new instance of ArticleView");
ArticleView {
content: ArticleContent::new(article),
scroll: scroll::Core::new(),
last_size: Vec2::zero(),
}
}
pub fn select_anchor(&mut self, anchor: &str) {
if let Some(anchor_coord) = self.content.anchor(anchor) {
self.scroll.set_offset((0, anchor_coord));
}
}
fn check_and_update_selection(&mut self) {
if !self.content.has_links() {
return;
}
let selection_coords = self.content.current_link_coords();
let viewport = self.scroll.content_viewport();
if viewport.contains(selection_coords) {
return;
}
if selection_coords.y < viewport.top() {
let (id, _) = self
.content
.links()
.skip(self.content.current_link_idx())
.find(|(_, pos)| viewport.contains(*pos))
.map(|x| x.to_owned())
.unwrap_or((self.content.current_link_element_id(), Vec2::zero()));
self.content.select_link_by_id(id);
return;
}
if selection_coords.y > viewport.bottom() {
let (id, _) = self
.content
.links()
.rev()
.find(|(_, pos)| viewport.contains(*pos))
.map(|x| x.to_owned())
.unwrap_or((self.content.current_link_element_id(), Vec2::zero()));
self.content.select_link_by_id(id);
}
}
fn check_and_update_viewport(&mut self) {
if !self.content.has_links() {
return;
}
let selection_coords = self.content.current_link_coords();
self.scroll.scroll_to_y(selection_coords.y);
self.scroll.scroll_to_x(selection_coords.x);
}
fn check_and_open_link(&self) -> EventResult {
let link = match self
.content
.element_by_id(self.content.current_link_element_id())
.map(|element| element.kind)
{
Some(ElementType::Link(ref link)) => link.to_owned(),
_ => {
warn!("selected element not a link");
return EventResult::Ignored;
}
};
EventResult::Consumed(Some(Callback::from_fn(move |s| open_link(s, link.clone()))))
}
pub fn list_article_language_switcher(&mut self) -> EventResult {
let available_languages = self.content.language_links();
if available_languages.is_none() {
warn!("no available languages for the article");
return EventResult::Consumed(Some(Callback::from_fn(|s| {
display_message(
s,
"Information",
"No alternate languages are available for the current article",
)
})));
}
EventResult::Consumed(Some(Callback::from_fn(move |s| {
article_language_selection_popup(s, available_languages.clone().unwrap())
})))
}
}
impl View for ArticleView {
fn draw(&self, printer: &cursive::Printer) {
let printer = self.scroll.sub_printer(printer);
let viewport = self.scroll.content_viewport();
for (y, line) in self
.content
.get_rendered_lines()
.enumerate()
.filter(|(y, _)| &viewport.top() <= y && y <= &viewport.bottom())
{
let mut x = 0;
for element in line {
let mut style = element.style;
if element.id == self.content.current_link_element_id() && CONFIG.features.links {
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) {
if self.last_size == size {
return;
}
self.content.compute_lines(size);
scroll::layout(
self,
size,
self.needs_relayout(),
|s, size| s.content.compute_lines(size),
|s, constraint| s.content.required_size(constraint),
);
self.last_size = size;
debug!("final size for the view is '({},{})'", size.x, size.y);
}
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
self.content.required_size(constraint)
}
fn needs_relayout(&self) -> bool {
self.scroll.needs_relayout()
}
fn take_focus(&mut self, _: cursive::direction::Direction) -> Result<EventResult, CannotFocus> {
Ok(EventResult::Consumed(None))
}
fn on_event(&mut self, event: cursive::event::Event) -> EventResult {
scroll::on_event(
self,
event,
|s, ev| match ev {
key if key == CONFIG.keybindings.right && CONFIG.features.links => {
s.content.select_next_link();
s.check_and_update_viewport();
EventResult::consumed()
}
key if key == CONFIG.keybindings.left && CONFIG.features.links => {
s.content.select_prev_link();
s.check_and_update_viewport();
EventResult::consumed()
}
Event::Key(Key::Enter) if CONFIG.features.links => s.check_and_open_link(),
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} => {
if let Some(element) = s.content.element_by_pos(position.saturating_sub(offset))
{
return match element.kind {
ElementType::Link(_) if CONFIG.features.links => {
s.content.select_link_by_id(element.id());
s.check_and_open_link()
}
_ => EventResult::Ignored,
};
}
EventResult::Ignored
}
_ => EventResult::Ignored,
},
|s| s.check_and_update_selection(),
|s, si| s.important_area(si),
)
}
}
impl_scroller!(ArticleView::scroll);