feat(gpui): add dock, effect stack, node graph, and timeline widgets
Implements four workspace widget modules with accompanying learn examples: - dock: dockable panel layout system (tabs, splits, drag-to-dock) with serde-based persistence via PanelRegistry / DockLayoutState - effect_stack: linear effect-stack inspector widget - node_graph: node-graph editor (nodes, ports, wires, pan/zoom canvas) - timeline: video-editing timeline (tracks, clips, ruler, playhead) Timeline snapping prefers the earlier frame when two snap points are equally close, with SnapKind priority breaking same-frame ties.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
//! Dock Layout Example (intended-usage sketch)
|
||||
//!
|
||||
//! Sketch of how the Oak video editor will wire up `gpui::dock`: an app with
|
||||
//! a single [`DockArea`] hosting four placeholder panels — project bin,
|
||||
//! viewer, inspector, and timeline — plus a [`PanelRegistry`] so layouts can
|
||||
//! be saved and restored.
|
||||
//!
|
||||
//! NOTE: the dock implementation is not finished yet (all of its methods are
|
||||
//! `todo!()`), so this example compiles but panics at runtime until the
|
||||
//! implementation lands.
|
||||
|
||||
// The modules under demo are skeletons whose bodies are `todo!()` by design.
|
||||
#![allow(clippy::todo)]
|
||||
|
||||
#[path = "../shared/prelude.rs"]
|
||||
mod example_prelude;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::dock::{
|
||||
DockArea, DockEvent, DockLayoutState, DockPanel, PanelEvent, PanelHandle, PanelId,
|
||||
PanelRegistry,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, Bounds, Context, Entity, EventEmitter, Render, SharedString, Window,
|
||||
WindowBounds, WindowOptions, div, prelude::*, px, size,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ============================================================================
|
||||
// Demo panels
|
||||
//
|
||||
// Each placeholder is a normal GPUI view plus a `DockPanel` impl. The panel
|
||||
// ids double as the persistence mapping below.
|
||||
// ============================================================================
|
||||
|
||||
const PROJECT_BIN_ID: PanelId = PanelId::new(1);
|
||||
const VIEWER_ID: PanelId = PanelId::new(2);
|
||||
const INSPECTOR_ID: PanelId = PanelId::new(3);
|
||||
const TIMELINE_ID: PanelId = PanelId::new(4);
|
||||
|
||||
/// Shared shape of the demo placeholders: a labeled box.
|
||||
struct PlaceholderPanel {
|
||||
id: PanelId,
|
||||
title: &'static str,
|
||||
}
|
||||
|
||||
impl PlaceholderPanel {
|
||||
fn new(id: PanelId, title: &'static str) -> Self {
|
||||
Self { id, title }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PlaceholderPanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(format!("{} (placeholder)", self.title))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for PlaceholderPanel {}
|
||||
|
||||
impl DockPanel for PlaceholderPanel {
|
||||
fn panel_id(&self) -> PanelId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> SharedString {
|
||||
self.title.into()
|
||||
}
|
||||
|
||||
fn tab_content(&self, _cx: &App) -> AnyElement {
|
||||
div().child(self.title).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Panel registry: bridges string keys (persisted) and live panel views.
|
||||
// ============================================================================
|
||||
|
||||
struct DemoPanelRegistry;
|
||||
|
||||
impl PanelRegistry for DemoPanelRegistry {
|
||||
fn panel_key(&self, id: PanelId) -> Option<String> {
|
||||
match id {
|
||||
PROJECT_BIN_ID => Some("project-bin".into()),
|
||||
VIEWER_ID => Some("viewer".into()),
|
||||
INSPECTOR_ID => Some("inspector".into()),
|
||||
TIMELINE_ID => Some("timeline".into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_panel(&self, key: &str, _window: &mut Window, cx: &mut App) -> Option<PanelHandle> {
|
||||
let (id, title) = match key {
|
||||
"project-bin" => (PROJECT_BIN_ID, "Project Bin"),
|
||||
"viewer" => (VIEWER_ID, "Viewer"),
|
||||
"inspector" => (INSPECTOR_ID, "Inspector"),
|
||||
"timeline" => (TIMELINE_ID, "Timeline"),
|
||||
_ => return None,
|
||||
};
|
||||
Some(PanelHandle::new(
|
||||
cx.new(|_| PlaceholderPanel::new(id, title)),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Root view: just hosts the dock area and logs its events.
|
||||
// ============================================================================
|
||||
|
||||
struct DockLayoutExample {
|
||||
dock: Entity<DockArea>,
|
||||
}
|
||||
|
||||
impl DockLayoutExample {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let dock = cx.new(|cx| DockArea::new(cx).with_registry(Arc::new(DemoPanelRegistry)));
|
||||
|
||||
// Seed a default workspace. Once the dock is implemented this will
|
||||
// instead attempt `restore_state` from a persisted `DockLayoutState`
|
||||
// first, falling back to this default when none exists.
|
||||
let panels: Vec<PanelHandle> = ["project-bin", "viewer", "inspector", "timeline"]
|
||||
.into_iter()
|
||||
.filter_map(|key| DemoPanelRegistry.build_panel(key, window, cx))
|
||||
.collect();
|
||||
dock.update(cx, |dock, cx| {
|
||||
for panel in panels {
|
||||
dock.add_panel(panel, None, cx);
|
||||
}
|
||||
});
|
||||
|
||||
// Autosave hook: persist on every layout change.
|
||||
cx.subscribe(&dock, |_this, dock: Entity<DockArea>, event: &DockEvent, cx| {
|
||||
if let DockEvent::LayoutChanged = event {
|
||||
let _state: DockLayoutState = dock.read(cx).save_state();
|
||||
todo!("serialize `_state` with serde_json and write it to the app data dir");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { dock }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DockLayoutExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.dock.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1200.), px(800.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| DockLayoutExample::new(window, cx)),
|
||||
)
|
||||
.expect("Failed to open window");
|
||||
|
||||
init_example(cx, "Dock Layout");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
//! Intended-usage sketch of the `gpui::effect_stack` widget.
|
||||
//!
|
||||
//! Builds a mock stack (Media → Transform → OCIO LUT → Output), hosts an
|
||||
//! [`EffectStackView`], and logs every edit request it emits. The mock data
|
||||
//! source simply applies requests in place and calls `cx.notify()`; a real
|
||||
//! app (Oak) would route them through its engine and undo stack.
|
||||
//!
|
||||
//! NOTE: the widget implementation itself is still a skeleton (`todo!()`),
|
||||
//! so this example compiles but does not render yet.
|
||||
|
||||
// The modules under demo are skeletons whose bodies are `todo!()` by design.
|
||||
#![allow(clippy::todo)]
|
||||
|
||||
#[path = "../shared/prelude.rs"]
|
||||
mod example_prelude;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use example_prelude::init_example;
|
||||
use gpui::effect_stack::{
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent, EffectStackView,
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, Context, Entity, Render, SharedString, Window, WindowBounds, WindowOptions, div,
|
||||
prelude::*, px, size,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct MockEffect {
|
||||
id: EffectId,
|
||||
kind: EffectCardKind,
|
||||
title: String,
|
||||
subtitle: Option<String>,
|
||||
enabled: bool,
|
||||
expanded: bool,
|
||||
badge: Option<usize>,
|
||||
}
|
||||
|
||||
impl EffectData for MockEffect {
|
||||
fn id(&self) -> EffectId {
|
||||
self.id
|
||||
}
|
||||
fn kind(&self) -> EffectCardKind {
|
||||
self.kind
|
||||
}
|
||||
fn title(&self) -> SharedString {
|
||||
self.title.clone().into()
|
||||
}
|
||||
fn subtitle(&self) -> Option<SharedString> {
|
||||
self.subtitle.clone().map(Into::into)
|
||||
}
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
fn is_expanded(&self) -> bool {
|
||||
self.expanded
|
||||
}
|
||||
fn badge_count(&self) -> Option<usize> {
|
||||
self.badge
|
||||
}
|
||||
}
|
||||
|
||||
/// The mock data source. In Oak this would be a view-model entity deriving
|
||||
/// the ordered card list from the node-graph path of the selected clip.
|
||||
struct MockStack {
|
||||
clip_name: String,
|
||||
effects: Vec<MockEffect>,
|
||||
/// Reserved for allocating ids to effects added at runtime.
|
||||
#[allow(dead_code)]
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl MockStack {
|
||||
fn demo() -> Self {
|
||||
Self {
|
||||
clip_name: "A001_C002_0103.mov".to_string(),
|
||||
effects: vec![
|
||||
MockEffect {
|
||||
id: EffectId(0),
|
||||
kind: EffectCardKind::Source,
|
||||
title: "Media".into(),
|
||||
subtitle: Some("A001_C002_0103.mov".into()),
|
||||
enabled: true,
|
||||
expanded: false,
|
||||
badge: None,
|
||||
},
|
||||
MockEffect {
|
||||
id: EffectId(1),
|
||||
kind: EffectCardKind::Effect,
|
||||
title: "Transform".into(),
|
||||
subtitle: Some("scale 100%, rotate 0°".into()),
|
||||
enabled: true,
|
||||
expanded: true,
|
||||
badge: Some(2),
|
||||
},
|
||||
MockEffect {
|
||||
id: EffectId(2),
|
||||
kind: EffectCardKind::Effect,
|
||||
title: "OCIO LUT".into(),
|
||||
subtitle: Some("filmic_to_display.cube".into()),
|
||||
enabled: true,
|
||||
expanded: false,
|
||||
badge: None,
|
||||
},
|
||||
MockEffect {
|
||||
id: EffectId(3),
|
||||
kind: EffectCardKind::Output,
|
||||
title: "Output".into(),
|
||||
subtitle: None,
|
||||
enabled: true,
|
||||
expanded: false,
|
||||
badge: None,
|
||||
},
|
||||
],
|
||||
next_id: 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectStackDataSource for MockStack {
|
||||
fn effects(&self) -> Vec<Arc<dyn EffectData>> {
|
||||
self.effects
|
||||
.iter()
|
||||
.map(|effect| {
|
||||
Arc::new(MockEffect {
|
||||
id: effect.id,
|
||||
kind: effect.kind,
|
||||
title: effect.title.clone(),
|
||||
subtitle: effect.subtitle.clone(),
|
||||
enabled: effect.enabled,
|
||||
expanded: effect.expanded,
|
||||
badge: effect.badge,
|
||||
}) as Arc<dyn EffectData>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn target_label(&self) -> Option<SharedString> {
|
||||
Some(self.clip_name.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Root view: hosts the stack and applies edit requests to the mock model.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Placeholder parameter view. A real app builds the effect's controls here
|
||||
/// and calls [`EffectStackView::notify_parameter_changed`] after edits.
|
||||
struct MockParams {
|
||||
effect: EffectId,
|
||||
}
|
||||
|
||||
impl Render for MockParams {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child(format!("parameters for {} (mock)", self.effect))
|
||||
}
|
||||
}
|
||||
|
||||
struct StackDemoRoot {
|
||||
data: Entity<MockStack>,
|
||||
stack: Entity<EffectStackView<MockStack>>,
|
||||
}
|
||||
|
||||
impl StackDemoRoot {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let data = cx.new(|_cx| MockStack::demo());
|
||||
|
||||
let stack = cx.new(|cx| {
|
||||
EffectStackView::new(data.clone(), cx).params_renderer(|id, _window, cx| {
|
||||
// Real apps build the effect's parameter controls here. The
|
||||
// mock just shows a placeholder label.
|
||||
cx.new(|_cx| MockParams { effect: *id }).into()
|
||||
})
|
||||
});
|
||||
|
||||
// The "edits are requests" loop: log each request, apply it to the
|
||||
// model (Oak: engine command + undo), then notify.
|
||||
cx.subscribe_in(&stack, window, {
|
||||
let data = data.clone();
|
||||
move |_root, _stack, event: &EffectStackEvent, _window, cx| {
|
||||
println!("[effect_stack] request: {event:?}");
|
||||
data.update(cx, |data, cx| {
|
||||
match event {
|
||||
EffectStackEvent::EnableToggled { effect, enabled } => {
|
||||
if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) {
|
||||
e.enabled = *enabled;
|
||||
}
|
||||
}
|
||||
EffectStackEvent::ExpansionToggled { effect, expanded } => {
|
||||
if let Some(e) = data.effects.iter_mut().find(|e| e.id == *effect) {
|
||||
e.expanded = *expanded;
|
||||
}
|
||||
}
|
||||
EffectStackEvent::ReorderRequested { .. }
|
||||
| EffectStackEvent::RemoveRequested(_)
|
||||
| EffectStackEvent::AddRequested { .. }
|
||||
| EffectStackEvent::ContextMenuRequested { .. }
|
||||
| EffectStackEvent::ParameterChanged { .. } => {
|
||||
todo!("apply {event:?} to the mock model (or engine, in a real app)")
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { data, stack }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackDemoRoot {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let _ = &self.data;
|
||||
let _ = cx;
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(self.stack.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
init_example(cx, "Effect Stack");
|
||||
|
||||
let bounds = Bounds::centered(None, size(px(420.0), px(640.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| StackDemoRoot::new(window, cx)),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
//! Intended-usage sketch for the `gpui::node_graph` node-graph editor.
|
||||
//!
|
||||
//! Builds a mock video pipeline (media → transform → output), implements the
|
||||
//! data-source traits over it, and subscribes to the view's edit-request
|
||||
//! events, logging each one. The real Oak integration maps these events onto
|
||||
//! engine operations wrapped in undo commands — see the "Wiring into Oak"
|
||||
//! section of [`gpui::node_graph`].
|
||||
//!
|
||||
//! NOTE: the widget itself is still an API skeleton (`todo!()` bodies), so
|
||||
//! running this example will panic as soon as the view renders. It exists to
|
||||
//! pin down the intended usage and keep it compiling.
|
||||
|
||||
// The modules under demo are skeletons whose bodies are `todo!()` by design.
|
||||
#![allow(clippy::todo)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Entity, Hsla, Pixels, Point, Render, SharedString, Window, WindowBounds,
|
||||
WindowOptions, div, point, prelude::*, px, size,
|
||||
};
|
||||
use gpui::node_graph::{
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeGraphView, NodeId,
|
||||
PortData, PortDataType, PortId, PortKind,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock graph data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockPort {
|
||||
id: PortId,
|
||||
kind: PortKind,
|
||||
label: &'static str,
|
||||
data_type: PortDataType,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
impl PortData for MockPort {
|
||||
fn id(&self) -> PortId {
|
||||
self.id
|
||||
}
|
||||
fn kind(&self) -> PortKind {
|
||||
self.kind
|
||||
}
|
||||
fn label(&self) -> SharedString {
|
||||
self.label.into()
|
||||
}
|
||||
fn data_type(&self) -> PortDataType {
|
||||
self.data_type.clone()
|
||||
}
|
||||
fn is_connected(&self) -> bool {
|
||||
self.connected
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockNode {
|
||||
id: NodeId,
|
||||
title: &'static str,
|
||||
position: Point<Pixels>,
|
||||
inputs: Vec<MockPort>,
|
||||
outputs: Vec<MockPort>,
|
||||
header_color: Option<Hsla>,
|
||||
}
|
||||
|
||||
impl NodeData for MockNode {
|
||||
type Port = MockPort;
|
||||
|
||||
fn id(&self) -> NodeId {
|
||||
self.id
|
||||
}
|
||||
fn title(&self) -> SharedString {
|
||||
self.title.into()
|
||||
}
|
||||
fn position(&self) -> Point<Pixels> {
|
||||
self.position
|
||||
}
|
||||
fn inputs(&self) -> Vec<MockPort> {
|
||||
self.inputs.clone()
|
||||
}
|
||||
fn outputs(&self) -> Vec<MockPort> {
|
||||
self.outputs.clone()
|
||||
}
|
||||
fn header_color(&self) -> Option<Hsla> {
|
||||
self.header_color
|
||||
}
|
||||
fn is_collapsed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MockEdge {
|
||||
id: EdgeId,
|
||||
from_node: NodeId,
|
||||
from_port: PortId,
|
||||
to_node: NodeId,
|
||||
to_port: PortId,
|
||||
}
|
||||
|
||||
impl EdgeData for MockEdge {
|
||||
fn id(&self) -> EdgeId {
|
||||
self.id
|
||||
}
|
||||
fn from_node(&self) -> NodeId {
|
||||
self.from_node
|
||||
}
|
||||
fn from_port(&self) -> PortId {
|
||||
self.from_port
|
||||
}
|
||||
fn to_node(&self) -> NodeId {
|
||||
self.to_node
|
||||
}
|
||||
fn to_port(&self) -> PortId {
|
||||
self.to_port
|
||||
}
|
||||
}
|
||||
|
||||
/// The mock pipeline: media → transform → output.
|
||||
struct MockGraph {
|
||||
nodes: Vec<MockNode>,
|
||||
edges: Vec<MockEdge>,
|
||||
}
|
||||
|
||||
impl MockGraph {
|
||||
fn new() -> Self {
|
||||
let video = PortDataType::new("video", Hsla::blue());
|
||||
// Port id packing: node id in the high bits, port index low. Inputs
|
||||
// and outputs share one index space per node.
|
||||
let port = |node: u64, index: u64| PortId((node << 32) | index);
|
||||
|
||||
let media = MockNode {
|
||||
id: NodeId(1),
|
||||
title: "Media",
|
||||
position: point(px(40.), px(80.)),
|
||||
inputs: vec![],
|
||||
outputs: vec![MockPort {
|
||||
id: port(1, 0),
|
||||
kind: PortKind::Output,
|
||||
label: "video",
|
||||
data_type: video.clone(),
|
||||
connected: true,
|
||||
}],
|
||||
header_color: Some(Hsla::green()),
|
||||
};
|
||||
let transform = MockNode {
|
||||
id: NodeId(2),
|
||||
title: "Transform",
|
||||
position: point(px(320.), px(140.)),
|
||||
inputs: vec![MockPort {
|
||||
id: port(2, 0),
|
||||
kind: PortKind::Input,
|
||||
label: "in",
|
||||
data_type: video.clone(),
|
||||
connected: true,
|
||||
}],
|
||||
outputs: vec![MockPort {
|
||||
id: port(2, 1),
|
||||
kind: PortKind::Output,
|
||||
label: "out",
|
||||
data_type: video.clone(),
|
||||
connected: true,
|
||||
}],
|
||||
header_color: None,
|
||||
};
|
||||
let output = MockNode {
|
||||
id: NodeId(3),
|
||||
title: "Output",
|
||||
position: point(px(600.), px(200.)),
|
||||
inputs: vec![MockPort {
|
||||
id: port(3, 0),
|
||||
kind: PortKind::Input,
|
||||
label: "in",
|
||||
data_type: video,
|
||||
connected: true,
|
||||
}],
|
||||
outputs: vec![],
|
||||
header_color: Some(Hsla::red()),
|
||||
};
|
||||
|
||||
let edges = vec![
|
||||
MockEdge {
|
||||
id: EdgeId(1),
|
||||
from_node: NodeId(1),
|
||||
from_port: port(1, 0),
|
||||
to_node: NodeId(2),
|
||||
to_port: port(2, 0),
|
||||
},
|
||||
MockEdge {
|
||||
id: EdgeId(2),
|
||||
from_node: NodeId(2),
|
||||
from_port: port(2, 1),
|
||||
to_node: NodeId(3),
|
||||
to_port: port(3, 0),
|
||||
},
|
||||
];
|
||||
|
||||
Self {
|
||||
nodes: vec![media, transform, output],
|
||||
edges,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeGraphDataSource for MockGraph {
|
||||
type Node = MockNode;
|
||||
type Edge = MockEdge;
|
||||
|
||||
fn nodes(&self) -> Vec<MockNode> {
|
||||
self.nodes.clone()
|
||||
}
|
||||
fn edges(&self) -> Vec<MockEdge> {
|
||||
self.edges.clone()
|
||||
}
|
||||
fn can_connect(&self, _from: PortId, _to: PortId) -> bool {
|
||||
// A real app checks type compatibility, cycles and cardinality here.
|
||||
// The mock allows everything between distinct ports.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App view: hosts the graph view and logs edit requests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct NodeGraphExample {
|
||||
graph: Entity<MockGraph>,
|
||||
view: Entity<NodeGraphView<MockGraph>>,
|
||||
}
|
||||
|
||||
impl NodeGraphExample {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let graph = cx.new(|_cx| MockGraph::new());
|
||||
let view = cx.new(|cx| NodeGraphView::new(graph.clone(), window, cx));
|
||||
|
||||
// In Oak, each event becomes an engine operation wrapped in an undo
|
||||
// command; here we just log the request.
|
||||
cx.subscribe(&view, |_this, _view, event: &NodeGraphEvent, cx| {
|
||||
println!("[node_graph] edit request: {event:?}");
|
||||
// After applying a request to the model, notify so the view
|
||||
// re-reads it, e.g.: `graph.update(cx, |_, cx| cx.notify())`.
|
||||
let _ = cx;
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { graph, view }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NodeGraphExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let _ = &self.graph;
|
||||
div().size_full().child(self.view.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(900.), px(600.)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| NodeGraphExample::new(window, cx)),
|
||||
)
|
||||
.expect("failed to open window");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Demo of the `gpui::timeline` video-editing timeline widget.
|
||||
//!
|
||||
//! This is the intended-usage sketch: a mock [`TimelineDataSource`] with two
|
||||
//! video and two audio tracks carrying a handful of static clips, a
|
||||
//! [`TimelineView`] placed in a window, and an event subscription that logs
|
||||
//! the edit requests the widget emits.
|
||||
//!
|
||||
//! In a real host (Oak), the `match` arm in `TimelineExample::new` is where
|
||||
//! each [`TimelineEvent`] becomes an undoable engine command, followed by a
|
||||
//! `cx.notify()` on the model entity.
|
||||
//!
|
||||
//! NOTE: the timeline's rendering and interaction internals are still
|
||||
//! `todo!()`; this example compiles and shows the wiring, not a usable UI.
|
||||
|
||||
// The modules under demo are skeletons whose bodies are `todo!()` by design.
|
||||
#![allow(clippy::todo)]
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Context, Entity, Pixels, Render, SharedString, Window, WindowBounds,
|
||||
WindowOptions, div, prelude::*, px, size,
|
||||
};
|
||||
use gpui::timeline::{
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, TimelineDataSource, TimelineEvent,
|
||||
TimelineView, TrackData, TrackKind,
|
||||
};
|
||||
|
||||
#[path = "../shared/prelude.rs"]
|
||||
mod example_prelude;
|
||||
|
||||
// --- mock model ------------------------------------------------------------
|
||||
|
||||
struct MockClip {
|
||||
id: ClipId,
|
||||
range: FrameRange,
|
||||
media_in: Frame,
|
||||
label: SharedString,
|
||||
}
|
||||
|
||||
impl ClipData for MockClip {
|
||||
fn id(&self) -> ClipId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn range(&self) -> FrameRange {
|
||||
self.range
|
||||
}
|
||||
|
||||
fn media_in(&self) -> Frame {
|
||||
self.media_in
|
||||
}
|
||||
|
||||
fn label(&self) -> SharedString {
|
||||
self.label.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct MockTrack {
|
||||
kind: TrackKind,
|
||||
name: SharedString,
|
||||
height: Pixels,
|
||||
clips: Vec<MockClip>,
|
||||
}
|
||||
|
||||
impl TrackData for MockTrack {
|
||||
type Clip = MockClip;
|
||||
|
||||
fn kind(&self) -> TrackKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn height(&self) -> Pixels {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn clips(&self) -> &[Self::Clip] {
|
||||
&self.clips
|
||||
}
|
||||
}
|
||||
|
||||
struct MockSequence {
|
||||
tracks: Vec<MockTrack>,
|
||||
}
|
||||
|
||||
impl MockSequence {
|
||||
fn demo() -> Self {
|
||||
let clip = |id: u64, start: i64, end: i64, label: &str| MockClip {
|
||||
id: ClipId(id),
|
||||
range: FrameRange::new(Frame(start), Frame(end)),
|
||||
media_in: Frame::ZERO,
|
||||
label: label.into(),
|
||||
};
|
||||
MockSequence {
|
||||
tracks: vec![
|
||||
MockTrack {
|
||||
kind: TrackKind::Video,
|
||||
name: "V1".into(),
|
||||
height: px(64.),
|
||||
clips: vec![clip(1, 0, 240, "opening.mov"), clip(2, 240, 600, "b-roll.mp4")],
|
||||
},
|
||||
MockTrack {
|
||||
kind: TrackKind::Video,
|
||||
name: "V2".into(),
|
||||
height: px(64.),
|
||||
clips: vec![clip(3, 120, 300, "title.mov")],
|
||||
},
|
||||
MockTrack {
|
||||
kind: TrackKind::Audio,
|
||||
name: "A1".into(),
|
||||
height: px(48.),
|
||||
clips: vec![clip(4, 0, 600, "dialog.wav")],
|
||||
},
|
||||
MockTrack {
|
||||
kind: TrackKind::Audio,
|
||||
name: "A2".into(),
|
||||
height: px(48.),
|
||||
clips: vec![clip(5, 0, 480, "score.flac")],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TimelineDataSource for MockSequence {
|
||||
type Track = MockTrack;
|
||||
|
||||
fn frame_rate(&self) -> FrameRate {
|
||||
FrameRate::NTSC_2997
|
||||
}
|
||||
|
||||
fn sequence_length(&self) -> Frame {
|
||||
Frame(600)
|
||||
}
|
||||
|
||||
fn track_count(&self) -> usize {
|
||||
self.tracks.len()
|
||||
}
|
||||
|
||||
fn track(&self, index: usize) -> Option<Self::Track> {
|
||||
// A real host returns a lightweight snapshot; the mock simply
|
||||
// reports the track's existence. Returning an owned value here is
|
||||
// what the trait requires, so the mock clones its clips.
|
||||
self.tracks.get(index).map(|t| MockTrack {
|
||||
kind: t.kind,
|
||||
name: t.name.clone(),
|
||||
height: t.height,
|
||||
clips: t
|
||||
.clips
|
||||
.iter()
|
||||
.map(|c| MockClip {
|
||||
id: c.id,
|
||||
range: c.range,
|
||||
media_in: c.media_in,
|
||||
label: c.label.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- the example view ------------------------------------------------------
|
||||
|
||||
struct TimelineExample {
|
||||
timeline: Entity<TimelineView<MockSequence>>,
|
||||
}
|
||||
|
||||
impl TimelineExample {
|
||||
fn new(model: Entity<MockSequence>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let timeline = cx.new(|cx| TimelineView::new(model, window, cx).zoom(2.0));
|
||||
cx.subscribe(
|
||||
&timeline,
|
||||
|_this, _timeline, event: &TimelineEvent, _cx| {
|
||||
// In Oak, each request becomes an undoable engine command
|
||||
// here, followed by `model.update(cx, |_, cx| cx.notify())`.
|
||||
println!("timeline edit request: {event:?}");
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
TimelineExample { timeline }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TimelineExample {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.timeline.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gpui_platform::application().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1000.), px(480.)), cx);
|
||||
let model = cx.new(|_cx| MockSequence::demo());
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| cx.new(|cx| TimelineExample::new(model, window, cx)),
|
||||
)
|
||||
.expect("Failed to open window");
|
||||
|
||||
example_prelude::init_example(cx, "Timeline");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user