Feat: add tachyonFX animations
This commit is contained in:
@@ -4,14 +4,14 @@ use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table};
|
||||
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
@@ -150,7 +150,7 @@ fn adjust_spans_for_line(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot, elapsed: Duration) {
|
||||
let term = frame.area();
|
||||
|
||||
let theme = theme::get();
|
||||
@@ -165,6 +165,14 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
|
||||
|
||||
if app.ui.show_title {
|
||||
title_view::render(frame, term, &app.ui);
|
||||
|
||||
let mut fx = app.ui.title_fx.borrow_mut();
|
||||
if let Some(effect) = fx.as_mut() {
|
||||
effect.process(elapsed, frame.buffer_mut(), term);
|
||||
if !effect.running() {
|
||||
*fx = None;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,7 +226,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
|
||||
}
|
||||
|
||||
render_footer(frame, app, footer_area);
|
||||
render_modal(frame, app, snapshot, term);
|
||||
let modal_area = render_modal(frame, app, snapshot, term);
|
||||
|
||||
let show_minimap = app
|
||||
.ui
|
||||
@@ -241,6 +249,21 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc
|
||||
let selected = app.page.grid_pos();
|
||||
NavMinimap::new(&tiles, selected).render_centered(frame, term);
|
||||
}
|
||||
|
||||
app.ui
|
||||
.effects
|
||||
.borrow_mut()
|
||||
.process_effects(elapsed, frame.buffer_mut(), term);
|
||||
|
||||
if let Some(area) = modal_area {
|
||||
let mut fx = app.ui.modal_fx.borrow_mut();
|
||||
if let Some(effect) = fx.as_mut() {
|
||||
effect.process(elapsed, frame.buffer_mut(), area);
|
||||
if !effect.running() {
|
||||
*fx = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header_height(width: u16) -> u16 {
|
||||
@@ -529,37 +552,35 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(footer, area);
|
||||
}
|
||||
|
||||
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) {
|
||||
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) -> Option<Rect> {
|
||||
let theme = theme::get();
|
||||
match &app.ui.modal {
|
||||
Modal::None => {}
|
||||
let inner = match &app.ui.modal {
|
||||
Modal::None => return None,
|
||||
Modal::ConfirmQuit { selected } => {
|
||||
ConfirmModal::new("Confirm", "Quit?", *selected).render_centered(frame, term);
|
||||
ConfirmModal::new("Confirm", "Quit?", *selected).render_centered(frame, term)
|
||||
}
|
||||
Modal::ConfirmDeleteStep { step, selected, .. } => {
|
||||
ConfirmModal::new("Confirm", &format!("Delete step {}?", step + 1), *selected)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::ConfirmDeleteSteps {
|
||||
steps, selected, ..
|
||||
} => {
|
||||
let nums: Vec<String> = steps.iter().map(|s| format!("{:02}", s + 1)).collect();
|
||||
let label = format!("Delete steps {}?", nums.join(", "));
|
||||
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term);
|
||||
ConfirmModal::new("Confirm", &label, *selected).render_centered(frame, term)
|
||||
}
|
||||
Modal::ConfirmResetPattern {
|
||||
pattern, selected, ..
|
||||
} => {
|
||||
ConfirmModal::new(
|
||||
"Confirm",
|
||||
&format!("Reset pattern {}?", pattern + 1),
|
||||
*selected,
|
||||
)
|
||||
.render_centered(frame, term);
|
||||
}
|
||||
} => ConfirmModal::new(
|
||||
"Confirm",
|
||||
&format!("Reset pattern {}?", pattern + 1),
|
||||
*selected,
|
||||
)
|
||||
.render_centered(frame, term),
|
||||
Modal::ConfirmResetBank { bank, selected } => {
|
||||
ConfirmModal::new("Confirm", &format!("Reset bank {}?", bank + 1), *selected)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::FileBrowser(state) => {
|
||||
use crate::state::file_browser::FileBrowserMode;
|
||||
@@ -579,32 +600,30 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.border_color(border_color)
|
||||
.width(60)
|
||||
.height(18)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::RenameBank { bank, name } => {
|
||||
TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name)
|
||||
.width(40)
|
||||
.border_color(theme.modal.rename)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::RenamePattern {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
} => {
|
||||
TextInputModal::new(
|
||||
&format!("Rename B{:02}:P{:02}", bank + 1, pattern + 1),
|
||||
name,
|
||||
)
|
||||
.width(40)
|
||||
.border_color(theme.modal.rename)
|
||||
.render_centered(frame, term);
|
||||
}
|
||||
} => TextInputModal::new(
|
||||
&format!("Rename B{:02}:P{:02}", bank + 1, pattern + 1),
|
||||
name,
|
||||
)
|
||||
.width(40)
|
||||
.border_color(theme.modal.rename)
|
||||
.render_centered(frame, term),
|
||||
Modal::RenameStep { step, name, .. } => {
|
||||
TextInputModal::new(&format!("Name Step {:02}", step + 1), name)
|
||||
.width(40)
|
||||
.border_color(theme.modal.input)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetPattern { field, input } => {
|
||||
let (title, hint) = match field {
|
||||
@@ -615,15 +634,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.hint(hint)
|
||||
.width(45)
|
||||
.border_color(theme.modal.confirm)
|
||||
.render_centered(frame, term);
|
||||
}
|
||||
Modal::SetTempo(input) => {
|
||||
TextInputModal::new("Set Tempo (20-300 BPM)", input)
|
||||
.hint("Enter BPM")
|
||||
.width(30)
|
||||
.border_color(theme.modal.rename)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::SetTempo(input) => TextInputModal::new("Set Tempo (20-300 BPM)", input)
|
||||
.hint("Enter BPM")
|
||||
.width(30)
|
||||
.border_color(theme.modal.rename)
|
||||
.render_centered(frame, term),
|
||||
Modal::AddSamplePath(state) => {
|
||||
use crate::widgets::FileBrowserModal;
|
||||
let entries: Vec<(String, bool, bool)> = state
|
||||
@@ -637,7 +654,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.border_color(theme.modal.rename)
|
||||
.width(60)
|
||||
.height(18)
|
||||
.render_centered(frame, term);
|
||||
.render_centered(frame, term)
|
||||
}
|
||||
Modal::Preview => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
@@ -714,6 +731,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
let paragraph = Paragraph::new(lines);
|
||||
frame.render_widget(paragraph, inner);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::Editor => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
@@ -871,6 +890,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::PatternProps {
|
||||
bank,
|
||||
@@ -884,19 +905,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
} => {
|
||||
use crate::state::PatternPropsField;
|
||||
|
||||
let width = 50u16;
|
||||
let height = 12u16;
|
||||
let x = (term.width.saturating_sub(width)) / 2;
|
||||
let y = (term.height.saturating_sub(height)) / 2;
|
||||
let area = Rect::new(x, y, width, height);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||
.border_style(Style::default().fg(theme.modal.input));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(block, area);
|
||||
let inner =
|
||||
ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||
.width(50)
|
||||
.height(12)
|
||||
.border_color(theme.modal.input)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let fields = [
|
||||
("Name", name.as_str(), *field == PatternPropsField::Name),
|
||||
@@ -962,6 +976,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
Span::styled(" cancel", Style::default().fg(theme.hint.text)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint_line), hint_area);
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let width = (term.width * 80 / 100).clamp(60, 100);
|
||||
@@ -1033,6 +1049,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
Paragraph::new(keybind_hint).alignment(Alignment::Right),
|
||||
hint_area,
|
||||
);
|
||||
|
||||
inner
|
||||
}
|
||||
Modal::EuclideanDistribution {
|
||||
source_step,
|
||||
@@ -1042,30 +1060,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
rotation,
|
||||
..
|
||||
} => {
|
||||
let width = 50u16;
|
||||
let height = 11u16;
|
||||
let x = (term.width.saturating_sub(width)) / 2;
|
||||
let y = (term.height.saturating_sub(height)) / 2;
|
||||
let area = Rect::new(x, y, width, height);
|
||||
|
||||
let block = Block::bordered()
|
||||
.title(format!(" Euclidean Distribution (Step {:02}) ", source_step + 1))
|
||||
.border_style(Style::default().fg(theme.modal.input));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
// Fill background with theme color
|
||||
let bg_fill = " ".repeat(area.width as usize);
|
||||
for row in 0..area.height {
|
||||
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||
frame.render_widget(
|
||||
Paragraph::new(bg_fill.clone()).style(Style::new().bg(theme.ui.bg)),
|
||||
line_area,
|
||||
);
|
||||
}
|
||||
|
||||
frame.render_widget(block, area);
|
||||
let inner = ModalFrame::new(&format!(
|
||||
" Euclidean Distribution (Step {:02}) ",
|
||||
source_step + 1
|
||||
))
|
||||
.width(50)
|
||||
.height(11)
|
||||
.border_color(theme.modal.input)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let fields = [
|
||||
(
|
||||
@@ -1140,8 +1142,18 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
Span::styled(" cancel", Style::default().fg(theme.hint.text)),
|
||||
]);
|
||||
frame.render_widget(Paragraph::new(hint_line), hint_area);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Expand inner rect to include the border
|
||||
Some(Rect::new(
|
||||
inner.x.saturating_sub(1),
|
||||
inner.y.saturating_sub(1),
|
||||
inner.width + 2,
|
||||
inner.height + 2,
|
||||
))
|
||||
}
|
||||
|
||||
fn format_euclidean_preview(pulses: usize, steps: usize, rotation: usize) -> String {
|
||||
|
||||
Reference in New Issue
Block a user