feat(app): OpenFX UI wiring - effect library, inspector params, startup glue

- src/oakui/ofx.rs: startup sequence (host scan, register_plugin_nodes,
  progress reporter factory -> app progress dialog channel, active
  viewer time provider, project extent sync); all failures degrade to
  logs. oak-worker runtime also scans and registers plugins.
- Effect library groups OpenFX entries by sub-category (Filter/
  Generator/Transition/General); effect insertion goes through
  Factory::create_any so dynamic plugin nodes resolve.
- Inspector renders OFX parameters from node inputs (sliders, combo
  boxes from repeated combo_option/combo_value properties, vec/color
  spinboxes, text with explicit commit, push buttons), edits are
  undoable; persistent plugin messages surface as a card badge.
- oakplugin: push_button_clicked and per-instance persistent message
  counting (thin public layers).
This commit is contained in:
2026-08-18 22:16:08 +08:00
parent d61acb9e0a
commit ec7b7e6d13
17 changed files with 1733 additions and 55 deletions
+75
View File
@@ -26,6 +26,7 @@
//! (include/plugin/host.h)。本模块按头文件契约建模。
//! (原 C ABI 出口层已随单库化删除;注册点由 facade 直接调用。)
use std::collections::HashMap;
use std::ffi::{c_char, c_int, c_void, CStr};
use crate::suites::status;
@@ -40,6 +41,40 @@ pub(crate) type MessageHandler =
static HANDLER: std::sync::Mutex<(Option<MessageHandler>, usize)> =
std::sync::Mutex::new((None, 0));
// ---------------------------------------------------------------------------
// 持久消息计数(检查器效果卡的徽标数据源,阶段 6b)
// ---------------------------------------------------------------------------
//
// C++ 侧 `OlivePluginInstance::persistentErrors_` 按实例累计持久消息,
// 并 emit `node_->message_count_changed()`。这里以实例 handle(props
// 地址,见 [`crate::suites::tag`])为键累计计数;app 检查器经
// [`crate::node_factory::instance_from_id`] 拿实例后按 props 地址查询。
static PERSISTENT: std::sync::OnceLock<std::sync::Mutex<HashMap<usize, usize>>> =
std::sync::OnceLock::new();
fn persistent_slot() -> &'static std::sync::Mutex<HashMap<usize, usize>> {
PERSISTENT.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
/// 实例 handle(props 地址)的持久消息数(未登记 → 0)。
pub fn persistent_message_count(handle: usize) -> usize {
persistent_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&handle)
.copied()
.unwrap_or(0)
}
/// 清空某实例的持久消息(实例释放/宿主关闭路径;未登记的键 no-op)。
pub fn clear_persistent_messages(handle: usize) {
persistent_slot()
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&handle);
}
/// 注册/注销消息出口(facade 调用;公开:测试直接注入捕获器)。
pub fn set_handler(f: Option<MessageHandler>, userdata: *mut c_void) {
let mut h = HANDLER.lock().unwrap_or_else(|e| e.into_inner());
@@ -88,6 +123,14 @@ pub unsafe extern "C" fn oak_ofx_message_impl(
if type_.is_null() || message.is_null() {
return status::FAILED;
}
// 持久消息:按实例 handle(剥标签后的 props 地址)累计计数
// (徽标数据源;C++ persistentErrors_ 的等价物)。
if !_handle.is_null() {
let key = crate::suites::tag::strip(_handle) as usize;
let mut map = persistent_slot().lock().unwrap_or_else(|e| e.into_inner());
*map.entry(key).or_insert(0) += 1;
drop(map);
}
let (handler, userdata) = handler();
if let Some(h) = handler {
// 头文件契约:返回值 0/1 即答复 NO/YES。
@@ -298,4 +341,36 @@ mod tests {
};
assert_eq!(r, status::REPLY_NO);
}
/// 持久消息按实例 handle 累计(徽标数据源);clear 摘除。
#[test]
fn persistent_messages_accumulate_per_handle() {
let _g = TEST_LOCK.lock().unwrap();
set_handler(None, std::ptr::null_mut());
let props = crate::property::PropertySet::new();
let handle = crate::suites::tag::make(&props, crate::suites::tag::INSTANCE);
let key = crate::suites::tag::strip(handle) as usize;
clear_persistent_messages(key);
let type_ = CString::new("OfxMessageError").unwrap();
let id = CString::new("id").unwrap();
let msg = CString::new("boom").unwrap();
unsafe {
oak_ofx_message_impl(handle, type_.as_ptr(), id.as_ptr(), msg.as_ptr());
oak_ofx_message_impl(handle, type_.as_ptr(), id.as_ptr(), msg.as_ptr());
}
assert_eq!(persistent_message_count(key), 2);
clear_persistent_messages(key);
assert_eq!(persistent_message_count(key), 0);
// 空 handle 不计数。
unsafe {
oak_ofx_message_impl(
std::ptr::null_mut(),
type_.as_ptr(),
id.as_ptr(),
msg.as_ptr(),
);
}
assert_eq!(persistent_message_count(key), 0);
}
}