deLyrium/src/lyrics.rs

363 lines
8.8 KiB
Rust

use iced::Space;
use iced_native::text_input::Value;
use core::ops::RangeInclusive;
use core::time::Duration;
use iced::Row;
use iced::Text;
use iced::Container;
use iced::Length;
use iced::Element;
use crate::styles::{Theme, FONT_VG5000};
use crate::app::Message;
use iced::widget::text_input::{self, TextInput};
use iced::widget::scrollable::{self, Scrollable};
use iced::Align;
use iced_native::widget::text_input::cursor::State;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LyricEvent {
LyricChanged(String),
TimestampChanged(String),
LineAdvanced,
}
impl LyricEvent {
fn into_msg(self, line_no: usize) -> Message {
Message::LyricEvent {
kind: self,
line_no,
}
}
}
pub struct Lyrics {
lines: Vec<Lyric>,
scroll_state: scrollable::State,
}
impl Lyrics {
pub fn new() -> Lyrics {
let mut lyric = Lyric::new();
lyric.select();
Self {
lines: vec![lyric],
scroll_state: scrollable::State::new(),
}
}
pub fn insert_text(&mut self, text: String) {
let mut pieces = text.trim_end()
.split('\n')
.map(str::trim);
let (line_no, current_line) = self.current_line_mut();
current_line.deselect();
current_line.value.push_str(pieces.next().unwrap());
let pieces = pieces
.collect::<Vec<_>>()
.into_iter()
.map(str::to_owned)
.map(Lyric::new_with_value);
let n_pieces = pieces.size_hint().0;
self.lines.splice((line_no + 1)..(line_no + 1), pieces);
self.lines[line_no + n_pieces].select();
}
pub fn handle_event(&mut self, line_no: usize, kind: LyricEvent) {
match kind {
LyricEvent::LyricChanged(newval) => {
self.update_line(newval, line_no)
},
LyricEvent::TimestampChanged(newval) => {
self.lines[line_no].timestamp_update(newval)
},
LyricEvent::LineAdvanced => {
self.advance_line(line_no)
},
}
}
pub fn update_line(&mut self, new_content: String, line_no: usize) {
self.lines[line_no].value = new_content;
}
pub fn advance_line(&mut self, current_line: usize) {
let new_line = current_line + 1;
let line = if new_line == self.lines.len() {
self.insert_line(new_line, None)
} else {
self.lines.get_mut(new_line)
.expect("Unexpected .advance_line with index beyond # of lines")
};
line.select();
self.lines.get_mut(current_line)
.unwrap()
.deselect();
}
pub fn insert_line(&mut self, index: usize, content: Option<String>) -> &mut Lyric {
self.lines.insert(index, match content {
Some(content) => Lyric::new_with_value(content),
None => Lyric::new(),
});
self.lines.get_mut(index).unwrap()
}
pub fn current_line_mut(&mut self) -> (usize, &mut Lyric) {
self.lines
.iter_mut()
.enumerate()
.find(|(_, l)| l.is_selected())
.expect("no line currently selected")
}
pub fn view(&mut self, theme: Theme) -> Element<Message> {
let is_sole_line = self.lines.len() == 1;
let spacers = (
Space::new(Length::Fill, Length::Units(30)),
Space::new(Length::Fill, Length::Units(30)),
);
let scroller = self.lines.iter_mut()
.enumerate()
.map(|(i, l)| l.view(is_sole_line, i, theme))
.fold(Scrollable::new(&mut self.scroll_state).push(spacers.0), |s, l| s.push(l))
.push(spacers.1)
.width(Length::Fill)
.align_items(Align::Center);
Container::new(scroller)
.height(Length::Fill)
.width(Length::Fill)
.center_y()
.into()
}
}
#[derive(Clone, Debug)]
pub struct Lyric {
main_state: text_input::State,
timestamp_state: text_input::State,
pub value: String,
pub timestamp: Duration,
timestamp_raw: String,
}
impl Lyric {
pub fn new() -> Self {
Self::new_with_value(String::with_capacity(70))
}
pub fn new_with_value(val: String) -> Self {
Lyric {
main_state: text_input::State::new(),
timestamp_state: text_input::State::new(),
timestamp: Duration::ZERO,
timestamp_raw: String::from("0:00.000"),
value: val,
}
}
pub fn view(&mut self, show_placeholder: bool, line_no: usize, theme: Theme) -> Element<Message> {
const SMALL_SIZE: u16 = 20;
const LARGE_SIZE: u16 = 25;
const TIMESTAMP_W: u16 = 67;
const LINE_HEIGHT: u16 = 26;
const TOTAL_W: u16 = 400;
let is_focused = self.is_selected();
let placeholder = if show_placeholder {
"Paste some lyrics to get started"
} else if is_focused {
"..."
} else { "" };
let size = if is_focused { LARGE_SIZE } else { SMALL_SIZE };
let timestamp_input = TextInput::new(
&mut self.timestamp_state,
"",
&self.timestamp_raw,
move|new_value| LyricEvent::TimestampChanged(new_value).into_msg(line_no),
)
.style(theme)
.size(SMALL_SIZE)
.width(Length::Units(TIMESTAMP_W))
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
.font(FONT_VG5000)
.into();
let text_input = TextInput::new(
&mut self.main_state,
placeholder,
&self.value,
move|new_value| LyricEvent::LyricChanged(new_value).into_msg(line_no),
)
.style(theme.active_lyric(is_focused))
.size(size)
.width(Length::Fill)
.on_submit(LyricEvent::LineAdvanced.into_msg(line_no))
.font(FONT_VG5000)
.into();
let l_bracket = Text::new("[")
.size(SMALL_SIZE)
.color(theme.reduced_text_color())
.font(FONT_VG5000)
.into();
let r_bracket = Text::new("] ")
.size(SMALL_SIZE)
.color(theme.reduced_text_color())
.font(FONT_VG5000)
.into();
Row::with_children(vec![l_bracket, timestamp_input, r_bracket, text_input])
.width(Length::Units(TOTAL_W))
.height(Length::Units(LINE_HEIGHT))
.align_items(Align::Center)
.into()
}
pub fn select(&mut self) {
self.main_state.focus();
self.main_state.move_cursor_to_end();
}
pub fn is_selected(&self) -> bool {
self.main_state.is_focused() || self.timestamp_state.is_focused()
}
pub fn deselect(&mut self) {
self.main_state.unfocus();
self.timestamp_state.unfocus();
}
pub (crate) fn timestamp_update(&mut self, newval: String) {
if let Some((shift, validated)) = Self::clean_timestamp(newval) {
self.timestamp = dbg!(Self::parse_validated_timestamp(&validated));
self.timestamp_raw = validated;
if shift != 0 {
self.timestamp_state.move_cursor_to(
match self.timestamp_state.cursor().state(&Value::new(&self.timestamp_raw)) {
State::Index(p) => ((p as isize) + shift) as usize,
State::Selection { start, ..} => {
// Should be impossible, but lets handle it anyway
((start as isize) + shift) as usize
},
}
);
}
}
}
fn clean_timestamp(mut raw: String) -> Option<(isize, String)> {
// Rules:
// - [R0] Must have exactly 1 colon (:)
// - [R1] Must have exactly 1 period (.)
// - [R2] The period must follow the colon
// - [R3] No characters outside 0-9, colon, and period
// - [R4] Unnecessary leading zeros besides normal padding are trimmed
// - [R5] Each section is padded to the appropriate length (reversed for millis)
const VALID_CHARS: RangeInclusive<char> = '0'..=':';
const MIN_DIGIT_COUNTS: [usize; 3] = [1, 2, 3];
let mut colon_count = 0;
let mut period_count = 0;
let mut digit_counts = [0; 3];
for c in raw.chars() {
match c {
':' => {
colon_count += 1;
if colon_count > 1 {
return None; // Rejected [R0]
}
},
'.' => {
period_count += 1;
if colon_count == 0 /* [R2] */ || period_count > 1 /* [R1] */ {
return None; // Rejected
}
},
_ if VALID_CHARS.contains(&c) || c == '.' => {
let section = colon_count + period_count;
digit_counts[section] += 1;
},
_ => {
return None; // Rejected [R3]
}
}
}
if period_count == 0 {
return None; //Rejected [R1]
}
let mut i = 0;
let mut cursor_shift = 0;
for section in 0..3 {
while digit_counts[section] < MIN_DIGIT_COUNTS[section] {
// [R5]
if section == 2 {
raw.push('0');
} else {
raw.insert(i, '0');
cursor_shift += 1;
}
digit_counts[section] += 1;
}
while
digit_counts[section] > MIN_DIGIT_COUNTS[section]
&& if section == 2 {
raw.ends_with('0')
} else {
raw.chars().nth(i).unwrap() == '0'
}
{
// [R4]
if section == 2 {
raw.truncate(raw.len() - 1);
} else {
raw.remove(i);
cursor_shift -= 1;
}
digit_counts[section] -= 1;
}
i += digit_counts[section] + 1;
}
Some((cursor_shift, raw))
}
fn parse_validated_timestamp(s: &str) -> Duration {
let (minutes, s) = s.split_at(s.find(':').expect(
"parse_validated_timestamp received a timestamp without a :"
));
let (seconds, millis) = s.split_at(s.find('.').expect(
"parse_validated_timestamp received a timestamp without a . after the :"
));
let minutes: u64 = minutes.parse()
.expect("parse_validated_timestamp received an invalid number of minutes");
let seconds: u64 = seconds[1..].parse()
.expect("parse_validated_timestamp received an invalid number of seconds");
let millis: u32 = millis[1..4].parse()
.expect("parse_validated_timestamp received an invalid number of millis");
Duration::new(seconds + minutes * 60, millis * 1_000_000)
}
}