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:
@@ -105,6 +105,34 @@ pub fn registered_instance_count() -> usize {
|
||||
instances().lock().unwrap_or_else(|e| e.into_inner()).len()
|
||||
}
|
||||
|
||||
/// Triggers a push-button parameter (the Rust counterpart of the C++
|
||||
/// `oakengine_plugin_node_push_button_clicked`; called by the
|
||||
/// inspector's button widget).
|
||||
///
|
||||
/// OFX push buttons carry no value: the host's press signal is a single
|
||||
/// set on the parameter (the plugin reacts in its own instanceChanged
|
||||
/// action). Locates the instance and parameter, type-checks, then
|
||||
/// `set_ofx`. Returns false when the instance/parameter is unknown or
|
||||
/// the parameter is not a push button.
|
||||
///
|
||||
/// TODO(instanceChanged): per the OFX contract the press should be
|
||||
/// routed to the plugin as kOfxActionInstanceChanged (UserEdited);
|
||||
/// oakplugin has no dispatcher for that action yet — for now the value
|
||||
/// is only marked, and the plugin-side reaction is future work.
|
||||
pub fn push_button_clicked(instance: u64, param_name: &str) -> bool {
|
||||
let Some(inst) = instance_from_id(instance) else {
|
||||
return false;
|
||||
};
|
||||
let Some(p) = inst.value.params.find(param_name) else {
|
||||
return false;
|
||||
};
|
||||
if p.def.ofx_type != ofx::TYPE_PUSHBUTTON {
|
||||
return false;
|
||||
}
|
||||
p.set_ofx(ParamValue::PushButton);
|
||||
true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 项目幅面(normalised 坐标默认值 → canonical 的换算基准)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -961,4 +989,73 @@ mod tests {
|
||||
assert!(instance_from_id(u64::MAX).is_none());
|
||||
unregister_instance(u64::MAX);
|
||||
}
|
||||
|
||||
/// 构造一个只含 push-button 参数的最小实例(直接登记进注册表)。
|
||||
fn instance_with_push_button() -> u64 {
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use crate::descriptor::EffectDescriptor;
|
||||
use crate::handle::RefBox;
|
||||
use crate::host::Plugin;
|
||||
use crate::param::{ParamDef, ParamInstance, ParamSetInstance};
|
||||
|
||||
unsafe extern "C" fn dummy_entry(
|
||||
_: *const c_char,
|
||||
_: *const c_void,
|
||||
_: *mut c_void,
|
||||
_: *mut c_void,
|
||||
) -> i32 {
|
||||
0
|
||||
}
|
||||
let plugin = Arc::new(Plugin {
|
||||
identifier: "test.plugin".into(),
|
||||
version: (1, 0),
|
||||
bundle_path: std::path::PathBuf::new(),
|
||||
contexts: vec![],
|
||||
descriptor: EffectDescriptor::new(),
|
||||
lib: std::ptr::null_mut(),
|
||||
entry: dummy_entry,
|
||||
ofx_plugin: std::ptr::null_mut(),
|
||||
});
|
||||
let mut params = ParamSetInstance { params: Vec::new() };
|
||||
params.params.push(Box::new(ParamInstance::from_def(ParamDef::new(
|
||||
"button",
|
||||
ofx::TYPE_PUSHBUTTON,
|
||||
))));
|
||||
params.params.push(Box::new(ParamInstance::from_def(ParamDef::new(
|
||||
"gain",
|
||||
ofx::TYPE_DOUBLE,
|
||||
))));
|
||||
let inst = crate::instance::Instance {
|
||||
props: crate::property::PropertySet::new(),
|
||||
plugin,
|
||||
context: "OfxImageEffectContextFilter".into(),
|
||||
params,
|
||||
clips: Vec::new(),
|
||||
node_identity: std::sync::atomic::AtomicUsize::new(0),
|
||||
destroyed: std::sync::atomic::AtomicBool::new(false),
|
||||
sequence_range: std::sync::Mutex::new(None),
|
||||
progress_cb: std::sync::Mutex::new(None),
|
||||
cancel: std::sync::atomic::AtomicBool::new(false),
|
||||
edit: std::sync::Mutex::new(crate::instance::EditTransaction::new()),
|
||||
render_lock: std::sync::Mutex::new(()),
|
||||
};
|
||||
register_instance(Arc::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value: inst,
|
||||
}))
|
||||
}
|
||||
|
||||
/// push_button_clicked:实例/参数查无与类型不匹配 → false;命中 →
|
||||
/// true 并触发一次 set。
|
||||
#[test]
|
||||
fn push_button_clicked_requires_a_push_button_param() {
|
||||
let id = instance_with_push_button();
|
||||
assert!(push_button_clicked(id, "button"));
|
||||
// 类型不匹配 / 查无参数 / 查无实例。
|
||||
assert!(!push_button_clicked(id, "gain"));
|
||||
assert!(!push_button_clicked(id, "nope"));
|
||||
assert!(!push_button_clicked(u64::MAX, "button"));
|
||||
unregister_instance(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user