Components / canvas
NodeGroup builder
A labelled group drawn over a canvas region the host already decided.
A builder is RenderOnce. Construct and mount it in one expression.
use gpui_kit::canvas::NodeGroup;
Reviewed in canvas-tools.
Construct
new(ident: impl Into<Ident>, label: impl Into<SharedString>) -> Self
Options
Chain onto the value.
selected(selected: bool) -> Self
Example · canvas-tools
fn canvas_tools(_window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneCanvasTools>() {
cx.set_global(SceneCanvasTools {
viewport: GraphViewport::default(),
fit: 0,
snap: true,
selected: vec!["scene.canvas.graph.validate".into()],
nodes: arranged_canvas_tools_nodes(),
});
}
let scene = cx.global::<SceneCanvasTools>();
let viewport = scene.viewport;
let fit = scene.fit;
let snap = scene.snap;
let selected = scene.selected.clone();
let [ingest, validate, observe, publish] = scene.nodes;
let theme = cx.theme().clone();
let graph_node =
|id: &'static str, title: &'static str, color: &'static str, state: NodeState| {
GraphNode::new(id, title)
.color(color)
.width(180.0)
.state(state)
.port(GraphPort::input("in", "In"))
.port(GraphPort::output("out", "Out"))
.selected(selected.iter().any(|selected| selected == id))
};
let minimap_mark = |id: &'static str, position: gpui::Point<f32>, color: &'static str| {
MinimapMark::new(
id,
position.x / CANVAS_TOOLS_WORLD_WIDTH,
position.y / CANVAS_TOOLS_WORLD_HEIGHT,
180.0 / CANVAS_TOOLS_WORLD_WIDTH,
120.0 / CANVAS_TOOLS_WORLD_HEIGHT,
)
.color(color)
};
let member = |id: &'static str,
title: &'static str,
color: &'static str,
state: NodeState,
action: &'static str,
metric: (&'static str, &'static str)| {
GraphNode::new(id, title)
.color(color)
.width(236.0)
.state(state)
.action(action)
.metric(metric.0, metric.1)
};
stack(&theme)
.w(px(840.0))
.child(caption(
&theme,
"the overview, glass chrome, snap, arrange, and fit all act on this controlled canvas",
))
.child(
div()
.row()
.items_start()
.gap_token(&theme, Space::Md)
.child(
div()
.w(px(CANVAS_TOOLS_FRAME_WIDTH))
.h(px(CANVAS_TOOLS_FRAME_HEIGHT))
.child(
NodeGraph::new("scene.canvas.graph")
.viewport(viewport)
.toolbar(
CanvasToolbar::new(
"scene.canvas.toolbar",
cx.numbers().percent(viewport.zoom),
)
.snap(snap)
.glass(GlassPreset::Frosted)
.on_action(|action, _, cx| {
cx.update_global::<SceneCanvasTools, ()>(
|scene, _| match action {
CanvasToolbarAction::Fit => {
scene.fit = scene.fit.wrapping_add(1);
}
CanvasToolbarAction::Snap => {
scene.snap = !scene.snap;
}
CanvasToolbarAction::Arrange => {
scene.nodes = arranged_canvas_tools_nodes();
scene.fit = scene.fit.wrapping_add(1);
}
},
);
cx.refresh_windows();
}),
)
.fit(GraphFit::Whole(fit))
.interaction(GraphInteraction::Arrange)
.node(
graph_node(
"scene.canvas.graph.ingest",
"Stream ingest",
"teal",
NodeState::Succeeded,
),
ingest.x,
ingest.y,
)
.node(
graph_node(
"scene.canvas.graph.validate",
"Validate & enrich",
"indigo",
NodeState::Running,
),
validate.x,
validate.y,
)
.node(
graph_node(
"scene.canvas.graph.observe",
"Observe quality",
"orange",
NodeState::Failed,
),
observe.x,
observe.y,
)
.node(
graph_node(
"scene.canvas.graph.publish",
"Publish artifact",
"lime",
NodeState::Pending,
),
publish.x,
publish.y,
)
.edges([
GraphEdge::new(
"scene.canvas.graph.ingest",
"scene.canvas.graph.validate",
)
.ports("out", "in")
.active(true),
GraphEdge::new(
"scene.canvas.graph.validate",
"scene.canvas.graph.observe",
)
.ports("out", "in"),
GraphEdge::new(
"scene.canvas.graph.observe",
"scene.canvas.graph.publish",
)
.ports("out", "in"),
])
.on_event(|event, _, cx| {
cx.update_global::<SceneCanvasTools, ()>(
|scene, _| match event {
NodeGraphEvent::ViewportChanged(viewport) => {
scene.viewport = *viewport;
}
NodeGraphEvent::SelectionChanged { ids } => {
scene.selected = ids.clone();
}
NodeGraphEvent::NodeMoved { id, position } => {
let position = if scene.snap {
canvas_tools_snap(*position)
} else {
*position
};
match id.as_ref() {
"scene.canvas.graph.ingest" => {
scene.nodes[0] = position;
}
"scene.canvas.graph.validate" => {
scene.nodes[1] = position;
}
"scene.canvas.graph.observe" => {
scene.nodes[2] = position;
}
"scene.canvas.graph.publish" => {
scene.nodes[3] = position;
}
_ => {}
}
}
NodeGraphEvent::NodeDeleted { .. }
| NodeGraphEvent::NodeResized { .. }
| NodeGraphEvent::SurfacePressed { .. }
| NodeGraphEvent::ConnectionRequested { .. }
| NodeGraphEvent::ConnectionDropped { .. }
| NodeGraphEvent::DisconnectRequested { .. } => {}
},
);
cx.refresh_windows();
}),
)
)
.child(
div()
.w(px(160.0))
.flex_none()
.child(
Minimap::new("scene.canvas.minimap")
.marks([
minimap_mark("ingest", ingest, "teal"),
minimap_mark("validate", validate, "indigo"),
minimap_mark("observe", observe, "orange"),
minimap_mark("publish", publish, "lime"),
])
.view(canvas_tools_minimap_view(viewport))
.on_pan(|x, y, _, cx| {
cx.update_global::<SceneCanvasTools, ()>(|scene, _| {
scene.viewport =
canvas_tools_viewport_at(scene.viewport, x, y);
});
cx.refresh_windows();
}),
),
),
)
.child(caption(
&theme,
"a NodeGroup remains host layout: it names these related cards, not a world-space region",
))
.child(
div().relative().w(px(540.0)).child(
NodeGroup::new("scene.canvas.group", "Ingest")
.selected(true)
.child(
div()
.row()
.items_start()
.gap_token(&theme, Space::Md)
.child(
member(
"scene.canvas.node.ingest",
"Stream ingest",
"teal",
NodeState::Succeeded,
"orders.v2 · partition 18",
("rate", "3.2k/s"),
)
.into_any_element(),
)
.child(
member(
"scene.canvas.node.validate",
"Validate & enrich",
"indigo",
NodeState::Running,
"schema + fraud signals",
("p95", "18 ms"),
)
.into_any_element(),
),
),
),
)
.into_any_element()
}