Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs

355 lines
16 KiB
Rust

/// Pane implementations for the editor
///
/// Each pane type has its own module with implementation details.
/// Panes can hold local state and access shared state through SharedPaneState.
use eframe::egui;
use lightningbeam_core::{pane::PaneType, tool::Tool};
use uuid::Uuid;
// Type alias for node paths (matches main.rs)
pub type NodePath = Vec<usize>;
/// Handler information for view actions (zoom, pan, etc.)
/// Used for two-phase dispatch: register during render, execute after
#[derive(Clone)]
pub struct ViewActionHandler {
pub priority: u32,
pub pane_path: NodePath,
pub zoom_center: egui::Vec2,
}
/// Clip type for drag-and-drop operations
/// Distinguishes between different clip/layer type combinations
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DragClipType {
/// Vector animation clip
Vector,
/// Video clip
Video,
/// Sampled audio clip (WAV, MP3, etc.)
AudioSampled,
/// MIDI clip
AudioMidi,
/// Static image asset
Image,
/// Effect (shader-based visual effect)
Effect,
}
/// Information about an asset being dragged from the Asset Library
#[derive(Debug, Clone)]
pub struct DraggingAsset {
/// The clip ID being dragged
pub clip_id: Uuid,
/// Type of clip (determines compatible layer types)
pub clip_type: DragClipType,
/// Display name
pub name: String,
/// Duration in seconds
#[allow(dead_code)] // Populated during drag, consumed when drag-and-drop features expand
pub duration: f64,
/// Dimensions (width, height) for vector/video clips, None for audio
pub dimensions: Option<(f64, f64)>,
/// Optional linked audio clip ID (for video clips with extracted audio)
pub linked_audio_clip_id: Option<Uuid>,
}
pub mod toolbar;
pub mod stage;
pub mod timeline;
pub mod infopanel;
pub mod outliner;
pub mod piano_roll;
pub mod virtual_piano;
pub mod node_editor;
pub mod node_graph;
pub mod preset_browser;
pub mod asset_library;
pub mod shader_editor;
/// Which color mode is active for the eyedropper tool
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorMode {
Fill,
Stroke,
}
impl Default for ColorMode {
fn default() -> Self {
ColorMode::Fill
}
}
/// Helper functions for layer/clip type matching and creation
/// Check if a clip type matches a layer type
pub fn layer_matches_clip_type(layer: &lightningbeam_core::layer::AnyLayer, clip_type: DragClipType) -> bool {
use lightningbeam_core::layer::*;
match (layer, clip_type) {
(AnyLayer::Vector(_), DragClipType::Vector) => true,
(AnyLayer::Vector(_), DragClipType::Image) => true, // Images go on vector layers as shapes
(AnyLayer::Video(_), DragClipType::Video) => true,
(AnyLayer::Audio(audio), DragClipType::AudioSampled) => {
audio.audio_layer_type == AudioLayerType::Sampled
}
(AnyLayer::Audio(audio), DragClipType::AudioMidi) => {
audio.audio_layer_type == AudioLayerType::Midi
}
(AnyLayer::Effect(_), DragClipType::Effect) => true,
_ => false,
}
}
/// Create a new layer of the appropriate type for a clip
pub fn create_layer_for_clip_type(clip_type: DragClipType, name: &str) -> lightningbeam_core::layer::AnyLayer {
use lightningbeam_core::layer::*;
use lightningbeam_core::effect_layer::EffectLayer;
match clip_type {
DragClipType::Vector => AnyLayer::Vector(VectorLayer::new(name)),
DragClipType::Video => AnyLayer::Video(VideoLayer::new(name)),
DragClipType::AudioSampled => AnyLayer::Audio(AudioLayer::new_sampled(name)),
DragClipType::AudioMidi => AnyLayer::Audio(AudioLayer::new_midi(name)),
// Images are placed as shapes on vector layers, not their own layer type
DragClipType::Image => AnyLayer::Vector(VectorLayer::new(name)),
DragClipType::Effect => AnyLayer::Effect(EffectLayer::new(name)),
}
}
/// Find an existing sampled audio track in the document
/// Returns the layer ID if found, None otherwise
pub fn find_sampled_audio_track(document: &lightningbeam_core::document::Document) -> Option<uuid::Uuid> {
use lightningbeam_core::layer::*;
for layer in &document.root.children {
if let AnyLayer::Audio(audio_layer) = layer {
if audio_layer.audio_layer_type == AudioLayerType::Sampled {
return Some(audio_layer.layer.id);
}
}
}
None
}
/// Shared state that all panes can access
pub struct SharedPaneState<'a> {
pub tool_icon_cache: &'a mut crate::ToolIconCache,
#[allow(dead_code)] // Used by pane chrome rendering in main.rs
pub icon_cache: &'a mut crate::IconCache,
pub selected_tool: &'a mut Tool,
pub fill_color: &'a mut egui::Color32,
pub stroke_color: &'a mut egui::Color32,
/// Tracks which color (fill or stroke) was last interacted with, for eyedropper tool
pub active_color_mode: &'a mut ColorMode,
pub pending_view_action: &'a mut Option<crate::menu::MenuAction>,
/// Tracks the priority of the best fallback pane for view actions
/// Lower number = higher priority. None = no fallback pane seen yet
/// Priority order: Stage(0) > Timeline(1) > PianoRoll(2) > NodeEditor(3)
pub fallback_pane_priority: &'a mut Option<u32>,
pub theme: &'a crate::theme::Theme,
/// Registry of handlers for the current pending action
/// Panes register themselves here during render, execution happens after
pub pending_handlers: &'a mut Vec<ViewActionHandler>,
/// Action executor for immediate action execution (for shape tools to avoid flicker)
/// Also provides read-only access to the document via action_executor.document()
pub action_executor: &'a mut lightningbeam_core::action::ActionExecutor,
/// Current selection state (mutable for tools to modify)
pub selection: &'a mut lightningbeam_core::selection::Selection,
/// Currently active layer ID
pub active_layer_id: &'a mut Option<uuid::Uuid>,
/// Current tool interaction state (mutable for tools to modify)
pub tool_state: &'a mut lightningbeam_core::tool::ToolState,
/// Actions to execute after rendering completes (two-phase dispatch)
pub pending_actions: &'a mut Vec<Box<dyn lightningbeam_core::action::Action>>,
/// Draw tool configuration
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 (wrapped in Arc<Mutex<>> for thread safety)
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
/// Video manager for video decoding and frame caching
pub video_manager: &'a std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>,
/// Mapping from Document layer UUIDs to daw-backend TrackIds
pub layer_to_track_map: &'a std::collections::HashMap<Uuid, daw_backend::TrackId>,
/// 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
/// Recording state
pub is_recording: &'a mut bool, // Whether recording is currently active
pub recording_clips: &'a mut std::collections::HashMap<uuid::Uuid, u32>, // layer_id -> clip_id
pub recording_start_time: &'a mut f64, // Playback time when recording started
pub recording_layer_id: &'a mut Option<uuid::Uuid>, // Layer being recorded to
/// Asset being dragged from Asset Library (for cross-pane drag-and-drop)
pub dragging_asset: &'a mut Option<DraggingAsset>,
// Tool-specific options for infopanel
/// Stroke width for drawing tools (Draw, Rectangle, Ellipse, Line, Polygon)
pub stroke_width: &'a mut f64,
/// Whether to fill shapes when drawing (Rectangle, Ellipse, Polygon)
pub fill_enabled: &'a mut bool,
/// Fill gap tolerance for paint bucket tool
pub paint_bucket_gap_tolerance: &'a mut f64,
/// Number of sides for polygon tool
pub polygon_sides: &'a mut u32,
/// Cache of MIDI events for rendering (keyed by backend midi_clip_id).
/// Mutable so panes can update the cache immediately on edits (avoiding 1-frame snap-back).
/// NOTE: If an action later fails during execution, the cache may be out of sync with the
/// backend. This is acceptable because MIDI note edits are simple and unlikely to fail.
/// Undo/redo rebuilds affected entries from the backend to restore consistency.
pub midi_event_cache: &'a mut std::collections::HashMap<u32, Vec<(f64, u8, u8, bool)>>,
/// Audio pool indices that got new raw audio data this frame (for thumbnail invalidation)
pub audio_pools_with_new_waveforms: &'a std::collections::HashSet<usize>,
/// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels))
pub raw_audio_cache: &'a std::collections::HashMap<usize, (Vec<f32>, u32, u32)>,
/// Pool indices needing GPU waveform texture upload
pub waveform_gpu_dirty: &'a mut std::collections::HashSet<usize>,
/// Effect ID to load into shader editor (set by asset library, consumed by shader editor)
pub effect_to_load: &'a mut Option<Uuid>,
/// Queue for effect thumbnail requests (effect IDs to generate thumbnails for)
pub effect_thumbnail_requests: &'a mut Vec<Uuid>,
/// Cache of generated effect thumbnails (effect_id -> RGBA data)
pub effect_thumbnail_cache: &'a std::collections::HashMap<Uuid, Vec<u8>>,
/// Effect IDs whose thumbnails should be invalidated (e.g., after shader edit)
pub effect_thumbnails_to_invalidate: &'a mut Vec<Uuid>,
/// Surface texture format for GPU rendering (Rgba8Unorm or Bgra8Unorm depending on platform)
pub target_format: wgpu::TextureFormat,
}
/// Trait for pane rendering
///
/// Panes implement this trait to provide custom rendering logic.
/// The header is optional and typically used for controls (e.g., Timeline playback).
/// The content area is the main body of the pane.
pub trait PaneRenderer {
/// Render the optional header section with controls
///
/// Returns true if a header was rendered, false if no header
fn render_header(&mut self, _ui: &mut egui::Ui, _shared: &mut SharedPaneState) -> bool {
false // Default: no header
}
/// Render the main content area
fn render_content(
&mut self,
ui: &mut egui::Ui,
rect: egui::Rect,
path: &NodePath,
shared: &mut SharedPaneState,
);
/// Get the display name of this pane
#[allow(dead_code)] // Implemented by all panes, dispatch infrastructure complete
fn name(&self) -> &str;
}
/// Enum wrapper for all pane implementations (enum dispatch pattern)
pub enum PaneInstance {
Stage(stage::StagePane),
Timeline(timeline::TimelinePane),
Toolbar(toolbar::ToolbarPane),
Infopanel(infopanel::InfopanelPane),
Outliner(outliner::OutlinerPane),
PianoRoll(piano_roll::PianoRollPane),
VirtualPiano(virtual_piano::VirtualPianoPane),
NodeEditor(node_editor::NodeEditorPane),
PresetBrowser(preset_browser::PresetBrowserPane),
AssetLibrary(asset_library::AssetLibraryPane),
ShaderEditor(shader_editor::ShaderEditorPane),
}
impl PaneInstance {
/// Create a new pane instance for the given type
pub fn new(pane_type: PaneType) -> Self {
match pane_type {
PaneType::Stage => PaneInstance::Stage(stage::StagePane::new()),
PaneType::Timeline => PaneInstance::Timeline(timeline::TimelinePane::new()),
PaneType::Toolbar => PaneInstance::Toolbar(toolbar::ToolbarPane::new()),
PaneType::Infopanel => PaneInstance::Infopanel(infopanel::InfopanelPane::new()),
PaneType::Outliner => PaneInstance::Outliner(outliner::OutlinerPane::new()),
PaneType::PianoRoll => PaneInstance::PianoRoll(piano_roll::PianoRollPane::new()),
PaneType::VirtualPiano => PaneInstance::VirtualPiano(virtual_piano::VirtualPianoPane::new()),
PaneType::NodeEditor => PaneInstance::NodeEditor(node_editor::NodeEditorPane::new()),
PaneType::PresetBrowser => {
PaneInstance::PresetBrowser(preset_browser::PresetBrowserPane::new())
}
PaneType::AssetLibrary => {
PaneInstance::AssetLibrary(asset_library::AssetLibraryPane::new())
}
PaneType::ShaderEditor => {
PaneInstance::ShaderEditor(shader_editor::ShaderEditorPane::new())
}
}
}
/// Get the pane type of this instance
pub fn pane_type(&self) -> PaneType {
match self {
PaneInstance::Stage(_) => PaneType::Stage,
PaneInstance::Timeline(_) => PaneType::Timeline,
PaneInstance::Toolbar(_) => PaneType::Toolbar,
PaneInstance::Infopanel(_) => PaneType::Infopanel,
PaneInstance::Outliner(_) => PaneType::Outliner,
PaneInstance::PianoRoll(_) => PaneType::PianoRoll,
PaneInstance::VirtualPiano(_) => PaneType::VirtualPiano,
PaneInstance::NodeEditor(_) => PaneType::NodeEditor,
PaneInstance::PresetBrowser(_) => PaneType::PresetBrowser,
PaneInstance::AssetLibrary(_) => PaneType::AssetLibrary,
PaneInstance::ShaderEditor(_) => PaneType::ShaderEditor,
}
}
}
impl PaneRenderer for PaneInstance {
fn render_header(&mut self, ui: &mut egui::Ui, shared: &mut SharedPaneState) -> bool {
match self {
PaneInstance::Stage(p) => p.render_header(ui, shared),
PaneInstance::Timeline(p) => p.render_header(ui, shared),
PaneInstance::Toolbar(p) => p.render_header(ui, shared),
PaneInstance::Infopanel(p) => p.render_header(ui, shared),
PaneInstance::Outliner(p) => p.render_header(ui, shared),
PaneInstance::PianoRoll(p) => p.render_header(ui, shared),
PaneInstance::VirtualPiano(p) => p.render_header(ui, shared),
PaneInstance::NodeEditor(p) => p.render_header(ui, shared),
PaneInstance::PresetBrowser(p) => p.render_header(ui, shared),
PaneInstance::AssetLibrary(p) => p.render_header(ui, shared),
PaneInstance::ShaderEditor(p) => p.render_header(ui, shared),
}
}
fn render_content(
&mut self,
ui: &mut egui::Ui,
rect: egui::Rect,
path: &NodePath,
shared: &mut SharedPaneState,
) {
match self {
PaneInstance::Stage(p) => p.render_content(ui, rect, path, shared),
PaneInstance::Timeline(p) => p.render_content(ui, rect, path, shared),
PaneInstance::Toolbar(p) => p.render_content(ui, rect, path, shared),
PaneInstance::Infopanel(p) => p.render_content(ui, rect, path, shared),
PaneInstance::Outliner(p) => p.render_content(ui, rect, path, shared),
PaneInstance::PianoRoll(p) => p.render_content(ui, rect, path, shared),
PaneInstance::VirtualPiano(p) => p.render_content(ui, rect, path, shared),
PaneInstance::NodeEditor(p) => p.render_content(ui, rect, path, shared),
PaneInstance::PresetBrowser(p) => p.render_content(ui, rect, path, shared),
PaneInstance::AssetLibrary(p) => p.render_content(ui, rect, path, shared),
PaneInstance::ShaderEditor(p) => p.render_content(ui, rect, path, shared),
}
}
fn name(&self) -> &str {
match self {
PaneInstance::Stage(p) => p.name(),
PaneInstance::Timeline(p) => p.name(),
PaneInstance::Toolbar(p) => p.name(),
PaneInstance::Infopanel(p) => p.name(),
PaneInstance::Outliner(p) => p.name(),
PaneInstance::PianoRoll(p) => p.name(),
PaneInstance::VirtualPiano(p) => p.name(),
PaneInstance::NodeEditor(p) => p.name(),
PaneInstance::PresetBrowser(p) => p.name(),
PaneInstance::AssetLibrary(p) => p.name(),
PaneInstance::ShaderEditor(p) => p.name(),
}
}
}