Use audio engine as source of truth for playback time
This commit is contained in:
parent
5761d48f1b
commit
5fbb2c078b
|
|
@ -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
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
25
src/main.js
25
src/main.js
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue