feat(app): track-header toggles, effect library, panel titles (design parity)
- track headers: name + visible/mute/lock toggles, undoable through graphops, honored by the montage (muted tracks are skipped) and by the edit guards (locked tracks reject trims/moves/deletes) - project explorer gains a panel title row; the tree/icons toggle is now small icon buttons with tooltips - new effect library dock panel (every addable effect; double-click appends to the selected clip's chain) - screenshots refreshed (zh/en, window/manager/preferences)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// 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 effect library panel (效果库): every effect type the engine can add
|
||||
//! to a clip's chain, as a flat list. Double-clicking an entry appends the
|
||||
//! effect to the selected clip's effect chain (the undoable
|
||||
//! [`AppEngine::add_effect`]; the insertion index is clamped to the chain
|
||||
//! end by the backend).
|
||||
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::{
|
||||
div, prelude::*, AnyElement, App, ClickEvent, Context, Entity, EventEmitter, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
|
||||
use crate::i18n;
|
||||
use crate::oakui::AppEngine;
|
||||
use crate::panels::ids::EFFECT_LIBRARY;
|
||||
|
||||
/// The effect library panel.
|
||||
pub struct EffectLibraryPanel<E: AppEngine> {
|
||||
engine: Entity<E>,
|
||||
}
|
||||
|
||||
impl<E: AppEngine> EffectLibraryPanel<E> {
|
||||
/// Builds the panel over `engine`'s addable-effect table.
|
||||
pub fn new(engine: Entity<E>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
// Re-read the effect table whenever the engine notifies (the table
|
||||
// itself is static, but the selection hint depends on the target).
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
Self { engine }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: AppEngine> Render for EffectLibraryPanel<E> {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
let effects = self.engine.read(cx).addable_effects();
|
||||
|
||||
let mut list = div()
|
||||
.id("effect-library-list")
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.p_2()
|
||||
.overflow_y_scroll();
|
||||
for (type_id, name) in effects {
|
||||
let engine = self.engine.clone();
|
||||
let row_id = type_id.clone();
|
||||
list = list.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("effect-library-{type_id}")))
|
||||
.debug_selector(move || format!("effect-library-row-{row_id}").into())
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_sm()
|
||||
.text_sm()
|
||||
.text_color(colors.text)
|
||||
.hover(|style| style.bg(colors.selected))
|
||||
.child(name)
|
||||
.on_click(move |event: &ClickEvent, _window, cx| {
|
||||
// Double-click appends the effect to the selected
|
||||
// clip's chain; the backend clamps the index to the
|
||||
// chain end.
|
||||
if event.click_count() == 2 {
|
||||
let type_id = type_id.clone();
|
||||
engine.update(cx, |engine, cx| {
|
||||
if let Err(err) = engine.add_effect(usize::MAX, &type_id, cx) {
|
||||
println!("[effect library] add effect failed: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(list)
|
||||
.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.border_t_1()
|
||||
.border_color(colors.border)
|
||||
.text_xs()
|
||||
.text_color(colors.disabled)
|
||||
.child(i18n::tr("effect_library.hint")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: AppEngine> EventEmitter<PanelEvent> for EffectLibraryPanel<E> {}
|
||||
|
||||
impl<E: AppEngine> DockPanel for EffectLibraryPanel<E> {
|
||||
fn panel_id(&self) -> gpui::dock::PanelId {
|
||||
EFFECT_LIBRARY
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> SharedString {
|
||||
i18n::tr("panel.effect_library").into()
|
||||
}
|
||||
|
||||
fn tab_content(&self, _cx: &App) -> AnyElement {
|
||||
div()
|
||||
.child(i18n::tr("panel.effect_library"))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::oakui::MockEngine;
|
||||
use gpui::{px, size, TestAppContext, VisualTestContext};
|
||||
|
||||
/// The panel renders one row per addable effect of the engine (the
|
||||
/// mock exposes the real factory's video-effect table).
|
||||
#[gpui::test]
|
||||
async fn lists_every_addable_effect(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
EffectLibraryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
|
||||
let expected = crate::oakui::effectchain::addable_effects();
|
||||
assert!(!expected.is_empty());
|
||||
for (type_id, _) in &expected {
|
||||
// `debug_bounds` takes a &'static selector; the per-row selector is
|
||||
// dynamic, so the test leaks it (process-lifetime, test-only).
|
||||
let selector: &'static str =
|
||||
Box::leak(format!("effect-library-row-{type_id}").into_boxed_str());
|
||||
assert!(
|
||||
cx.debug_bounds(selector).is_some(),
|
||||
"effect row {type_id} rendered"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user