Use audio engine as source of truth for playback time

This commit is contained in:
Skyler Lehmkuhl 2025-11-28 11:36:33 -05:00
parent 5761d48f1b
commit 5fbb2c078b
7 changed files with 1229 additions and 224 deletions

View File

@ -37,6 +37,8 @@ pub struct AudioSystem {
pub stream: cpal::Stream, pub stream: cpal::Stream,
pub sample_rate: u32, pub sample_rate: u32,
pub channels: u32, pub channels: u32,
/// Event receiver for polling audio events (only present when no EventEmitter is provided)
pub event_rx: Option<rtrb::Consumer<AudioEvent>>,
} }
impl AudioSystem { impl AudioSystem {
@ -152,6 +154,7 @@ impl AudioSystem {
stream: output_stream, stream: output_stream,
sample_rate, sample_rate,
channels, channels,
event_rx: None, // No event receiver when audio device unavailable
}); });
} }
}; };
@ -179,6 +182,7 @@ impl AudioSystem {
stream: output_stream, stream: output_stream,
sample_rate, sample_rate,
channels, channels,
event_rx: None, // No event receiver when audio device unavailable
}); });
} }
}; };
@ -205,16 +209,20 @@ impl AudioSystem {
// Leak the input stream to keep it alive // Leak the input stream to keep it alive
Box::leak(Box::new(input_stream)); Box::leak(Box::new(input_stream));
// Spawn emitter thread if provided // Spawn emitter thread if provided, or store event_rx for manual polling
if let Some(emitter) = event_emitter { let event_rx_option = if let Some(emitter) = event_emitter {
Self::spawn_emitter_thread(event_rx, emitter); Self::spawn_emitter_thread(event_rx, emitter);
} None
} else {
Some(event_rx)
};
Ok(Self { Ok(Self {
controller, controller,
stream: output_stream, stream: output_stream,
sample_rate, sample_rate,
channels, channels,
event_rx: event_rx_option,
}) })
} }

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
lightningbeam-core = { path = "../lightningbeam-core" } lightningbeam-core = { path = "../lightningbeam-core" }
daw-backend = { path = "../../daw-backend" } daw-backend = { path = "../../daw-backend" }
rtrb = "0.3"
# UI Framework # UI Framework
eframe = { workspace = true } eframe = { workspace = true }

View File

