improve folders a bit
This commit is contained in:
parent
b19f66e648
commit
f4ffa7ecdd
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue