// 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 source viewer panel (素材查看器): the `ViewerWidget` over the source //! monitor's clock, with its own transport and format chips. use gpui::colors::DefaultColors; use gpui::dock::{DockPanel, PanelEvent}; use gpui::{ div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton, Render, SharedString, Window, }; use gpui_widgets::viewer::{ViewerEvent, ViewerWidget}; use crate::actions::ActionId; use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered}; use crate::oakui::timecode::{format_fps, format_resolution}; use crate::oakui::{AppEngine, Monitor}; use crate::panels::commands::{self as panel_commands, PanelCommandHandler}; use crate::panels::{chip, viewer_title}; use crate::panels::ids::SOURCE_VIEWER; /// The source viewer panel. pub struct SourceViewerPanel { viewer: Entity>, engine: Entity, /// The source monitor's clock (also owned by the viewer widget; kept /// here so transport commands can read the playing state). clock: Entity, /// The last CPU frame handed to the viewer (compared by `Arc` identity so /// a paused playhead does not re-upload the picture every frame). last_cpu_frame: Option>, /// The right-click context menu. context_menu: ContextMenuHandle, } impl SourceViewerPanel { /// Builds a viewer over `clock` (the source monitor's clock). pub fn new( engine: Entity, clock: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let viewer = cx.new(|cx| ViewerWidget::new(2, clock.clone(), window, cx)); // Route every transport request to the engine's source monitor. cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| { let monitor = Monitor::Source; this.engine.update(cx, |engine, cx| match event { ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx), ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx), ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx), other => println!("[source viewer] request: {other:?}"), }); }) .detach(); let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx); Self { viewer, engine, clock, last_cpu_frame: None, context_menu, } } /// Handles the viewer's local (non-registry) context-menu items — all /// placeholders until the viewer widget grows the matching controls. fn on_local_menu_item(&mut self, item: usize, _cx: &mut Context) { println!("[source viewer] context-menu item {item} (not implemented yet)"); } /// Routes a transport command to the engine's source monitor through /// the shared viewer transport helper. fn transport(&mut self, action: ActionId, cx: &mut Context) -> bool { let engine = self.engine.clone(); let clock = self.clock.clone(); panel_commands::viewer_transport(&engine, &clock, Monitor::Source, action, cx) } /// Pushes the engine's synthetic test frame into the viewer, but only when /// it actually changed (the engine caches one image per playhead frame). fn sync_frame(&mut self, cx: &mut Context) { let frame = self.engine.read(cx).cpu_frame(Monitor::Source, cx); if self .last_cpu_frame .as_ref() .is_none_or(|last| !std::sync::Arc::ptr_eq(last, &frame)) { self.last_cpu_frame = Some(frame.clone()); let frame = frame.clone(); self.viewer .update(cx, |viewer, cx| viewer.set_cpu_frame(Some(frame), cx)); } } } impl Render for SourceViewerPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { self.sync_frame(cx); let colors = cx.default_colors().clone(); let format = self .engine .read(cx) .current_sequence() .map(|sequence| sequence.format) .unwrap_or(crate::oakui::VideoFormat::hd_1080p25()); let media = self.engine.read(cx).source_media_name(); let title = viewer_title("panel.source_viewer", &media); div() .size_full() .flex() .flex_col() .overflow_hidden() // Any click inside the panel makes it the focused panel (the // dock re-emits this as `DockEvent::PanelFocused`, which the // shell uses to route focused-panel commands). .on_mouse_down(MouseButton::Left, { cx.listener(|_this, _event: &gpui::MouseDownEvent, _window, cx| { cx.emit(PanelEvent::Focused); }) }) // The viewer widget has no right-click handling of its own, so // the panel opens the shared viewer menu here. .on_mouse_down(MouseButton::Right, { cx.listener(|this, event: &gpui::MouseDownEvent, _window, cx| { this.context_menu .show(event.position, crate::menus::shared::viewer_menu(), cx); }) }) .child( div() .flex() .items_center() .gap_2() .px_2() .py_1() .border_b_1() .border_color(colors.border) .child(chip(&colors, title)) .child(chip( &colors, format_resolution(format.width, format.height), )) .child(chip(&colors, format_fps(format.rate))), ) .child( div() .flex_1() .min_w_0() .min_h_0() .overflow_hidden() .child(self.viewer.clone()), ) // The right-click popup renders anchored above the panel. .child(self.context_menu.widget()) } } impl PanelCommandHandler for SourceViewerPanel { fn play_pause(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::PlayPause, cx) } fn prev_frame(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::PrevFrame, cx) } fn next_frame(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::NextFrame, cx) } fn go_to_start(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::GoToStart, cx) } fn go_to_end(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::GoToEnd, cx) } fn play_in_to_out(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::PlayInToOut, cx) } fn go_to_in(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::GoToIn, cx) } fn go_to_out(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::GoToOut, cx) } fn shuttle_left(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::ShuttleLeft, cx) } fn shuttle_stop(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::ShuttleStop, cx) } fn shuttle_right(&mut self, cx: &mut Context) -> bool { self.transport(ActionId::ShuttleRight, cx) } } impl EventEmitter for SourceViewerPanel {} impl EventEmitter for SourceViewerPanel {} impl DockPanel for SourceViewerPanel { fn panel_id(&self) -> gpui::dock::PanelId { SOURCE_VIEWER } fn title(&self, cx: &App) -> SharedString { viewer_title("panel.source_viewer", &self.engine.read(cx).source_media_name()).into() } fn tab_content(&self, cx: &App) -> AnyElement { div() .child(viewer_title( "panel.source_viewer", &self.engine.read(cx).source_media_name(), )) .into_any_element() } }