#![warn(missing_docs, rust_2018_idioms)]
#[cfg(feature = "html")]
pub mod html;
use std::rc;
use cursive::theme;
use unicode_width::UnicodeWidthStr as _;
pub struct MarkupView<R: Renderer + 'static> {
renderer: R,
doc: Option<RenderedDocument>,
on_link_focus: Option<rc::Rc<LinkCallback>>,
on_link_select: Option<rc::Rc<LinkCallback>>,
maximum_width: Option<usize>,
}
pub type LinkCallback = dyn Fn(&mut cursive::Cursive, &str);
pub trait Renderer {
fn render(&self, constraint: cursive::XY<usize>) -> RenderedDocument;
}
#[derive(Clone, Debug)]
pub struct RenderedDocument {
lines: Vec<Vec<RenderedElement>>,
link_handler: LinkHandler,
size: cursive::XY<usize>,
constraint: cursive::XY<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Element {
text: String,
style: theme::Style,
link_target: Option<String>,
}
#[derive(Clone, Debug, Default)]
struct RenderedElement {
text: String,
style: theme::Style,
link_idx: Option<usize>,
}
#[derive(Clone, Debug, Default)]
struct LinkHandler {
links: Vec<Link>,
focus: usize,
}
#[derive(Clone, Debug)]
struct Link {
position: cursive::XY<usize>,
width: usize,
target: String,
}
#[cfg(feature = "html")]
impl MarkupView<html::RichRenderer> {
pub fn html(html: &str) -> MarkupView<html::RichRenderer> {
MarkupView::with_renderer(html::Renderer::new(html))
}
}
impl<R: Renderer + 'static> MarkupView<R> {
pub fn with_renderer(renderer: R) -> MarkupView<R> {
MarkupView {
renderer,
doc: None,
on_link_focus: None,
on_link_select: None,
maximum_width: None,
}
}
pub fn on_link_focus<F: Fn(&mut cursive::Cursive, &str) + 'static>(&mut self, f: F) {
self.on_link_focus = Some(rc::Rc::new(f));
}
pub fn on_link_select<F: Fn(&mut cursive::Cursive, &str) + 'static>(&mut self, f: F) {
self.on_link_select = Some(rc::Rc::new(f));
}
pub fn set_maximum_width(&mut self, width: usize) {
self.maximum_width = Some(width);
}
fn render(&mut self, mut constraint: cursive::XY<usize>) -> cursive::XY<usize> {
let mut last_focus = 0;
if let Some(width) = self.maximum_width {
constraint.x = std::cmp::min(width, constraint.x);
}
if let Some(doc) = &self.doc {
if constraint.x == doc.constraint.x {
return doc.size;
}
last_focus = doc.link_handler.focus;
}
let mut doc = self.renderer.render(constraint);
if last_focus < doc.link_handler.links.len() {
doc.link_handler.focus = last_focus;
}
let size = doc.size;
self.doc = Some(doc);
size
}
}
impl<R: Renderer + 'static> cursive::View for MarkupView<R> {
fn draw(&self, printer: &cursive::Printer<'_, '_>) {
let doc = &self.doc.as_ref().expect("layout not called before draw");
for (y, line) in doc.lines.iter().enumerate() {
let mut x = 0;
for element in line {
let mut style = element.style;
if let Some(link_idx) = element.link_idx {
if printer.focused && doc.link_handler.focus == link_idx {
style = style.combine(cursive::theme::PaletteColor::Highlight);
}
}
printer.with_style(style, |printer| printer.print((x, y), &element.text));
x += element.text.width();
}
}
}
fn layout(&mut self, constraint: cursive::XY<usize>) {
self.render(constraint);
}
fn required_size(&mut self, constraint: cursive::XY<usize>) -> cursive::XY<usize> {
self.render(constraint)
}
fn take_focus(&mut self, direction: cursive::direction::Direction) -> bool {
self.doc
.as_mut()
.map(|doc| doc.link_handler.take_focus(direction))
.unwrap_or_default()
}
fn on_event(&mut self, event: cursive::event::Event) -> cursive::event::EventResult {
use cursive::direction::Absolute;
use cursive::event::{Callback, Event, EventResult, Key};
let link_handler = if let Some(doc) = self.doc.as_mut() {
if doc.link_handler.links.is_empty() {
return EventResult::Ignored;
} else {
&mut doc.link_handler
}
} else {
return EventResult::Ignored;
};
let focus_changed = match event {
Event::Key(Key::Left) => link_handler.move_focus(Absolute::Left),
Event::Key(Key::Right) => link_handler.move_focus(Absolute::Right),
Event::Key(Key::Up) => link_handler.move_focus(Absolute::Up),
Event::Key(Key::Down) => link_handler.move_focus(Absolute::Down),
_ => false,
};
if focus_changed {
let target = link_handler.links[link_handler.focus].target.clone();
EventResult::Consumed(
self.on_link_focus
.clone()
.map(|f| Callback::from_fn(move |s| f(s, &target))),
)
} else if event == Event::Key(Key::Enter) {
let target = link_handler.links[link_handler.focus].target.clone();
EventResult::Consumed(
self.on_link_select
.clone()
.map(|f| Callback::from_fn(move |s| f(s, &target))),
)
} else {
EventResult::Ignored
}
}
fn important_area(&self, _: cursive::XY<usize>) -> cursive::Rect {
if let Some(doc) = &self.doc {
doc.link_handler.important_area()
} else {
cursive::Rect::from((0, 0))
}
}
}
impl RenderedDocument {
pub fn new(constraint: cursive::XY<usize>) -> RenderedDocument {
RenderedDocument {
lines: Vec::new(),
link_handler: Default::default(),
size: (0, 0).into(),
constraint,
}
}
pub fn push_line<I: IntoIterator<Item = Element>>(&mut self, line: I) {
let mut rendered_line = Vec::new();
let y = self.lines.len();
let mut x = 0;
for element in line {
let width = element.text.width();
let link_idx = element.link_target.map(|target| {
self.link_handler.push(Link {
position: (x, y).into(),
width,
target,
})
});
x += width;
rendered_line.push(RenderedElement {
text: element.text,
style: element.style,
link_idx,
});
}
self.lines.push(rendered_line);
self.size = self.size.stack_vertical(&(x, 1).into());
}
}
impl Element {
pub fn new(text: String, style: theme::Style, link_target: Option<String>) -> Element {
Element {
text,
style,
link_target,
}
}
pub fn plain(text: String) -> Element {
Element {
text,
..Default::default()
}
}
pub fn styled(text: String, style: theme::Style) -> Element {
Element::new(text, style, None)
}
pub fn link(text: String, style: theme::Style, target: String) -> Element {
Element::new(text, style, Some(target))
}
}
impl From<String> for Element {
fn from(s: String) -> Element {
Element::plain(s)
}
}
impl From<Element> for RenderedElement {
fn from(element: Element) -> RenderedElement {
RenderedElement {
text: element.text,
style: element.style,
link_idx: None,
}
}
}
impl LinkHandler {
pub fn push(&mut self, link: Link) -> usize {
self.links.push(link);
self.links.len() - 1
}
pub fn take_focus(&mut self, direction: cursive::direction::Direction) -> bool {
if self.links.is_empty() {
false
} else {
use cursive::direction::{Absolute, Direction, Relative};
let rel = match direction {
Direction::Abs(abs) => match abs {
Absolute::Up | Absolute::Left | Absolute::None => Relative::Front,
Absolute::Down | Absolute::Right => Relative::Back,
},
Direction::Rel(rel) => rel,
};
self.focus = match rel {
Relative::Front => 0,
Relative::Back => self.links.len() - 1,
};
true
}
}
pub fn move_focus(&mut self, direction: cursive::direction::Absolute) -> bool {
use cursive::direction::{Absolute, Relative};
match direction {
Absolute::Left => self.move_focus_horizontal(Relative::Front),
Absolute::Right => self.move_focus_horizontal(Relative::Back),
Absolute::Up => self.move_focus_vertical(Relative::Front),
Absolute::Down => self.move_focus_vertical(Relative::Back),
Absolute::None => false,
}
}
fn move_focus_horizontal(&mut self, direction: cursive::direction::Relative) -> bool {
use cursive::direction::Relative;
if self.links.is_empty() {
return false;
}
let new_focus = match direction {
Relative::Front => self.focus.checked_sub(1),
Relative::Back => {
if self.focus < self.links.len() - 1 {
Some(self.focus + 1)
} else {
None
}
}
};
if let Some(new_focus) = new_focus {
if self.links[self.focus].position.y == self.links[new_focus].position.y {
self.focus = new_focus;
true
} else {
false
}
} else {
false
}
}
fn move_focus_vertical(&mut self, direction: cursive::direction::Relative) -> bool {
use cursive::direction::Relative;
if self.links.is_empty() {
return false;
}
let y = self.links[self.focus].position.y;
let iter = self.links.iter().enumerate();
let next = match direction {
Relative::Front => iter
.rev()
.skip(self.links.len() - self.focus)
.find(|(_, link)| link.position.y < y),
Relative::Back => iter
.skip(self.focus + 1)
.find(|(_, link)| link.position.y > y),
};
if let Some((idx, _)) = next {
self.focus = idx;
true
} else {
false
}
}
pub fn important_area(&self) -> cursive::Rect {
if self.links.is_empty() {
cursive::Rect::from((0, 0))
} else {
let link = &self.links[self.focus];
cursive::Rect::from_size(link.position, (link.width, 1))
}
}
}