TextView: print styled spans of text

This commit is contained in:
Alexandre Bury 2018-01-10 23:58:29 +01:00
parent ef7cfb2dd6
commit 890b3f13e1
6 changed files with 329 additions and 63 deletions

22
examples/markup.rs Normal file
View File

@ -0,0 +1,22 @@
extern crate cursive;
use cursive::Cursive;
use cursive::utils::markup::MarkdownText;
use cursive::views::{Dialog, TextView};
// Make sure you compile with the `markdown` feature!
//
// cargo run --example markup --features markdown
fn main() {
let mut siv = Cursive::new();
let text = MarkdownText("Isn't *that* **cool**?");
siv.add_layer(
Dialog::around(TextView::styled(text).unwrap())
.button("Hell yeah!", |s| s.quit()),
);
siv.run();
}

View File

@ -143,6 +143,8 @@ impl<'a> Printer<'a> {
let color = style.color; let color = style.color;
let effects = style.effects; let effects = style.effects;
// eprintln!("{:?}", effects);
if let Some(color) = color { if let Some(color) = color {
self.with_color(color, |printer| { self.with_color(color, |printer| {
printer.with_effects(effects, f); printer.with_effects(effects, f);
@ -172,7 +174,8 @@ impl<'a> Printer<'a> {
Some(effect) => { Some(effect) => {
let mut effects = effects; let mut effects = effects;
effects.remove(effect); effects.remove(effect);
self.with_effects(effects, f);
self.with_effect(effect, |s| s.with_effects(effects, f));
} }
} }
} }

View File

@ -9,6 +9,47 @@ use std::borrow::Cow;
use theme::{Effect, Style}; use theme::{Effect, Style};
use utils::lines::spans::Span; use utils::lines::spans::Span;
/// `Markup` trait implementation for markdown text.
///
/// Requires the `markdown` feature.
pub struct Markdown;
impl super::Markup for Markdown {
type Error = ();
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> {
Ok(parse(input))
}
}
/// Thin wrapper around text that should be parsed as Markdown.
///
/// This does not parse the text here, but indicates how it should be parsed.
///
/// # Examples
///
/// ```rust
/// // Can use `&str`
/// let text = MarkdownText("*Markdown* text!");
///
/// // Or `String`
/// let text = MarkdownText(String::from("*Right __here__!"));
/// ```
pub struct MarkdownText<S>(pub S)
where
S: Into<String>;
impl<S> super::MarkupText for MarkdownText<S>
where
S: Into<String>,
{
type M = Markdown;
fn to_string(self) -> String {
self.0.into()
}
}
/// Iterator that parse a markdown text and outputs styled spans. /// Iterator that parse a markdown text and outputs styled spans.
pub struct Parser<'a> { pub struct Parser<'a> {
first: bool, first: bool,
@ -116,19 +157,7 @@ impl<'a> Iterator for Parser<'a> {
/// This is a shortcut for `Parser::new(input).collect()`. /// This is a shortcut for `Parser::new(input).collect()`.
pub fn parse<'a>(input: &'a str) -> Vec<Span<'a>> { pub fn parse<'a>(input: &'a str) -> Vec<Span<'a>> {
Parser::new(input).collect() Parser::new(input).collect()
} // Parser::new(input).inspect(|span| eprintln!("{:?}", span)).collect()
/// `Markup` trait implementation for markdown text.
///
/// Requires the `markdown` feature.
pub struct Markdown;
impl super::Markup for Markdown {
type Error = ();
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> {
Ok(parse(input))
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -6,12 +6,14 @@
pub mod markdown; pub mod markdown;
#[cfg(feature = "markdown")] #[cfg(feature = "markdown")]
pub use self::markdown::Markdown; pub use self::markdown::MarkdownText;
use owning_ref::OwningHandle; use owning_ref::OwningHandle;
use owning_ref::StringRef; use owning_ref::StringRef;
use utils::lines::spans::Span;
use theme::Style;
use std::borrow::Cow; use std::borrow::Cow;
use std::ops::Deref;
use theme::Style;
use utils::lines::spans::Span;
/// Trait for parsing text into styled spans. /// Trait for parsing text into styled spans.
pub trait Markup { pub trait Markup {
@ -36,6 +38,28 @@ pub trait Markup {
} }
} }
/// Thin wrapper around a string, with a markup format.
///
/// This only wraps the text and indicates how it should be parsed;
/// it does not parse the text itself.
pub trait MarkupText {
/// Markup format to use to parse the string.
type M: Markup;
/// Access the inner string.
fn to_string(self) -> String;
}
/// Unwrapped text gets the "Plain" markup for free.
impl<S: Into<String>> MarkupText for S {
type M = Plain;
fn to_string(self) -> String {
self.into()
}
}
/// Dummy `Markup` implementation that returns the text as-is. /// Dummy `Markup` implementation that returns the text as-is.
pub struct Plain; pub struct Plain;
@ -43,10 +67,12 @@ impl Markup for Plain {
type Error = (); type Error = ();
fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> { fn parse<'a>(input: &'a str) -> Result<Vec<Span<'a>>, Self::Error> {
Ok(vec![Span { Ok(vec![
text: Cow::Borrowed(input), Span {
style: Style::none(), text: Cow::Borrowed(input),
}]) style: Style::none(),
},
])
} }
} }
@ -57,37 +83,105 @@ pub type StyledHandle = OwningHandle<StringRef, Vec<Span<'static>>>;
/// A String that parses a markup language. /// A String that parses a markup language.
pub struct StyledString { pub struct StyledString {
content: StyledHandle, content: Option<StyledHandle>,
} }
impl StyledString { impl StyledString {
/// Creates a new styled string, parsing the given content. /// Creates a new styled string, parsing the given content.
pub fn new<S, M>(content: S) -> Result<Self, M::Error> ///
/// # Examples
///
/// ```rust
/// # use cursive::utils::markup::StyledString;
/// let styled_string = StyledString::new("*plain* text");
/// ```
pub fn new<T>(content: T) -> Result<Self, <T::M as Markup>::Error>
where where
S: Into<String>, T: MarkupText,
M: Markup,
{ {
let content = M::make_handle(content)?; let content = content.to_string();
let content = Some(T::M::make_handle(content)?);
Ok(StyledString { content }) Ok(StyledString { content })
} }
/// Returns a plain StyledString without any style.
///
/// > You got no style, Dutch. You know that.
pub fn plain<S>(content: S) -> Self
where S: Into<String>
{
Self::new(content).unwrap()
}
/// Sets the content of this string. /// Sets the content of this string.
/// ///
/// The content will be parsed; if an error is found, /// # Examples
/// it will be returned here (and the content will be unchanged). ///
pub fn set_content<S, M>(&mut self, content: S) -> Result<(), M::Error> /// ```rust
/// # use cursive::utils::markup::StyledString;
/// # let mut styled_string = StyledString::new("").unwrap();
/// styled_string.set_content("*plain* text").unwrap();
/// ```
pub fn set_content<T>(
&mut self, content: T
) -> Result<(), <<T as MarkupText>::M as Markup>::Error>
where where
S: Into<String>, T: MarkupText,
M: Markup,
{ {
self.content = M::make_handle(content)?; let content = content.to_string();
self.content = Some(T::M::make_handle(content)?);
Ok(()) Ok(())
} }
/// Sets the content of this string to plain text.
pub fn set_plain<S>(&mut self, content: S) where S: Into<String> {
self.set_content(content).unwrap();
}
/// Append `content` to the end.
///
/// Re-parse everything after.
pub fn append_content<T>(&mut self, content: T) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText
{
self.with_content::<T::M, _, _>(|c| c.push_str(&content.to_string()))
}
/// Run a closure on the text content.
///
/// And re-parse everything after.
pub fn with_content<M, F, O>(&mut self, f: F) -> Result<O, M::Error>
where
M: Markup,
F: FnOnce(&mut String) -> O,
{
// Get hold of the StyledHandle
let content = self.content.take().unwrap();
// Get the inner String
let mut content = content.into_inner().into_inner();
// Do what we have to do
let out = f(&mut content);
// And re-parse everything
self.content = Some(M::make_handle(content)?);
Ok(out)
}
/// Gives access to the parsed styled spans. /// Gives access to the parsed styled spans.
pub fn spans<'a>(&'a self) -> &'a [Span<'a>] { pub fn spans<'a>(&'a self) -> &'a [Span<'a>] {
&self.content &self.content.as_ref().unwrap()
}
}
impl Deref for StyledString {
type Target = str;
fn deref(&self) -> &str {
&self.content.as_ref().unwrap().owner()
} }
} }

View File

@ -10,7 +10,8 @@ use std::sync::{Mutex, MutexGuard};
use std::sync::Arc; use std::sync::Arc;
use theme::Effect; use theme::Effect;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use utils::lines::simple::{LinesIterator, Row}; use utils::lines::spans::{Row, SpanLinesIterator};
use utils::markup::{Markup, MarkupText, StyledString};
use vec::Vec2; use vec::Vec2;
use view::{ScrollBase, ScrollStrategy, SizeCache, View}; use view::{ScrollBase, ScrollStrategy, SizeCache, View};
@ -38,13 +39,28 @@ pub struct TextContent {
impl TextContent { impl TextContent {
/// Creates a new text content around the given value. /// Creates a new text content around the given value.
pub fn new<S: Into<String>>(content: S) -> Self { pub fn new<S>(content: S) -> Self
TextContent { where
S: Into<String>,
{
Self::styled(content).unwrap()
}
/// Creates a new text content around the given value.
///
/// Parses the given value.
pub fn styled<T>(content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
let content = StyledString::new(content)?;
Ok(TextContent {
content: Arc::new(Mutex::new(TextContentInner { content: Arc::new(Mutex::new(TextContentInner {
content: content.into(), content,
size_cache: None, size_cache: None,
})), })),
} })
} }
} }
@ -70,13 +86,33 @@ impl Deref for TextContentRef {
impl TextContent { impl TextContent {
/// Replaces the content with the given value. /// Replaces the content with the given value.
pub fn set_content<S: Into<String>>(&mut self, content: S) { pub fn set_content<S>(&mut self, content: S)
self.with_content(|c| *c = content.into()); where
S: Into<String>,
{
self.with_content(|c| c.set_plain(content));
}
/// Replaces the content with the given value.
///
/// The given markup text will be parsed.
pub fn set_markup<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.with_content(|c| c.set_content(content))
} }
/// Append `content` to the end of a `TextView`. /// Append `content` to the end of a `TextView`.
pub fn append_content(&mut self, content: &str) { pub fn append_content<T>(
self.with_content(|c| c.push_str(content)); &mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.with_content(|c| c.append_content(content))
} }
/// Returns a reference to the content. /// Returns a reference to the content.
@ -87,20 +123,25 @@ impl TextContent {
TextContentInner::get_content(&self.content) TextContentInner::get_content(&self.content)
} }
fn with_content<F>(&mut self, f: F) fn with_content<F, O>(&mut self, f: F) -> O
where where
F: FnOnce(&mut String), F: FnOnce(&mut StyledString) -> O,
{ {
let mut lock = self.content.lock().unwrap(); let mut lock = self.content.lock().unwrap();
f(&mut lock.content); let out = f(&mut lock.content);
lock.size_cache = None; lock.size_cache = None;
out
} }
} }
struct TextContentInner { struct TextContentInner {
content: String, // content: String,
content: StyledString,
// We keep the cache here so it can be busted when we change the content.
size_cache: Option<XY<SizeCache>>, size_cache: Option<XY<SizeCache>>,
} }
@ -164,8 +205,28 @@ fn strip_last_newline(content: &str) -> &str {
impl TextView { impl TextView {
/// Creates a new TextView with the given content. /// Creates a new TextView with the given content.
pub fn new<S: Into<String>>(content: S) -> Self { pub fn new<S>(content: S) -> Self
TextView::new_with_content(TextContent::new(content)) where
S: Into<String>,
{
Self::styled(content).unwrap()
}
/// Creates a new TextView by parsing the given content.
///
/// # Examples
///
/// ```rust,ignore
/// # use cursive::views::TextView;
/// use cursive::utils::markup::MarkdownText;
/// // This will require the `markdown` feature!
/// let view = TextView::styled(MarkdownText("**Bold** text"));
/// ```
pub fn styled<T>(content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
TextContent::styled(content).map(TextView::new_with_content)
} }
/// Creates a new TextView using the given `Arc<Mutex<String>>`. /// Creates a new TextView using the given `Arc<Mutex<String>>`.
@ -263,21 +324,62 @@ impl TextView {
/// Replace the text in this view. /// Replace the text in this view.
/// ///
/// Chainable variant. /// Chainable variant.
pub fn content<S: Into<String>>(self, content: S) -> Self { pub fn content<S>(self, content: S) -> Self
self.with(|s| s.set_content(content)) where
S: Into<String>,
{
self.markup(content).unwrap()
} }
/// Replace the text in this view. /// Replace the text in this view.
pub fn set_content<S: Into<String>>(&mut self, content: S) { ///
let content = content.into(); /// Parse the given markup text.
self.content.lock().unwrap().content = content; ///
/// Chainable variant.
pub fn markup<T>(self, content: T) -> Result<Self, <T::M as Markup>::Error>
where
T: MarkupText,
{
self.try_with(|s| s.set_markup(content))
}
/// Replace the text in this view.
pub fn set_content<S>(&mut self, content: S)
where
S: Into<String>,
{
self.set_markup(content).unwrap();
}
/// Replace the text in this view.
///
/// Parses the given markup text.
pub fn set_markup<T>(
&mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.content.lock().unwrap().content.set_content(content)?;
self.invalidate(); self.invalidate();
Ok(())
} }
/// Append `content` to the end of a `TextView`. /// Append `content` to the end of a `TextView`.
pub fn append_content(&mut self, content: &str) { pub fn append_content<T>(
self.content.lock().unwrap().content.push_str(content); &mut self, content: T
) -> Result<(), <T::M as Markup>::Error>
where
T: MarkupText,
{
self.content
.lock()
.unwrap()
.content
.append_content(content)?;
self.invalidate(); self.invalidate();
Ok(())
} }
/// Returns the current text in this view. /// Returns the current text in this view.
@ -365,8 +467,7 @@ impl TextView {
// First attempt: naively hope that we won't need a scrollbar_width // First attempt: naively hope that we won't need a scrollbar_width
// (This means we try to use the entire available width for text). // (This means we try to use the entire available width for text).
self.rows = self.rows =
LinesIterator::new(strip_last_newline(&content.content), size.x) SpanLinesIterator::new(content.content.spans(), size.x).collect();
.collect();
// Width taken by the scrollbar. Without a scrollbar, it's 0. // Width taken by the scrollbar. Without a scrollbar, it's 0.
let mut scrollbar_width = 0; let mut scrollbar_width = 0;
@ -382,7 +483,8 @@ impl TextView {
}; };
self.rows = self.rows =
LinesIterator::new(&content.content, available).collect(); SpanLinesIterator::new(content.content.spans(), available)
.collect();
if self.rows.is_empty() && !content.content.is_empty() { if self.rows.is_empty() && !content.content.is_empty() {
// We have some content, we we didn't find any row for it? // We have some content, we we didn't find any row for it?
@ -431,10 +533,17 @@ impl View for TextView {
printer.with_effect(self.effect, |printer| { printer.with_effect(self.effect, |printer| {
self.scrollbase.draw(printer, |printer, i| { self.scrollbase.draw(printer, |printer, i| {
let row = &self.rows[i]; let row = &self.rows[i];
let text = &content.content[row.start..row.end]; let l = row.width;
let l = text.width(); let mut x = self.align.h.get_offset(l, printer.size.x);
let x = self.align.h.get_offset(l, printer.size.x);
printer.print((x, 0), text); for span in row.resolve(content.content.spans()) {
printer.with_style(span.style, |printer| {
printer.print((x, 0), &span.text);
x += span.text.len();
});
}
// let text = &content.content[row.start..row.end];
// printer.print((x, 0), text);
}); });
}); });
} }

View File

@ -5,6 +5,15 @@ pub trait With: Sized {
f(&mut self); f(&mut self);
self self
} }
/// Calls the given closure on `self`.
fn try_with<E, F>(mut self, f: F) -> Result<Self, E>
where
F: FnOnce(&mut Self) -> Result<(), E>,
{
f(&mut self)?;
Ok(self)
}
} }
impl<T: Sized> With for T {} impl<T: Sized> With for T {}