lowfi/src/ui/interface/window.rs
2026-01-09 20:10:22 +01:00

121 lines
3.8 KiB
Rust

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<String>) -> 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<String>,
) -> ui::Result<()> {
let (rendered, height) = self.render(content)?;
crossterm::execute!(
writer,
Clear(ClearType::FromCursorDown),
MoveToColumn(0),
Print(rendered),
MoveToColumn(0),
MoveUp(height - 1),
)?;
Ok(())
}
}