Position indicator
This commit is contained in:
parent
70a26a2ebb
commit
f3bab9461e
@ -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)?;
|
||||
}
|
||||
|
||||
@ -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" ] }
|
||||
|
||||
@ -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
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
|
||||
176
gui/src/ui/position_indicator.rs
Normal file
176
gui/src/ui/position_indicator.rs
Normal 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
|
||||
}
|
||||
}
|
||||
89
gui/src/ui/position_indicator_test.rs
Normal file
89
gui/src/ui/position_indicator_test.rs
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user