From f3bab9461e7129c5ab66ed2ba217a86533c45790 Mon Sep 17 00:00:00 2001 From: Geens Date: Sun, 22 Jun 2025 21:52:25 +0200 Subject: [PATCH] Position indicator --- audio_engine/src/column.rs | 2 +- gui/Cargo.toml | 4 + gui/src/ui/app.rs | 1 + gui/src/ui/column.rs | 43 +++++-- gui/src/ui/matrix.rs | 9 +- gui/src/ui/mod.rs | 2 + gui/src/ui/position_indicator.rs | 176 ++++++++++++++++++++++++++ gui/src/ui/position_indicator_test.rs | 89 +++++++++++++ 8 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 gui/src/ui/position_indicator.rs create mode 100644 gui/src/ui/position_indicator_test.rs diff --git a/audio_engine/src/column.rs b/audio_engine/src/column.rs index 00c62cc..0af32de 100644 --- a/audio_engine/src/column.rs +++ b/audio_engine/src/column.rs @@ -152,7 +152,7 @@ impl Column { let new_len = self.len(); - if old_len == 0 && new_len > 0 { + if old_len != new_len { let beats = new_len / self.frames_per_beat; controllers.send_column_beats_changed(beats)?; } diff --git a/gui/Cargo.toml b/gui/Cargo.toml index de79ed6..b314690 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -3,6 +3,10 @@ name = "gui" version = "0.1.0" edition = "2021" +[[bin]] +name = "position_indicator_test" +path = "src/ui/position_indicator_test.rs" + [dependencies] anyhow = "1.0" eframe = { version = "0.29", default-features = true, features = [ "default_fonts", "glow" ] } diff --git a/gui/src/ui/app.rs b/gui/src/ui/app.rs index c68725f..937bd38 100644 --- a/gui/src/ui/app.rs +++ b/gui/src/ui/app.rs @@ -35,6 +35,7 @@ impl eframe::App for App { &state.columns, state.selected_column, state.selected_row, + interpolated.position_in_beat, // Pass metronome position to Matrix )); }); }); diff --git a/gui/src/ui/column.rs b/gui/src/ui/column.rs index 9ed9df8..878a42d 100644 --- a/gui/src/ui/column.rs +++ b/gui/src/ui/column.rs @@ -1,26 +1,53 @@ use super::ui_rows_ext::UiRowsExt; pub struct Column<'a, const ROWS: usize> { - tracks: &'a [osc::Track; ROWS], + column: &'a osc::Column, selected_row: Option, + metronome_position: f32, } impl<'a, const ROWS: usize> Column<'a, ROWS> { - pub fn new(tracks: &'a [osc::Track; ROWS], selected_row: Option) -> Self { + pub fn new( + column: &'a osc::Column, + selected_row: Option, + metronome_position: f32, + ) -> Self { Self { - tracks, + column, selected_row, + metronome_position, } } } impl<'a, const ROWS: usize> egui::Widget for Column<'a, ROWS> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { - ui.rows(self.tracks.len(), |ui| { - for (i, track) in self.tracks.iter().enumerate() { - ui[i].add(super::Track::new(track, Some(i) == self.selected_row)); - } + let idle = self + .column + .tracks + .iter() + .all(|t| t.state == osc::TrackState::Idle || t.state == osc::TrackState::Empty); + + let first_recording = + self.column.tracks.iter().all(|t| { + t.state == osc::TrackState::Recording || t.state == osc::TrackState::Empty + }); + + let response = ui.vertical(|ui| { + ui.add(super::PositionIndicator::new( + idle, + first_recording, + self.column.beat_count, + self.column.current_beat, + self.metronome_position, + )); + + ui.rows(self.column.tracks.len(), |ui| { + for (i, track) in self.column.tracks.iter().enumerate() { + ui[i].add(super::Track::new(track, Some(i) == self.selected_row)); + } + }); }); - ui.response() + response.response } } diff --git a/gui/src/ui/matrix.rs b/gui/src/ui/matrix.rs index b8851bc..9e3161d 100644 --- a/gui/src/ui/matrix.rs +++ b/gui/src/ui/matrix.rs @@ -2,6 +2,7 @@ pub struct Matrix<'a, const COLS: usize, const ROWS: usize> { columns: &'a [osc::Column; COLS], selected_column: usize, selected_row: usize, + metronome_position: f32, } impl<'a, const COLS: usize, const ROWS: usize> Matrix<'a, COLS, ROWS> { @@ -9,11 +10,13 @@ impl<'a, const COLS: usize, const ROWS: usize> Matrix<'a, COLS, ROWS> { columns: &'a [osc::Column; COLS], selected_column: usize, selected_row: usize, + metronome_position: f32, ) -> Self { Self { columns, selected_column, selected_row, + metronome_position, } } } @@ -27,7 +30,11 @@ impl<'a, const COLS: usize, const ROWS: usize> egui::Widget for Matrix<'a, COLS, } else { None }; - ui[i].add(super::Column::new(&column.tracks, selected_row)); + ui[i].add(super::Column::new( + column, + selected_row, + self.metronome_position, + )); } }); ui.response() diff --git a/gui/src/ui/mod.rs b/gui/src/ui/mod.rs index 390eb0d..c4355af 100644 --- a/gui/src/ui/mod.rs +++ b/gui/src/ui/mod.rs @@ -2,6 +2,7 @@ mod app; mod column; mod matrix; mod metronome; +mod position_indicator; mod track; mod ui_rows_ext; @@ -9,4 +10,5 @@ pub use app::App; use column::Column; use matrix::Matrix; use metronome::Metronome; +use position_indicator::PositionIndicator; use track::Track; diff --git a/gui/src/ui/position_indicator.rs b/gui/src/ui/position_indicator.rs new file mode 100644 index 0000000..9701af5 --- /dev/null +++ b/gui/src/ui/position_indicator.rs @@ -0,0 +1,176 @@ +// src/ui/position_indicator.rs +pub struct PositionIndicator { + idle: bool, + first_recording: bool, + beat_count: usize, + current_beat: usize, + metronome_position: f32, +} + +impl PositionIndicator { + pub fn new( + idle: bool, + first_recording: bool, + beat_count: usize, + current_beat: usize, + metronome_position: f32, + ) -> Self { + Self { + idle, + first_recording, + beat_count, + current_beat, + metronome_position, + } + } + + /// Calculate the fill amount for the progress bar + fn calculate_fill_amount(&self) -> f32 { + if self.idle { + 0.0 + } else if self.first_recording { + if self.current_beat == 0 { + self.metronome_position + } else { + 1.0 + } + } else { + (self.current_beat as f32 + self.metronome_position) / self.beat_count as f32 + } + } + + /// Calculate positions of vertical divider bars during recording + fn calculate_recording_bar_positions(&self) -> Vec { + if !self.first_recording || self.current_beat == 0 { + return Vec::new(); + } + + let mut positions = Vec::new(); + let total_bars = self.current_beat; + let target_divisions = self.current_beat + 1; + + for bar_index in 0..total_bars { + // Calculate target position for this bar (1/(N+1), 2/(N+1), etc.) + let target_position = (bar_index + 1) as f32 / target_divisions as f32; + + // Calculate where this bar was positioned at the end of the previous beat + let prev_target = if self.current_beat == 1 { + // For second beat (current_beat=1), the single bar starts at right edge + 1.0 + } else if bar_index == (self.current_beat - 1) { + // The newest bar (last one) always starts from the right edge + 1.0 + } else { + // Existing bars start from their previous final positions + (bar_index + 1) as f32 / self.current_beat as f32 + }; + + // Interpolate from previous position to target position + let current_position = + prev_target + (target_position - prev_target) * self.metronome_position; + positions.push(current_position); + } + + positions + } + + /// Calculate positions of vertical divider bars during playback + fn calculate_playback_bar_positions(&self) -> Vec { + if self.beat_count <= 1 { + return Vec::new(); + } + + let mut positions = Vec::new(); + for i in 1..self.beat_count { + positions.push(i as f32 / self.beat_count as f32); + } + positions + } +} + +impl egui::Widget for PositionIndicator { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + let sf = ui.available_width() / 100.0; + + let bar_height = sf * 8.0; + let inner_margin = sf * 2.0; + + let background_color = egui::Color32::from_rgb(51, 51, 51); // #333333 + let fill_color = egui::Color32::from_rgb(0, 136, 204); // #0088cc (blue like playing tracks) + let divider_color = egui::Color32::from_rgb(102, 102, 102); // #666666 + + let frame_response = egui::Frame::none() + .outer_margin(egui::Margin::symmetric(0.0, 5.0 * sf)) + .show(ui, |ui| { + // Calculate widget size + let width = ui.available_width(); + let height = bar_height; + let size = egui::vec2(width, height); + + // Allocate the space we need + let (rect, response) = ui.allocate_exact_size(size, egui::Sense::hover()); + + // Calculate bar position (with inner margin) + let bar_left = rect.min.x + inner_margin; + let bar_right = rect.max.x - inner_margin; + let bar_width = bar_right - bar_left; + let bar_center_y = rect.center().y; + let bar_top = bar_center_y - bar_height / 2.0; + let bar_bottom = bar_center_y + bar_height / 2.0; + + let bar_rect = egui::Rect::from_min_max( + egui::pos2(bar_left, bar_top), + egui::pos2(bar_right, bar_bottom), + ); + + // Draw background bar + ui.painter().rect_filled( + bar_rect, + egui::Rounding::same(bar_height / 2.0), + background_color, + ); + + // Calculate and draw fill + let fill_amount = self.calculate_fill_amount(); + if fill_amount > 0.0 { + let fill_width = bar_width * fill_amount.clamp(0.0, 1.0); + if fill_width > 0.0 { + let fill_rect = egui::Rect::from_min_max( + egui::pos2(bar_left, bar_top), + egui::pos2(bar_left + fill_width, bar_bottom), + ); + + ui.painter().rect_filled( + fill_rect, + egui::Rounding::same(bar_height / 2.0), + fill_color, + ); + } + } + + // Draw vertical divider lines + let bar_positions = if self.idle { + self.calculate_playback_bar_positions() + } else if self.first_recording { + self.calculate_recording_bar_positions() + } else { + self.calculate_playback_bar_positions() + }; + + for position in bar_positions { + let divider_x = bar_left + (bar_width * position.clamp(0.0, 1.0)); + ui.painter().line_segment( + [ + egui::pos2(divider_x, bar_top), + egui::pos2(divider_x, bar_bottom), + ], + egui::Stroke::new(1.5 * sf, divider_color), + ); + } + + response + }); + + frame_response.response + } +} diff --git a/gui/src/ui/position_indicator_test.rs b/gui/src/ui/position_indicator_test.rs new file mode 100644 index 0000000..4a154a8 --- /dev/null +++ b/gui/src/ui/position_indicator_test.rs @@ -0,0 +1,89 @@ +use eframe::egui; + +mod position_indicator; +use position_indicator::PositionIndicator; + +fn main() -> Result<(), eframe::Error> { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([800.0, 400.0]) + .with_title("Position Indicator Test"), + ..Default::default() + }; + + eframe::run_native( + "Position Indicator Test", + options, + Box::new(|_cc| Ok(Box::new(TestApp::default()))), + ) +} + +#[derive(Default)] +struct TestApp { + idle: bool, + first_recording: bool, + beat_count: usize, + current_beat: usize, + metronome_position: f32, +} + +impl eframe::App for TestApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Position Indicator Test"); + ui.separator(); + + ui.spacing_mut().item_spacing.y = 10.0; + + // Controls section + ui.group(|ui| { + ui.label("Controls:"); + + ui.horizontal(|ui| { + ui.checkbox(&mut self.idle, "Idle"); + }); + + ui.horizontal(|ui| { + ui.checkbox(&mut self.first_recording, "First Recording"); + }); + + ui.horizontal(|ui| { + ui.label("Beat Count:"); + ui.add(egui::DragValue::new(&mut self.beat_count).range(0..=16)); + }); + + ui.horizontal(|ui| { + ui.label("Current Beat:"); + ui.add(egui::DragValue::new(&mut self.current_beat).range(0..=16)); + }); + + ui.horizontal(|ui| { + ui.label("Metronome Position:"); + ui.add( + egui::Slider::new(&mut self.metronome_position, 0.0..=1.0) + .step_by(0.01) + .show_value(true), + ); + }); + }); + + ui.separator(); + + // Position indicator display + ui.group(|ui| { + ui.label("Position Indicator:"); + ui.add_space(10.0); + + let position_indicator = PositionIndicator::new( + self.idle, + self.first_recording, + self.beat_count, + self.current_beat, + self.metronome_position, + ); + + ui.add(position_indicator); + }); + }); + } +}