118 lines
4.5 KiB
Rust
118 lines
4.5 KiB
Rust
//! The transport "floor": the always-present bottom bar with play/pause, the timecode, and a
|
|
//! project scrub. Wired to the audio controller exactly like `TimelinePane`'s header.
|
|
|
|
use eframe::egui;
|
|
use daw_backend::Seconds;
|
|
|
|
use super::{icons, Palette};
|
|
use crate::panes::SharedPaneState;
|
|
|
|
pub fn render(ui: &mut egui::Ui, rect: egui::Rect, shared: &mut SharedPaneState, pal: &Palette) {
|
|
let painter = ui.painter_at(rect);
|
|
painter.rect_filled(rect, 0.0, pal.surface);
|
|
painter.hline(rect.x_range(), rect.top(), egui::Stroke::new(1.0, pal.line));
|
|
|
|
let cy = rect.center().y;
|
|
|
|
// --- Play / pause button (circle on the left) ---
|
|
let btn_r = 18.0;
|
|
let btn_center = egui::pos2(rect.left() + 20.0 + btn_r, cy);
|
|
let btn_rect = egui::Rect::from_center_size(btn_center, egui::vec2(btn_r * 2.0, btn_r * 2.0));
|
|
let btn_resp = ui.interact(btn_rect, ui.id().with("mobile_transport_play"), egui::Sense::click());
|
|
painter.circle_filled(btn_center, btn_r, pal.accent);
|
|
let glyph = if *shared.is_playing { icons::PAUSE } else { icons::PLAY };
|
|
painter.text(
|
|
btn_center,
|
|
egui::Align2::CENTER_CENTER,
|
|
glyph,
|
|
icons::font(16.0),
|
|
pal.on_accent,
|
|
);
|
|
if btn_resp.clicked() {
|
|
*shared.is_playing = !*shared.is_playing;
|
|
if let Some(controller_arc) = shared.audio_controller {
|
|
let mut controller = controller_arc.lock().unwrap();
|
|
if *shared.is_playing {
|
|
controller.seek(Seconds(*shared.playback_time));
|
|
controller.play();
|
|
} else {
|
|
controller.pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Playhead readout: format follows the project's TimelineMode (set at creation by type). ---
|
|
let t = *shared.playback_time;
|
|
let tc = {
|
|
use lightningbeam_core::document::TimelineMode;
|
|
let doc = shared.action_executor.document();
|
|
match doc.timeline_mode {
|
|
TimelineMode::Measures => {
|
|
let pos = lightningbeam_core::beat_time::time_to_measure(t, doc.tempo_map(), &doc.time_signature);
|
|
format!("{}.{}.{:02}", pos.measure, pos.beat, pos.tick / 10)
|
|
}
|
|
TimelineMode::Frames => format_timecode(t, doc.framerate.max(1.0)),
|
|
TimelineMode::Seconds => {
|
|
let total = t.max(0.0);
|
|
let m = (total / 60.0).floor() as u32;
|
|
format!("{:02}:{:06.3}", m, total % 60.0)
|
|
}
|
|
}
|
|
};
|
|
let tc_left = btn_rect.right() + 12.0;
|
|
painter.text(
|
|
egui::pos2(tc_left, cy),
|
|
egui::Align2::LEFT_CENTER,
|
|
&tc,
|
|
egui::FontId::monospace(13.0),
|
|
pal.text,
|
|
);
|
|
let tc_width = 78.0;
|
|
|
|
// --- Project scrub (fills the remaining width) ---
|
|
let duration = shared.action_executor.document().duration.max(1.0);
|
|
let scrub_left = tc_left + tc_width;
|
|
let scrub_rect = egui::Rect::from_min_max(
|
|
egui::pos2(scrub_left, cy - 3.0),
|
|
egui::pos2(rect.right() - 14.0, cy + 3.0),
|
|
);
|
|
let scrub_resp = ui.interact(
|
|
scrub_rect.expand2(egui::vec2(0.0, 12.0)),
|
|
ui.id().with("mobile_transport_scrub"),
|
|
egui::Sense::click_and_drag(),
|
|
);
|
|
painter.rect_filled(scrub_rect, 3.0, pal.line);
|
|
let frac = (*shared.playback_time / duration).clamp(0.0, 1.0) as f32;
|
|
let filled = egui::Rect::from_min_max(
|
|
scrub_rect.min,
|
|
egui::pos2(scrub_rect.left() + scrub_rect.width() * frac, scrub_rect.bottom()),
|
|
);
|
|
painter.rect_filled(filled, 3.0, pal.accent.gamma_multiply(0.5));
|
|
let head_x = scrub_rect.left() + scrub_rect.width() * frac;
|
|
painter.vline(
|
|
head_x,
|
|
(scrub_rect.top() - 4.0)..=(scrub_rect.bottom() + 4.0),
|
|
egui::Stroke::new(2.0, pal.accent),
|
|
);
|
|
|
|
if (scrub_resp.dragged() || scrub_resp.clicked()) && scrub_rect.width() > 0.0 {
|
|
if let Some(pos) = scrub_resp.interact_pointer_pos() {
|
|
let f = ((pos.x - scrub_rect.left()) / scrub_rect.width()).clamp(0.0, 1.0) as f64;
|
|
let new_time = f * duration;
|
|
*shared.playback_time = new_time;
|
|
if let Some(controller_arc) = shared.audio_controller {
|
|
let mut controller = controller_arc.lock().unwrap();
|
|
controller.seek(Seconds(new_time));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn format_timecode(seconds: f64, fps: f64) -> String {
|
|
let total = seconds.max(0.0);
|
|
let minutes = (total / 60.0).floor() as u32;
|
|
let secs = (total % 60.0).floor() as u32;
|
|
let frames = ((total - total.floor()) * fps).floor() as u32;
|
|
format!("{:02}:{:02}:{:02}", minutes, secs, frames)
|
|
}
|