Files
oak-gpui/crates/gpui/examples/learn/node_graph.rs
T
Mike-Solar ad7c965c5a 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.
2026-08-09 03:31:06 +08:00

274 lines
7.6 KiB
Rust

//! 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);
});
}