Skip to content

Commit

Permalink
Autoscroll tablature during while playing (#10)
Browse files Browse the repository at this point in the history
* Autoscroll tablature during while playing

* stop audio player earlier on tab loading - fix #11
  • Loading branch information
agourlay authored Sep 1, 2024
1 parent dd0f576 commit 261604b
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 27 deletions.
59 changes: 45 additions & 14 deletions src/ui/application.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use iced::widget::{column, horizontal_space, pick_list, row, text};
use iced::{keyboard, stream, Alignment, Element, Subscription, Task, Theme};
use iced::widget::{column, container, horizontal_space, pick_list, row, text};
use iced::{keyboard, stream, window, Alignment, Element, Size, Subscription, Task, Theme};
use std::borrow::Cow;
use std::fmt::Display;

Expand All @@ -12,6 +12,7 @@ use crate::ui::utils::{action_gated, action_toggle, untitled_text_table_box};
use crate::ApplicationArgs;
use iced::futures::{SinkExt, Stream};
use iced::keyboard::key::Named::Space;
use iced::widget::container::visible_bounds;
use iced::widget::scrollable::{scroll_to, AbsoluteOffset, Id};
use std::path::PathBuf;
use std::rc::Rc;
Expand All @@ -26,6 +27,7 @@ pub struct RuxApplication {
track_selection: TrackSelection, // selected track
all_tracks: Vec<TrackSelection>, // all possible tracks
tablature: Option<Tablature>, // loaded tablature
tablature_id: container::Id, // tablature container id
audio_player: Option<AudioPlayer>, // audio player
tab_file_is_loading: bool, // file loading flag in progress
sound_font_file: Option<PathBuf>, // sound font file
Expand Down Expand Up @@ -75,11 +77,13 @@ pub enum Message {
OpenFile, // open file dialog
FileOpened(Result<(Vec<u8>, String), PickerError>), // file content & file name
TrackSelected(TrackSelection), // track selection
FocusMeasure(usize), // used when clicking on measure in tablature
FocusTick(usize), // focus on a specific tick in the tablature
PlayPause, // toggle play/pause
StopPlayer, // stop playback
ToggleSolo, // toggle solo mode
FocusMeasure(usize), // used when clicking on measure in tablature
FocusTick(usize), // focus on a specific tick in the tablature
PlayPause, // toggle play/pause
StopPlayer, // stop playback
ToggleSolo, // toggle solo mode
WindowResized, // window resized
TablatureResized(Size), // tablature resized
}

impl RuxApplication {
Expand All @@ -90,6 +94,7 @@ impl RuxApplication {
track_selection: TrackSelection::default(),
all_tracks: vec![],
tablature: None,
tablature_id: container::Id::new("tablature-outer-container"),
audio_player: None,
tab_file_is_loading: false,
sound_font_file,
Expand Down Expand Up @@ -145,6 +150,10 @@ impl RuxApplication {
}
Message::FileOpened(result) => {
self.tab_file_is_loading = false;
// stop previous audio player if any
if let Some(audio_player) = &mut self.audio_player {
audio_player.stop();
}
match result {
Ok((contents, file_name)) => {
if let Ok(song) = parse_gp_data(&contents) {
Expand Down Expand Up @@ -173,10 +182,6 @@ impl RuxApplication {
tablature_scroll_id.clone(),
);
self.tablature = Some(tablature);
// stop previous audio player if any
if let Some(audio_player) = &mut self.audio_player {
audio_player.stop();
}
// audio player initialization
let audio_player = AudioPlayer::new(
song_rc.clone(),
Expand Down Expand Up @@ -213,15 +218,25 @@ impl RuxApplication {
}
Message::FocusTick(tick) => {
if let Some(tablature) = &mut self.tablature {
tablature.focus_on_tick(tick);
if let Some(scroll_offset) = tablature.focus_on_tick(tick) {
// scroll to the focused measure
return scroll_to(
tablature.scroll_id.clone(),
AbsoluteOffset {
x: 0.0,
y: scroll_offset,
},
);
}
}
Task::none()
}
Message::PlayPause => {
if let Some(audio_player) = &mut self.audio_player {
audio_player.toggle_play();
}
Task::none()
// Hack to make sure the tablature is aware of its size
Task::done(Message::WindowResized)
}
Message::StopPlayer => {
if let (Some(audio_player), Some(tablature)) =
Expand All @@ -244,6 +259,17 @@ impl RuxApplication {
}
Task::none()
}
Message::WindowResized => {
// query tablature container size
visible_bounds(self.tablature_id.clone())
.map(|rect| Message::TablatureResized(rect.unwrap().size()))
}
Message::TablatureResized(tablature_container_size) => {
if let Some(tablature) = &mut self.tablature {
tablature.update_container_width(tablature_container_size.width);
}
Task::none()
}
}
}

Expand Down Expand Up @@ -324,7 +350,9 @@ impl RuxApplication {
.as_ref()
.map_or(untitled_text_table_box().into(), |t| t.view());

column![controls, tablature_view, status,]
let tablature = container(tablature_view).id(self.tablature_id.clone());

column![controls, tablature, status,]
.spacing(20)
.padding(10)
.into()
Expand Down Expand Up @@ -369,6 +397,9 @@ impl RuxApplication {
audio_player_beat_subscription,
));

let window_resized = window::resize_events().map(|_| Message::WindowResized);
subscriptions.push(window_resized);

Subscription::batch(subscriptions)
}
}
12 changes: 8 additions & 4 deletions src/ui/canvas_measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub struct CanvasMeasure {
focused_beat: usize,
canvas_cache: Cache,
measure_len: f32,
total_measure_len: f32,
pub total_measure_len: f32,
pub vertical_measure_height: f32,
}

impl CanvasMeasure {
Expand All @@ -56,6 +57,10 @@ impl CanvasMeasure {
let measure_len = MIN_MEASURE_WIDTH.max(beat_count as f32 * BEAT_LENGTH);
// total length of measure (padding on both sides)
let total_measure_len = measure_len + MEASURE_NOTES_PADDING * 2.0;
let string_count = track.strings.len();
// total height of measure (same for all measures in track)
let vertical_measure_height = STRING_LINE_HEIGHT * (string_count - 1) as f32;
let vertical_measure_height = vertical_measure_height + FIRST_STRING_Y * 2.0;
Self {
measure_id,
track_id,
Expand All @@ -65,14 +70,13 @@ impl CanvasMeasure {
canvas_cache: Cache::default(),
measure_len,
total_measure_len,
vertical_measure_height,
}
}

pub fn view(&self) -> Element<Message> {
let string_count = self.song.tracks[self.track_id].strings.len();
let vertical_measure_height = STRING_LINE_HEIGHT * (string_count - 1) as f32;
let canvas = Canvas::new(self)
.height(vertical_measure_height + FIRST_STRING_Y * 2.0)
.height(self.vertical_measure_height)
.width(Length::Fixed(self.total_measure_len));
canvas.into()
}
Expand Down
81 changes: 72 additions & 9 deletions src/ui/tablature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ use iced::widget::scrollable::Id;
use iced::{Element, Length};
use std::rc::Rc;

const INNER_PADDING: f32 = 10.0;

pub struct Tablature {
pub song: Rc<Song>,
pub track_id: usize,
pub canvas_measures: Vec<CanvasMeasure>,
pub focuses_measure: usize,
canvas_measure_height: f32,
focused_measure: usize,
line_tracker: LineTracker,
pub scroll_id: Id,
}

Expand All @@ -22,7 +26,9 @@ impl Tablature {
song,
track_id,
canvas_measures: Vec::with_capacity(measure_count),
focuses_measure: 0,
canvas_measure_height: 0.0,
focused_measure: 0,
line_tracker: LineTracker::default(),
scroll_id,
};
tab.load_measures();
Expand All @@ -37,31 +43,60 @@ impl Tablature {
let track = &self.song.tracks[self.track_id];
let measures = track.measures.len();
for i in 0..measures {
let focused = self.focuses_measure == i;
let focused = self.focused_measure == i;
let measure = CanvasMeasure::new(i, self.track_id, self.song.clone(), focused);
if i == 0 {
// all measures have the same height - grab first one
self.canvas_measure_height = measure.vertical_measure_height;
}
self.canvas_measures.push(measure);
}
}

pub fn focus_on_tick(&mut self, tick: usize) {
// TODO autoscroll if necessary
pub fn update_container_width(&mut self, width: f32) {
// recompute line tracker on width change
self.line_tracker = LineTracker::make(
&self.canvas_measures,
width - (INNER_PADDING * 2.0), // remove padding
);
}

/// Focus on the beat at the given tick
///
/// Returns the amount of scroll needed to focus on the beat
pub fn focus_on_tick(&mut self, tick: usize) -> Option<f32> {
let (new_measure_id, new_beat_id) =
self.song.get_measure_beat_for_tick(self.track_id, tick);
let current_focus_id = self.focuses_measure;
let current_focus_id = self.focused_measure;
let current_canvas = self.canvas_measures.get_mut(current_focus_id).unwrap();
if current_focus_id != new_measure_id {
// move to next measure
current_canvas.toggle_focused();
let next_focus_id = new_measure_id;
if next_focus_id < self.canvas_measures.len() {
self.focuses_measure = next_focus_id;
self.focused_measure = next_focus_id;
let next_canvas = self.canvas_measures.get_mut(next_focus_id).unwrap();
next_canvas.toggle_focused();

// compute progress of the measure within the song
let line_tracker = &self.line_tracker;
let focus_line = line_tracker.get_line(next_focus_id);
let estimated_y = (focus_line - 1) as f32 * self.canvas_measure_height;
if focus_line < 2 {
return None;
}
log::debug!(
"scrolling to focus_line {} estimated_y {}",
focus_line,
estimated_y
);
return Some(estimated_y);
}
} else {
// focus on beat id
// focus on beat id within the same measure
current_canvas.focus_beat(new_beat_id);
}
None
}

pub fn focus_on_measure(&mut self, new_measure_id: usize) {
Expand All @@ -78,7 +113,7 @@ impl Tablature {
.collect::<Vec<Element<Message>>>();

let column = Wrap::with_elements(measure_elements)
.padding(10.0)
.padding(INNER_PADDING)
.align_items(iced::Alignment::Center); // TODO does not work??

scrollable(column)
Expand All @@ -97,3 +132,31 @@ impl Tablature {
}
}
}

#[derive(Default)]
struct LineTracker {
measure_to_line: Vec<usize>, // measure id to line number
}

impl LineTracker {
pub fn make(measures: &[CanvasMeasure], tablature_container_width: f32) -> Self {
let mut line_tracker = LineTracker {
measure_to_line: vec![0; measures.len()],
};
let mut current_line = 1;
let mut horizontal_cursor = 0.0;
for measure in measures.iter() {
horizontal_cursor += measure.total_measure_len;
if horizontal_cursor >= tablature_container_width {
current_line += 1;
horizontal_cursor = measure.total_measure_len;
}
line_tracker.measure_to_line[measure.measure_id] = current_line;
}
line_tracker
}

pub fn get_line(&self, measure_id: usize) -> usize {
self.measure_to_line[measure_id]
}
}

0 comments on commit 261604b

Please sign in to comment.