@ -267,8 +267,10 @@ struct EditorApp {
rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0) rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0)
schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0) schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0)
// Audio engine integration // Audio engine integration
audio_controller: Option<daw_backend::EngineController>, // Audio engine controller for playback audio_system: Option<daw_backend::AudioSystem>, // Audio system (must be kept alive for stream)
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>, // Audio event receiver // Playback state (global for all panes)
playback_time: f64, // Current playback position in seconds (persistent - save with document)
is_playing: bool, // Whether playback is currently active (transient - don't save)
} }
impl EditorApp { impl EditorApp {
@ -302,6 +304,19 @@ impl EditorApp {
// Wrap document in ActionExecutor // Wrap document in ActionExecutor
let action_executor = lightningbeam_core::action::ActionExecutor::new(document); let action_executor = lightningbeam_core::action::ActionExecutor::new(document);
// Initialize audio system (keep the whole system to maintain the audio stream)
let audio_system = match daw_backend::AudioSystem::new(None, 256) {
Ok(audio_system) => {
println!("✅ Audio engine initialized successfully");
Some(audio_system)
}
Err(e) => {
eprintln!("❌ Failed to initialize audio engine: {}", e);
eprintln!(" Playback will be disabled");
None
}
};
Self { Self {
layouts, layouts,
current_layout_index: 0, current_layout_index: 0,
@ -327,6 +342,9 @@ impl EditorApp {
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode::Smooth, // Default to smooth curves draw_simplify_mode: lightningbeam_core::tool::SimplifyMode::Smooth, // Default to smooth curves
rdp_tolerance: 10.0, // Default RDP tolerance rdp_tolerance: 10.0, // Default RDP tolerance
schneider_max_error: 30.0, // Default Schneider max error schneider_max_error: 30.0, // Default Schneider max error
audio_system,
playback_time: 0.0, // Start at beginning
is_playing: false, // Start paused
} }
} }
@ -665,6 +683,29 @@ impl eframe::App for EditorApp {
} }
} }
// Poll audio events from the audio engine
if let Some(audio_system) = &mut self.audio_system {
if let Some(event_rx) = &mut audio_system.event_rx {
while let Ok(event) = event_rx.pop() {
use daw_backend::AudioEvent;
match event {
AudioEvent::PlaybackPosition(time) => {
self.playback_time = time;
}
AudioEvent::PlaybackStopped => {
self.is_playing = false;
}
_ => {} // Ignore other events for now
}
}
}
}
// Request continuous repaints when playing to update time display
if self.is_playing {
ctx.request_repaint();
}
// Check keyboard shortcuts (works on all platforms) // Check keyboard shortcuts (works on all platforms)
ctx.input(|i| { ctx.input(|i| {
// Check menu shortcuts // Check menu shortcuts
@ -726,6 +767,32 @@ impl eframe::App for EditorApp {
// Registry for actions to execute after rendering (two-phase dispatch) // Registry for actions to execute after rendering (two-phase dispatch)
let mut pending_actions: Vec<Box<dyn lightningbeam_core::action::Action>> = Vec::new(); let mut pending_actions: Vec<Box<dyn lightningbeam_core::action::Action>> = Vec::new();
// Create render context
let mut ctx = RenderContext {
tool_icon_cache: &mut self.tool_icon_cache,
icon_cache: &mut self.icon_cache,
selected_tool: &mut self.selected_tool,
fill_color: &mut self.fill_color,
stroke_color: &mut self.stroke_color,
active_color_mode: &mut self.active_color_mode,
pane_instances: &mut self.pane_instances,
pending_view_action: &mut self.pending_view_action,
fallback_pane_priority: &mut fallback_pane_priority,
pending_handlers: &mut pending_handlers,
theme: &self.theme,
action_executor: &mut self.action_executor,
selection: &mut self.selection,
active_layer_id: &mut self.active_layer_id,
tool_state: &mut self.tool_state,
pending_actions: &mut pending_actions,
draw_simplify_mode: &mut self.draw_simplify_mode,
rdp_tolerance: &mut self.rdp_tolerance,
schneider_max_error: &mut self.schneider_max_error,
audio_controller: self.audio_system.as_mut().map(|sys| &mut sys.controller),
playback_time: &mut self.playback_time,
is_playing: &mut self.is_playing,
};
render_layout_node( render_layout_node(
ui, ui,
&mut self.current_layout, &mut self.current_layout,
@ -735,26 +802,8 @@ impl eframe::App for EditorApp {
&mut self.selected_pane, &mut self.selected_pane,
&mut layout_action, &mut layout_action,
&mut self.split_preview_mode, &mut self.split_preview_mode,
&mut self.icon_cache,
&mut self.tool_icon_cache,
&mut self.selected_tool,
&mut self.fill_color,
&mut self.stroke_color,
&mut self.active_color_mode,
&mut self.pane_instances,
&Vec::new(), // Root path &Vec::new(), // Root path
&mut self.pending_view_action, &mut ctx,
&mut fallback_pane_priority,
&mut pending_handlers,
&self.theme,
&mut self.action_executor,
&mut self.selection,
&mut self.active_layer_id,
&mut self.tool_state,
&mut pending_actions,
&mut self.draw_simplify_mode,
&mut self.rdp_tolerance,
&mut self.schneider_max_error,
); );
// Execute action on the best handler (two-phase dispatch) // Execute action on the best handler (two-phase dispatch)
@ -782,9 +831,9 @@ impl eframe::App for EditorApp {
// Set cursor based on hover state // Set cursor based on hover state
if let Some((_, is_horizontal)) = self.hovered_divider { if let Some((_, is_horizontal)) = self.hovered_divider {
if is_horizontal { if is_horizontal {
ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal); ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
} else { } else {
ctx.set_cursor_icon(egui::CursorIcon::ResizeVertical); ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
} }
} }
}); });
@ -816,6 +865,33 @@ impl eframe::App for EditorApp {
} }
/// Context for rendering operations - bundles all mutable state needed during rendering
/// This avoids having 25+ individual parameters in rendering functions
struct RenderContext<'a> {
tool_icon_cache: &'a mut ToolIconCache,
icon_cache: &'a mut IconCache,
selected_tool: &'a mut Tool,
fill_color: &'a mut egui::Color32,
stroke_color: &'a mut egui::Color32,
active_color_mode: &'a mut panes::ColorMode,
pane_instances: &'a mut HashMap<NodePath, PaneInstance>,
pending_view_action: &'a mut Option<MenuAction>,
fallback_pane_priority: &'a mut Option<u32>,
pending_handlers: &'a mut Vec<panes::ViewActionHandler>,
theme: &'a Theme,
action_executor: &'a mut lightningbeam_core::action::ActionExecutor,
selection: &'a mut lightningbeam_core::selection::Selection,
active_layer_id: &'a mut Option<Uuid>,
tool_state: &'a mut lightningbeam_core::tool::ToolState,
pending_actions: &'a mut Vec<Box<dyn lightningbeam_core::action::Action>>,
draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
rdp_tolerance: &'a mut f64,
schneider_max_error: &'a mut f64,
audio_controller: Option<&'a mut daw_backend::EngineController>,
playback_time: &'a mut f64,
is_playing: &'a mut bool,
}
/// Recursively render a layout node with drag support /// Recursively render a layout node with drag support
fn render_layout_node( fn render_layout_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
@ -826,30 +902,12 @@ fn render_layout_node(
selected_pane: &mut Option<NodePath>, selected_pane: &mut Option<NodePath>,
layout_action: &mut Option<LayoutAction>, layout_action: &mut Option<LayoutAction>,
split_preview_mode: &mut SplitPreviewMode, split_preview_mode: &mut SplitPreviewMode,
icon_cache: &mut IconCache,
tool_icon_cache: &mut ToolIconCache,
selected_tool: &mut Tool,
fill_color: &mut egui::Color32,
stroke_color: &mut egui::Color32,
active_color_mode: &mut panes::ColorMode,
pane_instances: &mut HashMap<NodePath, PaneInstance>,
path: &NodePath, path: &NodePath,
pending_view_action: &mut Option<MenuAction>, ctx: &mut RenderContext,
fallback_pane_priority: &mut Option<u32>,
pending_handlers: &mut Vec<panes::ViewActionHandler>,
theme: &Theme,
action_executor: &mut lightningbeam_core::action::ActionExecutor,
selection: &mut lightningbeam_core::selection::Selection,
active_layer_id: &mut Option<Uuid>,
tool_state: &mut lightningbeam_core::tool::ToolState,
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
draw_simplify_mode: &mut lightningbeam_core::tool::SimplifyMode,
rdp_tolerance: &mut f64,
schneider_max_error: &mut f64,
) { ) {
match node { match node {
LayoutNode::Pane { name } => { LayoutNode::Pane { name } => {
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, active_color_mode, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers, theme, action_executor, selection, active_layer_id, tool_state, pending_actions, draw_simplify_mode, rdp_tolerance, schneider_max_error); render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, path, ctx);
} }
LayoutNode::HorizontalGrid { percent, children } => { LayoutNode::HorizontalGrid { percent, children } => {
// Handle dragging // Handle dragging
@ -882,26 +940,8 @@ fn render_layout_node(
selected_pane, selected_pane,
layout_action, layout_action,
split_preview_mode, split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&left_path, &left_path,
pending_view_action, ctx,
fallback_pane_priority,
pending_handlers,
theme,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
); );
let mut right_path = path.clone(); let mut right_path = path.clone();
@ -915,26 +955,8 @@ fn render_layout_node(
selected_pane, selected_pane,
layout_action, layout_action,
split_preview_mode, split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&right_path, &right_path,
pending_view_action, ctx,
fallback_pane_priority,
pending_handlers,
theme,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
); );
// Draw divider with interaction // Draw divider with interaction
@ -1040,26 +1062,8 @@ fn render_layout_node(
selected_pane, selected_pane,
layout_action, layout_action,
split_preview_mode, split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&top_path, &top_path,
pending_view_action, ctx,
fallback_pane_priority,
pending_handlers,
theme,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
); );
let mut bottom_path = path.clone(); let mut bottom_path = path.clone();
@ -1073,26 +1077,8 @@ fn render_layout_node(
selected_pane, selected_pane,
layout_action, layout_action,
split_preview_mode, split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&bottom_path, &bottom_path,
pending_view_action, ctx,
fallback_pane_priority,
pending_handlers,
theme,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
); );
// Draw divider with interaction // Draw divider with interaction
@ -1178,26 +1164,8 @@ fn render_pane(
selected_pane: &mut Option<NodePath>, selected_pane: &mut Option<NodePath>,
layout_action: &mut Option<LayoutAction>, layout_action: &mut Option<LayoutAction>,
split_preview_mode: &mut SplitPreviewMode, split_preview_mode: &mut SplitPreviewMode,
icon_cache: &mut IconCache,
tool_icon_cache: &mut ToolIconCache,
selected_tool: &mut Tool,
fill_color: &mut egui::Color32,
stroke_color: &mut egui::Color32,
active_color_mode: &mut panes::ColorMode,
pane_instances: &mut HashMap<NodePath, PaneInstance>,
path: &NodePath, path: &NodePath,
pending_view_action: &mut Option<MenuAction>, ctx: &mut RenderContext,
fallback_pane_priority: &mut Option<u32>,
pending_handlers: &mut Vec<panes::ViewActionHandler>,
theme: &Theme,
action_executor: &mut lightningbeam_core::action::ActionExecutor,
selection: &mut lightningbeam_core::selection::Selection,
active_layer_id: &mut Option<Uuid>,
tool_state: &mut lightningbeam_core::tool::ToolState,
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
draw_simplify_mode: &mut lightningbeam_core::tool::SimplifyMode,
rdp_tolerance: &mut f64,
schneider_max_error: &mut f64,
) { ) {
let pane_type = PaneType::from_name(pane_name); let pane_type = PaneType::from_name(pane_name);
@ -1260,7 +1228,7 @@ fn render_pane(
// Load and render icon if available // Load and render icon if available
if let Some(pane_type) = pane_type { if let Some(pane_type) = pane_type {
if let Some(icon) = icon_cache.get_or_load(pane_type) { if let Some(icon) = ctx.icon_cache.get_or_load(pane_type) {
let icon_texture_id = icon.texture_id(ui.ctx()); let icon_texture_id = icon.texture_id(ui.ctx());
let icon_rect = icon_button_rect.shrink(2.0); // Small padding inside button let icon_rect = icon_button_rect.shrink(2.0); // Small padding inside button
ui.painter().image( ui.painter().image(
@ -1297,7 +1265,7 @@ fn render_pane(
for pane_type_option in PaneType::all() { for pane_type_option in PaneType::all() {
// Load icon for this pane type // Load icon for this pane type
if let Some(icon) = icon_cache.get_or_load(*pane_type_option) { if let Some(icon) = ctx.icon_cache.get_or_load(*pane_type_option) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
// Show icon // Show icon
let icon_texture_id = icon.texture_id(ui.ctx()); let icon_texture_id = icon.texture_id(ui.ctx());
@ -1351,80 +1319,86 @@ fn render_pane(
// Render pane-specific header controls (if pane has them) // Render pane-specific header controls (if pane has them)
if let Some(pane_type) = pane_type { if let Some(pane_type) = pane_type {
// Get or create pane instance for header rendering // Get or create pane instance for header rendering
let needs_new_instance = pane_instances let needs_new_instance = ctx.pane_instances
.get(path) .get(path)
.map(|instance| instance.pane_type() != pane_type) .map(|instance| instance.pane_type() != pane_type)
.unwrap_or(true); .unwrap_or(true);
if needs_new_instance { if needs_new_instance {
pane_instances.insert(path.clone(), panes::PaneInstance::new(pane_type)); ctx.pane_instances.insert(path.clone(), panes::PaneInstance::new(pane_type));
} }
if let Some(pane_instance) = pane_instances.get_mut(path) { if let Some(pane_instance) = ctx.pane_instances.get_mut(path) {
let mut header_ui = ui.new_child(egui::UiBuilder::new().max_rect(header_controls_rect).layout(egui::Layout::left_to_right(egui::Align::Center))); let mut header_ui = ui.new_child(egui::UiBuilder::new().max_rect(header_controls_rect).layout(egui::Layout::left_to_right(egui::Align::Center)));
let mut shared = panes::SharedPaneState { let mut shared = panes::SharedPaneState {
tool_icon_cache, tool_icon_cache: ctx.tool_icon_cache,
icon_cache, icon_cache: ctx.icon_cache,
selected_tool, selected_tool: ctx.selected_tool,
fill_color, fill_color: ctx.fill_color,
stroke_color, stroke_color: ctx.stroke_color,
active_color_mode, active_color_mode: ctx.active_color_mode,
pending_view_action, pending_view_action: ctx.pending_view_action,
fallback_pane_priority, fallback_pane_priority: ctx.fallback_pane_priority,
theme, theme: ctx.theme,
pending_handlers, pending_handlers: ctx.pending_handlers,
action_executor, action_executor: ctx.action_executor,
selection, selection: ctx.selection,
active_layer_id, active_layer_id: ctx.active_layer_id,
tool_state, tool_state: ctx.tool_state,
pending_actions, pending_actions: ctx.pending_actions,
draw_simplify_mode, draw_simplify_mode: ctx.draw_simplify_mode,
rdp_tolerance, rdp_tolerance: ctx.rdp_tolerance,
schneider_max_error, schneider_max_error: ctx.schneider_max_error,
audio_controller: ctx.audio_controller.as_mut().map(|c| &mut **c),
playback_time: ctx.playback_time,
is_playing: ctx.is_playing,
}; };
pane_instance.render_header(&mut header_ui, &mut shared); pane_instance.render_header(&mut header_ui, &mut shared);
} }
} }
// Make pane content clickable (use full rect for split preview interaction) // Make pane content clickable (use content rect, not header, for split preview interaction)
let pane_id = ui.id().with(("pane", path)); let pane_id = ui.id().with(("pane", path));
let response = ui.interact(rect, pane_id, egui::Sense::click()); let response = ui.interact(content_rect, pane_id, egui::Sense::click());
// Render pane-specific content using trait-based system // Render pane-specific content using trait-based system
if let Some(pane_type) = pane_type { if let Some(pane_type) = pane_type {
// Get or create pane instance for this path // Get or create pane instance for this path
// Check if we need a new instance (either doesn't exist or type changed) // Check if we need a new instance (either doesn't exist or type changed)
let needs_new_instance = pane_instances let needs_new_instance = ctx.pane_instances
.get(path) .get(path)
.map(|instance| instance.pane_type() != pane_type) .map(|instance| instance.pane_type() != pane_type)
.unwrap_or(true); .unwrap_or(true);
if needs_new_instance { if needs_new_instance {
pane_instances.insert(path.clone(), PaneInstance::new(pane_type)); ctx.pane_instances.insert(path.clone(), PaneInstance::new(pane_type));
} }
// Get the pane instance and render its content // Get the pane instance and render its content
if let Some(pane_instance) = pane_instances.get_mut(path) { if let Some(pane_instance) = ctx.pane_instances.get_mut(path) {
// Create shared state // Create shared state
let mut shared = SharedPaneState { let mut shared = SharedPaneState {
tool_icon_cache, tool_icon_cache: ctx.tool_icon_cache,
icon_cache, icon_cache: ctx.icon_cache,
selected_tool, selected_tool: ctx.selected_tool,
fill_color, fill_color: ctx.fill_color,
stroke_color, stroke_color: ctx.stroke_color,
active_color_mode, active_color_mode: ctx.active_color_mode,
pending_view_action, pending_view_action: ctx.pending_view_action,
fallback_pane_priority, fallback_pane_priority: ctx.fallback_pane_priority,
theme, theme: ctx.theme,
pending_handlers, pending_handlers: ctx.pending_handlers,
action_executor, action_executor: ctx.action_executor,
selection, selection: ctx.selection,
active_layer_id, active_layer_id: ctx.active_layer_id,
tool_state, tool_state: ctx.tool_state,
pending_actions, pending_actions: ctx.pending_actions,
draw_simplify_mode, draw_simplify_mode: ctx.draw_simplify_mode,
rdp_tolerance, rdp_tolerance: ctx.rdp_tolerance,
schneider_max_error, schneider_max_error: ctx.schneider_max_error,
audio_controller: ctx.audio_controller.as_mut().map(|c| &mut **c),
playback_time: ctx.playback_time,
is_playing: ctx.is_playing,
}; };
// Render pane content (header was already rendered above) // Render pane content (header was already rendered above)

View File

@ -73,6 +73,11 @@ pub struct SharedPaneState<'a> {
pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode, pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
pub rdp_tolerance: &'a mut f64, pub rdp_tolerance: &'a mut f64,
pub schneider_max_error: &'a mut f64, pub schneider_max_error: &'a mut f64,
/// Audio engine controller for playback control
pub audio_controller: Option<&'a mut daw_backend::EngineController>,
/// Global playback state
pub playback_time: &'a mut f64, // Current playback position in seconds
pub is_playing: &'a mut bool, // Whether playback is currently active
} }
/// Trait for pane rendering /// Trait for pane rendering

View File

@ -25,9 +25,6 @@ enum ClipDragType {
} }
pub struct TimelinePane { pub struct TimelinePane {
/// Current playback time in seconds
current_time: f64,
/// Horizontal zoom level (pixels per second) /// Horizontal zoom level (pixels per second)
pixels_per_second: f32, pixels_per_second: f32,
@ -53,15 +50,11 @@ pub struct TimelinePane {
/// Cached mouse position from mousedown (used for edge detection when drag starts) /// Cached mouse position from mousedown (used for edge detection when drag starts)
mousedown_pos: Option<egui::Pos2>, mousedown_pos: Option<egui::Pos2>,
/// Is playback currently active?
is_playing: bool,
} }
impl TimelinePane { impl TimelinePane {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
current_time: 0.0,
pixels_per_second: 100.0, pixels_per_second: 100.0,
viewport_start_time: 0.0, viewport_start_time: 0.0,
viewport_scroll_y: 0.0, viewport_scroll_y: 0.0,
@ -72,7 +65,6 @@ impl TimelinePane {
clip_drag_state: None, clip_drag_state: None,
drag_offset: 0.0, drag_offset: 0.0,
mousedown_pos: None, mousedown_pos: None,
is_playing: false,
} }
} }
@ -302,8 +294,8 @@ impl TimelinePane {
} }
/// Render the playhead (current time indicator) /// Render the playhead (current time indicator)
fn render_playhead(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) { fn render_playhead(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme, playback_time: f64) {
let x = self.time_to_x(self.current_time); let x = self.time_to_x(playback_time);
if x >= 0.0 && x <= rect.width() { if x >= 0.0 && x <= rect.width() {
let painter = ui.painter(); let painter = ui.painter();
@ -684,6 +676,9 @@ impl TimelinePane {
active_layer_id: &mut Option<uuid::Uuid>, active_layer_id: &mut Option<uuid::Uuid>,
selection: &mut lightningbeam_core::selection::Selection, selection: &mut lightningbeam_core::selection::Selection,
pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>, pending_actions: &mut Vec<Box<dyn lightningbeam_core::action::Action>>,
playback_time: &mut f64,
is_playing: &mut bool,
audio_controller: Option<&mut daw_backend::EngineController>,
) { ) {
let response = ui.allocate_rect(full_timeline_rect, egui::Sense::click_and_drag()); let response = ui.allocate_rect(full_timeline_rect, egui::Sense::click_and_drag());
@ -1034,7 +1029,8 @@ impl TimelinePane {
if cursor_over_ruler && !alt_held && (response.clicked() || (response.dragged() && !self.is_panning)) { if cursor_over_ruler && !alt_held && (response.clicked() || (response.dragged() && !self.is_panning)) {
if let Some(pos) = response.interact_pointer_pos() { if let Some(pos) = response.interact_pointer_pos() {
let x = (pos.x - content_rect.min.x).max(0.0); let x = (pos.x - content_rect.min.x).max(0.0);
self.current_time = self.x_to_time(x).max(0.0); let new_time = self.x_to_time(x).max(0.0);
*playback_time = new_time;
self.is_scrubbing = true; self.is_scrubbing = true;
} }
} }
@ -1042,12 +1038,17 @@ impl TimelinePane {
else if self.is_scrubbing && response.dragged() && !self.is_panning { else if self.is_scrubbing && response.dragged() && !self.is_panning {
if let Some(pos) = response.interact_pointer_pos() { if let Some(pos) = response.interact_pointer_pos() {
let x = (pos.x - content_rect.min.x).max(0.0); let x = (pos.x - content_rect.min.x).max(0.0);
self.current_time = self.x_to_time(x).max(0.0); let new_time = self.x_to_time(x).max(0.0);
*playback_time = new_time;
} }
} }
// Stop scrubbing when drag ends // Stop scrubbing when drag ends - seek the audio engine
else if !response.dragged() { else if !response.dragged() && self.is_scrubbing {
self.is_scrubbing = false; self.is_scrubbing = false;
// Seek the audio engine to the new position
if let Some(controller) = audio_controller {
controller.seek(*playback_time);
}
} }
// Distinguish between mouse wheel (discrete) and trackpad (smooth) // Distinguish between mouse wheel (discrete) and trackpad (smooth)
@ -1154,29 +1155,54 @@ impl PaneRenderer for TimelinePane {
// Go to start // Go to start
if ui.add_sized(button_size, egui::Button::new("|◀")).clicked() { if ui.add_sized(button_size, egui::Button::new("|◀")).clicked() {
self.current_time = 0.0; *shared.playback_time = 0.0;
if let Some(controller) = shared.audio_controller.as_mut() {
controller.seek(0.0);
}
} }
// Rewind (step backward) // Rewind (step backward)
if ui.add_sized(button_size, egui::Button::new("◀◀")).clicked() { if ui.add_sized(button_size, egui::Button::new("◀◀")).clicked() {
self.current_time = (self.current_time - 0.1).max(0.0); *shared.playback_time = (*shared.playback_time - 0.1).max(0.0);
if let Some(controller) = shared.audio_controller.as_mut() {
controller.seek(*shared.playback_time);
}
} }
// Play/Pause toggle // Play/Pause toggle
let play_pause_text = if self.is_playing { "" } else { "" }; let play_pause_text = if *shared.is_playing { "" } else { "" };
if ui.add_sized(button_size, egui::Button::new(play_pause_text)).clicked() { if ui.add_sized(button_size, egui::Button::new(play_pause_text)).clicked() {
self.is_playing = !self.is_playing; *shared.is_playing = !*shared.is_playing;
// TODO: Actually start/stop playback println!("🔘 Play/Pause button clicked! is_playing = {}", *shared.is_playing);
// Send play/pause command to audio engine
if let Some(controller) = shared.audio_controller.as_mut() {
if *shared.is_playing {
controller.play();
println!("▶ Started playback");
} else {
controller.pause();
println!("⏸ Paused playback");
}
} else {
println!("⚠️ No audio controller available (audio system failed to initialize)");
}
} }
// Fast forward (step forward) // Fast forward (step forward)
if ui.add_sized(button_size, egui::Button::new("▶▶")).clicked() { if ui.add_sized(button_size, egui::Button::new("▶▶")).clicked() {
self.current_time = (self.current_time + 0.1).min(self.duration); *shared.playback_time = (*shared.playback_time + 0.1).min(self.duration);
if let Some(controller) = shared.audio_controller.as_mut() {
controller.seek(*shared.playback_time);
}
} }
// Go to end // Go to end
if ui.add_sized(button_size, egui::Button::new("▶|")).clicked() { if ui.add_sized(button_size, egui::Button::new("▶|")).clicked() {
self.current_time = self.duration; *shared.playback_time = self.duration;
if let Some(controller) = shared.audio_controller.as_mut() {
controller.seek(self.duration);
}
} }
}); });
}); });
@ -1188,7 +1214,7 @@ impl PaneRenderer for TimelinePane {
let text_color = text_style.text_color.unwrap_or(egui::Color32::from_gray(200)); let text_color = text_style.text_color.unwrap_or(egui::Color32::from_gray(200));
// Time display // Time display
ui.colored_label(text_color, format!("Time: {:.2}s / {:.2}s", self.current_time, self.duration)); ui.colored_label(text_color, format!("Time: {:.2}s / {:.2}s", *shared.playback_time, self.duration));
ui.separator(); ui.separator();
@ -1205,8 +1231,8 @@ impl PaneRenderer for TimelinePane {
_path: &NodePath, _path: &NodePath,
shared: &mut SharedPaneState, shared: &mut SharedPaneState,
) { ) {
// Sync timeline's current_time to document // Sync playback_time to document
shared.action_executor.document_mut().current_time = self.current_time; shared.action_executor.document_mut().current_time = *shared.playback_time;
// Get document from action executor // Get document from action executor
let document = shared.action_executor.document(); let document = shared.action_executor.document();
@ -1306,7 +1332,7 @@ impl PaneRenderer for TimelinePane {
// Render playhead on top (clip to timeline area) // Render playhead on top (clip to timeline area)
ui.set_clip_rect(timeline_rect.intersect(original_clip_rect)); ui.set_clip_rect(timeline_rect.intersect(original_clip_rect));
self.render_playhead(ui, timeline_rect, shared.theme); self.render_playhead(ui, timeline_rect, shared.theme, *shared.playback_time);
// Restore original clip rect // Restore original clip rect
ui.set_clip_rect(original_clip_rect); ui.set_clip_rect(original_clip_rect);
@ -1323,6 +1349,9 @@ impl PaneRenderer for TimelinePane {
shared.active_layer_id, shared.active_layer_id,
shared.selection, shared.selection,
shared.pending_actions, shared.pending_actions,
shared.playback_time,
shared.is_playing,
shared.audio_controller.as_mut().map(|c| &mut **c),
); );
// Register handler for pending view actions (two-phase dispatch) // Register handler for pending view actions (two-phase dispatch)

View File

@ -3,6 +3,7 @@ const { listen } = window.__TAURI__.event;
import * as fitCurve from "/fit-curve.js"; import * as fitCurve from "/fit-curve.js";
import { Bezier } from "/bezier.js"; import { Bezier } from "/bezier.js";
import { Quadtree } from "./quadtree.js"; import { Quadtree } from "./quadtree.js";
import { initRenderWindow, updateGradient, syncPosition, closeRenderWindow } from "./render-window-integration.js";
import { import {
createNewFileDialog, createNewFileDialog,
showNewFileDialog, showNewFileDialog,
@ -822,6 +823,30 @@ window.addEventListener("DOMContentLoaded", () => {
} }
} }
})(); })();
// Initialize native render window after layout is ready
(async () => {
try {
// Wait for layout to be fully positioned
await new Promise(resolve => setTimeout(resolve, 200));
// Find the stage canvas (should be first in canvases array)
const stageCanvas = canvases[0];
if (stageCanvas) {
console.log('Initializing native render window...');
await initRenderWindow(stageCanvas);
console.log('Native render window initialized successfully');
// Make functions available globally for testing
window.updateRenderGradient = updateGradient;
window.syncRenderWindow = syncPosition;
} else {
console.warn('Stage canvas not found, skipping render window initialization');
}
} catch (error) {
console.error('Failed to initialize native render window:', error);
}
})();
}); });
window.addEventListener("resize", () => { window.addEventListener("resize", () => {