lowfi/src/ui/interface/window.rs
2025-12-17 10:31:30 +01:00

130 lines
4.0 KiB
Rust

use std::{
fmt::Display,
io::{stdout, Stdout},
};
use crossterm::{
cursor::{MoveToColumn, MoveUp},
style::{Print, Stylize as _},
terminal::{Clear, ClearType},
};
use std::fmt::Write as _;
use unicode_segmentation::UnicodeSegmentation as _;
use crate::ui;
/// 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 top & bottom borders, which are here since they can be
/// prerendered, as they don't change every single draw.
///
/// If the option to not include borders is set, these will just be empty [String]s.
pub(crate) borders: [String; 2],
/// The inner width of the window.
width: usize,
/// The output, currently just an [`Stdout`].
out: Stdout,
}
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) -> Self {
let borders = if borderless {
[String::new(), String::new()]
} else {
let middle = "".repeat(width + 2);
[format!("{middle}"), format!("{middle}")]
};
Self {
borders,
borderless,
width,
out: stdout(),
}
}
/// Adds text to the top of the window.
pub fn display(&mut self, display: impl Display, len: usize) {
let new = format!("┌─ {} {}─┐", display, "".repeat(self.width - len - 2));
self.borders[0] = new;
}
/// 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>,
space: bool,
testing: bool,
) -> ui::Result<(String, u16)> {
let linefeed = if testing { "\n" } else { "\r\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 space {
" ".repeat(self.width.saturating_sub(x.graphemes(true).count()))
} else {
String::new()
};
let center = if testing { x } else { x.reset().to_string() };
write!(output, "{padding} {center}{space} {padding}{linefeed}").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, linefeed);
#[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!(
"{}{linefeed}{menu}{}{suffix}",
self.borders[0], self.borders[1]
),
height,
))
}
/// Actually draws the window, with each element in `content` being on a new line.
pub fn draw(&mut self, content: Vec<String>, space: bool) -> ui::Result<()> {
let (rendered, height) = self.render(content, space, false)?;
crossterm::execute!(
self.out,
Clear(ClearType::FromCursorDown),
MoveToColumn(0),
Print(rendered),
MoveToColumn(0),
MoveUp(height - 1),
)?;
Ok(())
}
}