use crate::ui::{self, interface::TitleBar}; use crossterm::{ cursor::{MoveToColumn, MoveUp}, style::{Print, Stylize as _}, terminal::{Clear, ClearType}, }; use std::fmt::Write as _; use unicode_segmentation::UnicodeSegmentation as _; /// Represents an abstraction for drawing the actual lowfi window itself. /// /// The main purpose of this struct is just to add the fancy border, /// as well as clear the screen before drawing. pub struct Window { /// Whether or not to include borders in the output. borderless: bool, /// The titlebar of this window. pub titlebar: TitleBar, /// The status (bottom) bar of the window, which for now shouldn't change since initialization. pub(crate) statusbar: String, /// The inner width of the window. width: usize, /// Whether content items should be automatically padded (spaced). spaced: bool, /// Whether to cautiously handle ANSI sequences by adding [`style::Attribute::Reset`] generously. fancy: bool, } impl Window { /// Initializes a new [Window]. /// /// * `width` - Inner width of the window. /// * `borderless` - Whether to include borders in the window, or not. pub fn new(width: usize, borderless: bool, spaced: bool, fancy: bool) -> Self { let statusbar = if borderless { String::new() } else { let middle = "─".repeat(width + 2); format!("└{middle}┘") }; Self { spaced, statusbar, borderless, width, fancy, titlebar: TitleBar::new(width, borderless), } } /// Renders the window itself, but doesn't actually draw it. /// /// `testing` just determines whether to add special features /// like color resets and carriage returns. /// /// This returns both the final rendered window and also the full /// height of the rendered window. pub(crate) fn render(&self, content: Vec) -> ui::Result<(String, u16)> { let newline: &str = if self.fancy { "\r\n" } else { "\n" }; let len: u16 = content.len().try_into()?; // Note that this will have a trailing newline, which we use later. let menu: String = content.into_iter().fold(String::new(), |mut output, x| { // Horizontal Padding & Border let padding = if self.borderless { " " } else { "│" }; let space = if self.spaced { " ".repeat(self.width.saturating_sub(x.graphemes(true).count())) } else { String::new() }; let center = if self.fancy { x.reset().to_string() } else { x }; write!(output, "{padding} {center}{space} {padding}{newline}").unwrap(); output }); // We're doing this because Windows is stupid and can't stand // writing to the last line repeatedly. #[cfg(windows)] let (height, suffix) = (len + 3, newline); #[cfg(not(windows))] let (height, suffix) = (len + 2, ""); // There's no need for another newline after the main menu content, because it already has one. Ok(( format!( "{}{newline}{menu}{}{suffix}", self.titlebar.content, self.statusbar, ), height, )) } /// Actually draws the window, with each element in `content` being on a new line. pub fn draw( &mut self, mut writer: impl std::io::Write, content: Vec, ) -> ui::Result<()> { let (rendered, height) = self.render(content)?; crossterm::execute!( writer, Clear(ClearType::FromCursorDown), MoveToColumn(0), Print(rendered), MoveToColumn(0), MoveUp(height - 1), )?; Ok(()) } }