// 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 . //! 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 { stack: Entity>, engine: Entity, /// 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, } impl InspectorPanel { /// Builds the stack over `engine`'s effect model. pub fn new(engine: Entity, _window: &mut Window, cx: &mut Context) -> 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, ) -> 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 Render for InspectorPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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 EventEmitter for InspectorPanel {} impl DockPanel for InspectorPanel { 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) -> impl IntoElement { let colors = cx.default_colors().clone(); div() .px_3() .py_2() .text_color(colors.disabled) .child(crate::i18n::tr("inspector.params")) } }