diff --git a/src/message.rs b/src/message.rs index d40c96d..8535c72 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,6 +1,6 @@ /// Handles communication between different parts of the program. #[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 { /// Deliberate user request to go to the next song, also sent when the /// song is over by the waiter. diff --git a/src/tests/ui.rs b/src/tests/ui.rs index 9c558b1..445b17d 100644 --- a/src/tests/ui.rs +++ b/src/tests/ui.rs @@ -65,12 +65,12 @@ mod window { #[test] fn new_border_strings() { let w = Window::new(10, false); - assert!(w.borders[0].starts_with('┌')); - assert!(w.borders[1].starts_with('└')); + assert!(w.titlebar.content.starts_with('┌')); + assert!(w.statusbar.starts_with('└')); let w2 = Window::new(5, true); - assert!(w2.borders[0].is_empty()); - assert!(w2.borders[1].is_empty()); + assert!(w2.titlebar.content.is_empty()); + assert!(w2.statusbar.is_empty()); } fn sided(text: &str) -> String { @@ -114,7 +114,7 @@ mod window { #[test] fn zero_width_window() { let w = Window::new(0, false); - assert!(!w.borders[0].is_empty()); + assert!(!w.titlebar.content.is_empty()); } } diff --git a/src/tracks/error.rs b/src/tracks/error.rs index 4e712d4..8517498 100644 --- a/src/tracks/error.rs +++ b/src/tracks/error.rs @@ -25,7 +25,7 @@ pub enum Kind { } #[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 track: Option, pub kind: Kind, diff --git a/src/ui/interface.rs b/src/ui/interface.rs index 13f4ab8..f9d828c 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -6,9 +6,11 @@ use std::{env, time::Duration}; pub mod clock; pub mod components; +pub mod titlebar; pub mod window; pub use clock::Clock; +pub use titlebar::TitleBar; pub use window::Window; /// UI-specific parameters and options. @@ -76,12 +78,12 @@ impl TryFrom<&Args> for Params { /// All of the state related to the interface itself, /// which is displayed each frame to the standard output. pub struct Interface { + /// The [`Window`] to render to. + pub(crate) window: Window, + /// The interval to wait between frames. interval: tokio::time::Interval, - /// The [`Window`] to render to. - window: Window, - /// The visual clock, which is [`None`] if it has /// been disabled by the [`Params`]. clock: Option, diff --git a/src/ui/interface/clock.rs b/src/ui/interface/clock.rs index 64d57d1..0a3eb60 100644 --- a/src/ui/interface/clock.rs +++ b/src/ui/interface/clock.rs @@ -19,15 +19,14 @@ impl Clock { /// is somewhat expensive because of timezones. pub fn update(&mut self, window: &mut Window) { if self.0.elapsed().as_millis() >= 200 { - window.display(Self::now(), 8); + window.titlebar.display(Self::now()); self.0 = Instant::now(); } } /// Simply creates a new clock, and renders it's initial state to the top of the window. pub fn new(window: &mut Window) -> Self { - window.display(Self::now(), 8); - + window.titlebar.display(Self::now()); Self(Instant::now()) } } diff --git a/src/ui/interface/titlebar.rs b/src/ui/interface/titlebar.rs new file mode 100644 index 0000000..43f4169 --- /dev/null +++ b/src/ui/interface/titlebar.rs @@ -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::()); + 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, + } + } +} diff --git a/src/ui/interface/window.rs b/src/ui/interface/window.rs index eaecc77..3503028 100644 --- a/src/ui/interface/window.rs +++ b/src/ui/interface/window.rs @@ -1,8 +1,6 @@ -use std::{ - fmt::Display, - io::{stdout, Stdout}, -}; +use std::io::{stdout, Stdout}; +use crate::ui::{self, interface::TitleBar}; use crossterm::{ cursor::{MoveToColumn, MoveUp}, style::{Print, Stylize as _}, @@ -11,8 +9,6 @@ use crossterm::{ 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, @@ -21,11 +17,11 @@ 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 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, @@ -40,28 +36,22 @@ impl 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()] + let statusbar = if borderless { + String::new() } else { let middle = "─".repeat(width + 2); - - [format!("┌{middle}┐"), format!("└{middle}┘")] + format!("└{middle}┘") }; Self { - borders, + statusbar, borderless, width, + titlebar: TitleBar::new(width, borderless), 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 @@ -105,7 +95,7 @@ impl Window { Ok(( format!( "{}{linefeed}{menu}{}{suffix}", - self.borders[0], self.borders[1] + self.titlebar.content, self.statusbar, ), height, ))