Files
ZacharyZhang-NY 07b875b0d3 Canvas · Panels: layers, inspector, alignment, assets, style and layout editors
LayerPanel lists the canvas's layers topmost first on lists::Tree: a box
shows or hides each, F2 renames, a drag restacks into a new paint order.
InspectorPanel edits the one selected shape's frame, fill, visibility and
lock. AlignmentToolbar lines up or spreads the selection through
`aligned`. AssetPanel finds shapes by name in a picks grid, each drawn
to fit its tile. ColorPanel, ShadowEditor, BorderEditor and
TypographyPanel edit a look; GradientPanel is forms::GradientEditor.
ConstraintsEditor pins a shape to its frame, with `Constraints::placed`;
AutoLayoutControls sets flow, gap, padding and a place grid, with
`AutoLayout::laid`; ExportPanel names each file once. The panels share
`canvas::panel`: editing, pair, caption, heading and color_well.
2026-09-27 07:58:27 -04:00

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.
pub(super) struct Studio {
pub(super) shapes: Vec<Shape>,
pub(super) selected: Vec<SharedString>,
tool: Tool,
brush: Brush,
view: Viewport,
steps: Vec<(SharedString, Vec<Shape>)>,
at: usize,
pub(super) 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.
pub(super) fn record(&mut self, words: String) {
self.steps.truncate(self.at);
self.steps.push((words.into(), self.shapes.clone()));
self.at = self.steps.len();
}
}
pub(super) 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();
})
})),
),
)
}