Add a timestamp doodad to the editor
This commit is contained in:
parent
b5c1b088f3
commit
c39b545029
15
src/app.rs
15
src/app.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::lyrics::LyricEvent;
|
||||||
use crate::controls::ControlsEvent;
|
use crate::controls::ControlsEvent;
|
||||||
use crate::load_song::load_song;
|
use crate::load_song::load_song;
|
||||||
use crate::editor::Editor;
|
use crate::editor::Editor;
|
||||||
|
@ -25,11 +26,10 @@ pub struct DelyriumApp {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
LyricChanged {
|
LyricEvent {
|
||||||
line_no: usize,
|
line_no: usize,
|
||||||
new_value: String,
|
kind: LyricEvent,
|
||||||
},
|
},
|
||||||
LineAdvanced(usize),
|
|
||||||
PasteSent,
|
PasteSent,
|
||||||
Tick,
|
Tick,
|
||||||
PromptForFile,
|
PromptForFile,
|
||||||
|
@ -62,14 +62,9 @@ impl Application for DelyriumApp {
|
||||||
fn update(&mut self, message: Message, clipboard: &mut Clipboard) -> Command<Message>{
|
fn update(&mut self, message: Message, clipboard: &mut Clipboard) -> Command<Message>{
|
||||||
let mut command = None;
|
let mut command = None;
|
||||||
match message {
|
match message {
|
||||||
Message::LyricChanged { line_no, new_value } => {
|
Message::LyricEvent { line_no, kind } => {
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
||||||
lyrics.update_line(new_value, line_no);
|
lyrics.handle_lyric_event(line_no, kind);
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::LineAdvanced(current_line) => {
|
|
||||||
if let Some(lyrics) = self.lyrics_component.as_mut() {
|
|
||||||
lyrics.advance_line(current_line);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::PasteSent => {
|
Message::PasteSent => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::lyrics::LyricEvent;
|
||||||
use iced::Command;
|
use iced::Command;
|
||||||
use iced::Image;
|
use iced::Image;
|
||||||
use iced::image::Handle;
|
use iced::image::Handle;
|
||||||
|
@ -63,6 +64,9 @@ impl Editor {
|
||||||
pub fn insert_text(&mut self, text: String) {
|
pub fn insert_text(&mut self, text: String) {
|
||||||
self.lyrics.insert_text(text);
|
self.lyrics.insert_text(text);
|
||||||
}
|
}
|
||||||
|
pub fn handle_lyric_event(&mut self, line_no: usize, kind: LyricEvent) {
|
||||||
|
self.lyrics.handle_event(line_no, kind)
|
||||||
|
}
|
||||||
pub fn update_line(&mut self, new_content: String, line_no: usize) {
|
pub fn update_line(&mut self, new_content: String, line_no: usize) {
|
||||||
self.lyrics.update_line(new_content, line_no);
|
self.lyrics.update_line(new_content, line_no);
|
||||||
}
|
}
|
||||||
|
@ -79,7 +83,7 @@ impl Editor {
|
||||||
|
|
||||||
fn calculate_margin_width(&self) -> u32 {
|
fn calculate_margin_width(&self) -> u32 {
|
||||||
let (w, _h) = self.dimensions;
|
let (w, _h) = self.dimensions;
|
||||||
let body_size = (w / 5).max(450);
|
let body_size = (w / 5).max(550);
|
||||||
|
|
||||||
w.saturating_sub(body_size) / 2
|
w.saturating_sub(body_size) / 2
|
||||||
}
|
}
|
||||||
|
|
210
src/lyrics.rs
210
src/lyrics.rs
|
@ -1,3 +1,8 @@
|
||||||
|
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::Container;
|
||||||
use iced::Length;
|
use iced::Length;
|
||||||
use iced::Element;
|
use iced::Element;
|
||||||
|
@ -7,6 +12,23 @@ use crate::app::Message;
|
||||||
use iced::widget::text_input::{self, TextInput};
|
use iced::widget::text_input::{self, TextInput};
|
||||||
use iced::widget::scrollable::{self, Scrollable};
|
use iced::widget::scrollable::{self, Scrollable};
|
||||||
use iced::Align;
|
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 to_msg(self, line_no: usize) -> Message {
|
||||||
|
Message::LyricEvent {
|
||||||
|
kind: self,
|
||||||
|
line_no,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Lyrics {
|
pub struct Lyrics {
|
||||||
lines: Vec<Lyric>,
|
lines: Vec<Lyric>,
|
||||||
|
@ -47,6 +69,20 @@ impl Lyrics {
|
||||||
self.lines[line_no + n_pieces].select();
|
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) {
|
pub fn update_line(&mut self, new_content: String, line_no: usize) {
|
||||||
self.lines[line_no].value = new_content;
|
self.lines[line_no].value = new_content;
|
||||||
}
|
}
|
||||||
|
@ -106,8 +142,11 @@ impl Lyrics {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Lyric {
|
pub struct Lyric {
|
||||||
state: text_input::State,
|
main_state: text_input::State,
|
||||||
|
timestamp_state: text_input::State,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub timestamp: Duration,
|
||||||
|
timestamp_raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Lyric {
|
impl Lyric {
|
||||||
|
@ -117,44 +156,189 @@ impl Lyric {
|
||||||
|
|
||||||
pub fn new_with_value(val: String) -> Self {
|
pub fn new_with_value(val: String) -> Self {
|
||||||
Lyric {
|
Lyric {
|
||||||
state: text_input::State::new(),
|
main_state: text_input::State::new(),
|
||||||
|
timestamp_state: text_input::State::new(),
|
||||||
|
timestamp: Duration::ZERO,
|
||||||
|
timestamp_raw: String::from("0:00.000"),
|
||||||
value: val,
|
value: val,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(&mut self, show_placeholder: bool, line_no: usize, theme: Theme) -> Element<Message> {
|
pub fn view(&mut self, show_placeholder: bool, line_no: usize, theme: Theme) -> Element<Message> {
|
||||||
|
|
||||||
|
let is_focused = self.is_selected();
|
||||||
|
|
||||||
let placeholder = if show_placeholder {
|
let placeholder = if show_placeholder {
|
||||||
"Paste some lyrics to get started"
|
"Paste some lyrics to get started"
|
||||||
} else if self.state.is_focused() {
|
} else if is_focused {
|
||||||
"..."
|
"..."
|
||||||
} else { "" };
|
} else { "" };
|
||||||
|
|
||||||
let size = if self.state.is_focused() { 30 } else { 20 };
|
let size = if is_focused { 30 } else { 20 };
|
||||||
|
|
||||||
TextInput::new(
|
let timestamp_input = TextInput::new(
|
||||||
&mut self.state,
|
&mut self.timestamp_state,
|
||||||
|
"",
|
||||||
|
&self.timestamp_raw,
|
||||||
|
move|new_value| LyricEvent::TimestampChanged(new_value).to_msg(line_no),
|
||||||
|
)
|
||||||
|
.style(theme)
|
||||||
|
.size(size - 5)
|
||||||
|
.width(Length::Units(97))
|
||||||
|
.on_submit(LyricEvent::LineAdvanced.to_msg(line_no))
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let text_input = TextInput::new(
|
||||||
|
&mut self.main_state,
|
||||||
placeholder,
|
placeholder,
|
||||||
&self.value,
|
&self.value,
|
||||||
move|new_value| Message::LyricChanged { line_no, new_value },
|
move|new_value| LyricEvent::LyricChanged(new_value).to_msg(line_no),
|
||||||
)
|
)
|
||||||
.style(theme)
|
.style(theme)
|
||||||
.size(size)
|
.size(size)
|
||||||
.width(Length::Units(350))
|
.width(Length::Fill)
|
||||||
.on_submit(Message::LineAdvanced(line_no))
|
.on_submit(LyricEvent::LineAdvanced.to_msg(line_no))
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let l_bracket = Text::new("[")
|
||||||
|
.size(size)
|
||||||
|
.into();
|
||||||
|
let r_bracket = Text::new("]")
|
||||||
|
.size(size)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
Row::with_children(vec![l_bracket, timestamp_input, r_bracket, text_input])
|
||||||
|
.width(Length::Units(400))
|
||||||
|
.align_items(Align::Center)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self) {
|
pub fn select(&mut self) {
|
||||||
self.state.focus();
|
self.main_state.focus();
|
||||||
self.state.move_cursor_to_end();
|
self.main_state.move_cursor_to_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_selected(&self) -> bool {
|
pub fn is_selected(&self) -> bool {
|
||||||
self.state.is_focused()
|
self.main_state.is_focused() || self.timestamp_state.is_focused()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deselect(&mut self) {
|
pub fn deselect(&mut self) {
|
||||||
self.state.unfocus();
|
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.chars().next_back().unwrap() == '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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue