Files
Ely-GPUI-Components/examples/gallery/pages/canvas/plane.rs
T
ZacharyZhang-NY c474a3eb33 Canvas · Tools: select, move and snap, resize, draw, pen, text, brush
ToolLayer lies over an InfiniteCanvas and carries its tools. Select
takes the topmost shape a press reaches, Shift adds or drops one, a
drag moves the selection and snaps its nearest edge or middle to
another shape's within reach, a handle resizes it, Shift keeping a
corner's shape, and a drag on empty space draws a SelectionBox that
takes what lies wholly inside. The shape tools draw by a drag toward
any corner, Shift keeping them square; the pen adds a point per press
and a double press ends the path; text drops a line where pressed; the
brush strokes a path its width. A drag may end in the layer or out of
it; each gesture ends once and asks the owner. SelectionBox,
TransformHandles and SnapIndicator draw the marks.

ToolPalette picks a tool from icons named in tips; BrushSettings sets a
brush's width and hue; HistoryPanel lists the steps done, the undone
ones quiet, and takes the drawing to a step on Enter or a double press.

The model changes: lines and arrows end at a named corner, so they run
either way, and paths carry their width. Three Lucide icons: hexagon,
slash and move-up-right.
2026-09-27 07:27:27 -04:00

164 lines
5.0 KiB
Rust

use ely_gpui_component::{
canvas::{
Artboard, Corner, Frame, Guide, InfiniteCanvas, MiniMap, Shape, ShapeKind, Viewport,
ZoomControls,
},
theme::{ActiveTheme, Radius},
};
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use crate::{
probe::probe,
ui::{keep, section, set},
};
/// The demo's view, in pixels.
const VIEW: (f32, f32) = (820.0, 460.0);
fn boards() -> Vec<Artboard> {
vec![
Artboard::new("home", "Home · 390", Frame::new(0.0, 0.0, 390.0, 640.0)),
Artboard::new(
"detail",
"Detail · 390",
Frame::new(470.0, 0.0, 390.0, 640.0),
),
]
}
fn shapes() -> Vec<Shape> {
vec![
Shape::new(
"hero",
"Hero",
ShapeKind::Rect,
Frame::new(24.0, 32.0, 342.0, 180.0),
)
.hue(0),
Shape::new(
"title",
"Title",
ShapeKind::Text("Studio Atrium".into()),
Frame::new(24.0, 232.0, 342.0, 32.0),
),
Shape::new(
"dot",
"Status",
ShapeKind::Ellipse,
Frame::new(24.0, 290.0, 56.0, 56.0),
)
.hue(5),
Shape::new(
"badge",
"Badge",
ShapeKind::Polygon(6),
Frame::new(100.0, 290.0, 56.0, 56.0),
)
.hue(2),
Shape::new(
"rule",
"Rule",
ShapeKind::Line(Corner::BottomRight),
Frame::new(24.0, 380.0, 342.0, 0.0),
)
.hue(4),
Shape::new(
"flow",
"Flow",
ShapeKind::Arrow(Corner::BottomRight),
Frame::new(390.0, 120.0, 80.0, 0.0),
)
.hue(3),
Shape::new(
"sketch",
"Sketch",
ShapeKind::Path {
points: vec![
(0.0, 40.0),
(40.0, 0.0),
(80.0, 30.0),
(120.0, 10.0),
(160.0, 50.0),
],
width: 2.0,
},
Frame::new(494.0, 60.0, 160.0, 50.0),
)
.hue(1),
Shape::new(
"note",
"Note",
ShapeKind::Note("Ask Chloé about the stair".into()),
Frame::new(494.0, 150.0, 180.0, 120.0),
)
.hue(2),
]
}
pub fn plane(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let view = keep(
"canvas-view",
|| Viewport::new(-60.0, -60.0, 0.7),
window,
cx,
);
let now = *view.read(cx);
let frames: Vec<Frame> = boards().iter().map(|board| board.frame).collect();
let whole = frames
.iter()
.skip(1)
.fold(frames[0], |whole, frame| whole.union(frame));
let (panned, zoomed, fitted, steered) = (view.clone(), view.clone(), view.clone(), view);
let theme = cx.theme();
let canvas = InfiniteCanvas::new("canvas-plane", now)
.artboards(boards())
.shapes(shapes())
.guides([Guide::Vertical(24.0), Guide::Horizontal(212.0)])
.rulers()
.on_viewport(move |next, _, cx| set(&panned, next, cx))
.layer(
div()
.absolute()
.bottom_3()
.right_3()
.p_1()
.rounded(theme.radius(Radius::Md))
.bg(theme.colors.surface)
.border_1()
.border_color(theme.colors.border)
.child(
ZoomControls::new("canvas-zoom", now.zoom)
.on_zoom(move |zoom, _, cx| {
let now = *zoomed.read(cx);
let middle = (VIEW.0 / 2.0, VIEW.1 / 2.0);
set(&zoomed, now.zoomed(zoom / now.zoom, middle), cx)
})
.on_fit(move |_, cx| {
set(&fitted, Viewport::fitting(whole, VIEW, 48.0), cx)
}),
),
)
.layer(
div().absolute().bottom_3().left_8().child(
MiniMap::new("canvas-map", frames, now, VIEW)
.on_viewport(move |next, _, cx| set(&steered, next, cx)),
),
);
section(
"InfiniteCanvas · CanvasGrid / Guides · Ruler · ArtboardFrame · ZoomControls / ZoomIndicator · MiniMap",
"An endless plane: a dot grid that thins as it zooms out, rulers in canvas units, guides, and artboards with their names. Drag to pan, scroll to move, and hold Command to zoom about the pointer. The zoom steps in fixed stops, and the minimap moves the view to a press.",
cx,
)
.child(probe(
"canvas-plane",
div()
.w(px(VIEW.0))
.h(px(VIEW.1))
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(theme.colors.border)
.overflow_hidden()
.child(canvas),
))
}