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.
This commit is contained in:
1 parent
50259aff77
commit
c474a3eb33
20 files changed
+1812
-34
No files matched your search
@@ -1,6 +1,7 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod plane;
|
||||
mod tools;
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
@@ -20,8 +21,26 @@ const SCRIPT: &[Step] = &[
|
||||
Step::UpAt("canvas-plane", 300.0, 240.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("panned"),
|
||||
Step::DownAt("canvas-tools", 150.0, 110.0),
|
||||
Step::DragTo("canvas-tools", 170.0, 112.0),
|
||||
Step::DragTo("canvas-tools", 187.0, 112.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("snapping"),
|
||||
Step::UpAt("canvas-tools", 187.0, 112.0),
|
||||
Step::Wait(200),
|
||||
Step::DownAt("canvas-palette", 83.0, 16.0),
|
||||
Step::UpAt("canvas-palette", 83.0, 16.0),
|
||||
Step::Wait(150),
|
||||
Step::DownAt("canvas-tools", 60.0, 230.0),
|
||||
Step::DragTo("canvas-tools", 200.0, 300.0),
|
||||
Step::UpAt("canvas-tools", 200.0, 300.0),
|
||||
Step::Wait(250),
|
||||
Step::Shot("drawn"),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div().child(plane::plane(window, cx)).into_any_element()
|
||||
div()
|
||||
.child(plane::plane(window, cx))
|
||||
.child(tools::tools(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use ely_gpui_component::{
|
||||
canvas::{
|
||||
Artboard, Frame, Guide, InfiniteCanvas, MiniMap, Shape, ShapeKind, Viewport, ZoomControls,
|
||||
Artboard, Corner, Frame, Guide, InfiniteCanvas, MiniMap, Shape, ShapeKind, Viewport,
|
||||
ZoomControls,
|
||||
},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
@@ -57,27 +58,30 @@ fn shapes() -> Vec<Shape> {
|
||||
Shape::new(
|
||||
"rule",
|
||||
"Rule",
|
||||
ShapeKind::Line,
|
||||
ShapeKind::Line(Corner::BottomRight),
|
||||
Frame::new(24.0, 380.0, 342.0, 0.0),
|
||||
)
|
||||
.hue(4),
|
||||
Shape::new(
|
||||
"flow",
|
||||
"Flow",
|
||||
ShapeKind::Arrow,
|
||||
ShapeKind::Arrow(Corner::BottomRight),
|
||||
Frame::new(390.0, 120.0, 80.0, 0.0),
|
||||
)
|
||||
.hue(3),
|
||||
Shape::new(
|
||||
"sketch",
|
||||
"Sketch",
|
||||
ShapeKind::Path(vec![
|
||||
(0.0, 40.0),
|
||||
(40.0, 0.0),
|
||||
(80.0, 30.0),
|
||||
(120.0, 10.0),
|
||||
(160.0, 50.0),
|
||||
]),
|
||||
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),
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
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();
|
||||
})
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user