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 sample_rate: 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 {
@ -152,6 +154,7 @@ impl AudioSystem {
stream: output_stream,
sample_rate,
channels,
event_rx: None, // No event receiver when audio device unavailable
});
}
};
@ -179,6 +182,7 @@ impl AudioSystem {
stream: output_stream,
sample_rate,
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
Box::leak(Box::new(input_stream));
// Spawn emitter thread if provided
if let Some(emitter) = event_emitter {
// Spawn emitter thread if provided, or store event_rx for manual polling
let event_rx_option = if let Some(emitter) = event_emitter {
Self::spawn_emitter_thread(event_rx, emitter);
}
None
} else {
Some(event_rx)
};
Ok(Self {
controller,
stream: output_stream,
sample_rate,
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]
lightningbeam-core = { path = "../lightningbeam-core" }
daw-backend = { path = "../../daw-backend" }
rtrb = "0.3"
# UI Framework
eframe = { workspace = true }

View File

@ -267,8 +267,10 @@ struct EditorApp {
rdp_tolerance: f64, // RDP simplification tolerance (default: 10.0)
schneider_max_error: f64, // Schneider curve fitting max error (default: 30.0)
// Audio engine integration
audio_controller: Option<daw_backend::EngineController>, // Audio engine controller for playback
audio_event_rx: Option<rtrb::Consumer<daw_backend::AudioEvent>>, // Audio event receiver
audio_system: Option<daw_backend::AudioSystem>, // Audio system (must be kept alive for stream)
// 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 {
@ -302,6 +304,19 @@ impl EditorApp {
// Wrap document in ActionExecutor
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 {
layouts,
current_layout_index: 0,
@ -327,6 +342,9 @@ impl EditorApp {
draw_simplify_mode: lightningbeam_core::tool::SimplifyMode::Smooth, // Default to smooth curves
rdp_tolerance: 10.0, // Default RDP tolerance
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)
ctx.input(|i| {
// Check menu shortcuts
@ -726,6 +767,32 @@ impl eframe::App for EditorApp {
// Registry for actions to execute after rendering (two-phase dispatch)
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(
ui,
&mut self.current_layout,
@ -735,26 +802,8 @@ impl eframe::App for EditorApp {
&mut self.selected_pane,
&mut layout_action,
&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
&mut self.pending_view_action,
&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,
&mut ctx,
);
// Execute action on the best handler (two-phase dispatch)
@ -782,9 +831,9 @@ impl eframe::App for EditorApp {
// Set cursor based on hover state
if let Some((_, is_horizontal)) = self.hovered_divider {
if is_horizontal {
ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
} 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
fn render_layout_node(
ui: &mut egui::Ui,
@ -826,30 +902,12 @@ fn render_layout_node(
selected_pane: &mut Option<NodePath>,
layout_action: &mut Option<LayoutAction>,
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,
pending_view_action: &mut Option<MenuAction>,
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,
ctx: &mut RenderContext,
) {
match node {
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 } => {
// Handle dragging
@ -882,26 +940,8 @@ fn render_layout_node(
selected_pane,
layout_action,
split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&left_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,
ctx,
);
let mut right_path = path.clone();
@ -915,26 +955,8 @@ fn render_layout_node(
selected_pane,
layout_action,
split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&right_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,
ctx,
);
// Draw divider with interaction
@ -1040,26 +1062,8 @@ fn render_layout_node(
selected_pane,
layout_action,
split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&top_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,
ctx,
);
let mut bottom_path = path.clone();
@ -1073,26 +1077,8 @@ fn render_layout_node(
selected_pane,
layout_action,
split_preview_mode,
icon_cache,
tool_icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pane_instances,
&bottom_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,
ctx,
);
// Draw divider with interaction
@ -1178,26 +1164,8 @@ fn render_pane(
selected_pane: &mut Option<NodePath>,
layout_action: &mut Option<LayoutAction>,
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,
pending_view_action: &mut Option<MenuAction>,
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,
ctx: &mut RenderContext,
) {
let pane_type = PaneType::from_name(pane_name);
@ -1260,7 +1228,7 @@ fn render_pane(
// Load and render icon if available
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_rect = icon_button_rect.shrink(2.0); // Small padding inside button
ui.painter().image(
@ -1297,7 +1265,7 @@ fn render_pane(
for pane_type_option in PaneType::all() {
// 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| {
// Show icon
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)
if let Some(pane_type) = pane_type {
// Get or create pane instance for header rendering
let needs_new_instance = pane_instances
let needs_new_instance = ctx.pane_instances
.get(path)
.map(|instance| instance.pane_type() != pane_type)
.unwrap_or(true);
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 shared = panes::SharedPaneState {
tool_icon_cache,
icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pending_view_action,
fallback_pane_priority,
theme,
pending_handlers,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
tool_icon_cache: ctx.tool_icon_cache,
icon_cache: ctx.icon_cache,
selected_tool: ctx.selected_tool,
fill_color: ctx.fill_color,
stroke_color: ctx.stroke_color,
active_color_mode: ctx.active_color_mode,
pending_view_action: ctx.pending_view_action,
fallback_pane_priority: ctx.fallback_pane_priority,
theme: ctx.theme,
pending_handlers: ctx.pending_handlers,
action_executor: ctx.action_executor,
selection: ctx.selection,
active_layer_id: ctx.active_layer_id,
tool_state: ctx.tool_state,
pending_actions: ctx.pending_actions,
draw_simplify_mode: ctx.draw_simplify_mode,
rdp_tolerance: ctx.rdp_tolerance,
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);
}
}
// 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 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
if let Some(pane_type) = pane_type {
// Get or create pane instance for this path
// 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)
.map(|instance| instance.pane_type() != pane_type)
.unwrap_or(true);
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
if let Some(pane_instance) = pane_instances.get_mut(path) {
if let Some(pane_instance) = ctx.pane_instances.get_mut(path) {
// Create shared state
let mut shared = SharedPaneState {
tool_icon_cache,
icon_cache,
selected_tool,
fill_color,
stroke_color,
active_color_mode,
pending_view_action,
fallback_pane_priority,
theme,
pending_handlers,
action_executor,
selection,
active_layer_id,
tool_state,
pending_actions,
draw_simplify_mode,
rdp_tolerance,
schneider_max_error,
tool_icon_cache: ctx.tool_icon_cache,
icon_cache: ctx.icon_cache,
selected_tool: ctx.selected_tool,
fill_color: ctx.fill_color,
stroke_color: ctx.stroke_color,
active_color_mode: ctx.active_color_mode,
pending_view_action: ctx.pending_view_action,
fallback_pane_priority: ctx.fallback_pane_priority,
theme: ctx.theme,
pending_handlers: ctx.pending_handlers,
action_executor: ctx.action_executor,
selection: ctx.selection,
active_layer_id: ctx.active_layer_id,
tool_state: ctx.tool_state,
pending_actions: ctx.pending_actions,
draw_simplify_mode: ctx.draw_simplify_mode,
rdp_tolerance: ctx.rdp_tolerance,
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)

View File

@ -73,6 +73,11 @@ pub struct SharedPaneState<'a> {
pub draw_simplify_mode: &'a mut lightningbeam_core::tool::SimplifyMode,
pub rdp_tolerance: &'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

View File

@ -25,9 +25,6 @@ enum ClipDragType {
}
pub struct TimelinePane {
/// Current playback time in seconds
current_time: f64,
/// Horizontal zoom level (pixels per second)
pixels_per_second: f32,
@ -53,15 +50,11 @@ pub struct TimelinePane {
/// Cached mouse position from mousedown (used for edge detection when drag starts)
mousedown_pos: Option<egui::Pos2>,
/// Is playback currently active?
is_playing: bool,
}
impl TimelinePane {
pub fn new() -> Self {
Self {
current_time: 0.0,
pixels_per_second: 100.0,
viewport_start_time: 0.0,
viewport_scroll_y: 0.0,
@ -72,7 +65,6 @@ impl TimelinePane {
clip_drag_state: None,
drag_offset: 0.0,
mousedown_pos: None,
is_playing: false,
}
}
@ -302,8 +294,8 @@ impl TimelinePane {
}
/// Render the playhead (current time indicator)
fn render_playhead(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme) {
let x = self.time_to_x(self.current_time);
fn render_playhead(&self, ui: &mut egui::Ui, rect: egui::Rect, theme: &crate::theme::Theme, playback_time: f64) {
let x = self.time_to_x(playback_time);
if x >= 0.0 && x <= rect.width() {
let painter = ui.painter();
@ -684,6 +676,9 @@ impl TimelinePane {
active_layer_id: &mut Option<uuid::Uuid>,
selection: &mut lightningbeam_core::selection::Selection,
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());
@ -1034,7 +1029,8 @@ impl TimelinePane {
if cursor_over_ruler && !alt_held && (response.clicked() || (response.dragged() && !self.is_panning)) {
if let Some(pos) = response.interact_pointer_pos() {
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;
}
}
@ -1042,12 +1038,17 @@ impl TimelinePane {
else if self.is_scrubbing && response.dragged() && !self.is_panning {
if let Some(pos) = response.interact_pointer_pos() {
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
else if !response.dragged() {
// Stop scrubbing when drag ends - seek the audio engine
else if !response.dragged() && self.is_scrubbing {
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)
@ -1154,29 +1155,54 @@ impl PaneRenderer for TimelinePane {
// Go to start
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)
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
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() {
self.is_playing = !self.is_playing;
// TODO: Actually start/stop playback
*shared.is_playing = !*shared.is_playing;
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)
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
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));
// 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();
@ -1205,8 +1231,8 @@ impl PaneRenderer for TimelinePane {
_path: &NodePath,
shared: &mut SharedPaneState,
) {
// Sync timeline's current_time to document
shared.action_executor.document_mut().current_time = self.current_time;
// Sync playback_time to document
shared.action_executor.document_mut().current_time = *shared.playback_time;
// Get document from action executor
let document = shared.action_executor.document();
@ -1306,7 +1332,7 @@ impl PaneRenderer for TimelinePane {
// Render playhead on top (clip to timeline area)
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
ui.set_clip_rect(original_clip_rect);
@ -1323,6 +1349,9 @@ impl PaneRenderer for TimelinePane {
shared.active_layer_id,
shared.selection,
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)

View File

@ -3,6 +3,7 @@ const { listen } = window.__TAURI__.event;
import * as fitCurve from "/fit-curve.js";
import { Bezier } from "/bezier.js";
import { Quadtree } from "./quadtree.js";
import { initRenderWindow, updateGradient, syncPosition, closeRenderWindow } from "./render-window-integration.js";
import {
createNewFileDialog,
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", () => {