/// 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; /// 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, } /// Command for webcam recording (issued by timeline, processed by main) #[derive(Debug)] #[allow(dead_code)] pub enum WebcamRecordCommand { /// Start recording on the given video layer // TODO: remove layer_id — recording_layer_ids now tracks which layers are recording Start { layer_id: uuid::Uuid }, /// Stop current webcam recording Stop, } pub mod toolbar; pub mod stage; pub mod gradient_editor; pub mod timeline; pub mod infopanel; pub mod outliner; pub mod keyboard_layout; 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 { 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 /// Onion-skinning view settings (editor-only; not saved with the document). Ghosts the /// active layer's neighbouring keyframes, warm-tinted for past and cool for future. #[derive(Clone, Copy, Debug)] pub struct OnionSkinSettings { pub enabled: bool, pub frames_before: usize, pub frames_after: usize, /// Opacity of the nearest ghost; further ghosts fall off linearly. pub opacity: f32, } impl Default for OnionSkinSettings { fn default() -> Self { Self { enabled: false, frames_before: 2, frames_after: 2, opacity: 0.35 } } } impl OnionSkinSettings { /// RGB tint multipliers for past (warm) and future (cool) ghosts. pub const PAST_TINT: [f32; 3] = [1.0, 0.45, 0.45]; pub const FUTURE_TINT: [f32; 3] = [0.45, 0.6, 1.0]; /// Opacity for the `n`-th ghost away from the current frame (n = 1 is nearest), /// linearly falling off so the furthest ghost is faintest. pub fn ghost_opacity(&self, n: usize, total: usize) -> f32 { if total == 0 { return self.opacity; } let falloff = 1.0 - (n.saturating_sub(1) as f32) / (total as f32); self.opacity * falloff.clamp(0.15, 1.0) } } pub struct SharedPaneState<'a> { /// Current `.beam` container path (for lazily paging image-asset bytes in the /// renderer's ImageCache). `None` before the project is first saved/loaded. pub container_path: Option, /// Effective onion-skin settings (already gated to off during playback by main.rs). pub onion: OnionSkinSettings, /// The raw onion-skin settings, mutable — edited by the Info Panel's controls. pub onion_skin: &'a mut OnionSkinSettings, 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, /// 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, 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, /// 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, /// Document-level focus: tracks the most recently selected thing(s) of any type pub focus: &'a mut lightningbeam_core::selection::FocusSelection, /// Which VectorClip is being edited (None = document root) pub editing_clip_id: Option, /// The clip instance ID being edited pub editing_instance_id: Option, /// The parent layer ID containing the clip instance being edited pub editing_parent_layer_id: Option, /// The full clip_id path being edited, outermost → current (for breadcrumbs). Empty at root. pub editing_clip_path: Vec, /// Request to enter a movie clip for editing: (clip_id, instance_id, parent_layer_id) pub pending_enter_clip: &'a mut Option<(uuid::Uuid, uuid::Uuid, uuid::Uuid)>, /// Request to exit the current movie clip pub pending_exit_clip: &'a mut bool, /// Request to exit up to a specific editing depth (number of clips to keep); e.g. a breadcrumb /// click. `Some(0)` exits all the way to the document root. pub pending_exit_to_depth: &'a mut Option, /// Currently active layer ID pub active_layer_id: &'a mut Option, /// 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>, /// 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, /// All per-tool raster paint settings (replaces 20+ individual fields). pub raster_settings: &'a mut crate::tools::RasterToolSettings, /// Audio engine controller for playback control (wrapped in Arc> for thread safety) pub audio_controller: Option<&'a std::sync::Arc>>, /// Snapshot of all audio/MIDI clip instances from the backend (for timeline rendering). /// Updated by the audio thread after each mutation; UI reads it each frame. pub clip_snapshot: Option>>, /// Opener for the microphone/line-in stream — consumed on first use. pub audio_input_opener: &'a mut Option, /// Live input stream handle; kept alive while recording is active. pub audio_input_stream: &'a mut Option, /// Buffer size (frames) used for the output stream, passed to the input stream opener. pub audio_buffer_size: u32, /// Video manager for video decoding and frame caching pub video_manager: &'a std::sync::Arc>, /// Maps all layer/group/clip UUIDs to backend track IDs (audio, MIDI, and metatracks) pub layer_to_track_map: &'a std::collections::HashMap, /// Maps document clip instance UUIDs to backend clip instance IDs (for action dispatch) pub clip_instance_to_backend_map: &'a std::collections::HashMap, /// 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 metronome_enabled: &'a mut bool, // Whether metronome clicks during recording pub count_in_enabled: &'a mut bool, // Whether count-in fires before recording pub recording_clips: &'a mut std::collections::HashMap, // layer_id -> clip_id pub recording_start_time: &'a mut f64, // Playback time when recording started pub recording_layer_ids: &'a mut Vec, // Layers being recorded to /// Asset being dragged from Asset Library (for cross-pane drag-and-drop) pub dragging_asset: &'a mut Option, // 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, /// Whether to snap to geometry when editing vectors pub snap_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>, /// Audio pool indices that got new raw audio data this frame (for thumbnail invalidation) pub audio_pools_with_new_waveforms: &'a std::collections::HashSet, /// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels)) pub raw_audio_cache: &'a std::collections::HashMap>, u32, u32)>, /// Pool indices needing GPU waveform texture upload pub waveform_gpu_dirty: &'a mut std::collections::HashSet, /// Pools whose `raw_audio_cache` entry is a packed min/max floor rather than /// raw samples (pool_index -> `B`, floor frames-per-texel). Drives the GPU /// min/max upload path and the floor's effective rate `sr/B` in the renderer. pub waveform_minmax_pools: &'a std::collections::HashMap, /// Miss-sink for on-demand raster keyframe pixel faulting (Phase 3 paging). /// The canvas inserts the id of any raster keyframe it wants to upload whose /// `raw_pixels` aren't resident; the App drains this at the top of the next /// `update()` and faults the pixels in from the project container. pub raster_fault_requests: &'a std::sync::Arc>>, /// Effect ID to load into shader editor (set by asset library, consumed by shader editor) pub effect_to_load: &'a mut Option, /// Queue for effect thumbnail requests (effect IDs to generate thumbnails for) pub effect_thumbnail_requests: &'a mut Vec, /// Cache of generated effect thumbnails (effect_id -> RGBA data) pub effect_thumbnail_cache: &'a std::collections::HashMap>, /// Effect IDs whose thumbnails should be invalidated (e.g., after shader edit) pub effect_thumbnails_to_invalidate: &'a mut Vec, /// Latest webcam capture frame (None if no camera is active) pub webcam_frame: Option, /// Pending webcam recording commands (processed by main.rs after render) pub webcam_record_command: &'a mut Option, /// Surface texture format for GPU rendering (Rgba8Unorm or Bgra8Unorm depending on platform) pub target_format: wgpu::TextureFormat, /// Menu actions queued by panes (e.g. context menu items), processed by main after rendering pub pending_menu_actions: &'a mut Vec, /// Clipboard manager for cut/copy/paste operations pub clipboard_manager: &'a mut lightningbeam_core::clipboard::ClipboardManager, // VU meter levels pub input_level: f32, pub output_level: (f32, f32), pub track_levels: &'a std::collections::HashMap, #[allow(dead_code)] // Available for panes that need reverse track->layer lookup pub track_to_layer_map: &'a std::collections::HashMap, /// Whether to show waveforms as stacked stereo (true) or combined mono (false) pub waveform_stereo: bool, /// Generation counter - incremented on project load to force reloads pub project_generation: &'a mut u64, /// Incremented whenever node graph topology changes (add/remove node or connection). /// Used by the timeline to know when to refresh automation lane caches. pub graph_topology_generation: &'a mut u64, /// Script ID to open in the script editor (set by node graph "Edit Script" action) pub script_to_edit: &'a mut Option, /// Script ID that was just saved (triggers auto-recompile of nodes using it) pub script_saved: &'a mut Option, /// Undo-stack depth recorded before the first region-select cut of the current /// selection session. `Some` while a region selection is live and unchanged; lets a /// later deselect heal (undo) the cut if nothing was edited. See `resolve_pending_region_cut`. pub pending_region_cut_base: &'a mut Option, /// Region select mode (Rectangle or Lasso) pub region_select_mode: &'a mut lightningbeam_core::tool::RegionSelectMode, /// Lasso select sub-mode (Freehand / Polygonal / Magnetic) pub lasso_mode: &'a mut lightningbeam_core::tool::LassoMode, /// Counter for in-flight graph preset loads — increment when sending a /// GraphLoadPreset command so the repaint loop stays alive until the /// audio thread sends GraphPresetLoaded back pub pending_graph_loads: &'a std::sync::Arc, /// Set by panes (e.g. piano roll) when they handle Ctrl+C/X/V internally, /// so main.rs skips its own clipboard handling for the current frame pub clipboard_consumed: &'a mut bool, /// Remappable keyboard shortcut manager pub keymap: &'a crate::keymap::KeymapManager, /// Set by MenuAction::Group when focus is Nodes — consumed by node graph pane pub pending_node_group: &'a mut bool, /// Set by MenuAction::Group (ungroup variant) when focus is Nodes — consumed by node graph pane pub pending_node_ungroup: &'a mut bool, /// Test mode state for event recording (debug builds only) #[cfg(debug_assertions)] pub test_mode: &'a mut crate::test_mode::TestModeState, /// Synthetic input from test mode replay (debug builds only) #[cfg(debug_assertions)] pub synthetic_input: &'a mut Option, /// GPU-rendered brush preview thumbnails. Populated by `VelloCallback::prepare()` /// on the first frame; panes (e.g. infopanel) convert the pixel data to egui /// TextureHandles. Each entry is `(width, height, sRGB-premultiplied RGBA bytes)`. pub brush_preview_pixels: &'a std::sync::Arc)>>>, /// True when rendering the phone/mobile shell (panes can render more compactly). pub is_mobile: bool, /// Device orientation for the mobile shell: portrait (tall) vs landscape (wide). Defaults to /// `true`; the mobile shell sets it from the available rect. Panes that reflow (e.g. the Piano /// Roll's vertical vs conventional layout) key off this. pub is_portrait: bool, /// Shared keyboard octave offset (C4-relative), so the mobile Virtual Piano and the portrait /// Piano Roll agree on which keys are visible and stay column-aligned. pub keyboard_octave: &'a mut i8, /// Shared horizontal keyboard pan (px) for smooth left/right scroll of the mobile keyboard+roll. pub keyboard_pan_x: &'a mut f32, /// Whether the mobile instrument pane should show the falling-notes roll above the keys. Driven /// by the shell from the *snapped* pane size-class so the reveal happens at a stack snap point. pub instrument_show_roll: bool, /// Set by the mobile instrument header's "Presets" button; the shell opens the Preset Browser. pub open_instrument_browser: &'a mut bool, /// Set by the mobile instrument header's REC button; the Timeline pane picks it up and toggles /// recording (reusing its full count-in / clip-creation flow). pub pending_record_toggle: &'a mut bool, /// Mobile long-press context menu request. A pane sets this on `response.secondary_clicked()` /// (which fires on long-press) with the items relevant to what was pressed; the mobile shell /// renders one persistent popup and dispatches the chosen `MenuAction`. `None` = no menu. pub mobile_context_menu: &'a mut Option, } /// A mobile long-press context menu: a screen position and a list of `(label, action)` items. /// Rendered by the mobile shell; each item dispatches its `MenuAction` via `pending_menu_actions`. #[derive(Clone)] pub struct MobileContextMenu { pub pos: egui::Pos2, pub items: Vec<(String, crate::menu::MenuAction)>, } /// 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), ScriptEditor(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::ScriptEditor => { PaneInstance::ScriptEditor(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::ScriptEditor(_) => PaneType::ScriptEditor, } } } 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::ScriptEditor(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::ScriptEditor(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::ScriptEditor(p) => p.name(), } } }