diff --git a/src/ui/application.rs b/src/ui/application.rs index 8465d1c..0eb8cab 100644 --- a/src/ui/application.rs +++ b/src/ui/application.rs @@ -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; @@ -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; @@ -26,6 +27,7 @@ pub struct RuxApplication { track_selection: TrackSelection, // selected track all_tracks: Vec, // all possible tracks tablature: Option, // loaded tablature + tablature_id: container::Id, // tablature container id audio_player: Option, // audio player tab_file_is_loading: bool, // file loading flag in progress sound_font_file: Option, // sound font file @@ -75,11 +77,13 @@ pub enum Message { OpenFile, // open file dialog FileOpened(Result<(Vec, 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 { @@ -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, @@ -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) { @@ -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(), @@ -213,7 +218,16 @@ 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() } @@ -221,7 +235,8 @@ impl RuxApplication { 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)) = @@ -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() + } } } @@ -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() @@ -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) } } diff --git a/src/ui/canvas_measure.rs b/src/ui/canvas_measure.rs index 5550cb0..651bb39 100644 --- a/src/ui/canvas_measure.rs +++ b/src/ui/canvas_measure.rs @@ -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 { @@ -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, @@ -65,14 +70,13 @@ impl CanvasMeasure { canvas_cache: Cache::default(), measure_len, total_measure_len, + vertical_measure_height, } } pub fn view(&self) -> Element { - 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() } diff --git a/src/ui/tablature.rs b/src/ui/tablature.rs index 13b28ac..0351704 100644 --- a/src/ui/tablature.rs +++ b/src/ui/tablature.rs @@ -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, pub track_id: usize, pub canvas_measures: Vec, - pub focuses_measure: usize, + canvas_measure_height: f32, + focused_measure: usize, + line_tracker: LineTracker, pub scroll_id: Id, } @@ -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(); @@ -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 { 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) { @@ -78,7 +113,7 @@ impl Tablature { .collect::>>(); let column = Wrap::with_elements(measure_elements) - .padding(10.0) + .padding(INNER_PADDING) .align_items(iced::Alignment::Center); // TODO does not work?? scrollable(column) @@ -97,3 +132,31 @@ impl Tablature { } } } + +#[derive(Default)] +struct LineTracker { + measure_to_line: Vec, // 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] + } +}