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.
196 lines
7.1 KiB
Rust
196 lines
7.1 KiB
Rust
use ely_gpui_component::{
|
|
canvas::{
|
|
Brush, BrushSettings, Frame, HistoryPanel, InfiniteCanvas, Shape, ShapeKind, Tool,
|
|
ToolLayer, ToolPalette, Viewport,
|
|
},
|
|
theme::{ActiveTheme, Radius},
|
|
};
|
|
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
|
|
|
use crate::{
|
|
probe::probe,
|
|
ui::{change, keep, section},
|
|
};
|
|
|
|
/// The tools demo's drawing: its shapes, what is selected, the tool and brush, the view, and each step with the shapes after it.
|
|
struct Studio {
|
|
shapes: Vec<Shape>,
|
|
selected: Vec<SharedString>,
|
|
tool: Tool,
|
|
brush: Brush,
|
|
view: Viewport,
|
|
steps: Vec<(SharedString, Vec<Shape>)>,
|
|
at: usize,
|
|
made: usize,
|
|
}
|
|
|
|
impl Studio {
|
|
fn new() -> Self {
|
|
let shapes = vec![
|
|
Shape::new(
|
|
"card",
|
|
"Card",
|
|
ShapeKind::Rect,
|
|
Frame::new(40.0, 40.0, 220.0, 140.0),
|
|
)
|
|
.hue(0),
|
|
Shape::new(
|
|
"badge",
|
|
"Badge",
|
|
ShapeKind::Ellipse,
|
|
Frame::new(300.0, 60.0, 90.0, 90.0),
|
|
)
|
|
.hue(5),
|
|
Shape::new(
|
|
"label",
|
|
"Label",
|
|
ShapeKind::Text("Drag me".into()),
|
|
Frame::new(60.0, 210.0, 160.0, 32.0),
|
|
),
|
|
];
|
|
Self {
|
|
shapes: shapes.clone(),
|
|
selected: vec!["card".into()],
|
|
tool: Tool::Select,
|
|
brush: Brush { size: 4.0, hue: 3 },
|
|
view: Viewport::new(0.0, 0.0, 1.0),
|
|
steps: vec![("Open the drawing".into(), shapes)],
|
|
at: 1,
|
|
made: 0,
|
|
}
|
|
}
|
|
|
|
/// Keeps the shapes as a new step, dropping any steps undone.
|
|
fn record(&mut self, words: String) {
|
|
self.steps.truncate(self.at);
|
|
self.steps.push((words.into(), self.shapes.clone()));
|
|
self.at = self.steps.len();
|
|
}
|
|
}
|
|
|
|
fn studio(window: &mut Window, cx: &mut App) -> Entity<Studio> {
|
|
keep("canvas-studio", Studio::new, window, cx)
|
|
}
|
|
|
|
pub fn tools(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
|
let studio = studio(window, cx);
|
|
let now = studio.read(cx);
|
|
let (tool, view, brush, at) = (now.tool, now.view, now.brush, now.at);
|
|
let (shapes, selected) = (now.shapes.clone(), now.selected.clone());
|
|
let steps: Vec<SharedString> = now.steps.iter().map(|(words, _)| words.clone()).collect();
|
|
let [
|
|
picked,
|
|
viewed,
|
|
chosen,
|
|
drawn,
|
|
moved,
|
|
sized,
|
|
brushed,
|
|
travelled,
|
|
] = [(); 8].map(|_| studio.clone());
|
|
let layer = ToolLayer::new("canvas-tools-layer", tool, view, shapes.clone())
|
|
.selected(selected)
|
|
.brush(brush)
|
|
.on_select(move |keys, _, cx| change(&chosen, cx, |studio| studio.selected = keys.to_vec()))
|
|
.on_draw(move |kind, frame, _, cx| {
|
|
change(&drawn, cx, |studio| {
|
|
studio.made += 1;
|
|
let key = SharedString::from(format!("made-{}", studio.made));
|
|
let hue = match kind {
|
|
ShapeKind::Path { .. } => studio.brush.hue,
|
|
_ => studio.made % 8,
|
|
};
|
|
let words = format!(
|
|
"Draw {}",
|
|
match &kind {
|
|
ShapeKind::Path { .. } => "a stroke",
|
|
ShapeKind::Text(_) => "text",
|
|
_ => "a shape",
|
|
}
|
|
);
|
|
studio
|
|
.shapes
|
|
.push(Shape::new(key.clone(), "Drawn", kind, frame).hue(hue));
|
|
studio.selected = vec![key];
|
|
studio.record(words);
|
|
})
|
|
})
|
|
.on_move(move |keys, (dx, dy), _, cx| {
|
|
change(&moved, cx, |studio| {
|
|
studio
|
|
.shapes
|
|
.iter_mut()
|
|
.filter(|shape| keys.contains(&shape.key))
|
|
.for_each(|shape| {
|
|
shape.frame.x += dx;
|
|
shape.frame.y += dy;
|
|
});
|
|
studio.record(format!("Move {}", keys.len()));
|
|
})
|
|
})
|
|
.on_resize(move |key, frame, _, cx| {
|
|
change(&sized, cx, |studio| {
|
|
let shape = studio
|
|
.shapes
|
|
.iter_mut()
|
|
.find(|shape| shape.key == *key)
|
|
.expect("a listed shape");
|
|
shape.frame = frame;
|
|
studio.record(format!("Resize {key}"));
|
|
})
|
|
});
|
|
let canvas = InfiniteCanvas::new("canvas-tools", view)
|
|
.shapes(shapes)
|
|
.on_viewport(move |next, _, cx| change(&viewed, cx, |studio| studio.view = next))
|
|
.layer(layer);
|
|
let theme = cx.theme();
|
|
section(
|
|
"ToolPalette · SelectionBox · TransformHandles · SnapIndicator · ShapeTools · PenTool / PathEditor · TextTool · BrushTool / BrushSettings · HistoryPanel",
|
|
"Tools at work on the plane. Select presses a shape, Shift adds, a drag moves it and snaps its edges to the others, a handle resizes it, and a drag on empty space draws a marquee. The shape tools draw by a drag, Shift keeping them square; the pen adds a point per press and a double press ends the path; text drops a line; the brush strokes. Each change is a step in the history, which a double press or Enter takes back to.",
|
|
cx,
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.flex_wrap()
|
|
.gap_4()
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.child(probe(
|
|
"canvas-palette",
|
|
ToolPalette::new("canvas-palette", tool)
|
|
.on_change(move |tool, _, cx| change(&picked, cx, |studio| studio.tool = tool)),
|
|
))
|
|
.child(probe(
|
|
"canvas-tools",
|
|
div()
|
|
.w(px(560.))
|
|
.h(px(360.))
|
|
.rounded(theme.radius(Radius::Lg))
|
|
.border_1()
|
|
.border_color(theme.colors.border)
|
|
.overflow_hidden()
|
|
.child(canvas),
|
|
)),
|
|
)
|
|
.child(
|
|
div()
|
|
.w(px(260.))
|
|
.flex()
|
|
.flex_col()
|
|
.gap_4()
|
|
.child(BrushSettings::new("canvas-brush", brush).on_change(move |brush, _, cx| change(&brushed, cx, |studio| studio.brush = brush)))
|
|
.child(HistoryPanel::new("canvas-history", steps, at).on_step(move |at, _, cx| {
|
|
change(&travelled, cx, |studio| {
|
|
studio.at = at;
|
|
studio.shapes = studio.steps[at - 1].1.clone();
|
|
studio.selected.clear();
|
|
})
|
|
})),
|
|
),
|
|
)
|
|
}
|