Add stage pane with scrolling
This commit is contained in:
parent
652b9e6cbb
commit
08232454a7
|
|
@ -2447,6 +2447,7 @@ name = "lightningbeam-editor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
|
"egui-wgpu",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
"image",
|
"image",
|
||||||
"kurbo 0.11.3",
|
"kurbo 0.11.3",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ members = [
|
||||||
# UI Framework (using eframe for simplified integration)
|
# UI Framework (using eframe for simplified integration)
|
||||||
eframe = { version = "0.29", default-features = true, features = ["wgpu"] }
|
eframe = { version = "0.29", default-features = true, features = ["wgpu"] }
|
||||||
egui_extras = { version = "0.29", features = ["image", "svg"] }
|
egui_extras = { version = "0.29", features = ["image", "svg"] }
|
||||||
|
egui-wgpu = "0.29"
|
||||||
|
|
||||||
# GPU Rendering
|
# GPU Rendering
|
||||||
vello = "0.3"
|
vello = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ lightningbeam-core = { path = "../lightningbeam-core" }
|
||||||
# UI Framework
|
# UI Framework
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
egui_extras = { workspace = true }
|
egui_extras = { workspace = true }
|
||||||
|
egui-wgpu = { workspace = true }
|
||||||
|
|
||||||
# GPU
|
# GPU
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ struct EditorApp {
|
||||||
stroke_color: egui::Color32, // Stroke color for drawing
|
stroke_color: egui::Color32, // Stroke color for drawing
|
||||||
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
||||||
menu_system: Option<MenuSystem>, // Native menu system for event checking
|
menu_system: Option<MenuSystem>, // Native menu system for event checking
|
||||||
|
pending_view_action: Option<MenuAction>, // Pending view action (zoom, recenter) to be handled by hovered pane
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorApp {
|
impl EditorApp {
|
||||||
|
|
@ -214,6 +215,7 @@ impl EditorApp {
|
||||||
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
||||||
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
||||||
menu_system,
|
menu_system,
|
||||||
|
pending_view_action: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,20 +414,16 @@ impl EditorApp {
|
||||||
|
|
||||||
// View menu
|
// View menu
|
||||||
MenuAction::ZoomIn => {
|
MenuAction::ZoomIn => {
|
||||||
println!("Menu: Zoom In");
|
self.pending_view_action = Some(MenuAction::ZoomIn);
|
||||||
// TODO: Implement zoom in
|
|
||||||
}
|
}
|
||||||
MenuAction::ZoomOut => {
|
MenuAction::ZoomOut => {
|
||||||
println!("Menu: Zoom Out");
|
self.pending_view_action = Some(MenuAction::ZoomOut);
|
||||||
// TODO: Implement zoom out
|
|
||||||
}
|
}
|
||||||
MenuAction::ActualSize => {
|
MenuAction::ActualSize => {
|
||||||
println!("Menu: Actual Size");
|
self.pending_view_action = Some(MenuAction::ActualSize);
|
||||||
// TODO: Implement actual size (100% zoom)
|
|
||||||
}
|
}
|
||||||
MenuAction::RecenterView => {
|
MenuAction::RecenterView => {
|
||||||
println!("Menu: Recenter View");
|
self.pending_view_action = Some(MenuAction::RecenterView);
|
||||||
// TODO: Implement recenter view
|
|
||||||
}
|
}
|
||||||
MenuAction::NextLayout => {
|
MenuAction::NextLayout => {
|
||||||
println!("Menu: Next Layout");
|
println!("Menu: Next Layout");
|
||||||
|
|
@ -469,6 +467,12 @@ impl EditorApp {
|
||||||
|
|
||||||
impl eframe::App for EditorApp {
|
impl eframe::App for EditorApp {
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// Disable egui's built-in Ctrl+Plus/Minus zoom behavior
|
||||||
|
// We handle zoom ourselves for the Stage pane
|
||||||
|
ctx.options_mut(|o| {
|
||||||
|
o.zoom_with_keyboard = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Check for native menu events (macOS)
|
// Check for native menu events (macOS)
|
||||||
if let Some(menu_system) = &self.menu_system {
|
if let Some(menu_system) = &self.menu_system {
|
||||||
if let Some(action) = menu_system.check_events() {
|
if let Some(action) = menu_system.check_events() {
|
||||||
|
|
@ -498,6 +502,12 @@ impl eframe::App for EditorApp {
|
||||||
// Reset hovered divider each frame
|
// Reset hovered divider each frame
|
||||||
self.hovered_divider = None;
|
self.hovered_divider = None;
|
||||||
|
|
||||||
|
// Track fallback pane priority for view actions (reset each frame)
|
||||||
|
let mut fallback_pane_priority: Option<u32> = None;
|
||||||
|
|
||||||
|
// Registry for view action handlers (two-phase dispatch)
|
||||||
|
let mut pending_handlers: Vec<panes::ViewActionHandler> = Vec::new();
|
||||||
|
|
||||||
render_layout_node(
|
render_layout_node(
|
||||||
ui,
|
ui,
|
||||||
&mut self.current_layout,
|
&mut self.current_layout,
|
||||||
|
|
@ -514,8 +524,28 @@ impl eframe::App for EditorApp {
|
||||||
&mut self.stroke_color,
|
&mut self.stroke_color,
|
||||||
&mut self.pane_instances,
|
&mut self.pane_instances,
|
||||||
&Vec::new(), // Root path
|
&Vec::new(), // Root path
|
||||||
|
&mut self.pending_view_action,
|
||||||
|
&mut fallback_pane_priority,
|
||||||
|
&mut pending_handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Execute action on the best handler (two-phase dispatch)
|
||||||
|
if let Some(action) = &self.pending_view_action {
|
||||||
|
if let Some(best_handler) = pending_handlers.iter().min_by_key(|h| h.priority) {
|
||||||
|
// Look up the pane instance and execute the action
|
||||||
|
if let Some(pane_instance) = self.pane_instances.get_mut(&best_handler.pane_path) {
|
||||||
|
match pane_instance {
|
||||||
|
panes::PaneInstance::Stage(stage_pane) => {
|
||||||
|
stage_pane.execute_view_action(action, best_handler.zoom_center);
|
||||||
|
}
|
||||||
|
_ => {} // Other pane types don't handle view actions yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the pending action after execution
|
||||||
|
self.pending_view_action = None;
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
@ -550,6 +580,7 @@ impl eframe::App for EditorApp {
|
||||||
self.apply_layout_action(action);
|
self.apply_layout_action(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively render a layout node with drag support
|
/// Recursively render a layout node with drag support
|
||||||
|
|
@ -569,10 +600,13 @@ fn render_layout_node(
|
||||||
stroke_color: &mut egui::Color32,
|
stroke_color: &mut egui::Color32,
|
||||||
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
||||||
path: &NodePath,
|
path: &NodePath,
|
||||||
|
pending_view_action: &mut Option<MenuAction>,
|
||||||
|
fallback_pane_priority: &mut Option<u32>,
|
||||||
|
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||||
) {
|
) {
|
||||||
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, pane_instances, path);
|
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, pane_instances, path, pending_view_action, fallback_pane_priority, pending_handlers);
|
||||||
}
|
}
|
||||||
LayoutNode::HorizontalGrid { percent, children } => {
|
LayoutNode::HorizontalGrid { percent, children } => {
|
||||||
// Handle dragging
|
// Handle dragging
|
||||||
|
|
@ -612,6 +646,9 @@ fn render_layout_node(
|
||||||
stroke_color,
|
stroke_color,
|
||||||
pane_instances,
|
pane_instances,
|
||||||
&left_path,
|
&left_path,
|
||||||
|
pending_view_action,
|
||||||
|
fallback_pane_priority,
|
||||||
|
pending_handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut right_path = path.clone();
|
let mut right_path = path.clone();
|
||||||
|
|
@ -632,6 +669,9 @@ fn render_layout_node(
|
||||||
stroke_color,
|
stroke_color,
|
||||||
pane_instances,
|
pane_instances,
|
||||||
&right_path,
|
&right_path,
|
||||||
|
pending_view_action,
|
||||||
|
fallback_pane_priority,
|
||||||
|
pending_handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw divider with interaction
|
// Draw divider with interaction
|
||||||
|
|
@ -744,6 +784,9 @@ fn render_layout_node(
|
||||||
stroke_color,
|
stroke_color,
|
||||||
pane_instances,
|
pane_instances,
|
||||||
&top_path,
|
&top_path,
|
||||||
|
pending_view_action,
|
||||||
|
fallback_pane_priority,
|
||||||
|
pending_handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut bottom_path = path.clone();
|
let mut bottom_path = path.clone();
|
||||||
|
|
@ -764,6 +807,9 @@ fn render_layout_node(
|
||||||
stroke_color,
|
stroke_color,
|
||||||
pane_instances,
|
pane_instances,
|
||||||
&bottom_path,
|
&bottom_path,
|
||||||
|
pending_view_action,
|
||||||
|
fallback_pane_priority,
|
||||||
|
pending_handlers,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw divider with interaction
|
// Draw divider with interaction
|
||||||
|
|
@ -856,6 +902,9 @@ fn render_pane(
|
||||||
stroke_color: &mut egui::Color32,
|
stroke_color: &mut egui::Color32,
|
||||||
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
||||||
path: &NodePath,
|
path: &NodePath,
|
||||||
|
pending_view_action: &mut Option<MenuAction>,
|
||||||
|
fallback_pane_priority: &mut Option<u32>,
|
||||||
|
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||||
) {
|
) {
|
||||||
let pane_type = PaneType::from_name(pane_name);
|
let pane_type = PaneType::from_name(pane_name);
|
||||||
|
|
||||||
|
|
@ -1115,6 +1164,9 @@ fn render_pane(
|
||||||
selected_tool,
|
selected_tool,
|
||||||
fill_color,
|
fill_color,
|
||||||
stroke_color,
|
stroke_color,
|
||||||
|
pending_view_action,
|
||||||
|
fallback_pane_priority,
|
||||||
|
pending_handlers,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render pane header (if it has one)
|
// Render pane header (if it has one)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,15 @@ use lightningbeam_core::{pane::PaneType, tool::Tool};
|
||||||
// Type alias for node paths (matches main.rs)
|
// Type alias for node paths (matches main.rs)
|
||||||
pub type NodePath = Vec<usize>;
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
pub mod toolbar;
|
pub mod toolbar;
|
||||||
pub mod stage;
|
pub mod stage;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
|
@ -25,6 +34,14 @@ pub struct SharedPaneState<'a> {
|
||||||
pub selected_tool: &'a mut Tool,
|
pub selected_tool: &'a mut Tool,
|
||||||
pub fill_color: &'a mut egui::Color32,
|
pub fill_color: &'a mut egui::Color32,
|
||||||
pub stroke_color: &'a mut egui::Color32,
|
pub stroke_color: &'a mut egui::Color32,
|
||||||
|
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>,
|
||||||
|
/// Registry of handlers for the current pending action
|
||||||
|
/// Panes register themselves here during render, execution happens after
|
||||||
|
pub pending_handlers: &'a mut Vec<ViewActionHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for pane rendering
|
/// Trait for pane rendering
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Simple fullscreen blit shader for rendering Vello texture to screen
|
||||||
|
|
||||||
|
@group(0) @binding(0) var tex: texture_2d<f32>;
|
||||||
|
@group(0) @binding(1) var tex_sampler: sampler;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vertex shader - generates fullscreen triangle strip
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
// Triangle strip covering the screen
|
||||||
|
// Vertices: (0,0), (2,0), (0,2), (2,2) in UV space
|
||||||
|
let x = f32((vertex_index & 1u) << 1u);
|
||||||
|
let y = f32(vertex_index & 2u);
|
||||||
|
|
||||||
|
out.position = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
|
||||||
|
out.uv = vec2<f32>(x, y);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fragment shader - sample and display texture
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return textureSample(tex, tex_sampler, in.uv);
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,452 @@
|
||||||
/// Stage pane - main animation canvas
|
/// Stage pane - main animation canvas with Vello rendering
|
||||||
///
|
///
|
||||||
/// This will eventually render the composited layers using Vello.
|
/// Renders composited layers using Vello GPU renderer via egui callbacks.
|
||||||
/// For now, it's a placeholder.
|
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use super::{NodePath, PaneRenderer, SharedPaneState};
|
use super::{NodePath, PaneRenderer, SharedPaneState};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Resources for a single Vello instance
|
||||||
|
struct VelloResources {
|
||||||
|
renderer: Arc<Mutex<vello::Renderer>>,
|
||||||
|
texture: Option<wgpu::Texture>,
|
||||||
|
texture_view: Option<wgpu::TextureView>,
|
||||||
|
// Blit pipeline for rendering texture to screen
|
||||||
|
blit_pipeline: wgpu::RenderPipeline,
|
||||||
|
blit_bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
sampler: wgpu::Sampler,
|
||||||
|
blit_bind_group: Option<wgpu::BindGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container for all Vello instances, stored in egui's CallbackResources
|
||||||
|
pub struct VelloResourcesMap {
|
||||||
|
instances: std::collections::HashMap<u64, VelloResources>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VelloResources {
|
||||||
|
pub fn new(device: &wgpu::Device) -> Result<Self, String> {
|
||||||
|
let renderer = vello::Renderer::new(
|
||||||
|
device,
|
||||||
|
vello::RendererOptions {
|
||||||
|
surface_format: None,
|
||||||
|
use_cpu: false,
|
||||||
|
antialiasing_support: vello::AaSupport::all(),
|
||||||
|
num_init_threads: std::num::NonZeroUsize::new(1),
|
||||||
|
},
|
||||||
|
).map_err(|e| format!("Failed to create Vello renderer: {}", e))?;
|
||||||
|
|
||||||
|
// Create blit shader for rendering texture to screen
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("vello_blit_shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/blit.wgsl").into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group layout for texture + sampler
|
||||||
|
let blit_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("vello_blit_bind_group_layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create pipeline layout
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("vello_blit_pipeline_layout"),
|
||||||
|
bind_group_layouts: &[&blit_bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create render pipeline for blitting
|
||||||
|
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("vello_blit_pipeline"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: wgpu::TextureFormat::Rgba8Unorm, // egui's target format
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleStrip,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
unclipped_depth: false,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create sampler
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("vello_blit_sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("✅ Vello renderer and blit pipeline initialized");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
renderer: Arc::new(Mutex::new(renderer)),
|
||||||
|
texture: None,
|
||||||
|
texture_view: None,
|
||||||
|
blit_pipeline,
|
||||||
|
blit_bind_group_layout,
|
||||||
|
sampler,
|
||||||
|
blit_bind_group: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_texture(&mut self, device: &wgpu::Device, width: u32, height: u32) {
|
||||||
|
// Clamp to GPU limits (most GPUs support up to 8192)
|
||||||
|
let max_texture_size = 8192;
|
||||||
|
let width = width.min(max_texture_size);
|
||||||
|
let height = height.min(max_texture_size);
|
||||||
|
|
||||||
|
// Only recreate if size changed
|
||||||
|
if let Some(tex) = &self.texture {
|
||||||
|
if tex.width() == width && tex.height() == height {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("vello_output"),
|
||||||
|
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||||
|
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
// Create bind group for blit pipeline
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("vello_blit_bind_group"),
|
||||||
|
layout: &self.blit_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
self.texture = Some(texture);
|
||||||
|
self.texture_view = Some(texture_view);
|
||||||
|
self.blit_bind_group = Some(bind_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback for Vello rendering within egui
|
||||||
|
struct VelloCallback {
|
||||||
|
rect: egui::Rect,
|
||||||
|
pan_offset: egui::Vec2,
|
||||||
|
zoom: f32,
|
||||||
|
instance_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VelloCallback {
|
||||||
|
fn new(rect: egui::Rect, pan_offset: egui::Vec2, zoom: f32, instance_id: u64) -> Self {
|
||||||
|
Self { rect, pan_offset, zoom, instance_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
|
fn prepare(
|
||||||
|
&self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
queue: &wgpu::Queue,
|
||||||
|
_screen_descriptor: &egui_wgpu::ScreenDescriptor,
|
||||||
|
_egui_encoder: &mut wgpu::CommandEncoder,
|
||||||
|
resources: &mut egui_wgpu::CallbackResources,
|
||||||
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
|
// Get or create the resources map
|
||||||
|
if !resources.contains::<VelloResourcesMap>() {
|
||||||
|
resources.insert(VelloResourcesMap {
|
||||||
|
instances: std::collections::HashMap::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let map: &mut VelloResourcesMap = resources.get_mut().unwrap();
|
||||||
|
|
||||||
|
// Get or create resources for this specific instance
|
||||||
|
let vello_resources = map.instances.entry(self.instance_id).or_insert_with(|| {
|
||||||
|
VelloResources::new(device).expect("Failed to initialize Vello renderer")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure texture is the right size
|
||||||
|
let width = self.rect.width() as u32;
|
||||||
|
let height = self.rect.height() as u32;
|
||||||
|
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
vello_resources.ensure_texture(device, width, height);
|
||||||
|
|
||||||
|
// Build Vello scene with a test rectangle
|
||||||
|
let mut scene = vello::Scene::new();
|
||||||
|
|
||||||
|
// Draw a colored rectangle as proof of concept
|
||||||
|
use vello::kurbo::{RoundedRect, Affine};
|
||||||
|
use vello::peniko::Color;
|
||||||
|
|
||||||
|
let rect = RoundedRect::new(
|
||||||
|
100.0, 100.0,
|
||||||
|
400.0, 300.0,
|
||||||
|
10.0, // corner radius
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply camera transform: translate for pan, scale for zoom
|
||||||
|
let transform = Affine::translate((self.pan_offset.x as f64, self.pan_offset.y as f64))
|
||||||
|
* Affine::scale(self.zoom as f64);
|
||||||
|
|
||||||
|
scene.fill(
|
||||||
|
vello::peniko::Fill::NonZero,
|
||||||
|
transform,
|
||||||
|
Color::rgb8(100, 150, 250),
|
||||||
|
None,
|
||||||
|
&rect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render scene to texture
|
||||||
|
if let Some(texture_view) = &vello_resources.texture_view {
|
||||||
|
let render_params = vello::RenderParams {
|
||||||
|
base_color: vello::peniko::Color::rgb8(45, 45, 48), // Dark background
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
antialiasing_method: vello::AaConfig::Msaa16,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(mut renderer) = vello_resources.renderer.lock() {
|
||||||
|
renderer
|
||||||
|
.render_to_texture(device, queue, &scene, texture_view, &render_params)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(
|
||||||
|
&self,
|
||||||
|
_info: egui::PaintCallbackInfo,
|
||||||
|
render_pass: &mut wgpu::RenderPass<'static>,
|
||||||
|
resources: &egui_wgpu::CallbackResources,
|
||||||
|
) {
|
||||||
|
// Get Vello resources map
|
||||||
|
let map: &VelloResourcesMap = match resources.get() {
|
||||||
|
Some(m) => m,
|
||||||
|
None => return, // Resources not initialized yet
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get resources for this specific instance
|
||||||
|
let vello_resources = match map.instances.get(&self.instance_id) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return, // Instance not initialized yet
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if we have a bind group (texture ready)
|
||||||
|
let bind_group = match &vello_resources.blit_bind_group {
|
||||||
|
Some(bg) => bg,
|
||||||
|
None => return, // Texture not ready yet
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render fullscreen quad with our texture
|
||||||
|
render_pass.set_pipeline(&vello_resources.blit_pipeline);
|
||||||
|
render_pass.set_bind_group(0, bind_group, &[]);
|
||||||
|
render_pass.draw(0..4, 0..1); // Triangle strip: 4 vertices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StagePane {
|
pub struct StagePane {
|
||||||
// TODO: Add state for camera, selection, etc.
|
// Camera state
|
||||||
|
pan_offset: egui::Vec2,
|
||||||
|
zoom: f32,
|
||||||
|
// Interaction state
|
||||||
|
is_panning: bool,
|
||||||
|
last_pan_pos: Option<egui::Pos2>,
|
||||||
|
// Unique ID for this stage instance (for Vello resources)
|
||||||
|
instance_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global counter for generating unique instance IDs
|
||||||
|
static INSTANCE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||||
|
|
||||||
impl StagePane {
|
impl StagePane {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {}
|
let instance_id = INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
Self {
|
||||||
|
pan_offset: egui::Vec2::ZERO,
|
||||||
|
zoom: 1.0,
|
||||||
|
is_panning: false,
|
||||||
|
last_pan_pos: None,
|
||||||
|
instance_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a view action with the given parameters
|
||||||
|
/// Called from main.rs after determining this is the best handler
|
||||||
|
pub fn execute_view_action(&mut self, action: &crate::menu::MenuAction, zoom_center: egui::Vec2) {
|
||||||
|
use crate::menu::MenuAction;
|
||||||
|
match action {
|
||||||
|
MenuAction::ZoomIn => self.zoom_in(zoom_center),
|
||||||
|
MenuAction::ZoomOut => self.zoom_out(zoom_center),
|
||||||
|
MenuAction::ActualSize => self.actual_size(),
|
||||||
|
MenuAction::RecenterView => self.recenter(),
|
||||||
|
_ => {} // Not a view action we handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zoom in by a fixed increment (to center of viewport)
|
||||||
|
pub fn zoom_in(&mut self, center: egui::Vec2) {
|
||||||
|
self.apply_zoom_at_point(0.2, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zoom out by a fixed increment (to center of viewport)
|
||||||
|
pub fn zoom_out(&mut self, center: egui::Vec2) {
|
||||||
|
self.apply_zoom_at_point(-0.2, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset zoom to 100% (1.0)
|
||||||
|
pub fn actual_size(&mut self) {
|
||||||
|
self.zoom = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset pan to center (0,0) and zoom to 100%
|
||||||
|
pub fn recenter(&mut self) {
|
||||||
|
self.pan_offset = egui::Vec2::ZERO;
|
||||||
|
self.zoom = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply zoom while keeping the point under the mouse cursor stationary
|
||||||
|
fn apply_zoom_at_point(&mut self, zoom_delta: f32, mouse_canvas_pos: egui::Vec2) {
|
||||||
|
let old_zoom = self.zoom;
|
||||||
|
|
||||||
|
// Calculate world position under mouse before zoom
|
||||||
|
let world_pos = (mouse_canvas_pos - self.pan_offset) / old_zoom;
|
||||||
|
|
||||||
|
// Apply zoom
|
||||||
|
let new_zoom = (old_zoom * (1.0 + zoom_delta)).clamp(0.1, 10.0);
|
||||||
|
self.zoom = new_zoom;
|
||||||
|
|
||||||
|
// Adjust pan so the same world point stays under the mouse
|
||||||
|
self.pan_offset = mouse_canvas_pos - (world_pos * new_zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_input(&mut self, ui: &mut egui::Ui, rect: egui::Rect) {
|
||||||
|
let response = ui.allocate_rect(rect, egui::Sense::click_and_drag());
|
||||||
|
|
||||||
|
// Only process input if mouse is over the stage pane
|
||||||
|
if !response.hovered() {
|
||||||
|
self.is_panning = false;
|
||||||
|
self.last_pan_pos = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
|
||||||
|
let alt_held = ui.input(|i| i.modifiers.alt);
|
||||||
|
let ctrl_held = ui.input(|i| i.modifiers.ctrl || i.modifiers.command);
|
||||||
|
|
||||||
|
// Get mouse position for zoom-to-cursor
|
||||||
|
let mouse_pos = response.hover_pos().unwrap_or(rect.center());
|
||||||
|
let mouse_canvas_pos = mouse_pos - rect.min;
|
||||||
|
|
||||||
|
// Distinguish between mouse wheel (discrete) and trackpad (smooth)
|
||||||
|
let mut handled = false;
|
||||||
|
ui.input(|i| {
|
||||||
|
for event in &i.raw.events {
|
||||||
|
if let egui::Event::MouseWheel { unit, delta, modifiers, .. } = event {
|
||||||
|
match unit {
|
||||||
|
egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => {
|
||||||
|
// Real mouse wheel (discrete clicks) -> always zoom
|
||||||
|
let zoom_delta = if ctrl_held || modifiers.ctrl {
|
||||||
|
delta.y * 0.01 // Ctrl+wheel: faster zoom
|
||||||
|
} else {
|
||||||
|
delta.y * 0.005 // Normal zoom
|
||||||
|
};
|
||||||
|
self.apply_zoom_at_point(zoom_delta, mouse_canvas_pos);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
egui::MouseWheelUnit::Point => {
|
||||||
|
// Trackpad (smooth scrolling) -> only zoom if Ctrl held
|
||||||
|
if ctrl_held || modifiers.ctrl {
|
||||||
|
let zoom_delta = delta.y * 0.005;
|
||||||
|
self.apply_zoom_at_point(zoom_delta, mouse_canvas_pos);
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
// Otherwise let scroll_delta handle panning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle scroll_delta for trackpad panning (when Ctrl not held)
|
||||||
|
if !handled && (scroll_delta.x.abs() > 0.0 || scroll_delta.y.abs() > 0.0) {
|
||||||
|
self.pan_offset.x += scroll_delta.x;
|
||||||
|
self.pan_offset.y += scroll_delta.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle panning with Alt+Drag
|
||||||
|
if alt_held && response.dragged() {
|
||||||
|
// Alt+Click+Drag panning
|
||||||
|
if let Some(last_pos) = self.last_pan_pos {
|
||||||
|
if let Some(current_pos) = response.interact_pointer_pos() {
|
||||||
|
let delta = current_pos - last_pos;
|
||||||
|
self.pan_offset += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_pan_pos = response.interact_pointer_pos();
|
||||||
|
self.is_panning = true;
|
||||||
|
} else {
|
||||||
|
if !response.dragged() {
|
||||||
|
self.is_panning = false;
|
||||||
|
self.last_pan_pos = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,22 +456,83 @@ impl PaneRenderer for StagePane {
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
rect: egui::Rect,
|
rect: egui::Rect,
|
||||||
_path: &NodePath,
|
_path: &NodePath,
|
||||||
_shared: &mut SharedPaneState,
|
shared: &mut SharedPaneState,
|
||||||
) {
|
) {
|
||||||
// Placeholder rendering
|
// Handle input for pan/zoom controls
|
||||||
ui.painter().rect_filled(
|
self.handle_input(ui, rect);
|
||||||
|
|
||||||
|
// Register handler for pending view actions (two-phase dispatch)
|
||||||
|
// Priority: Mouse-over (0-99) > Fallback Stage(1000) > Fallback Timeline(1001) etc.
|
||||||
|
const STAGE_MOUSE_OVER_PRIORITY: u32 = 0;
|
||||||
|
const STAGE_FALLBACK_PRIORITY: u32 = 1000;
|
||||||
|
|
||||||
|
let mouse_over = ui.rect_contains_pointer(rect);
|
||||||
|
|
||||||
|
// Determine our priority for this action
|
||||||
|
let our_priority = if mouse_over {
|
||||||
|
STAGE_MOUSE_OVER_PRIORITY // High priority - mouse is over this pane
|
||||||
|
} else {
|
||||||
|
STAGE_FALLBACK_PRIORITY // Low priority - just a fallback option
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if we should register as a handler (better priority than current best)
|
||||||
|
let should_register = shared.pending_view_action.is_some() &&
|
||||||
|
shared.fallback_pane_priority.map_or(true, |p| our_priority < p);
|
||||||
|
|
||||||
|
if should_register {
|
||||||
|
// Update fallback priority tracker
|
||||||
|
*shared.fallback_pane_priority = Some(our_priority);
|
||||||
|
|
||||||
|
// Register as a handler (don't execute yet - that happens after all panes render)
|
||||||
|
if let Some(action) = &shared.pending_view_action {
|
||||||
|
use crate::menu::MenuAction;
|
||||||
|
|
||||||
|
// Determine zoom center point
|
||||||
|
let center = if mouse_over {
|
||||||
|
// Use mouse position for zoom-to-cursor
|
||||||
|
let mouse_pos = ui.input(|i| i.pointer.hover_pos()).unwrap_or(rect.center());
|
||||||
|
mouse_pos - rect.min
|
||||||
|
} else {
|
||||||
|
// Use center of viewport for fallback
|
||||||
|
rect.size() / 2.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only register for actions we can handle
|
||||||
|
match action {
|
||||||
|
MenuAction::ZoomIn | MenuAction::ZoomOut |
|
||||||
|
MenuAction::ActualSize | MenuAction::RecenterView => {
|
||||||
|
shared.pending_handlers.push(super::ViewActionHandler {
|
||||||
|
priority: our_priority,
|
||||||
|
pane_path: _path.clone(),
|
||||||
|
zoom_center: center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Not a view action we handle - reset priority so others can try
|
||||||
|
*shared.fallback_pane_priority = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use egui's custom painting callback for Vello
|
||||||
|
let callback = VelloCallback::new(rect, self.pan_offset, self.zoom, self.instance_id);
|
||||||
|
|
||||||
|
let cb = egui_wgpu::Callback::new_paint_callback(
|
||||||
rect,
|
rect,
|
||||||
0.0,
|
callback,
|
||||||
egui::Color32::from_rgb(30, 40, 50),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let text = "Stage Pane\n(TODO: Implement Vello rendering)";
|
ui.painter().add(cb);
|
||||||
|
|
||||||
|
// Show camera info overlay
|
||||||
ui.painter().text(
|
ui.painter().text(
|
||||||
rect.center(),
|
rect.min + egui::vec2(10.0, 10.0),
|
||||||
egui::Align2::CENTER_CENTER,
|
egui::Align2::LEFT_TOP,
|
||||||
text,
|
format!("Vello Stage (zoom: {:.2}, pan: {:.0},{:.0})",
|
||||||
egui::FontId::proportional(16.0),
|
self.zoom, self.pan_offset.x, self.pan_offset.y),
|
||||||
egui::Color32::from_gray(150),
|
egui::FontId::proportional(14.0),
|
||||||
|
egui::Color32::from_gray(200),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue