diff --git a/src/main.rs b/src/main.rs index 7d6e002..f920ed3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,10 @@ fn main() { let mut main_storage = Storage::init(); - main_storage.add_bpm_change(0, 120); + main_storage.add_bpm_change(0, 120.0); + main_storage.add_keyframe_channel(String::from("keyframe channel 0")); + main_storage.add_keyframe_channel(String::from("keyframe channel 1")); + main_storage.add_keyframe_channel(String::from("keyframe channel 2")); main_app.main_loop(move |_, ui, display| { main_window.build(ui, display, &mut main_storage); diff --git a/src/main_window.rs b/src/main_window.rs index 747248f..8f15954 100644 --- a/src/main_window.rs +++ b/src/main_window.rs @@ -10,7 +10,7 @@ const DRAG_AREA_MIN_SIZE: [f32; 2] = [300.0, 300.0]; const WIN_SIZE: [f32; 2] = [ DRAG_AREA_MIN_SIZE[0] + 15.0, - DRAG_AREA_MIN_SIZE[1] + 100.0 + DRAG_AREA_MIN_SIZE[1] + 120.0 ]; const WIN_MIN_SIZE: [f32; 2] = WIN_SIZE; @@ -40,9 +40,19 @@ impl MainWindow { display: &Display, storage: &mut Storage ) { + let d_size = display + .gl_window() + .window() + .inner_size(); + Window::new("Main Window") - .size(self.size, Condition::FirstUseEver) - .size_constraints(self.min_size, [1000.0, 1000.0]) + .position([0.0, 0.0], Condition::FirstUseEver) + .size([d_size.width as f32, d_size.height as f32], Condition::Always) + //.size_constraints(self.min_size, [s000.0, 1000.0]) + .scrollable(false) + .title_bar(false) + .resizable(false) + .movable(false) .build(&ui, || { MainWindow::build_window_contents( self, @@ -63,7 +73,7 @@ impl MainWindow { self.editor.drag_area.size = [ win_size[0] - 20.0, - win_size[1] - 100.0 + win_size[1] - 120.0 ]; ui.same_line(); @@ -75,8 +85,8 @@ impl MainWindow { ui.same_line(); let inc_bpm = ui.button("+"); - if dec_bpm { storage.bpm_changes[0].bpm -= 1; } - if inc_bpm { storage.bpm_changes[0].bpm += 1; } + if dec_bpm { storage.bpm_changes[0].bpm -= 1.0; } + if inc_bpm { storage.bpm_changes[0].bpm += 1.0; } ui.same_line(); let kf_name_input = ui @@ -99,14 +109,15 @@ impl MainWindow { ui.separator(); self.editor.build(ui, display, storage); - draw_mouse_pos_text(ui); + draw_info_text(ui); } } -fn draw_mouse_pos_text(ui: &Ui) { +fn draw_info_text(ui: &Ui) { let mouse_pos = ui.io().mouse_pos; ui.text(format!( - "mouse pos: ({:.1}, {:.1})", - mouse_pos[0], mouse_pos[1] + "mouse pos: ({:.1}, {:.1}), {} fps", + mouse_pos[0], mouse_pos[1], + ui.io().framerate )); } diff --git a/src/storage.rs b/src/storage.rs index 5c33e04..e340c7d 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -2,7 +2,7 @@ pub struct Storage { // TODO: add custom timestamp class which can be queried by // beats/measures/etc. - pub timestamp: u64, + pub timestamp: i64, pub playback_mode: PlaybackMode, pub bpm_changes: Vec, pub time_changes: Vec, @@ -16,12 +16,12 @@ pub enum PlaybackMode { } pub struct BpmChange { - pub timestamp: u64, - pub bpm: u32 + pub timestamp: i64, + pub bpm: f32 } pub struct TimeChange { - pub timestamp: u64, + pub timestamp: i64, pub time: (u8, u8) } @@ -37,7 +37,7 @@ pub enum KeyframeType { pub struct Keyframe { pub kf_type: KeyframeType, - pub timestamp: u64 + pub timestamp: i64 } impl Storage { @@ -51,7 +51,7 @@ impl Storage { } } - pub fn add_bpm_change(&mut self, timestamp: u64, bpm: u32) { + pub fn add_bpm_change(&mut self, timestamp: i64, bpm: f32) { self.bpm_changes.push(BpmChange::new(timestamp, bpm)); } @@ -59,7 +59,7 @@ impl Storage { self.bpm_changes.pop(); } - pub fn get_bpm_change_index(&self, timestamp: u64) -> Option { + pub fn get_bpm_change_index(&self, timestamp: i64) -> Option { for (idx, bc) in self.bpm_changes.iter().enumerate() { if (bc.timestamp) == timestamp { return Some(idx); @@ -68,11 +68,11 @@ impl Storage { None } - pub fn initial_bpm(&self) -> u32 { + pub fn initial_bpm(&self) -> f32 { self.bpm_changes[0].bpm } - pub fn add_time_change(&mut self, timestamp: u64, time: (u8, u8)) { + pub fn add_time_change(&mut self, timestamp: i64, time: (u8, u8)) { self.time_changes.push(TimeChange::new(timestamp, time)); } @@ -94,13 +94,13 @@ impl Storage { } impl BpmChange { - pub fn new(timestamp: u64, bpm: u32) -> BpmChange { + pub fn new(timestamp: i64, bpm: f32) -> BpmChange { BpmChange { timestamp, bpm } } } impl TimeChange { - pub fn new(timestamp: u64, time: (u8, u8)) -> TimeChange { + pub fn new(timestamp: i64, time: (u8, u8)) -> TimeChange { TimeChange { timestamp, time } } } @@ -114,7 +114,7 @@ impl KeyframeChannel { } } - pub fn add_keyframe(&mut self, timestamp: u64) { + pub fn add_keyframe(&mut self, timestamp: i64) { self.keyframes.push(Keyframe { kf_type: KeyframeType::Tick, timestamp @@ -122,7 +122,7 @@ impl KeyframeChannel { } // TODO: improve efficiency of this method - pub fn keyframe_at_timestamp(&self, timestamp: u64) -> Option<&Keyframe> { + pub fn keyframe_at_timestamp(&self, timestamp: i64) -> Option<&Keyframe> { for keyframe in self.keyframes.iter() { if keyframe.timestamp == timestamp { return Some(keyframe); diff --git a/src/ui_generic/drag_area.rs b/src/ui_generic/drag_area.rs index d454494..a7994c5 100644 --- a/src/ui_generic/drag_area.rs +++ b/src/ui_generic/drag_area.rs @@ -17,6 +17,14 @@ pub struct DragArea { active: bool } +pub struct DragAreaMut<'a> { + pub draw_list: &'a DrawListMut<'a>, + pub area_min: [f32; 2], + pub area_max: [f32; 2], + pub offset: &'a mut [f32; 2], + pub hovered: bool +} + impl DragArea { pub fn new(size: [f32; 2], offset: [f32; 2]) -> DragArea { DragArea { @@ -30,7 +38,7 @@ impl DragArea { } pub fn build - + (&mut self, ui: &Ui, window: &GlutinWindow, mut build_children: F) { let mouse_delta = ui.io().mouse_delta; @@ -45,13 +53,12 @@ impl DragArea { let rect_min = ui.item_rect_min(); let rect_max = ui.item_rect_max(); - let lmb_down = ui.is_mouse_dragging(MouseButton::Left); let mmb_down = ui.is_mouse_dragging(MouseButton::Middle); let hovered = ui.is_item_hovered(); - if !self.active && ((lmb_down || mmb_down) && hovered) { + if !self.active && mmb_down && hovered { self.active = true; - } else if self.active && !(lmb_down || mmb_down) { + } else if self.active && !mmb_down { self.active = false; } @@ -91,8 +98,18 @@ impl DragArea { let draw_list = ui.get_window_draw_list(); draw_list.with_clip_rect(rect_min, rect_max, || { - build_children(&draw_list, &rect_min, &rect_max, &self.offset); draw_list.add_rect(rect_min, rect_max, self.border_color).build(); + build_children( + DragAreaMut { + draw_list: &draw_list, + area_min: rect_min, + area_max: rect_max, + offset: &mut self.offset, + hovered + } + ); }); + + // ui.set_cursor_screen_pos([rect_min[0], rect_max[1] + 10.0]); } } diff --git a/src/ui_generic/keyframe_editor.rs b/src/ui_generic/keyframe_editor.rs index 0c211e1..619f501 100644 --- a/src/ui_generic/keyframe_editor.rs +++ b/src/ui_generic/keyframe_editor.rs @@ -1,28 +1,55 @@ use glium::Display; -use imgui::Ui; +use imgui::{Ui, MouseButton}; use imgui::color::ImColor32; +use imgui::draw_list::DrawListMut; -use crate::ui_generic::drag_area::DragArea; +use crate::ui_generic::drag_area::{DragArea, DragAreaMut}; use crate::storage::Storage; +const MIN_SCALE: f32 = 0.05; +const MAX_SCALE: f32 = 2.0; + const KF_CHAN_HEIGHT: f32 = 20.0; -const KF_CHAN_TITLE_DEFAULT_COLOR: ImColor32 = +const KF_CHAN_TITLE_BAR_WIDTH: f32 = 200.0; +const TS_BAR_HEIGHT: f32 = 20.0; +const TS_TICK_LINE_DIST: f32 = 5.0; +const TS_TICK_LINE_HEIGHT: f32 = 4.0; +const TS_TICK_LINE_TALL_HEIGHT: f32 = 18.0; +const KF_CHAN_TITLE_DEFAULT_FILL: ImColor32 = ImColor32::from_rgb(38, 38, 38); -const KF_CHAN_TITLE_HOVER_COLOR: ImColor32 = +const KF_CHAN_TITLE_HOVER_FILL: ImColor32 = ImColor32::from_rgb(79, 79, 79); -const KF_CHAN_CONTENT_DEFAULT_COLOR: ImColor32 = +const KF_CHAN_CONTENT_DEFAULT_FILL: ImColor32 = ImColor32::from_rgb(16, 16, 16); -const KF_CHAN_CONTENT_HOVER_COLOR: ImColor32 = +const KF_CHAN_CONTENT_HOVER_FILL: ImColor32 = ImColor32::from_rgb(54, 54, 54); +const TS_BAR_BG_FILL: ImColor32 = + ImColor32::from_rgb(10, 10, 10); +const TS_TRACKER_RECT_FILL: ImColor32 = + ImColor32::from_rgba(237, 66, 69, 60); +const TS_TRACKER_HDL_FILL: ImColor32 = + ImColor32::from_rgb(237, 66, 69); pub struct KeyframeEditor { - pub drag_area: DragArea + pub drag_area: DragArea, + pub first_timestamp_in_view: i64, + pub scale: f32, + pub ts_bar_active: bool +} + +struct KeyframeEditorMut { + pub first_timestamp_in_view: i64, + pub scale: f32, + pub ts_bar_active: bool } impl KeyframeEditor { pub fn new(size: [f32; 2]) -> KeyframeEditor { KeyframeEditor { - drag_area: DragArea::new(size, [0.0, 0.0]) + drag_area: DragArea::new(size, [0.0, 0.0]), + first_timestamp_in_view: 0, + scale: 1.0, + ts_bar_active: false } } @@ -32,122 +59,316 @@ impl KeyframeEditor { display: &Display, storage: &mut Storage ) { + let mut k = KeyframeEditorMut { + first_timestamp_in_view: self.first_timestamp_in_view, + scale: self.scale, + ts_bar_active: self.ts_bar_active + }; + self.drag_area.build( &ui, &display.gl_window().window(), - |draw_list, area_min, area_max, offset| { - let mouse_y = ui.io().mouse_pos[1]; + |d| { + KeyframeEditor::build_drag_area_contents( + ui, + storage, + &mut k, + d + ); + } + ); + + self.first_timestamp_in_view = k.first_timestamp_in_view; + self.scale = k.scale; + self.ts_bar_active = k.ts_bar_active; + } + + fn build_drag_area_contents( + ui: &Ui, + storage: &mut Storage, + k: &mut KeyframeEditorMut, + mut d: DragAreaMut + ) { + // Establish ia = inner area + let ia_min = [d.area_min[0] + 1.0, d.area_min[1] + 1.0]; + let ia_max = [d.area_max[0] - 1.0, d.area_max[1] - 1.0]; + + // No keyframes text + if storage.keyframe_channels.len() == 0 { + let text = "no keyframes to show."; + let text_size = ui.calc_text_size(text); + let text_pos: [f32; 2] = [ + (ia_max[0] - ia_min[0]) / 2.0 + (text_size[0] / 2.0), + (ia_max[1] - ia_min[1]) / 2.0 + (text_size[1] / 2.0) + ]; + + d.draw_list.add_text(text_pos, KF_CHAN_TITLE_HOVER_FILL, text); + + return; + } + + // Movement processing + let mouse_pos = ui.io().mouse_pos; + let scaled_ts_dist = TS_TICK_LINE_DIST * k.scale; + if ui.is_item_hovered() { + let wheel = ui.io().mouse_wheel; + let ctrl = ui.io().key_ctrl; + let shift = ui.io().key_shift; + + if ctrl { + k.scale += wheel * 0.05; + if k.scale < MIN_SCALE { + k.scale = MIN_SCALE + } else if k.scale > MAX_SCALE { + k.scale = MAX_SCALE + }; + } else if shift { + let inverse_scale = MAX_SCALE - k.scale; + d.offset[0] -= wheel * scaled_ts_dist * inverse_scale; + } else { + d.offset[1] += wheel * 10.0; + } + } + + if d.offset[1] > 0.0 { + d.offset[1] = 0.0; + } + + if d.offset[0] > 0.0 { + d.offset[0] = 0.0; + } + + k.first_timestamp_in_view = + (-d.offset[0] / scaled_ts_dist) as i64; + + let origin = [ia_min[0] + d.offset[0], ia_min[1] + d.offset[1]]; + + // Draw each keyframe channel + let mut chan_y: f32 = origin[1] + TS_BAR_HEIGHT; + let mut next_chan_y: f32 = chan_y + KF_CHAN_HEIGHT; + + for (idx, chan) in storage.keyframe_channels.iter().enumerate() { + // Determine colors + let title_bg: ImColor32; + let content_bg: ImColor32; + + if mouse_pos[1] <= next_chan_y && mouse_pos[1] > chan_y { + content_bg = KF_CHAN_CONTENT_HOVER_FILL; + title_bg = KF_CHAN_TITLE_HOVER_FILL; + } else { + content_bg = KF_CHAN_CONTENT_DEFAULT_FILL; + title_bg = KF_CHAN_TITLE_DEFAULT_FILL; + } + + // Draw keyframe channel title bar and text + let chan_title_start = [ia_min[0], chan_y]; + let chan_title_end = [ + ia_min[0] + KF_CHAN_TITLE_BAR_WIDTH, + next_chan_y - 1.0 + ]; + + d.draw_list + .add_rect(chan_title_start, chan_title_end, title_bg) + .filled(true) + .thickness(0.0) + .build(); + + let title_pos = [ + chan_title_start[0] + 3.0, + chan_title_start[1] + 3.0 + ]; + + d.draw_list.add_text( + title_pos, + ImColor32::WHITE, + String::from(&chan.name) + ); - let origin: [f32; 2]; + // Draw keyframe content bar + let chan_content_end = [ia_max[0], chan_title_start[1]]; - if storage.keyframe_channels.len() == 0 { - let text_pos: [f32; 2] = [ - area_min[0] + (area_max[0] - area_min[0]) / 2.0 - 50.0, - area_min[1] + (area_max[1] - area_min[1]) / 2.0 - 2.5 - ]; + d.draw_list + .add_rect(chan_title_end, chan_content_end, content_bg) + .filled(true) + .thickness(0.0) + .build(); - draw_list.add_text( - text_pos, - KF_CHAN_TITLE_HOVER_COLOR, - "No keyframes to show." - ); + // Draw keyframe channel border line + if idx < storage.keyframe_channels.len() - 1 { + let border_start = [chan_title_start[0], next_chan_y]; + let border_end = [ia_max[0], next_chan_y]; - return; + d.draw_list + .add_line(border_start, border_end, title_bg) + .build(); + } + + chan_y += KF_CHAN_HEIGHT; + next_chan_y += KF_CHAN_HEIGHT; + } + + // Draw time marker bar + let ts_bar_start = [ + ia_min[0] + KF_CHAN_TITLE_BAR_WIDTH, + ia_min[1] + ]; + + let ts_bar_end = [ + ia_max[0], + ia_min[1] + TS_BAR_HEIGHT + ]; + + d.draw_list + .add_rect( + ts_bar_start, + ts_bar_end, + TS_BAR_BG_FILL + ) + .filled(true) + .thickness(0.0) + .build(); + + // Process interactivity for timestamp bar + let ts_bar_button_size = [ + ts_bar_end[0] - ts_bar_start[0], + ts_bar_end[1] - ts_bar_start[1] + ]; + + let ts_bar_hovered = + ts_bar_start[0] <= mouse_pos[0] + && ts_bar_end[0] >= mouse_pos[0] + && ts_bar_start[1] <= mouse_pos[1] + && ts_bar_end[1] >= mouse_pos[1]; + + let lmb_down = ui.is_mouse_down(MouseButton::Left); + + if !k.ts_bar_active && lmb_down && ts_bar_hovered { + k.ts_bar_active = true; + } else if k.ts_bar_active && !lmb_down { + k.ts_bar_active = false; + } + + if k.ts_bar_active { + let new_ts = k.first_timestamp_in_view + + ( + (mouse_pos[0] - ts_bar_start[0]) + / scaled_ts_dist + ) as i64; + if new_ts >= 0 { + storage.timestamp = new_ts; + } else { + storage.timestamp = 0; + } + } + + // Draw time marker ticks and numbers + let mut ts_x = ts_bar_start[0]; + let mut timestamp_inc = k.first_timestamp_in_view; + + while ts_x < ia_max[0] { + let is_10_mult = timestamp_inc % 10 == 0; + + let line_start = [ts_x, ts_bar_end[1]]; + let line_end = [ + ts_x, + if is_10_mult { + ts_bar_end[1] - TS_TICK_LINE_TALL_HEIGHT + } else { + ts_bar_end[1] - TS_TICK_LINE_HEIGHT } + ]; + + d.draw_list + .add_line(line_start, line_end, KF_CHAN_TITLE_HOVER_FILL) + .build(); - let origin = [ - area_min[0] + offset[0], - area_min[1] + offset[1] + if is_10_mult { + let text = format!("{}", timestamp_inc); + let text_pos = [ + ts_x + 2.0, + ts_bar_end[1] - TS_TICK_LINE_TALL_HEIGHT ]; - let mut chan_y: f32 = 1.0; - let mut next_chan_y: f32 = chan_y + KF_CHAN_HEIGHT; - - for (idx, chan) in storage.keyframe_channels.iter().enumerate() { - let title_bg: ImColor32; - let content_bg: ImColor32; - - if ( - (mouse_y < origin[1] + next_chan_y) && - (mouse_y > origin[1] + chan_y) - ) { - content_bg = KF_CHAN_CONTENT_HOVER_COLOR; - title_bg = KF_CHAN_TITLE_HOVER_COLOR; - } else { - content_bg = KF_CHAN_CONTENT_DEFAULT_COLOR; - title_bg = KF_CHAN_TITLE_DEFAULT_COLOR; - } - - let chan_title_start: [f32; 2] = [ - origin[0] + 1.0, - origin[1] + chan_y + 1.0 - ]; - - let chan_title_end: [f32; 2] = [ - origin[0] + 100.0, - origin[1] + next_chan_y + 1.0 - ]; - - draw_list - .add_rect( - chan_title_start, - chan_title_end, - title_bg - ) - .filled(true) - .thickness(0.0) - .build(); - - let title_pos: [f32; 2] = [ - origin[0] + 1.0 + 3.0, - origin[1] + chan_y + 2.0 - ]; - - draw_list.add_text( - title_pos, - ImColor32::WHITE, - String::from(&chan.name) - ); - - let chan_content_end: [f32; 2] = [ - area_max[0], - chan_title_start[1] - ]; - - draw_list - .add_rect( - chan_title_end, - chan_content_end, - content_bg - ) - .filled(true) - .thickness(0.0) - .build(); - - if true { - let border_start = [ - origin[0], - origin[1] + next_chan_y - ]; - - let border_end = [ - area_max[0], - origin[1] + next_chan_y - ]; - - draw_list - .add_line( - border_start, - border_end, - title_bg - ) - .build(); - } - - chan_y += KF_CHAN_HEIGHT; - next_chan_y += KF_CHAN_HEIGHT; - } + d.draw_list.add_text( + text_pos, + KF_CHAN_TITLE_HOVER_FILL, + text + ); } + + timestamp_inc += 1; + ts_x += TS_TICK_LINE_DIST * k.scale; + } + + if storage.timestamp >= k.first_timestamp_in_view { + // Draw timestamp tracker rect + let current_ts_offset = + storage.timestamp - k.first_timestamp_in_view; + + let ts_rect_start = [ + ts_bar_start[0] + + (scaled_ts_dist * current_ts_offset as f32), + ts_bar_end[1] + ]; + + let ts_rect_end = [ + ts_rect_start[0] + scaled_ts_dist, + chan_y + ]; + + d.draw_list + .add_rect( + ts_rect_start, + ts_rect_end, + TS_TRACKER_RECT_FILL + ) + .filled(true) + .thickness(0.0) + .build(); + + // Draw timestamp tracker handle thingy + let text = format!("{}", storage.timestamp); + let text_width = ui.calc_text_size(&text)[0]; + + let ts_hdl_p1 = [ts_rect_start[0], ia_min[1]]; + let ts_hdl_p2 = [ + ts_rect_start[0] + text_width + 6.0, + ts_bar_end[1] + ]; + + d.draw_list + .add_rect( + ts_hdl_p1, + ts_hdl_p2, + TS_TRACKER_HDL_FILL + ) + .rounding(3.0) + .round_top_left(true) + .round_top_right(true) + .round_bot_right(true) + .round_bot_left(false) + .filled(true) + .thickness(0.0) + .build(); + + let ts_hdl_text_pos = [ + ts_rect_start[0] + 3.0, + ts_bar_end[1] - TS_TICK_LINE_TALL_HEIGHT + ]; + + d.draw_list.add_text( + ts_hdl_text_pos, + ImColor32::WHITE, + text + ); + } + + // Draw scale text + let scale_text_pos = [ia_max[0] - 100.0, ia_max[1] - 50.0]; + d.draw_list.add_text( + scale_text_pos, + KF_CHAN_TITLE_HOVER_FILL, + format!("{:.2}x scale", k.scale) ); } }