Position indicator

This commit is contained in:
Geens 2025-06-22 21:52:25 +02:00
parent 70a26a2ebb
commit f3bab9461e
8 changed files with 316 additions and 10 deletions

View File

@ -152,7 +152,7 @@ impl<const ROWS: usize> Column<ROWS> {
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)?;
}

View File

@ -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" ] }

View File

@ -35,6 +35,7 @@ impl<const COLS: usize, const ROWS: usize> eframe::App for App<COLS, ROWS> {
&state.columns,
state.selected_column,
state.selected_row,
interpolated.position_in_beat, // Pass metronome position to Matrix
));
});
});

View File

@ -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<ROWS>,
selected_row: Option<usize>,
metronome_position: f32,
}
impl<'a, const ROWS: usize> Column<'a, ROWS> {
pub fn new(tracks: &'a [osc::Track; ROWS], selected_row: Option<usize>) -> Self {
pub fn new(
column: &'a osc::Column<ROWS>,
selected_row: Option<usize>,
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
}
}

View File

@ -2,6 +2,7 @@ pub struct Matrix<'a, const COLS: usize, const ROWS: usize> {
columns: &'a [osc::Column<ROWS>; 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<ROWS>; 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()

View File

@ -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;

View File

@ -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<f32> {
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<f32> {
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
}
}

View File

@ -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);
});
});
}
}