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"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui-wgpu",
|
||||
"egui_extras",
|
||||
"image",
|
||||
"kurbo 0.11.3",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ members = [
|
|||
# UI Framework (using eframe for simplified integration)
|
||||
eframe = { version = "0.29", default-features = true, features = ["wgpu"] }
|
||||
egui_extras = { version = "0.29", features = ["image", "svg"] }
|
||||
egui-wgpu = "0.29"
|
||||
|
||||
# GPU Rendering
|
||||
vello = "0.3"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ lightningbeam-core = { path = "../lightningbeam-core" }
|
|||
# UI Framework
|
||||
eframe = { workspace = true }
|
||||
egui_extras = { workspace = true }
|
||||
egui-wgpu = { workspace = true }
|
||||
|
||||
# GPU
|
||||
wgpu = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ struct EditorApp {
|
|||
stroke_color: egui::Color32, // Stroke color for drawing
|
||||
pane_instances: HashMap<NodePath, PaneInstance>, // Pane instances per path
|
||||
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 {
|
||||
|
|
@ -214,6 +215,7 @@ impl EditorApp {
|
|||
stroke_color: egui::Color32::from_rgb(0, 0, 0), // Default black stroke
|
||||
pane_instances: HashMap::new(), // Initialize empty, panes created on-demand
|
||||
menu_system,
|
||||
pending_view_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,20 +414,16 @@ impl EditorApp {
|
|||
|
||||
// View menu
|
||||
MenuAction::ZoomIn => {
|
||||
println!("Menu: Zoom In");
|
||||
// TODO: Implement zoom in
|
||||
self.pending_view_action = Some(MenuAction::ZoomIn);
|
||||
}
|
||||
MenuAction::ZoomOut => {
|
||||
println!("Menu: Zoom Out");
|
||||
// TODO: Implement zoom out
|
||||
self.pending_view_action = Some(MenuAction::ZoomOut);
|
||||
}
|
||||
MenuAction::ActualSize => {
|
||||
println!("Menu: Actual Size");
|
||||
// TODO: Implement actual size (100% zoom)
|
||||
self.pending_view_action = Some(MenuAction::ActualSize);
|
||||
}
|
||||
MenuAction::RecenterView => {
|
||||
println!("Menu: Recenter View");
|
||||
// TODO: Implement recenter view
|
||||
self.pending_view_action = Some(MenuAction::RecenterView);
|
||||
}
|
||||
MenuAction::NextLayout => {
|
||||
println!("Menu: Next Layout");
|
||||
|
|
@ -469,6 +467,12 @@ impl EditorApp {
|
|||
|
||||
impl eframe::App for EditorApp {
|
||||
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)
|
||||
if let Some(menu_system) = &self.menu_system {
|
||||
if let Some(action) = menu_system.check_events() {
|
||||
|
|
@ -498,6 +502,12 @@ impl eframe::App for EditorApp {
|
|||
// Reset hovered divider each frame
|
||||
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(
|
||||
ui,
|
||||
&mut self.current_layout,
|
||||
|
|
@ -514,8 +524,28 @@ impl eframe::App for EditorApp {
|
|||
&mut self.stroke_color,
|
||||
&mut self.pane_instances,
|
||||
&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
|
||||
if let Some((_, is_horizontal)) = self.hovered_divider {
|
||||
if is_horizontal {
|
||||
|
|
@ -550,6 +580,7 @@ impl eframe::App for EditorApp {
|
|||
self.apply_layout_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Recursively render a layout node with drag support
|
||||
|
|
@ -569,10 +600,13 @@ fn render_layout_node(
|
|||
stroke_color: &mut egui::Color32,
|
||||
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
||||
path: &NodePath,
|
||||
pending_view_action: &mut Option<MenuAction>,
|
||||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
) {
|
||||
match node {
|
||||
LayoutNode::Pane { name } => {
|
||||
render_pane(ui, name, rect, selected_pane, layout_action, split_preview_mode, icon_cache, tool_icon_cache, selected_tool, fill_color, stroke_color, 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 } => {
|
||||
// Handle dragging
|
||||
|
|
@ -612,6 +646,9 @@ fn render_layout_node(
|
|||
stroke_color,
|
||||
pane_instances,
|
||||
&left_path,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
);
|
||||
|
||||
let mut right_path = path.clone();
|
||||
|
|
@ -632,6 +669,9 @@ fn render_layout_node(
|
|||
stroke_color,
|
||||
pane_instances,
|
||||
&right_path,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -744,6 +784,9 @@ fn render_layout_node(
|
|||
stroke_color,
|
||||
pane_instances,
|
||||
&top_path,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
);
|
||||
|
||||
let mut bottom_path = path.clone();
|
||||
|
|
@ -764,6 +807,9 @@ fn render_layout_node(
|
|||
stroke_color,
|
||||
pane_instances,
|
||||
&bottom_path,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
);
|
||||
|
||||
// Draw divider with interaction
|
||||
|
|
@ -856,6 +902,9 @@ fn render_pane(
|
|||
stroke_color: &mut egui::Color32,
|
||||
pane_instances: &mut HashMap<NodePath, PaneInstance>,
|
||||
path: &NodePath,
|
||||
pending_view_action: &mut Option<MenuAction>,
|
||||
fallback_pane_priority: &mut Option<u32>,
|
||||
pending_handlers: &mut Vec<panes::ViewActionHandler>,
|
||||
) {
|
||||
let pane_type = PaneType::from_name(pane_name);
|
||||
|
||||
|
|
@ -1115,6 +1164,9 @@ fn render_pane(
|
|||
selected_tool,
|
||||
fill_color,
|
||||
stroke_color,
|
||||
pending_view_action,
|
||||
fallback_pane_priority,
|
||||
pending_handlers,
|
||||
};
|
||||
|
||||
// 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)
|
||||
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 stage;
|
||||
pub mod timeline;
|
||||
|
|
@ -25,6 +34,14 @@ pub struct SharedPaneState<'a> {
|
|||
pub selected_tool: &'a mut Tool,
|
||||
pub fill_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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// For now, it's a placeholder.
|
||||
/// Renders composited layers using Vello GPU renderer via egui callbacks.
|
||||
|
||||
use eframe::egui;
|
||||
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 {
|
||||
// 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 {
|
||||
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,
|
||||
rect: egui::Rect,
|
||||
_path: &NodePath,
|
||||
_shared: &mut SharedPaneState,
|
||||
shared: &mut SharedPaneState,
|
||||
) {
|
||||
// Placeholder rendering
|
||||
ui.painter().rect_filled(
|
||||
// Handle input for pan/zoom controls
|
||||
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,
|
||||
0.0,
|
||||
egui::Color32::from_rgb(30, 40, 50),
|
||||
callback,
|
||||
);
|
||||
|
||||
let text = "Stage Pane\n(TODO: Implement Vello rendering)";
|
||||
ui.painter().add(cb);
|
||||
|
||||
// Show camera info overlay
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
egui::FontId::proportional(16.0),
|
||||
egui::Color32::from_gray(150),
|
||||
rect.min + egui::vec2(10.0, 10.0),
|
||||
egui::Align2::LEFT_TOP,
|
||||
format!("Vello Stage (zoom: {:.2}, pan: {:.0},{:.0})",
|
||||
self.zoom, self.pan_offset.x, self.pan_offset.y),
|
||||
egui::FontId::proportional(14.0),
|
||||
egui::Color32::from_gray(200),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue