//! Provide higher-level abstraction to draw things on backends. use crate::backend::Backend; use crate::direction::Orientation; use crate::rect::Rect; use crate::theme::{ BorderStyle, ColorStyle, Effect, PaletteColor, Style, Theme, }; use crate::utils::lines::simple::{prefix, suffix}; use crate::with::With; use crate::Vec2; use enumset::EnumSet; use std::cmp::min; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; /// Convenient interface to draw on a subset of the screen. /// /// The area it can print on is defined by `offset` and `size`. /// /// The part of the content it will print is defined by `content_offset` /// and `size`. #[derive(Clone)] pub struct Printer<'a, 'b> { /// Offset into the window this printer should start drawing at. /// /// A print request at `x` will really print at `x + offset`. pub offset: Vec2, /// Size of the area we are allowed to draw on. /// /// Anything outside of this should be discarded. /// /// 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, /// Offset into the view for this printer. /// /// 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. /// /// A print request `x`, will really print at `x - content_offset`. pub content_offset: Vec2, /// Whether the view to draw is currently focused or not. pub focused: bool, /// Whether the view to draw is currently enabled or not. pub enabled: bool, /// Currently used theme pub theme: &'a Theme, /// Backend used to actually draw things backend: &'b dyn Backend, } impl<'a, 'b> Printer<'a, 'b> { /// Creates a new printer on the given window. /// /// But nobody needs to know that. #[doc(hidden)] pub fn new>( size: T, theme: &'a Theme, backend: &'b dyn Backend, ) -> Self { let size = size.into(); Printer { offset: Vec2::zero(), content_offset: Vec2::zero(), output_size: size, size, focused: true, enabled: true, theme, backend, } } /// Clear the screen. /// /// It will discard anything drawn before. /// /// Users rarely need to call this directly. pub fn clear(&self) { self.backend .clear(self.theme.palette[PaletteColor::Background]); } /// Prints some styled text at the given position. pub fn print_styled( &self, start: S, text: crate::utils::span::SpannedStr<'_, Style>, ) where S: Into, { let Vec2 { mut x, y } = start.into(); for span in text.spans() { self.with_style(*span.attr, |printer| { printer.print_with_width((x, y), span.content, |_| span.width); x += span.width; }); } } // 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? /// Prints some text at the given position pub fn print>(&self, start: S, text: &str) { 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(&self, start: S, text: &str, width: F) where S: Into, F: FnOnce(&str) -> usize, { // Where we are asked to start printing. Oh boy. It's not that simple. let start = start.into(); // We accept requests between `content_offset` and // `content_offset + output_size`. if !start.strictly_lt(self.output_size + self.content_offset) { return; } // 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); if hidden_part.y > 0 { // Since we are printing a single line, there's nothing we can do. return; } let mut text_width = width(text); // 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; } assert!(start.fits(self.content_offset)); // What we did before should guarantee that this won't overflow. start = start - self.content_offset; // Do we have enough room for the entire line? let room = self.output_size.x - start.x; 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); } let start = start + self.offset; self.backend.print_at(start, text); } /// Prints a vertical line using the given character. pub fn print_vline>( &self, start: T, height: usize, c: &str, ) { let start = start.into(); // Here again, we can abort if we're trying to print too far right or // too low. if !start.strictly_lt(self.output_size + self.content_offset) { return; } // hidden_part describes how far to the top left of the viewport we are. let hidden_part = self.content_offset.saturating_sub(start); if hidden_part.x > 0 || hidden_part.y >= height { // We're printing a single column, so we can't do much here. return; } // Skip `hidden_part` let start = start + hidden_part; assert!(start.fits(self.content_offset)); let height = height - hidden_part.y; // 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); let start = start + self.offset; for y in 0..height { self.backend.print_at(start + (0, y), c); } } /// Prints a line using the given character. pub fn print_line>( &self, orientation: Orientation, start: T, length: usize, c: &str, ) { 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. pub fn print_hline>(&self, start: T, width: usize, c: &str) { let start = start.into(); // Nothing to be done if the start if too far to the bottom/right if !start.strictly_lt(self.output_size + self.content_offset) { return; } let hidden_part = self.content_offset.saturating_sub(start); if hidden_part.y > 0 || hidden_part.x >= width { // We're printing a single line, so we can't do much here. return; } // Skip `hidden_part` let start = start + hidden_part; assert!(start.fits(self.content_offset)); let width = width - hidden_part.x; // Don't go too far 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(); let start = start + self.offset; self.backend.print_at_rep(start, repetitions, c); } /// Call the given closure with a colored printer, /// that will apply the given color on prints. /// /// # Examples /// /// ```rust /// # use cursive_core::Printer; /// # use cursive_core::theme; /// # use cursive_core::backend; /// # let b = backend::Dummy::init(); /// # let t = theme::load_default(); /// # let printer = Printer::new((6,4), &t, &*b); /// printer.with_color(theme::ColorStyle::highlight(), |printer| { /// printer.print((0,0), "This text is highlighted!"); /// }); /// ``` pub fn with_color(&self, c: ColorStyle, f: F) where F: FnOnce(&Printer<'_, '_>), { 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(&self, style: T, f: F) where F: FnOnce(&Printer<'_, '_>), T: Into