style: split off titlebar into seperate file

This commit is contained in:
talwat 2025-12-17 22:08:53 +01:00
parent 4b2fac3529
commit 287a61ca80
7 changed files with 89 additions and 36 deletions

View File

@ -1,6 +1,6 @@
/// Handles communication between different parts of the program. /// Handles communication between different parts of the program.
#[allow(dead_code, reason = "this code may not be dead depending on features")] #[allow(dead_code, reason = "this code may not be dead depending on features")]
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone, Copy)]
pub enum Message { pub enum Message {
/// Deliberate user request to go to the next song, also sent when the /// Deliberate user request to go to the next song, also sent when the
/// song is over by the waiter. /// song is over by the waiter.

View File

@ -65,12 +65,12 @@ mod window {
#[test] #[test]
fn new_border_strings() { fn new_border_strings() {
let w = Window::new(10, false); let w = Window::new(10, false);
assert!(w.borders[0].starts_with('┌')); assert!(w.titlebar.content.starts_with('┌'));
assert!(w.borders[1].starts_with('└')); assert!(w.statusbar.starts_with('└'));
let w2 = Window::new(5, true); let w2 = Window::new(5, true);
assert!(w2.borders[0].is_empty()); assert!(w2.titlebar.content.is_empty());
assert!(w2.borders[1].is_empty()); assert!(w2.statusbar.is_empty());
} }
fn sided(text: &str) -> String { fn sided(text: &str) -> String {
@ -114,7 +114,7 @@ mod window {
#[test] #[test]
fn zero_width_window() { fn zero_width_window() {
let w = Window::new(0, false); let w = Window::new(0, false);
assert!(!w.borders[0].is_empty()); assert!(!w.titlebar.content.is_empty());
} }
} }

View File

@ -25,7 +25,7 @@ pub enum Kind {
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
#[error("{kind} (track: {track:?})")] #[error("{kind}{}", self.track.as_ref().map_or(String::new(), |t| format!(" (track: {t:?}) ")))]
pub struct Error { pub struct Error {
pub track: Option<String>, pub track: Option<String>,
pub kind: Kind, pub kind: Kind,

View File

@ -6,9 +6,11 @@ use std::{env, time::Duration};
pub mod clock; pub mod clock;
pub mod components; pub mod components;
pub mod titlebar;
pub mod window; pub mod window;
pub use clock::Clock; pub use clock::Clock;
pub use titlebar::TitleBar;
pub use window::Window; pub use window::Window;
/// UI-specific parameters and options. /// UI-specific parameters and options.
@ -76,12 +78,12 @@ impl TryFrom<&Args> for Params {
/// All of the state related to the interface itself, /// All of the state related to the interface itself,
/// which is displayed each frame to the standard output. /// which is displayed each frame to the standard output.
pub struct Interface { pub struct Interface {
/// The [`Window`] to render to.
pub(crate) window: Window,
/// The interval to wait between frames. /// The interval to wait between frames.
interval: tokio::time::Interval, interval: tokio::time::Interval,
/// The [`Window`] to render to.
window: Window,
/// The visual clock, which is [`None`] if it has /// The visual clock, which is [`None`] if it has
/// been disabled by the [`Params`]. /// been disabled by the [`Params`].
clock: Option<Clock>, clock: Option<Clock>,

View File

@ -19,15 +19,14 @@ impl Clock {
/// is somewhat expensive because of timezones. /// is somewhat expensive because of timezones.
pub fn update(&mut self, window: &mut Window) { pub fn update(&mut self, window: &mut Window) {
if self.0.elapsed().as_millis() >= 200 { if self.0.elapsed().as_millis() >= 200 {
window.display(Self::now(), 8); window.titlebar.display(Self::now());
self.0 = Instant::now(); self.0 = Instant::now();
} }
} }
/// Simply creates a new clock, and renders it's initial state to the top of the window. /// Simply creates a new clock, and renders it's initial state to the top of the window.
pub fn new(window: &mut Window) -> Self { pub fn new(window: &mut Window) -> Self {
window.display(Self::now(), 8); window.titlebar.display(Self::now());
Self(Instant::now()) Self(Instant::now())
} }
} }

View File

@ -0,0 +1,62 @@
use std::fmt::Display;
use unicode_segmentation::UnicodeSegmentation;
/// The titlebar, which is essentially the entire top of the window.
///
/// The struct offers a basic API for displaying messages to it.
pub struct TitleBar {
/// The actual content of the titlebar.
pub(crate) content: String,
/// The width of the titlebar, identical to the width of the parent window.
width: usize,
/// Whether to render a bordered or borderless titlebar.
borderless: bool,
}
impl TitleBar {
/// Returns a blank default titlebar string for use elsewhere.
fn blank_content(width: usize, borderless: bool) -> String {
if borderless {
String::new()
} else {
let middle = "".repeat(width + 2);
format!("{middle}")
}
}
/// Empties the contents of the titlebar.
pub fn empty(&mut self) {
self.content = Self::blank_content(self.width, self.borderless);
}
/// Adds text to the top of the titlebar.
pub fn display(&mut self, display: impl Display) {
let mut display = display.to_string();
let graphemes = display.graphemes(true);
let mut len = graphemes.clone().count();
let inner = self.width - 2;
if len > inner {
display = format!("{}...", graphemes.take(inner - 3).collect::<String>());
len = inner;
}
let (prefix, middle, suffix) = if self.borderless {
(" ", " ", " ")
} else {
("┌─", "", "─┐")
};
self.content = format!("{prefix} {display} {}{suffix}", middle.repeat(inner - len));
}
pub fn new(width: usize, borderless: bool) -> Self {
Self {
content: Self::blank_content(width, borderless),
width,
borderless,
}
}
}

View File

@ -1,8 +1,6 @@
use std::{ use std::io::{stdout, Stdout};
fmt::Display,
io::{stdout, Stdout},
};
use crate::ui::{self, interface::TitleBar};
use crossterm::{ use crossterm::{
cursor::{MoveToColumn, MoveUp}, cursor::{MoveToColumn, MoveUp},
style::{Print, Stylize as _}, style::{Print, Stylize as _},
@ -11,8 +9,6 @@ use crossterm::{
use std::fmt::Write as _; use std::fmt::Write as _;
use unicode_segmentation::UnicodeSegmentation as _; use unicode_segmentation::UnicodeSegmentation as _;
use crate::ui;
/// Represents an abstraction for drawing the actual lowfi window itself. /// Represents an abstraction for drawing the actual lowfi window itself.
/// ///
/// The main purpose of this struct is just to add the fancy border, /// The main purpose of this struct is just to add the fancy border,
@ -21,11 +17,11 @@ pub struct Window {
/// Whether or not to include borders in the output. /// Whether or not to include borders in the output.
borderless: bool, borderless: bool,
/// The top & bottom borders, which are here since they can be /// The titlebar of this window.
/// prerendered, as they don't change every single draw. pub titlebar: TitleBar,
///
/// If the option to not include borders is set, these will just be empty [String]s. /// The status (bottom) bar of the window, which for now shouldn't change since initialization.
pub(crate) borders: [String; 2], pub(crate) statusbar: String,
/// The inner width of the window. /// The inner width of the window.
width: usize, width: usize,
@ -40,28 +36,22 @@ impl Window {
/// * `width` - Inner width of the window. /// * `width` - Inner width of the window.
/// * `borderless` - Whether to include borders in the window, or not. /// * `borderless` - Whether to include borders in the window, or not.
pub fn new(width: usize, borderless: bool) -> Self { pub fn new(width: usize, borderless: bool) -> Self {
let borders = if borderless { let statusbar = if borderless {
[String::new(), String::new()] String::new()
} else { } else {
let middle = "".repeat(width + 2); let middle = "".repeat(width + 2);
format!("{middle}")
[format!("{middle}"), format!("{middle}")]
}; };
Self { Self {
borders, statusbar,
borderless, borderless,
width, width,
titlebar: TitleBar::new(width, borderless),
out: stdout(), 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. /// Renders the window itself, but doesn't actually draw it.
/// ///
/// `testing` just determines whether to add special features /// `testing` just determines whether to add special features
@ -105,7 +95,7 @@ impl Window {
Ok(( Ok((
format!( format!(
"{}{linefeed}{menu}{}{suffix}", "{}{linefeed}{menu}{}{suffix}",
self.borders[0], self.borders[1] self.titlebar.content, self.statusbar,
), ),
height, height,
)) ))