Files
oak-editor/src/panels/inspector.rs
T
Mike-Solar cdda643d64 feat(engine,app): effect chain facade + app effect stack wiring
facade (oakengine/oaknode, API only extended):
- oaknode_node_get_effect_input / oaknode_node_get_flags module exports
- oakengine_clip_as_node, oakengine_node_effect_count/at/insert/remove/
  move/set_enabled, oakengine_node_identity/is_enabled/get_type_id,
  oakengine_node_factory_id_at — chain edits wrap disconnect/reconnect
  into single undo groups; insert/move/remove validated against the
  factory and the host's effect input
- it_node covers enumeration/insert/remove/move/toggle/undo-redo and
  the NULL/illegal-input matrix

app:
- RealEngine binds the selected clip's effect chain to the inspector's
  effect stack (cards, enable, reorder, remove, expansion state), all
  edits undoable through the facade
- AppEngine gains set_selected_clips / addable_effects / add_effect
  (default no-ops keep the mock untouched); the timeline selection
  drives the stack target; the inspector gets a small add-effect menu
- known module gap (documented): ClipBlockBehavior sets no effect_input
  yet, so timeline clips cannot host effects until oaknode grows tex_in
2026-08-11 20:04:44 +08:00

196 lines
6.0 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/>.
//! The inspector panel (检查器·效果栈): the `EffectStackView` over the
//! engine's node chain, shown as linear cards (媒体 → 变换 → OCIO LUT →
//! 输出) with add / remove / reorder — no engine, the mock applies the edits
//! to its own model.
use gpui::colors::DefaultColors;
use gpui::dock::{DockPanel, PanelEvent};
use gpui::effect_stack::{EffectStackEvent, EffectStackView};
use gpui::{
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, Render, SharedString, Window,
};
use crate::oakui::AppEngine;
use crate::panels::ids::INSPECTOR;
/// The inspector / effect stack panel.
pub struct InspectorPanel<E: AppEngine> {
stack: Entity<EffectStackView<E>>,
engine: Entity<E>,
/// An in-flight "add effect" flow: the stack insertion index carried
/// by the last [`EffectStackEvent::AddRequested`]. While set (and the
/// engine has a stack target), the panel renders a small menu of the
/// engine's addable effects instead of forwarding the bare request.
pending_add: Option<usize>,
}
impl<E: AppEngine> InspectorPanel<E> {
/// Builds the stack over `engine`'s effect model.
pub fn new(engine: Entity<E>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
let stack = cx.new(|cx| {
EffectStackView::new(engine.clone(), cx)
.params_renderer(|_effect, _window, cx| cx.new(|_cx| ParamPlaceholder).into())
});
// The "edits are requests" loop: forward each request to the engine,
// which applies it to its model and notifies. `AddRequested` carries
// no effect type, so the panel records the insertion index and lets
// the user pick one from the small menu below (the actual insert
// runs through `AppEngine::add_effect`).
cx.subscribe(&stack, |this, _stack, event: &EffectStackEvent, cx| {
if let EffectStackEvent::AddRequested { index } = event {
this.pending_add = Some(*index);
}
this.engine
.update(cx, |engine, cx| engine.apply_effect_event(event, cx));
})
.detach();
Self {
stack,
engine,
pending_add: None,
}
}
/// The "add effect" menu: one clickable row per addable effect of the
/// engine. Selecting a row inserts that effect at the recorded stack
/// index; a dismiss row closes the menu without adding.
fn render_add_menu(
&mut self,
index: usize,
colors: &gpui::colors::Colors,
cx: &mut Context<Self>,
) -> impl IntoElement {
let effects = self.engine.read(cx).addable_effects();
let mut menu = div()
.id("inspector-add-menu")
.px_2()
.py_1()
.border_t_1()
.border_color(colors.separator)
.flex()
.flex_col()
.gap_1();
for (type_id, name) in &effects {
let engine = self.engine.clone();
let type_id = type_id.clone();
let name = name.clone();
let index = index;
menu = menu.child(
div()
.id(SharedString::from(format!("add-effect-{type_id}")))
.cursor_pointer()
.px_2()
.py_1()
.rounded_sm()
.hover(|style| style.bg(colors.selected))
.text_color(colors.text)
.text_sm()
.child(name)
.on_click(
cx.listener(move |this, _event: &gpui::ClickEvent, _window, cx| {
this.pending_add = None;
engine.update(cx, |engine, cx| {
if let Err(err) = engine.add_effect(index, &type_id, cx) {
println!("[inspector] add effect failed: {err}");
}
});
cx.notify();
}),
),
);
}
// A dismiss row, so a cancelled pick does not linger.
menu = menu.child(
div()
.id("add-effect-dismiss")
.cursor_pointer()
.px_2()
.py_1()
.rounded_sm()
.hover(|style| style.bg(colors.selected))
.text_color(colors.disabled)
.text_sm()
.child("✕")
.on_click(
cx.listener(move |this, _event: &gpui::ClickEvent, _window, cx| {
this.pending_add = None;
cx.notify();
}),
),
);
menu
}
}
impl<E: AppEngine> Render for InspectorPanel<E> {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.default_colors().clone();
let mut root = div().id("inspector-panel").size_full().flex().flex_col();
root = root.child(self.stack.clone());
// The add-effect menu sits below the stack while an add is
// pending. It only makes sense while the engine has a stack
// target (a clip that can host effects).
if let Some(index) = self.pending_add {
if self.engine.read(cx).target_label().is_some() {
root = root.child(self.render_add_menu(index, colors.as_ref(), cx));
} else {
self.pending_add = None;
}
}
root
}
}
impl<E: AppEngine> EventEmitter<PanelEvent> for InspectorPanel<E> {}
impl<E: AppEngine> DockPanel for InspectorPanel<E> {
fn panel_id(&self) -> gpui::dock::PanelId {
INSPECTOR
}
fn title(&self, _cx: &App) -> SharedString {
crate::i18n::tr("panel.inspector").into()
}
fn tab_content(&self, _cx: &App) -> AnyElement {
div()
.child(crate::i18n::tr("panel.inspector"))
.into_any_element()
}
}
/// Placeholder parameter view rendered inside expanded effect cards.
/// A real app builds the effect's controls here and calls
/// [`EffectStackView::notify_parameter_changed`] after edits.
struct ParamPlaceholder;
impl Render for ParamPlaceholder {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.default_colors().clone();
div()
.px_3()
.py_2()
.text_color(colors.disabled)
.child(crate::i18n::tr("inspector.params"))
}
}