Files
oak-editor/crates/oak-node/src/folder.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

188 lines
5.4 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Bin folders (C++ `olive::Folder`): tree structure over `NodeId`s.
use crate::factory::NodeMeta;
use crate::id::NodeId;
use crate::node::{Category, NodeBehavior, NodeCore};
/// A folder node behavior: children are folder-tree members (folders
/// and footage), not graph edges.
pub struct FolderBehavior {
/// Display name.
pub name: String,
/// Child node ids (folders/footage) in bin order.
pub children: Vec<NodeId>,
}
impl FolderBehavior {
/// New empty folder.
pub fn new(name: &str) -> Self {
FolderBehavior {
name: name.to_string(),
children: Vec::new(),
}
}
/// Add a child (C++ `Folder::add_child`).
pub fn add_child(&mut self, child: NodeId) {
if !self.children.contains(&child) {
self.children.push(child);
}
}
/// Remove a child; false when absent.
pub fn remove_child(&mut self, child: NodeId) -> bool {
let before = self.children.len();
self.children.retain(|c| *c != child);
self.children.len() != before
}
/// Index of `child` in the direct children.
pub fn index_of_child(&self, child: NodeId) -> Option<usize> {
self.children.iter().position(|c| *c == child)
}
/// True when `child` is contained recursively (C++
/// `Folder::has_child_recursive`).
pub fn has_child_recursive(&self, child: NodeId, graph: &crate::graph::Graph) -> bool {
for c in &self.children {
if *c == child {
return true;
}
if let Some(entry) = graph.get(*c) {
if let Some(f) = entry
.behavior
.as_any()
.and_then(|a| a.downcast_ref::<FolderBehavior>())
{
if f.has_child_recursive(child, graph) {
return true;
}
}
}
}
false
}
}
impl NodeBehavior for FolderBehavior {
/// Human-readable name (C++ `name()`): the folder's display name.
fn name(&self) -> &str {
&self.name
}
/// Concrete downcast for the folder family.
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
/// Concrete downcast for the folder family.
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.folder"
}
/// Categories (C++ `category()`): items live in the bin, not the
/// render graph.
fn categories(&self) -> &[Category] {
&[Category::Timeline]
}
/// Deep copy (C++ `copy()`).
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(FolderBehavior::new(&self.name)))
}
/// Custom project save: the bin children (C++ attaches children
/// through the `child_in` input connections; the Rust model keeps
/// them in [`FolderBehavior::children`], so the custom segment is
/// the persistence channel — old readers skip it).
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
let _ = core;
if !self.children.is_empty() {
writer.start_element("children");
for c in &self.children {
writer.text_element("child", &c.identity().to_string());
}
writer.end_element(); // children
}
}
/// Custom project load; the child references resolve in the
/// serializer's post-load pass (which also folds in `child_in`
/// connections from C++ files).
fn load_custom(
&mut self,
_core: &mut NodeCore,
reader: &mut dyn crate::serializer::XmlRead,
) -> bool {
while reader.next_start_element() {
match reader.name() {
"children" => {
self.children.clear();
while reader.next_start_element() {
if reader.name() == "child" {
if let Some(id) =
crate::serializer::parse_node_ref(&reader.read_element_text())
{
self.children.push(id);
}
} else {
reader.skip_current_element();
}
}
}
_ => reader.skip_current_element(),
}
}
true
}
}
/// Constructor (C++ `Folder::Folder()`): a folder node declares the
/// `child_in` array input (C++ attaches bin children through it) but no
/// `enabled_in` (`// CPP-PARITY: folder.cpp:26`).
pub fn create(name: &str) -> (NodeCore, Box<dyn NodeBehavior>) {
let mut core = NodeCore::empty();
// Bin item (C++ `folder.cpp:38` `set_flag(k_is_item)`).
core.flags |= crate::node::flags::IS_ITEM;
let mut child = crate::input::Input::new(
"child_in",
crate::value::ValueType::None,
crate::value::NodeValue::None,
);
child.flags |= crate::input::flags::ARRAY | crate::input::flags::NOT_KEYFRAMABLE;
core.add_input(child);
(core, Box::new(FolderBehavior::new(name)))
}
/// Register a folder-typed node (used by the serializer for bin folders;
/// folders are not in the factory menu).
pub fn register(meta: &mut Vec<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.folder",
name: "Folder",
categories: &[Category::Timeline],
create: || create("Folder"),
});
}