use std::vec;
use crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::{
Frame,
layout::{Constraint, Flex, HorizontalAlignment, Layout, Margin, Position, Rect},
style::{Style, Stylize as _},
symbols,
text::{Line, Span},
widgets::{Block, Paragraph, Widget as _},
};
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler as _;
use crate::model::trim_model::TrimData;
use crate::ui::modal::{KeyboardHandler, ModalResult, UiModal};
use crate::ui::widget::BgClear;
use crate::ui::{Theme, checkbox_line, input_value_and_pos, is_portrait};
#[derive(Debug)]
pub(crate) struct TrimModal {
active_input: usize,
ss: Input,
to: Input,
precise: bool,
use_to: bool, duration: Option<f64>,
error: Option<String>,
}
impl UiModal for TrimModal {
fn render(&mut self, frame: &mut Frame, theme: &Theme) {
let area = frame.area();
let portrait = is_portrait(area);
let [modal_area] = Layout::vertical([Constraint::Length(8)])
.horizontal_margin(if portrait { 1 } else { area.width / 5 })
.flex(Flex::Center)
.areas(area);
let [inputs_area, chackbox_area, hints_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Length(2),
Constraint::Length(1),
])
.flex(Flex::SpaceBetween)
.areas(modal_area.inner(Margin::new(2, 1)));
let [ss_area, to_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(inputs_area);
let [precise_area, use_to_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(chackbox_area);
let (ss_value, ss_x) = input_value_and_pos(&self.ss, ss_area.width);
let (to_value, to_x) = input_value_and_pos(&self.to, to_area.width);
let active_border_style = theme.border_input_color();
let inactive_border_style = theme.border_input_inactive_color();
let active_input_label = Style::new().fg(active_border_style);
let inactive_input_label = Style::new().fg(inactive_border_style);
let start_label_style = if self.active_input == 0 {
active_input_label
} else {
inactive_input_label
};
let to_label_style = if self.active_input == 1 {
active_input_label
} else {
inactive_input_label
};
frame.render_widget(BgClear::new(theme.background_color()), modal_area);
Block::bordered()
.title("Trim".fg(theme.modal_title_color()))
.border_set(symbols::border::THICK)
.border_style(theme.border_modal_style())
.render(modal_area, frame.buffer_mut());
let mut borders = [inactive_border_style; 4];
borders[self.active_input] = active_border_style;
Paragraph::new(ss_value)
.block(
Block::bordered()
.border_style(borders[0])
.style(if self.active_input == 0 {
theme.text_input_color()
} else {
theme.text_muted_color()
})
.title(Span::styled("Start", start_label_style)),
)
.render(ss_area, frame.buffer_mut());
let title = if self.use_to { "To" } else { "Duration" };
let to_title = if let Some(dur) = self.duration {
format!("{title} (max {dur:.2})")
} else {
title.to_owned()
};
Paragraph::new(to_value)
.block(
Block::bordered()
.border_style(borders[1])
.style(if self.active_input == 1 {
theme.text_input_color()
} else {
theme.text_muted_color()
})
.title(Span::styled(to_title, to_label_style)),
)
.render(to_area, frame.buffer_mut());
if self.active_input <= 1 {
let (x, y) = if self.active_input == 0 {
(ss_area.x + ss_x, ss_area.y + 1)
} else {
(to_area.x + to_x, to_area.y + 1)
};
frame.set_cursor_position(Position { x, y });
}
let precise_line = checkbox_line(self.precise, "Precise", self.active_input == 2, theme);
Paragraph::new(precise_line)
.alignment(HorizontalAlignment::Center)
.render(precise_area, frame.buffer_mut());
let use_to_line = checkbox_line(
self.use_to,
"Use Duration/To",
self.active_input == 3,
theme,
);
Paragraph::new(use_to_line)
.alignment(HorizontalAlignment::Center)
.render(use_to_area, frame.buffer_mut());
self.render_status(hints_area, frame, theme);
}
}
impl KeyboardHandler for TrimModal {
fn handle_key(&mut self, key: KeyEvent) -> ModalResult {
self.error.take();
match key.code {
KeyCode::Esc => return ModalResult::Close,
KeyCode::BackTab => self.active_input = (self.active_input + 3) % 4,
KeyCode::Tab => self.active_input = (self.active_input + 1) % 4,
KeyCode::Char(x) => match (self.active_input, x) {
(0, '0'..='9' | '.' | ':' | '%' | '-')
if Self::prevalidate_value(x, self.ss.value()) =>
{
self.ss.handle_event(&Event::Key(key));
}
(1, '0'..='9' | '.' | ':' | '%' | '-')
if Self::prevalidate_value(x, self.to.value()) =>
{
self.to.handle_event(&Event::Key(key));
}
(2, ' ') => self.precise = !self.precise,
(3, ' ') => self.use_to = !self.use_to,
_ => {}
},
KeyCode::Backspace | KeyCode::Delete => match self.active_input {
0 => {
self.ss.handle_event(&Event::Key(key));
}
1 => {
self.to.handle_event(&Event::Key(key));
}
_ => {}
},
KeyCode::Enter => {
if let Some(msg) =
TrimData::validate(self.ss.value(), self.to.value(), self.use_to, self.duration)
{
self.error = Some(msg.to_owned());
return ModalResult::None;
}
return ModalResult::Trim;
}
_ => {}
}
ModalResult::None
}
}
impl From<&TrimModal> for TrimData {
fn from(model: &TrimModal) -> TrimData {
TrimData {
ss: Some(model.ss.value().to_owned()).filter(|x| !x.is_empty()),
to: Some(model.to.value().to_owned()).filter(|x| !x.is_empty()),
precise: model.precise,
use_to: model.use_to,
}
}
}
impl TrimModal {
pub fn new(data: TrimData, duration: Option<f64>) -> Self {
Self {
active_input: 0,
ss: Input::new(data.ss.unwrap_or_default()),
to: Input::new(data.to.unwrap_or_default()),
precise: data.precise,
use_to: data.use_to,
duration,
error: None,
}
}
fn render_status(&self, area: Rect, frame: &mut Frame, theme: &Theme) {
let line = if let Some(error) = &self.error {
Line::from(Span::styled(error, theme.error_style().bold())).centered()
} else {
let key_style = theme.key_style();
let text_style = theme.text_color();
let mut parts = vec![
Span::styled("Enter", key_style),
Span::styled(": confirm ", text_style),
Span::styled("Esc", key_style),
Span::styled(": close ", text_style),
Span::styled("Tab", key_style),
Span::styled(": switch focus", text_style),
];
if self.active_input > 1 {
parts.append(&mut vec![
Span::styled(" Space", key_style),
Span::styled(": toggle", text_style),
]);
}
Line::from(parts)
};
frame.render_widget(Paragraph::new(line), area);
}
fn prevalidate_value(x: char, value: &str) -> bool {
value.len() < (12 + if value.contains("-") { 1 } else { 0 }) && !((x == '.' || x == '%') && value.contains(x)) && !(x == ':' && value.contains('.')) && !(x == ':' && value.matches(':').count() >= 2) && !(x == '%' && value.contains(':')) && !((x == '%' || x == ':') && value.is_empty()) && (x != '-' || value.is_empty()) && !value.ends_with("%") }
}