cursive/cursive-core/src/printer.rs

640 lines
20 KiB
Rust
Raw Normal View History

2018-04-17 23:18:59 +00:00
//! Provide higher-level abstraction to draw things on backends.
2015-05-15 19:16:58 +00:00
use crate::backend::Backend;
use crate::direction::Orientation;
2020-06-29 22:24:41 +00:00
use crate::rect::Rect;
2019-03-01 00:04:14 +00:00
use crate::theme::{
BorderStyle, ColorStyle, Effect, PaletteColor, Style, Theme,
};
use crate::utils::lines::simple::{prefix, suffix};
use crate::with::With;
2020-01-06 23:51:38 +00:00
use crate::Vec2;
2020-06-29 22:24:41 +00:00
use enumset::EnumSet;
2016-10-02 22:22:29 +00:00
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
2018-04-17 23:18:59 +00:00
use unicode_width::UnicodeWidthStr;
/// Convenient interface to draw on a subset of the screen.
2018-04-17 23:18:59 +00:00
///
/// The area it can print on is defined by `offset` and `size`.
///
2018-04-25 00:19:07 +00:00
/// The part of the content it will print is defined by `content_offset`
2018-04-17 23:18:59 +00:00
/// and `size`.
2018-11-09 22:08:43 +00:00
#[derive(Clone)]
2018-05-21 13:53:38 +00:00
pub struct Printer<'a, 'b> {
/// Offset into the window this printer should start drawing at.
2018-04-17 23:18:59 +00:00
///
2018-04-25 00:19:07 +00:00
/// A print request at `x` will really print at `x + offset`.
pub offset: Vec2,
2018-04-05 00:48:51 +00:00
/// Size of the area we are allowed to draw on.
2018-04-05 00:48:51 +00:00
///
/// Anything outside of this should be discarded.
2019-03-08 06:33:09 +00:00
///
/// The view being drawn can ingore this, but anything further than that
/// will be ignored.
pub output_size: Vec2,
/// Size allocated to the view.
///
/// This should be the same value as the one given in the last call to
/// `View::layout`.
pub size: Vec2,
2018-04-05 00:48:51 +00:00
2018-04-17 23:18:59 +00:00
/// Offset into the view for this printer.
///
2019-03-08 06:33:09 +00:00
/// The view being drawn can ignore this, but anything to the top-left of
/// this will actually be ignored, so it can be used to skip this part.
///
2018-04-25 00:19:07 +00:00
/// A print request `x`, will really print at `x - content_offset`.
2018-04-17 23:18:59 +00:00
pub content_offset: Vec2,
/// Whether the view to draw is currently focused or not.
pub focused: bool,
2018-04-05 00:48:51 +00:00
2018-11-09 22:08:43 +00:00
/// Whether the view to draw is currently enabled or not.
pub enabled: bool,
/// Currently used theme
pub theme: &'a Theme,
2016-10-09 22:47:06 +00:00
/// Backend used to actually draw things
2019-02-28 23:55:02 +00:00
backend: &'b dyn Backend,
}
2018-05-21 13:53:38 +00:00
impl<'a, 'b> Printer<'a, 'b> {
2015-05-22 06:29:49 +00:00
/// Creates a new printer on the given window.
///
/// But nobody needs to know that.
#[doc(hidden)]
2017-10-12 23:38:55 +00:00
pub fn new<T: Into<Vec2>>(
2019-07-30 23:08:05 +00:00
size: T,
theme: &'a Theme,
backend: &'b dyn Backend,
2017-10-12 23:38:55 +00:00
) -> Self {
let size = size.into();
2015-05-22 06:29:49 +00:00
Printer {
offset: Vec2::zero(),
2018-04-05 00:48:51 +00:00
content_offset: Vec2::zero(),
output_size: size,
size,
focused: true,
2018-11-09 22:08:43 +00:00
enabled: true,
2018-04-10 18:53:25 +00:00
theme,
backend,
2015-05-22 06:29:49 +00:00
}
}
2016-10-09 22:47:06 +00:00
/// Clear the screen.
///
2016-10-15 01:18:19 +00:00
/// It will discard anything drawn before.
///
/// Users rarely need to call this directly.
2016-10-09 22:47:06 +00:00
pub fn clear(&self) {
2018-01-22 19:55:56 +00:00
self.backend
.clear(self.theme.palette[PaletteColor::Background]);
2016-10-09 22:47:06 +00:00
}
2018-11-30 21:19:08 +00:00
/// Prints some styled text at the given position.
pub fn print_styled<S>(
2019-07-30 23:08:05 +00:00
&self,
start: S,
text: crate::utils::span::SpannedStr<'_, Style>,
2018-11-30 21:19:08 +00:00
) where
S: Into<Vec2>,
{
let Vec2 { mut x, y } = start.into();
for span in text.spans() {
self.with_style(*span.attr, |printer| {
2019-03-20 23:57:48 +00:00
printer.print_with_width((x, y), span.content, |_| span.width);
x += span.width;
2018-11-30 21:19:08 +00:00
});
}
}
2016-06-28 05:10:59 +00:00
// TODO: use &mut self? We don't *need* it, but it may make sense.
// We don't want people to start calling prints in parallel?
2018-11-30 21:19:08 +00:00
/// Prints some text at the given position
2018-04-25 00:19:07 +00:00
pub fn print<S: Into<Vec2>>(&self, start: S, text: &str) {
2019-03-20 23:57:48 +00:00
self.print_with_width(start, text, UnicodeWidthStr::width);
}
/// Prints some text, using the given callback to compute width.
///
/// Mostly used with `UnicodeWidthStr::width`.
/// If you already know the width, you can give it as a constant instead.
fn print_with_width<S, F>(&self, start: S, text: &str, width: F)
where
S: Into<Vec2>,
F: FnOnce(&str) -> usize,
{
// Where we are asked to start printing. Oh boy. It's not that simple.
2018-04-25 00:19:07 +00:00
let start = start.into();
2018-04-18 00:22:56 +00:00
2018-04-25 00:19:07 +00:00
// We accept requests between `content_offset` and
// `content_offset + output_size`.
2019-03-04 18:31:36 +00:00
if !start.strictly_lt(self.output_size + self.content_offset) {
2018-04-18 00:22:56 +00:00
return;
}
2018-04-25 00:19:07 +00:00
// If start < content_offset, part of the text will not be visible.
// This is the part of the text that's hidden:
// (It should always be smaller than the content offset)
let hidden_part = self.content_offset.saturating_sub(start);
2018-04-18 00:22:56 +00:00
if hidden_part.y > 0 {
// Since we are printing a single line, there's nothing we can do.
2016-03-15 22:37:57 +00:00
return;
}
2018-04-18 00:22:56 +00:00
2019-03-20 23:57:48 +00:00
let mut text_width = width(text);
2018-04-18 00:22:56 +00:00
// If we're waaaay too far left, just give up.
if hidden_part.x > text_width {
return;
}
let mut text = text;
let mut start = start;
if hidden_part.x > 0 {
// We have to drop hidden_part.x width from the start of the string.
// prefix() may be too short if there's a double-width character.
// So instead, keep the suffix and drop the prefix.
// TODO: use a different prefix method that is *at least* the width
// (and not *at most*)
let tail =
suffix(text.graphemes(true), text_width - hidden_part.x, "");
let skipped_len = text.len() - tail.length;
let skipped_width = text_width - tail.width;
assert_eq!(text[..skipped_len].width(), skipped_width);
// This should be equal most of the time, except when there's a double
// character preventing us from splitting perfectly.
assert!(skipped_width >= hidden_part.x);
// Drop part of the text, and move the cursor correspondingly.
text = &text[skipped_len..];
start = start + (skipped_width, 0);
text_width -= skipped_width;
}
2018-04-25 00:19:07 +00:00
assert!(start.fits(self.content_offset));
2018-04-18 00:22:56 +00:00
// What we did before should guarantee that this won't overflow.
start = start - self.content_offset;
2018-04-18 00:22:56 +00:00
// Do we have enough room for the entire line?
let room = self.output_size.x - start.x;
2018-04-17 23:18:59 +00:00
if room < text_width {
// Drop the end of the text if it's too long
// We want the number of CHARACTERS, not bytes.
// (Actually we want the "width" of the string, see unicode-width)
let prefix_len = prefix(text.graphemes(true), room, "").length;
text = &text[..prefix_len];
assert!(text.width() <= room);
}
2015-05-29 00:37:00 +00:00
2018-04-25 00:19:07 +00:00
let start = start + self.offset;
self.backend.print_at(start, text);
}
/// Prints a vertical line using the given character.
pub fn print_vline<T: Into<Vec2>>(
2019-07-30 23:08:05 +00:00
&self,
start: T,
height: usize,
c: &str,
) {
2018-04-17 23:18:59 +00:00
let start = start.into();
2018-04-18 00:22:56 +00:00
2018-04-25 00:19:07 +00:00
// Here again, we can abort if we're trying to print too far right or
// too low.
2019-03-04 18:31:36 +00:00
if !start.strictly_lt(self.output_size + self.content_offset) {
2018-04-18 00:22:56 +00:00
return;
}
2018-04-25 00:19:07 +00:00
// hidden_part describes how far to the top left of the viewport we are.
2018-04-18 00:22:56 +00:00
let hidden_part = self.content_offset.saturating_sub(start);
2018-04-25 00:19:07 +00:00
if hidden_part.x > 0 || hidden_part.y >= height {
2018-04-18 00:22:56 +00:00
// We're printing a single column, so we can't do much here.
2016-03-15 22:37:57 +00:00
return;
}
2018-04-18 00:22:56 +00:00
// Skip `hidden_part`
let start = start + hidden_part;
2018-04-25 00:19:07 +00:00
assert!(start.fits(self.content_offset));
let height = height - hidden_part.y;
2018-04-18 00:22:56 +00:00
// What we did before ensures this won't overflow.
let start = start - self.content_offset;
// Don't go overboard
let height = min(height, self.output_size.y - start.y);
2018-04-17 23:18:59 +00:00
let start = start + self.offset;
2018-04-25 00:19:07 +00:00
for y in 0..height {
self.backend.print_at(start + (0, y), c);
}
}
2018-06-15 23:41:20 +00:00
/// Prints a line using the given character.
pub fn print_line<T: Into<Vec2>>(
2019-07-30 23:08:05 +00:00
&self,
orientation: Orientation,
start: T,
length: usize,
c: &str,
2018-06-15 23:41:20 +00:00
) {
match orientation {
Orientation::Vertical => self.print_vline(start, length, c),
Orientation::Horizontal => self.print_hline(start, length, c),
}
}
/// Prints a horizontal line using the given character.
2018-04-25 00:19:07 +00:00
pub fn print_hline<T: Into<Vec2>>(&self, start: T, width: usize, c: &str) {
2018-04-17 23:18:59 +00:00
let start = start.into();
2018-04-18 00:22:56 +00:00
2018-04-25 00:19:07 +00:00
// Nothing to be done if the start if too far to the bottom/right
2019-03-04 18:31:36 +00:00
if !start.strictly_lt(self.output_size + self.content_offset) {
2016-03-15 22:37:57 +00:00
return;
}
2018-04-18 00:22:56 +00:00
let hidden_part = self.content_offset.saturating_sub(start);
2018-04-25 00:19:07 +00:00
if hidden_part.y > 0 || hidden_part.x >= width {
2018-04-18 00:22:56 +00:00
// We're printing a single line, so we can't do much here.
return;
}
// Skip `hidden_part`
let start = start + hidden_part;
2018-04-25 00:19:07 +00:00
assert!(start.fits(self.content_offset));
2018-04-18 00:22:56 +00:00
2018-04-25 00:19:07 +00:00
let width = width - hidden_part.x;
// Don't go too far
2018-04-18 00:22:56 +00:00
let start = start - self.content_offset;
// Don't write too much if we're close to the end
let repetitions = min(width, self.output_size.x - start.x) / c.width();
2018-04-17 23:18:59 +00:00
let start = start + self.offset;
self.backend.print_at_rep(start, repetitions, c);
2015-05-22 06:29:49 +00:00
}
2015-05-27 07:09:22 +00:00
/// Call the given closure with a colored printer,
/// that will apply the given color on prints.
2015-05-26 23:48:27 +00:00
///
/// # Examples
///
/// ```rust
/// # use cursive_core::Printer;
/// # use cursive_core::theme;
/// # use cursive_core::backend;
/// # let b = backend::Dummy::init();
2017-06-13 01:03:52 +00:00
/// # let t = theme::load_default();
2018-04-10 19:50:40 +00:00
/// # let printer = Printer::new((6,4), &t, &*b);
/// printer.with_color(theme::ColorStyle::highlight(), |printer| {
2015-05-26 23:48:27 +00:00
/// printer.print((0,0), "This text is highlighted!");
/// });
/// ```
pub fn with_color<F>(&self, c: ColorStyle, f: F)
2017-10-12 23:38:55 +00:00
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
{
2018-05-18 00:37:39 +00:00
let old = self.backend.set_color(c.resolve(&self.theme.palette));
f(self);
self.backend.set_color(old);
}
/// Call the given closure with a styled printer,
/// that will apply the given style on prints.
pub fn with_style<F, T>(&self, style: T, f: F)
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
T: Into<Style>,
{
let style = style.into();
let color = style.color;
let effects = style.effects;
if let Some(color) = color {
self.with_color(color, |printer| {
printer.with_effects(effects, f);
});
} else {
self.with_effects(effects, f);
}
}
/// Call the given closure with a modified printer
/// that will apply the given effect on prints.
pub fn with_effect<F>(&self, effect: Effect, f: F)
2017-10-12 23:38:55 +00:00
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
{
self.backend.set_effect(effect);
f(self);
self.backend.unset_effect(effect);
}
2018-05-21 13:53:38 +00:00
/// Call the given closure with a modified printer
/// that will apply the given theme on prints.
pub fn with_theme<F>(&self, theme: &Theme, f: F)
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
2018-05-21 13:53:38 +00:00
{
2018-11-09 22:08:43 +00:00
f(&self.theme(theme));
}
/// Create a new sub-printer with the given theme.
pub fn theme<'c>(&self, theme: &'c Theme) -> Printer<'c, 'b>
where
'a: 'c,
{
Printer {
2018-06-21 21:44:30 +00:00
theme,
2018-11-09 22:08:43 +00:00
..self.clone()
}
2018-05-21 13:53:38 +00:00
}
/// Call the given closure with a modified printer
/// that will apply each given effect on prints.
pub fn with_effects<F>(&self, effects: EnumSet<Effect>, f: F)
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
{
match effects.iter().next() {
None => f(self),
Some(effect) => {
let mut effects = effects;
effects.remove(effect);
2018-01-10 22:58:29 +00:00
self.with_effect(effect, |s| s.with_effects(effects, f));
}
}
}
2015-05-20 17:36:35 +00:00
/// Prints a rectangular box.
///
2016-08-05 00:38:26 +00:00
/// If `invert` is `true`, and the theme uses `Outset` borders, then the
/// box will use an "inset" style instead.
///
2015-05-20 17:36:35 +00:00
/// # Examples
///
/// ```rust
/// # use cursive_core::Printer;
/// # use cursive_core::theme;
/// # use cursive_core::backend;
/// # let b = backend::Dummy::init();
2017-06-13 01:03:52 +00:00
/// # let t = theme::load_default();
2018-04-10 19:50:40 +00:00
/// # let printer = Printer::new((6,4), &t, &*b);
2016-08-05 00:38:26 +00:00
/// printer.print_box((0,0), (6,4), false);
2015-05-20 17:36:35 +00:00
/// ```
2017-10-12 23:38:55 +00:00
pub fn print_box<T: Into<Vec2>, S: Into<Vec2>>(
2019-07-30 23:08:05 +00:00
&self,
start: T,
size: S,
invert: bool,
2017-10-12 23:38:55 +00:00
) {
2016-07-17 08:20:41 +00:00
let start = start.into();
let size = size.into();
2017-08-14 23:32:01 +00:00
2016-07-17 08:20:41 +00:00
if size.x < 2 || size.y < 2 {
return;
}
let size = size - (1, 1);
2016-08-05 17:49:16 +00:00
self.with_high_border(invert, |s| {
s.print(start, "");
s.print(start + size.keep_y(), "");
s.print_hline(start + (1, 0), size.x - 1, "");
s.print_vline(start + (0, 1), size.y - 1, "");
});
2016-07-17 08:20:41 +00:00
2016-08-05 17:49:16 +00:00
self.with_low_border(invert, |s| {
s.print(start + size.keep_x(), "");
s.print(start + size, "");
2018-05-18 00:37:39 +00:00
s.print_hline(start + (1, 0) + size.keep_y(), size.x - 1, "");
s.print_vline(start + (0, 1) + size.keep_x(), size.y - 1, "");
});
2016-06-28 05:10:59 +00:00
}
2016-08-05 17:49:16 +00:00
/// Runs the given function using a color depending on the theme.
///
/// * If the theme's borders is `None`, return without calling `f`.
/// * If the theme's borders is "outset" and `invert` is `false`,
/// use `ColorStyle::Tertiary`.
/// * Otherwise, use `ColorStyle::Primary`.
pub fn with_high_border<F>(&self, invert: bool, f: F)
2017-10-12 23:38:55 +00:00
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
2016-08-05 17:49:16 +00:00
{
let color = match self.theme.borders {
BorderStyle::None => return,
BorderStyle::Outset if !invert => ColorStyle::tertiary(),
_ => ColorStyle::primary(),
2016-08-05 17:49:16 +00:00
};
self.with_color(color, f);
}
/// Runs the given function using a color depending on the theme.
///
/// * If the theme's borders is `None`, return without calling `f`.
/// * If the theme's borders is "outset" and `invert` is `true`,
/// use `ColorStyle::tertiary()`.
/// * Otherwise, use `ColorStyle::primary()`.
2016-08-05 17:49:16 +00:00
pub fn with_low_border<F>(&self, invert: bool, f: F)
2017-10-12 23:38:55 +00:00
where
2019-02-28 23:55:02 +00:00
F: FnOnce(&Printer<'_, '_>),
2016-08-05 17:49:16 +00:00
{
let color = match self.theme.borders {
BorderStyle::None => return,
BorderStyle::Outset if invert => ColorStyle::tertiary(),
_ => ColorStyle::primary(),
2016-08-05 17:49:16 +00:00
};
self.with_color(color, f);
}
/// Apply a selection style and call the given function.
///
/// * If `selection` is `false`, simply uses `ColorStyle::primary()`.
/// * If `selection` is `true`:
/// * If the printer currently has the focus,
/// uses `ColorStyle::highlight()`.
/// * Otherwise, uses `ColorStyle::highlight_inactive()`.
2019-03-01 00:04:14 +00:00
pub fn with_selection<F: FnOnce(&Printer<'_, '_>)>(
2019-07-30 23:08:05 +00:00
&self,
selection: bool,
f: F,
2019-03-01 00:04:14 +00:00
) {
2017-10-12 23:38:55 +00:00
self.with_color(
if selection {
if self.focused {
ColorStyle::highlight()
2017-10-12 23:38:55 +00:00
} else {
ColorStyle::highlight_inactive()
2017-10-12 23:38:55 +00:00
}
} else {
ColorStyle::primary()
2017-10-12 23:38:55 +00:00
},
f,
);
2016-07-02 08:01:09 +00:00
}
/// Prints a horizontal delimiter with side border `├` and `┤`.
pub fn print_hdelim<T>(&self, start: T, len: usize)
where
T: Into<Vec2>,
{
let start = start.into();
2016-06-28 05:10:59 +00:00
self.print(start, "");
2017-08-14 23:32:01 +00:00
self.print_hline(start + (1, 0), len.saturating_sub(2), "");
self.print(start + (len.saturating_sub(1), 0), "");
}
/// Returns a sub-printer with the given offset.
2018-04-25 00:19:07 +00:00
///
/// It will print in an area slightly to the bottom/right.
pub fn offset<S>(&self, offset: S) -> Self
where
S: Into<Vec2>,
{
let offset = offset.into();
self.clone().with(|s| {
2018-04-25 00:19:07 +00:00
// If we are drawing a part of the content,
// let's reduce this first.
let consumed = Vec2::min(s.content_offset, offset);
2018-04-25 00:19:07 +00:00
let offset = offset - consumed;
s.content_offset = s.content_offset - consumed;
2018-04-25 00:19:07 +00:00
s.offset = s.offset + offset;
s.output_size = s.output_size.saturating_sub(offset);
s.size = s.size.saturating_sub(offset);
})
}
/// Returns a new sub-printer inheriting the given focus.
///
/// If `self` is focused and `focused == true`, the child will be focused.
///
/// Otherwise, he will be unfocused.
pub fn focused(&self, focused: bool) -> Self {
self.clone().with(|s| {
s.focused &= focused;
})
}
2018-11-09 22:08:43 +00:00
/// Returns a new sub-printer inheriting the given enabled state.
///
/// If `self` is enabled and `enabled == true`, the child will be enabled.
///
/// Otherwise, he will be disabled.
pub fn enabled(&self, enabled: bool) -> Self {
self.clone().with(|s| s.enabled &= enabled)
}
2020-06-29 22:24:41 +00:00
/// Returns a new sub-printer for the given viewport.
///
/// This is a combination of offset + cropped.
pub fn windowed(&self, viewport: Rect) -> Self {
self.offset(viewport.top_left()).cropped(viewport.size())
}
/// Returns a new sub-printer with a cropped area.
///
/// The new printer size will be the minimum of `size` and its current size.
///
/// Any size reduction happens at the bottom-right.
pub fn cropped<S>(&self, size: S) -> Self
where
S: Into<Vec2>,
{
self.clone().with(|s| {
let size = size.into();
s.output_size = Vec2::min(s.output_size, size);
s.size = Vec2::min(s.size, size);
})
}
/// Returns a new sub-printer with a cropped area.
///
/// The new printer size will be the minimum of `size` and its current size.
///
/// The view will stay centered.
///
/// Note that if shrinking by an odd number, the view will round to the top-left.
pub fn cropped_centered<S>(&self, size: S) -> Self
where
S: Into<Vec2>,
{
let size = size.into();
let borders = self.size.saturating_sub(size);
let half_borders = borders / 2;
2019-03-08 21:57:05 +00:00
self.cropped(size - half_borders).offset(half_borders)
}
/// Returns a new sub-printer with a shrinked area.
///
/// The printer size will be reduced by the given border from the bottom-right.
pub fn shrinked<S>(&self, borders: S) -> Self
where
S: Into<Vec2>,
{
self.cropped(self.size.saturating_sub(borders))
}
/// Returns a new sub-printer with a shrinked area.
///
/// The printer size will be reduced by the given border, and will stay centered.
///
/// Note that if shrinking by an odd number, the view will round to the top-left.
pub fn shrinked_centered<S>(&self, borders: S) -> Self
where
S: Into<Vec2>,
{
let borders = borders.into();
let half_borders = borders / 2;
2019-03-08 21:57:05 +00:00
self.shrinked(borders - half_borders).offset(half_borders)
}
/// Returns a new sub-printer with a content offset.
2020-06-29 22:24:41 +00:00
///
/// This is useful for parent views that only show a subset of their
/// child, like `ScrollView`.
pub fn content_offset<S>(&self, offset: S) -> Self
where
S: Into<Vec2>,
{
self.clone().with(|s| {
s.content_offset = s.content_offset + offset;
})
}
/// Returns a sub-printer with a different inner size.
///
/// This will not change the actual output size, but will appear bigger to
/// users of this printer.
///
/// Useful to give to children who think they're big, but really aren't.
pub fn inner_size<S>(&self, size: S) -> Self
where
S: Into<Vec2>,
{
self.clone().with(|s| {
s.size = size.into();
})
}
}