feat(app): project browser on real project data + drag-drop import (M12 P3)

- ProjectDataSource<RealEngine> reads the real bin tree (roots/
  children via the facade folder/footage enumeration)
- AppEngine::import_footage implemented (facade project_import_footage,
  last_error surfaced); double-click opens in the source viewer
- dragging files onto the browser imports them (also fixes a real gpui
  bug: ExternalPaths arrives as the bare type, not Arc-wrapped, so
  drops never fired)
- tests: real-engine import->browser listing, widget drop routing,
  facade folder enumeration failure matrix
This commit is contained in:
2026-08-16 15:05:01 +08:00
parent 0f82092e95
commit 693e2df5eb
5 changed files with 204 additions and 10 deletions
+57 -8
View File
@@ -29,18 +29,20 @@ use std::ffi::{c_char, c_int};
use crate::node::{
oakengine_footage_borrow, oakengine_footage_last_error, oakengine_footage_probe,
oakengine_folder_add_child, oakengine_folder_item_child, oakengine_folder_item_child_count,
oakengine_node_connect, oakengine_node_disconnect, oakengine_node_factory_create_from_id,
oakengine_node_factory_id_count, oakengine_node_factory_name_from_id,
oakengine_node_factory_node_at, oakengine_node_get_input, oakengine_node_get_input_at_time,
oakengine_node_get_label, oakengine_node_get_name, oakengine_node_get_type_id,
oakengine_node_input_get_type, oakengine_node_input_id, oakengine_node_input_is_connected,
oakengine_node_is_clip, oakengine_node_is_folder, oakengine_node_is_track,
oakengine_node_is_viewer_output, oakengine_node_keyframe_count, oakengine_node_set_input,
oakengine_node_set_input_at_time, oakengine_node_set_label, oakengine_project_add_node,
oakengine_project_create, oakengine_project_filename, oakengine_project_free,
oakengine_project_import_footage, oakengine_project_load, oakengine_project_name,
oakengine_project_new, oakengine_project_node_at, oakengine_project_node_count,
oakengine_project_save, oakengine_project_set_filename, OakNodeValue,
oakengine_node_identity, oakengine_node_input_get_type, oakengine_node_input_id,
oakengine_node_input_is_connected, oakengine_node_is_clip, oakengine_node_is_folder,
oakengine_node_is_track, oakengine_node_is_viewer_output, oakengine_node_keyframe_count,
oakengine_node_free, oakengine_node_set_input, oakengine_node_set_input_at_time, oakengine_node_set_label,
oakengine_project_add_node, oakengine_project_create, oakengine_project_filename,
oakengine_project_free, oakengine_project_import_footage, oakengine_project_load,
oakengine_project_name, oakengine_project_new, oakengine_project_node_at,
oakengine_project_node_count, oakengine_project_root, oakengine_project_save,
oakengine_project_set_filename, OakNodeValue,
};
/// Registered generator node ids used by the tests.
@@ -214,6 +216,53 @@ fn project_node_keyframe_lifecycle() {
assert_eq!(unsafe { oakengine_node_is_folder(solid) }, 0);
assert_eq!(unsafe { oakengine_node_is_viewer_output(solid) }, 0);
// ---- the project browser's folder-tree walk (M12 P3) -----------------
// The traversal exports the material bin walks: `project_root` resolves
// the root folder and `folder_item_child_count` / `folder_item_child`
// enumerate its children (folders and media alike), all resolved by the
// nodes' stable identities. The added nodes sit in the graph only until
// an explicit graft (the same FolderAddChild the footage import uses).
let root = unsafe { oakengine_project_root(project) };
assert!(!root.is_null(), "a new project has a root folder");
assert_eq!(unsafe { oakengine_node_is_folder(root) }, 1);
assert_eq!(unsafe { oakengine_folder_item_child_count(root) }, 0);
// Graft two graph nodes under the root folder.
assert_eq!(unsafe { oakengine_folder_add_child(root, solid) }, 0);
assert_eq!(unsafe { oakengine_folder_add_child(root, transform) }, 0);
assert_eq!(
unsafe { oakengine_folder_item_child_count(root) },
2,
"the grafted nodes are listed under the root folder"
);
let solid_id = unsafe { oakengine_node_identity(solid) };
let mut seen_solid = false;
for i in 0..2 {
let child = unsafe { oakengine_folder_item_child(root, i) };
assert!(!child.is_null(), "child {i} resolves");
assert!(unsafe { oakengine_node_identity(child) } != 0);
if unsafe { oakengine_node_identity(child) } == solid_id {
seen_solid = true;
// The child carries the node's factory type id.
let len = unsafe { oakengine_node_get_type_id(child, buf.as_mut_ptr(), 256) };
assert!(len > 0);
assert_eq!(unsafe { read_buf(&mut buf) }, TYPE_ID_SOLID);
assert_eq!(unsafe { oakengine_node_is_folder(child) }, 0);
}
unsafe { oakengine_node_free(child) };
}
assert!(seen_solid, "the solid generator is a root child");
// Failure paths: NULL project/folder, out-of-range index, non-folder
// handles (the app never hands those to the browser).
assert!(unsafe { oakengine_project_root(std::ptr::null_mut()) }.is_null());
assert_eq!(unsafe { oakengine_folder_item_child_count(std::ptr::null()) }, 0);
assert!(unsafe { oakengine_folder_item_child(std::ptr::null(), 0) }.is_null());
assert!(unsafe { oakengine_folder_item_child(root, 99) }.is_null());
assert_eq!(unsafe { oakengine_folder_item_child_count(solid) }, 0);
assert!(unsafe { oakengine_folder_item_child(solid, 0) }.is_null());
unsafe { oakengine_node_free(root) };
// ---- undoable label + readback --------------------------------------
assert_eq!(
unsafe { oakengine_node_set_label(solid, c"My Solid".as_ptr()) },