improve folders a bit

This commit is contained in:
Skyler Lehmkuhl 2026-01-13 20:30:16 -05:00
parent b19f66e648
commit f4ffa7ecdd
5 changed files with 362 additions and 25 deletions

View File

@ -621,6 +621,8 @@ struct EditorApp {
debug_overlay_visible: bool, debug_overlay_visible: bool,
debug_stats_collector: debug_overlay::DebugStatsCollector, debug_stats_collector: debug_overlay::DebugStatsCollector,
gpu_info: Option<wgpu::AdapterInfo>, gpu_info: Option<wgpu::AdapterInfo>,
/// Surface texture format for GPU rendering (Rgba8Unorm or Bgra8Unorm depending on platform)
target_format: wgpu::TextureFormat,
} }
/// Import filter types for the file dialog /// Import filter types for the file dialog
@ -711,6 +713,11 @@ impl EditorApp {
// Extract GPU info for debug overlay // Extract GPU info for debug overlay
let gpu_info = cc.wgpu_render_state.as_ref().map(|rs| rs.adapter.get_info()); let gpu_info = cc.wgpu_render_state.as_ref().map(|rs| rs.adapter.get_info());
// Get surface format (defaults to Rgba8Unorm if render_state not available)
let target_format = cc.wgpu_render_state.as_ref()
.map(|rs| rs.target_format)
.unwrap_or(wgpu::TextureFormat::Rgba8Unorm);
Self { Self {
layouts, layouts,
current_layout_index: 0, current_layout_index: 0,
@ -780,6 +787,7 @@ impl EditorApp {
debug_overlay_visible: false, debug_overlay_visible: false,
debug_stats_collector: debug_overlay::DebugStatsCollector::new(), debug_stats_collector: debug_overlay::DebugStatsCollector::new(),
gpu_info, gpu_info,
target_format,
} }
} }
@ -3182,6 +3190,7 @@ impl eframe::App for EditorApp {
.map(|g| g.thumbnail_cache()) .map(|g| g.thumbnail_cache())
.unwrap_or(&empty_thumbnail_cache), .unwrap_or(&empty_thumbnail_cache),
effect_thumbnails_to_invalidate: &mut self.effect_thumbnails_to_invalidate, effect_thumbnails_to_invalidate: &mut self.effect_thumbnails_to_invalidate,
target_format: self.target_format,
}; };
render_layout_node( render_layout_node(
@ -3413,6 +3422,8 @@ struct RenderContext<'a> {
effect_thumbnail_cache: &'a HashMap<Uuid, Vec<u8>>, effect_thumbnail_cache: &'a HashMap<Uuid, Vec<u8>>,
/// Effect IDs whose thumbnails should be invalidated /// Effect IDs whose thumbnails should be invalidated
effect_thumbnails_to_invalidate: &'a mut Vec<Uuid>, effect_thumbnails_to_invalidate: &'a mut Vec<Uuid>,
/// Surface texture format for GPU rendering (Rgba8Unorm or Bgra8Unorm depending on platform)
target_format: wgpu::TextureFormat,
} }
/// Recursively render a layout node with drag support /// Recursively render a layout node with drag support
@ -3887,6 +3898,7 @@ fn render_pane(
effect_thumbnail_requests: ctx.effect_thumbnail_requests, effect_thumbnail_requests: ctx.effect_thumbnail_requests,
effect_thumbnail_cache: ctx.effect_thumbnail_cache, effect_thumbnail_cache: ctx.effect_thumbnail_cache,
effect_thumbnails_to_invalidate: ctx.effect_thumbnails_to_invalidate, effect_thumbnails_to_invalidate: ctx.effect_thumbnails_to_invalidate,
target_format: ctx.target_format,
}; };
pane_instance.render_header(&mut header_ui, &mut shared); pane_instance.render_header(&mut header_ui, &mut shared);
} }
@ -3950,6 +3962,7 @@ fn render_pane(
effect_thumbnail_requests: ctx.effect_thumbnail_requests, effect_thumbnail_requests: ctx.effect_thumbnail_requests,
effect_thumbnail_cache: ctx.effect_thumbnail_cache, effect_thumbnail_cache: ctx.effect_thumbnail_cache,
effect_thumbnails_to_invalidate: ctx.effect_thumbnails_to_invalidate, effect_thumbnails_to_invalidate: ctx.effect_thumbnails_to_invalidate,
target_format: ctx.target_format,
}; };
// Render pane content (header was already rendered above) // Render pane content (header was already rendered above)

View File

@ -661,6 +661,8 @@ pub struct AssetEntry {
pub extra_info: String, pub extra_info: String,
/// True for built-in effects from the registry (not editable/deletable) /// True for built-in effects from the registry (not editable/deletable)
pub is_builtin: bool, pub is_builtin: bool,
/// Folder this asset belongs to (None = root)
pub folder_id: Option<Uuid>,
} }
/// Folder entry for display /// Folder entry for display
@ -712,6 +714,14 @@ struct RenameState {
edit_text: String, edit_text: String,
} }
/// Inline folder rename editing state
#[derive(Debug, Clone)]
struct FolderRenameState {
folder_id: Uuid,
category: AssetCategory,
edit_text: String,
}
/// Context menu state with position /// Context menu state with position
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct ContextMenuState { struct ContextMenuState {
@ -719,6 +729,12 @@ struct ContextMenuState {
position: egui::Pos2, position: egui::Pos2,
} }
#[derive(Debug, Clone)]
struct FolderContextMenuState {
folder_id: Uuid,
position: egui::Pos2,
}
pub struct AssetLibraryPane { pub struct AssetLibraryPane {
/// Current search filter text /// Current search filter text
search_filter: String, search_filter: String,
@ -732,15 +748,21 @@ pub struct AssetLibraryPane {
/// Context menu state with position (for assets) /// Context menu state with position (for assets)
context_menu: Option<ContextMenuState>, context_menu: Option<ContextMenuState>,
/// Folder context menu state (for folders)
folder_context_menu: Option<FolderContextMenuState>,
/// Pane context menu position (for background right-click) /// Pane context menu position (for background right-click)
pane_context_menu: Option<egui::Pos2>, pane_context_menu: Option<egui::Pos2>,
/// Pending delete confirmation /// Pending delete confirmation
pending_delete: Option<PendingDelete>, pending_delete: Option<PendingDelete>,
/// Active rename state /// Active rename state (for assets)
rename_state: Option<RenameState>, rename_state: Option<RenameState>,
/// Active folder rename state
folder_rename_state: Option<FolderRenameState>,
/// Current view mode (list or grid) /// Current view mode (list or grid)
view_mode: AssetViewMode, view_mode: AssetViewMode,
@ -768,9 +790,11 @@ impl AssetLibraryPane {
selected_category: AssetCategory::All, selected_category: AssetCategory::All,
selected_asset: None, selected_asset: None,
context_menu: None, context_menu: None,
folder_context_menu: None,
pane_context_menu: None, pane_context_menu: None,
pending_delete: None, pending_delete: None,
rename_state: None, rename_state: None,
folder_rename_state: None,
view_mode: AssetViewMode::default(), view_mode: AssetViewMode::default(),
thumbnail_cache: ThumbnailCache::new(), thumbnail_cache: ThumbnailCache::new(),
current_folders: HashMap::new(), current_folders: HashMap::new(),
@ -853,6 +877,28 @@ impl AssetLibraryPane {
} }
} }
/// Convert DragClipType to core AssetCategory
fn drag_clip_type_to_core_category(clip_type: DragClipType) -> lightningbeam_core::document::AssetCategory {
match clip_type {
DragClipType::Vector => lightningbeam_core::document::AssetCategory::Vector,
DragClipType::Video => lightningbeam_core::document::AssetCategory::Video,
DragClipType::AudioSampled | DragClipType::AudioMidi => lightningbeam_core::document::AssetCategory::Audio,
DragClipType::Image => lightningbeam_core::document::AssetCategory::Images,
DragClipType::Effect => lightningbeam_core::document::AssetCategory::Effects,
}
}
/// Convert DragClipType to UI AssetCategory
fn drag_clip_type_to_category(clip_type: DragClipType) -> AssetCategory {
match clip_type {
DragClipType::Vector => AssetCategory::Vector,
DragClipType::Video => AssetCategory::Video,
DragClipType::AudioSampled | DragClipType::AudioMidi => AssetCategory::Audio,
DragClipType::Image => AssetCategory::Images,
DragClipType::Effect => AssetCategory::Effects,
}
}
/// Collect all assets from the document into a unified list /// Collect all assets from the document into a unified list
fn collect_assets(&self, document: &Document) -> Vec<AssetEntry> { fn collect_assets(&self, document: &Document) -> Vec<AssetEntry> {
let mut assets = Vec::new(); let mut assets = Vec::new();
@ -868,6 +914,7 @@ impl AssetLibraryPane {
dimensions: Some((clip.width, clip.height)), dimensions: Some((clip.width, clip.height)),
extra_info: format!("{}x{}", clip.width as u32, clip.height as u32), extra_info: format!("{}x{}", clip.width as u32, clip.height as u32),
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
}); });
} }
@ -882,6 +929,7 @@ impl AssetLibraryPane {
dimensions: Some((clip.width, clip.height)), dimensions: Some((clip.width, clip.height)),
extra_info: format!("{:.0}fps", clip.frame_rate), extra_info: format!("{:.0}fps", clip.frame_rate),
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
}); });
} }
@ -911,6 +959,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info, extra_info,
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
}); });
} }
@ -925,6 +974,7 @@ impl AssetLibraryPane {
dimensions: Some((asset.width as f64, asset.height as f64)), dimensions: Some((asset.width as f64, asset.height as f64)),
extra_info: format!("{}x{}", asset.width, asset.height), extra_info: format!("{}x{}", asset.width, asset.height),
is_builtin: false, is_builtin: false,
folder_id: asset.folder_id,
}); });
} }
@ -939,6 +989,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info: format!("{:?}", effect_def.category), extra_info: format!("{:?}", effect_def.category),
is_builtin: true, // Built-in from registry is_builtin: true, // Built-in from registry
folder_id: None, // Built-in effects are at root
}); });
} }
@ -960,6 +1011,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info: format!("{:?}", effect_def.category), extra_info: format!("{:?}", effect_def.category),
is_builtin: false, // User effect is_builtin: false, // User effect
folder_id: effect_def.folder_id,
}); });
} }
} }
@ -1057,6 +1109,7 @@ impl AssetLibraryPane {
dimensions: Some((clip.width, clip.height)), dimensions: Some((clip.width, clip.height)),
extra_info: format!("{}x{}", clip.width as u32, clip.height as u32), extra_info: format!("{}x{}", clip.width as u32, clip.height as u32),
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
})); }));
} }
} }
@ -1073,6 +1126,7 @@ impl AssetLibraryPane {
dimensions: Some((clip.width, clip.height)), dimensions: Some((clip.width, clip.height)),
extra_info: format!("{:.0}fps", clip.frame_rate), extra_info: format!("{:.0}fps", clip.frame_rate),
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
})); }));
} }
} }
@ -1105,6 +1159,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info, extra_info,
is_builtin: false, is_builtin: false,
folder_id: clip.folder_id,
})); }));
} }
} }
@ -1121,6 +1176,7 @@ impl AssetLibraryPane {
dimensions: Some((asset.width as f64, asset.height as f64)), dimensions: Some((asset.width as f64, asset.height as f64)),
extra_info: format!("{}x{}", asset.width, asset.height), extra_info: format!("{}x{}", asset.width, asset.height),
is_builtin: false, is_builtin: false,
folder_id: asset.folder_id,
})); }));
} }
} }
@ -1138,6 +1194,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info: format!("{:?}", effect_def.category), extra_info: format!("{:?}", effect_def.category),
is_builtin: true, is_builtin: true,
folder_id: None, // Built-in effects are always at root
})); }));
} }
} }
@ -1154,6 +1211,7 @@ impl AssetLibraryPane {
dimensions: None, dimensions: None,
extra_info: format!("{:?}", effect.category), extra_info: format!("{:?}", effect.category),
is_builtin: false, is_builtin: false,
folder_id: effect.folder_id,
})); }));
} }
} }
@ -1922,14 +1980,35 @@ impl AssetLibraryPane {
if viewport.intersects(item_rect) { if viewport.intersects(item_rect) {
let response = ui.allocate_rect(item_rect, egui::Sense::click()); let response = ui.allocate_rect(item_rect, egui::Sense::click());
// Check if an asset is being dragged and matches this folder's category
let is_valid_drop_target = shared.dragging_asset.as_ref().map(|drag| {
let drag_category = Self::drag_clip_type_to_category(drag.clip_type);
drag_category == self.selected_category
}).unwrap_or(false);
let is_drop_hover = is_valid_drop_target && response.hovered();
// Background // Background
let bg_color = if response.hovered() { let bg_color = if is_drop_hover {
// Highlight as drop target
egui::Color32::from_rgb(60, 100, 140)
} else if response.hovered() {
egui::Color32::from_rgb(50, 50, 50) egui::Color32::from_rgb(50, 50, 50)
} else { } else {
egui::Color32::from_rgb(35, 35, 35) egui::Color32::from_rgb(35, 35, 35)
}; };
ui.painter().rect_filled(item_rect, 0.0, bg_color); ui.painter().rect_filled(item_rect, 0.0, bg_color);
// Draw drop target indicator border
if is_drop_hover {
ui.painter().rect_stroke(
item_rect,
0.0,
egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 180, 255)),
egui::StrokeKind::Middle,
);
}
// Folder icon // Folder icon
if let Some(ref icon) = folder_icon { if let Some(ref icon) = folder_icon {
let icon_size = LIST_THUMBNAIL_SIZE; let icon_size = LIST_THUMBNAIL_SIZE;
@ -1945,14 +2024,33 @@ impl AssetLibraryPane {
); );
} }
// Folder name // Folder name (or inline edit field)
ui.painter().text( let is_renaming = self.folder_rename_state.as_ref().map(|s| s.folder_id == folder.id).unwrap_or(false);
item_rect.min + egui::vec2(LIST_THUMBNAIL_SIZE + 12.0, ITEM_HEIGHT / 2.0),
egui::Align2::LEFT_CENTER, if is_renaming {
&folder.name, // Inline rename text field
egui::FontId::proportional(13.0), let name_rect = egui::Rect::from_min_size(
egui::Color32::WHITE, item_rect.min + egui::vec2(LIST_THUMBNAIL_SIZE + 8.0, (ITEM_HEIGHT - 22.0) / 2.0),
); egui::vec2(200.0, 22.0),
);
if let Some(ref mut state) = self.folder_rename_state {
let mut child_ui = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
ImeTextField::new(&mut state.edit_text)
.font_size(13.0)
.desired_width(name_rect.width())
.request_focus()
.show(&mut child_ui);
}
} else {
ui.painter().text(
item_rect.min + egui::vec2(LIST_THUMBNAIL_SIZE + 12.0, ITEM_HEIGHT / 2.0),
egui::Align2::LEFT_CENTER,
&folder.name,
egui::FontId::proportional(13.0),
egui::Color32::WHITE,
);
}
// Item count // Item count
let count_text = format!("{} items", folder.item_count); let count_text = format!("{} items", folder.item_count);
@ -1964,10 +2062,32 @@ impl AssetLibraryPane {
egui::Color32::from_rgb(150, 150, 150), egui::Color32::from_rgb(150, 150, 150),
); );
// Handle drop: move asset to folder
if is_drop_hover && ui.input(|i| i.pointer.any_released()) {
if let Some(ref drag) = shared.dragging_asset.clone() {
let core_category = Self::drag_clip_type_to_core_category(drag.clip_type);
let action = lightningbeam_core::actions::MoveAssetToFolderAction::new(
core_category,
drag.clip_id,
Some(folder.id),
);
let _ = shared.action_executor.execute(Box::new(action));
*shared.dragging_asset = None;
}
}
// Handle double-click to navigate into folder // Handle double-click to navigate into folder
if response.double_clicked() { if response.double_clicked() {
self.set_current_folder(Some(folder.id)); self.set_current_folder(Some(folder.id));
} }
// Handle right-click for context menu
if response.secondary_clicked() {
self.folder_context_menu = Some(FolderContextMenuState {
folder_id: folder.id,
position: ui.ctx().pointer_interact_pos().unwrap_or(egui::pos2(0.0, 0.0)),
});
}
} else { } else {
ui.allocate_space(egui::vec2(rect.width(), ITEM_HEIGHT)); ui.allocate_space(egui::vec2(rect.width(), ITEM_HEIGHT));
} }
@ -2031,14 +2151,35 @@ impl AssetLibraryPane {
egui::Sense::click(), egui::Sense::click(),
); );
// Check if an asset is being dragged and matches this folder's category
let is_valid_drop_target = shared.dragging_asset.as_ref().map(|drag| {
let drag_category = Self::drag_clip_type_to_category(drag.clip_type);
drag_category == self.selected_category
}).unwrap_or(false);
let is_drop_hover = is_valid_drop_target && response.hovered();
// Background // Background
let bg_color = if response.hovered() { let bg_color = if is_drop_hover {
// Highlight as drop target
egui::Color32::from_rgb(60, 100, 140)
} else if response.hovered() {
egui::Color32::from_rgb(50, 50, 50) egui::Color32::from_rgb(50, 50, 50)
} else { } else {
egui::Color32::from_rgb(35, 35, 35) egui::Color32::from_rgb(35, 35, 35)
}; };
ui.painter().rect_filled(rect, 4.0, bg_color); ui.painter().rect_filled(rect, 4.0, bg_color);
// Draw drop target indicator border
if is_drop_hover {
ui.painter().rect_stroke(
rect,
4.0,
egui::Stroke::new(2.0, egui::Color32::from_rgb(100, 180, 255)),
egui::StrokeKind::Middle,
);
}
// Folder icon (centered) // Folder icon (centered)
if let Some(ref icon) = folder_icon { if let Some(ref icon) = folder_icon {
let icon_size = 48.0; let icon_size = 48.0;
@ -2077,10 +2218,32 @@ impl AssetLibraryPane {
egui::Color32::from_rgb(150, 150, 150), egui::Color32::from_rgb(150, 150, 150),
); );
// Handle drop: move asset to folder
if is_drop_hover && ui.input(|i| i.pointer.any_released()) {
if let Some(ref drag) = shared.dragging_asset.clone() {
let core_category = Self::drag_clip_type_to_core_category(drag.clip_type);
let action = lightningbeam_core::actions::MoveAssetToFolderAction::new(
core_category,
drag.clip_id,
Some(folder.id),
);
let _ = shared.action_executor.execute(Box::new(action));
*shared.dragging_asset = None;
}
}
// Handle double-click to navigate into folder // Handle double-click to navigate into folder
if response.double_clicked() { if response.double_clicked() {
self.set_current_folder(Some(folder.id)); self.set_current_folder(Some(folder.id));
} }
// Handle right-click for context menu
if response.secondary_clicked() {
self.folder_context_menu = Some(FolderContextMenuState {
folder_id: folder.id,
position: ui.ctx().pointer_interact_pos().unwrap_or(egui::pos2(0.0, 0.0)),
});
}
} }
LibraryItem::Asset(asset) => { LibraryItem::Asset(asset) => {
// Allocate rect for asset grid item (with space for name below) // Allocate rect for asset grid item (with space for name below)
@ -3118,11 +3281,14 @@ impl PaneRenderer for AssetLibraryPane {
// Detect right-click on pane background (not on items) // Detect right-click on pane background (not on items)
// Only allow folder creation in categories with folder support (not "All") // Only allow folder creation in categories with folder support (not "All")
// Don't trigger if we already opened a folder or asset context menu
if self.selected_category != AssetCategory::All { if self.selected_category != AssetCategory::All {
if ui.input(|i| i.pointer.secondary_clicked()) { if ui.input(|i| i.pointer.secondary_clicked()) {
if let Some(pos) = ui.ctx().pointer_interact_pos() { if self.folder_context_menu.is_none() && self.context_menu.is_none() {
if list_rect.contains(pos) { if let Some(pos) = ui.ctx().pointer_interact_pos() {
self.pane_context_menu = Some(pos); if list_rect.contains(pos) {
self.pane_context_menu = Some(pos);
}
} }
} }
} }
@ -3145,12 +3311,23 @@ impl PaneRenderer for AssetLibraryPane {
let asset_name = asset.name.clone(); let asset_name = asset.name.clone();
let asset_category = asset.category; let asset_category = asset.category;
let asset_is_builtin = asset.is_builtin; let asset_is_builtin = asset.is_builtin;
let asset_folder_id = asset.folder_id;
let in_use = Self::is_asset_in_use( let in_use = Self::is_asset_in_use(
shared.action_executor.document(), shared.action_executor.document(),
context_asset_id, context_asset_id,
asset_category, asset_category,
); );
// Get folders for this category (for Move to Folder submenu)
let folders: Vec<(Uuid, String)> = if let Some(core_cat) = Self::to_core_category(asset_category) {
let tree = document_arc.get_folder_tree(core_cat);
tree.folders.iter()
.map(|(id, f)| (*id, f.name.clone()))
.collect()
} else {
Vec::new()
};
// Show context menu popup at the stored position // Show context menu popup at the stored position
let menu_id = egui::Id::new("asset_context_menu"); let menu_id = egui::Id::new("asset_context_menu");
let menu_response = egui::Area::new(menu_id) let menu_response = egui::Area::new(menu_id)
@ -3195,6 +3372,47 @@ impl PaneRenderer for AssetLibraryPane {
}); });
self.context_menu = None; self.context_menu = None;
} }
// Move to Folder submenu (only show if there are folders or asset is not at root)
if !folders.is_empty() || asset_folder_id.is_some() {
ui.separator();
ui.menu_button("Move to Folder", |ui| {
// Move to Root option (if not already at root)
if asset_folder_id.is_some() {
if ui.button("Root").clicked() {
if let Some(core_cat) = Self::to_core_category(asset_category) {
let action = lightningbeam_core::actions::MoveAssetToFolderAction::new(
core_cat,
context_asset_id,
None,
);
let _ = shared.action_executor.execute(Box::new(action));
}
self.context_menu = None;
}
if !folders.is_empty() {
ui.separator();
}
}
// List all folders (except current folder)
for (folder_id, folder_name) in &folders {
if asset_folder_id != Some(*folder_id) {
if ui.button(folder_name).clicked() {
if let Some(core_cat) = Self::to_core_category(asset_category) {
let action = lightningbeam_core::actions::MoveAssetToFolderAction::new(
core_cat,
context_asset_id,
Some(*folder_id),
);
let _ = shared.action_executor.execute(Box::new(action));
}
self.context_menu = None;
}
}
}
});
}
} }
}); });
}); });
@ -3251,9 +3469,14 @@ impl PaneRenderer for AssetLibraryPane {
}) })
}); });
// Close menu if clicked outside // Close menu on click outside (using primary button release to avoid first-frame issue)
if menu_response.response.clicked_elsewhere() { let menu_rect = menu_response.response.rect;
self.pane_context_menu = None; if ui.input(|i| i.pointer.primary_released()) {
if let Some(pos) = ui.ctx().pointer_interact_pos() {
if !menu_rect.contains(pos) {
self.pane_context_menu = None;
}
}
} }
// Also close on Escape // Also close on Escape
@ -3262,6 +3485,72 @@ impl PaneRenderer for AssetLibraryPane {
} }
} }
// Folder context menu (for rename/delete/etc)
if let Some(ref folder_state) = self.folder_context_menu.clone() {
let folder_id = folder_state.folder_id;
let menu_pos = folder_state.position;
// Get the folder from the document
if let Some(core_category) = Self::to_core_category(self.selected_category) {
let folder_tree = document_arc.get_folder_tree(core_category);
if let Some(folder) = folder_tree.folders.get(&folder_id) {
let folder_name = folder.name.clone();
let menu_id = egui::Id::new("folder_context_menu");
let menu_response = egui::Area::new(menu_id)
.order(egui::Order::Foreground)
.fixed_pos(menu_pos)
.show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style()).show(ui, |ui| {
ui.set_min_width(150.0);
if ui.button("Rename").clicked() {
// Enter rename mode for folder
self.folder_rename_state = Some(FolderRenameState {
folder_id,
category: self.selected_category,
edit_text: folder_name.clone(),
});
self.folder_context_menu = None;
}
if ui.button("Delete").clicked() {
// Execute delete folder action
let action = lightningbeam_core::actions::DeleteFolderAction::new(
core_category,
folder_id,
lightningbeam_core::actions::DeleteStrategy::MoveToParent,
);
let _ = shared.action_executor.execute(Box::new(action));
self.folder_context_menu = None;
}
})
});
// Close menu on click outside (using primary button release to avoid first-frame issue)
let menu_rect = menu_response.response.rect;
if ui.input(|i| i.pointer.primary_released()) {
if let Some(pos) = ui.ctx().pointer_interact_pos() {
if !menu_rect.contains(pos) {
self.folder_context_menu = None;
}
}
}
// Also close on Escape
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
self.folder_context_menu = None;
}
} else {
self.folder_context_menu = None;
}
} else {
self.folder_context_menu = None;
}
}
// Delete confirmation dialog // Delete confirmation dialog
if let Some(ref pending) = self.pending_delete.clone() { if let Some(ref pending) = self.pending_delete.clone() {
let window_id = egui::Id::new("delete_confirm_dialog"); let window_id = egui::Id::new("delete_confirm_dialog");
@ -3345,6 +3634,39 @@ impl PaneRenderer for AssetLibraryPane {
self.rename_state = None; self.rename_state = None;
} }
} }
// Handle folder rename state (Enter to confirm, Escape to cancel)
if let Some(ref state) = self.folder_rename_state.clone() {
let mut should_confirm = false;
let mut should_cancel = false;
// Check for Enter or Escape
ui.input(|i| {
if i.key_pressed(egui::Key::Enter) {
should_confirm = true;
} else if i.key_pressed(egui::Key::Escape) {
should_cancel = true;
}
});
if should_confirm {
let new_name = state.edit_text.trim();
if !new_name.is_empty() {
// Execute rename folder action
if let Some(core_category) = Self::to_core_category(state.category) {
let action = lightningbeam_core::actions::RenameFolderAction::new(
core_category,
state.folder_id,
new_name.to_string(),
);
let _ = shared.action_executor.execute(Box::new(action));
}
}
self.folder_rename_state = None;
} else if should_cancel {
self.folder_rename_state = None;
}
}
} }
fn name(&self) -> &str { fn name(&self) -> &str {

View File

@ -201,6 +201,8 @@ pub struct SharedPaneState<'a> {
pub effect_thumbnail_cache: &'a std::collections::HashMap<Uuid, Vec<u8>>, pub effect_thumbnail_cache: &'a std::collections::HashMap<Uuid, Vec<u8>>,
/// Effect IDs whose thumbnails should be invalidated (e.g., after shader edit) /// Effect IDs whose thumbnails should be invalidated (e.g., after shader edit)
pub effect_thumbnails_to_invalidate: &'a mut Vec<Uuid>, 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 /// Trait for pane rendering

View File

@ -629,9 +629,6 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
// Allocate the rect and render the graph editor within it // Allocate the rect and render the graph editor within it
ui.allocate_ui_at_rect(rect, |ui| { ui.allocate_ui_at_rect(rect, |ui| {
// Disable debug warning for unaligned widgets (happens when zoomed)
ui.style_mut().debug.show_unaligned = false;
// Check for scroll input to override library's default zoom behavior // Check for scroll input to override library's default zoom behavior
let modifiers = ui.input(|i| i.modifiers); let modifiers = ui.input(|i| i.modifiers);
let has_ctrl = modifiers.ctrl || modifiers.command; let has_ctrl = modifiers.ctrl || modifiers.command;

View File

@ -59,7 +59,7 @@ pub struct VelloResourcesMap {
} }
impl SharedVelloResources { impl SharedVelloResources {
pub fn new(device: &wgpu::Device, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>) -> Result<Self, String> { pub fn new(device: &wgpu::Device, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, target_format: wgpu::TextureFormat) -> Result<Self, String> {
let renderer = vello::Renderer::new( let renderer = vello::Renderer::new(
device, device,
vello::RendererOptions { vello::RendererOptions {
@ -120,7 +120,7 @@ impl SharedVelloResources {
module: &shader, module: &shader,
entry_point: Some("fs_main"), entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm, // egui's target format format: target_format, // Use egui's actual target format
blend: Some(wgpu::BlendState::ALPHA_BLENDING), blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL, write_mask: wgpu::ColorWrites::ALL,
})], })],
@ -161,7 +161,7 @@ impl SharedVelloResources {
module: &hdr_shader, module: &hdr_shader,
entry_point: Some("fs_main"), entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState { targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm, // Output to display-ready texture format: wgpu::TextureFormat::Rgba8Unorm, // Intermediate texture format (not swapchain)
blend: None, // No blending - direct replacement blend: None, // No blending - direct replacement
write_mask: wgpu::ColorWrites::ALL, write_mask: wgpu::ColorWrites::ALL,
})], })],
@ -359,6 +359,7 @@ struct VelloCallback {
playback_time: f64, // Current playback time for animation evaluation playback_time: f64, // Current playback time for animation evaluation
video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>,
shape_editing_cache: Option<ShapeEditingCache>, // Cache for vector editing preview shape_editing_cache: Option<ShapeEditingCache>, // Cache for vector editing preview
target_format: wgpu::TextureFormat, // Surface format for blit pipelines
} }
impl VelloCallback { impl VelloCallback {
@ -380,8 +381,9 @@ impl VelloCallback {
playback_time: f64, playback_time: f64,
video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>,
shape_editing_cache: Option<ShapeEditingCache>, shape_editing_cache: Option<ShapeEditingCache>,
target_format: wgpu::TextureFormat,
) -> Self { ) -> Self {
Self { rect, pan_offset, zoom, instance_id, document, tool_state, active_layer_id, drag_delta, selection, fill_color, stroke_color, stroke_width, selected_tool, eyedropper_request, playback_time, video_manager, shape_editing_cache } Self { rect, pan_offset, zoom, instance_id, document, tool_state, active_layer_id, drag_delta, selection, fill_color, stroke_color, stroke_width, selected_tool, eyedropper_request, playback_time, video_manager, shape_editing_cache, target_format }
} }
} }
@ -407,7 +409,7 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
// Initialize shared resources if not yet created (only happens once for first Stage pane) // Initialize shared resources if not yet created (only happens once for first Stage pane)
if map.shared.is_none() { if map.shared.is_none() {
map.shared = Some(Arc::new( map.shared = Some(Arc::new(
SharedVelloResources::new(device, self.video_manager.clone()).expect("Failed to initialize shared Vello resources") SharedVelloResources::new(device, self.video_manager.clone(), self.target_format).expect("Failed to initialize shared Vello resources")
)); ));
} }
@ -6345,6 +6347,7 @@ impl PaneRenderer for StagePane {
*shared.playback_time, *shared.playback_time,
shared.video_manager.clone(), shared.video_manager.clone(),
self.shape_editing_cache.clone(), self.shape_editing_cache.clone(),
shared.target_format,
); );
let cb = egui_wgpu::Callback::new_paint_callback( let cb = egui_wgpu::Callback::new_paint_callback(