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.
This commit is contained in:
1 parent
c637347a2a
commit
07b875b0d3
32 files changed
+3118
-18
No files matched your search
@@ -0,0 +1,317 @@
|
||||
use ely_gpui_component::{
|
||||
canvas::{
|
||||
AutoLayout, AutoLayoutControls, Border, BorderEditor, ColorPanel, Constraints,
|
||||
ConstraintsEditor, Dash, ExportFormat, ExportPanel, ExportSetting, Flow, Frame, Pin, Place,
|
||||
Shadow, ShadowEditor, TypeStyle, TypographyPanel,
|
||||
},
|
||||
forms::{GradientEditor, GradientStop, TextInput},
|
||||
theme::{ActiveTheme, TextSize},
|
||||
};
|
||||
use gpui::{
|
||||
App, BoxShadow, FontWeight, Hsla, IntoElement, ParentElement, SharedString, Styled, TextAlign,
|
||||
Window, div, point, prelude::*, px, rgb, rgba,
|
||||
};
|
||||
|
||||
use crate::ui::{change, keep, section};
|
||||
|
||||
/// The style demo's values.
|
||||
struct Look {
|
||||
color: Hsla,
|
||||
stops: Vec<GradientStop>,
|
||||
shadows: Vec<Shadow>,
|
||||
border: Border,
|
||||
style: TypeStyle,
|
||||
}
|
||||
|
||||
impl Look {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
color: rgb(0xf4efe6).into(),
|
||||
stops: vec![
|
||||
GradientStop {
|
||||
at: 0.0,
|
||||
color: rgb(0xe9e4da).into(),
|
||||
},
|
||||
GradientStop {
|
||||
at: 1.0,
|
||||
color: rgb(0x8a7f6d).into(),
|
||||
},
|
||||
],
|
||||
shadows: vec![Shadow {
|
||||
x: 0.0,
|
||||
y: 8.0,
|
||||
blur: 24.0,
|
||||
spread: 0.0,
|
||||
color: rgba(0x1816132e).into(),
|
||||
}],
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
dash: Dash::Solid,
|
||||
color: rgb(0xd8d1c4).into(),
|
||||
radii: [12.0; 4],
|
||||
},
|
||||
style: TypeStyle {
|
||||
family: "Inter".into(),
|
||||
weight: FontWeight::MEDIUM,
|
||||
size: 20.0,
|
||||
line_height: 1.3,
|
||||
tracking: 0.0,
|
||||
align: TextAlign::Left,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SWATCHES: [(&str, u32); 6] = [
|
||||
("Paper", 0xf4efe6),
|
||||
("Sand", 0xe9e4da),
|
||||
("Stone", 0xb8ad9b),
|
||||
("Moss", 0x7d8a6a),
|
||||
("Ink", 0x2b2a27),
|
||||
("Clay", 0xb86f50),
|
||||
];
|
||||
|
||||
pub fn styles(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let look = keep("canvas-look", Look::new, window, cx);
|
||||
let search = window.use_keyed_state("canvas-type-search", cx, |window, cx| {
|
||||
TextInput::new(window, cx).placeholder("Font")
|
||||
});
|
||||
let now = look.read(cx);
|
||||
let (color, stops, shadows, border, style) = (
|
||||
now.color,
|
||||
now.stops.clone(),
|
||||
now.shadows.clone(),
|
||||
now.border,
|
||||
now.style.clone(),
|
||||
);
|
||||
let [colored, graded, shaded, bordered, typed] = [(); 5].map(|_| look.clone());
|
||||
let [r0, r1, r2, r3] = border.radii;
|
||||
let card = div()
|
||||
.w(px(200.))
|
||||
.h(px(120.))
|
||||
.bg(color)
|
||||
.border(px(border.width))
|
||||
.border_color(border.color)
|
||||
.when(border.dash != Dash::Solid, |card| card.border_dashed())
|
||||
.rounded_tl(px(r0))
|
||||
.rounded_tr(px(r1))
|
||||
.rounded_br(px(r2))
|
||||
.rounded_bl(px(r3))
|
||||
.shadow(
|
||||
shadows
|
||||
.iter()
|
||||
.map(|shadow| BoxShadow {
|
||||
color: shadow.color,
|
||||
offset: point(px(shadow.x), px(shadow.y)),
|
||||
blur_radius: px(shadow.blur),
|
||||
spread_radius: px(shadow.spread),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
section(
|
||||
"ColorPanel · GradientPanel · ShadowEditor · BorderEditor · TypographyPanel",
|
||||
"What a shape looks like. The color panel sets its fill, over the document's colors; the gradient panel is forms::GradientEditor; shadows stack; a border's corners join or split; the type panel sets a style over a line of it. The card takes each edit.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(260.)).child(
|
||||
ColorPanel::new("canvas-color", color)
|
||||
.swatches(SWATCHES.map(|(name, hex)| (name, Hsla::from(rgb(hex)))))
|
||||
.on_change(move |next, _, cx| change(&colored, cx, |look| look.color = next)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(280.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_6()
|
||||
.child(
|
||||
GradientEditor::new("canvas-gradient", stops)
|
||||
.on_change(move |next, _, cx| change(&graded, cx, |look| look.stops = next.to_vec())),
|
||||
)
|
||||
.child(
|
||||
ShadowEditor::new("canvas-shadows", shadows)
|
||||
.on_change(move |next, _, cx| change(&shaded, cx, |look| look.shadows = next)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(260.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_6()
|
||||
.child(
|
||||
BorderEditor::new("canvas-border", border)
|
||||
.on_change(move |next, _, cx| change(&bordered, cx, |look| look.border = next)),
|
||||
)
|
||||
.child(div().p_6().child(card)),
|
||||
)
|
||||
.child(
|
||||
div().w(px(280.)).child(
|
||||
TypographyPanel::new("canvas-type", style, &search)
|
||||
.on_change(move |next, _, cx| change(&typed, cx, |look| look.style = next)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// The layout demo's values.
|
||||
struct Rules {
|
||||
constraints: Constraints,
|
||||
layout: AutoLayout,
|
||||
exports: Vec<ExportSetting>,
|
||||
exported: Vec<String>,
|
||||
}
|
||||
|
||||
impl Rules {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
constraints: Constraints {
|
||||
across: Pin::Both,
|
||||
down: Pin::Start,
|
||||
},
|
||||
layout: AutoLayout {
|
||||
flow: Flow::Row,
|
||||
gap: 12.0,
|
||||
padding: (16.0, 16.0),
|
||||
place: (Place::Start, Place::Center),
|
||||
},
|
||||
exports: vec![
|
||||
ExportSetting {
|
||||
scale: 1.0,
|
||||
format: ExportFormat::Png,
|
||||
},
|
||||
ExportSetting {
|
||||
scale: 2.0,
|
||||
format: ExportFormat::Png,
|
||||
},
|
||||
ExportSetting {
|
||||
scale: 1.0,
|
||||
format: ExportFormat::Svg,
|
||||
},
|
||||
],
|
||||
exported: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame in a preview, in its own pixels.
|
||||
fn boxed(frame: Frame) -> gpui::Div {
|
||||
div()
|
||||
.absolute()
|
||||
.left(px(frame.x))
|
||||
.top(px(frame.y))
|
||||
.w(px(frame.w))
|
||||
.h(px(frame.h))
|
||||
}
|
||||
|
||||
pub fn rules(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let rules = keep("canvas-rules", Rules::new, window, cx);
|
||||
let now = rules.read(cx);
|
||||
let (constraints, layout, exports, exported) = (
|
||||
now.constraints,
|
||||
now.layout,
|
||||
now.exports.clone(),
|
||||
now.exported.clone(),
|
||||
);
|
||||
let [pinned, laid, set, sent] = [(); 4].map(|_| rules.clone());
|
||||
let theme = cx.theme();
|
||||
let (line, fill, quiet) = (
|
||||
theme.colors.border,
|
||||
theme.colors.active,
|
||||
theme.colors.fg_subtle,
|
||||
);
|
||||
let (before, child) = (
|
||||
Frame::new(0.0, 0.0, 120.0, 80.0),
|
||||
Frame::new(16.0, 16.0, 56.0, 24.0),
|
||||
);
|
||||
let grown = Frame::new(0.0, 0.0, 240.0, 120.0);
|
||||
let pinned_preview = div()
|
||||
.relative()
|
||||
.w(px(grown.w))
|
||||
.h(px(grown.h))
|
||||
.border_1()
|
||||
.border_color(line)
|
||||
.child(boxed(before).border_1().border_color(line).border_dashed())
|
||||
.child(
|
||||
boxed(constraints.placed(child, before, grown))
|
||||
.bg(fill)
|
||||
.border_1()
|
||||
.border_color(quiet),
|
||||
);
|
||||
let room = Frame::new(0.0, 0.0, 240.0, 120.0);
|
||||
let laid_preview = div()
|
||||
.relative()
|
||||
.w(px(room.w))
|
||||
.h(px(room.h))
|
||||
.border_1()
|
||||
.border_color(line)
|
||||
.children(
|
||||
layout
|
||||
.laid(room, &[(40.0, 24.0), (56.0, 40.0), (32.0, 32.0)])
|
||||
.into_iter()
|
||||
.map(|frame| boxed(frame).bg(fill).border_1().border_color(quiet)),
|
||||
);
|
||||
let note = |text: SharedString| {
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(quiet)
|
||||
.child(text)
|
||||
};
|
||||
section(
|
||||
"ConstraintsEditor · AutoLayoutControls · ExportPanel",
|
||||
"How a shape keeps to its frame and lays out its children, and what it exports to. The first box shows where a child pinned so lands once its frame grows from the dashed size; the second lays three children by the controls; export names each file once.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.w(px(280.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
ConstraintsEditor::new("canvas-constraints", constraints)
|
||||
.on_change(move |next, _, cx| change(&pinned, cx, |rules| rules.constraints = next)),
|
||||
)
|
||||
.child(pinned_preview),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(280.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
AutoLayoutControls::new("canvas-auto-layout", layout)
|
||||
.on_change(move |next, _, cx| change(&laid, cx, |rules| rules.layout = next)),
|
||||
)
|
||||
.child(laid_preview),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(260.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
ExportPanel::new("canvas-export", "Card", exports)
|
||||
.on_change(move |next, _, cx| change(&set, cx, |rules| rules.exports = next))
|
||||
.on_export(move |files, _, cx| change(&sent, cx, |rules| rules.exported = files)),
|
||||
)
|
||||
.when(!exported.is_empty(), |panel| {
|
||||
panel.child(note(format!("Exported {}", exported.join(", ")).into()))
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod looks;
|
||||
mod panels;
|
||||
mod plane;
|
||||
mod tools;
|
||||
|
||||
@@ -42,5 +44,8 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(plane::plane(window, cx))
|
||||
.child(tools::tools(window, cx))
|
||||
.child(panels::arrange(window, cx))
|
||||
.child(looks::styles(window, cx))
|
||||
.child(looks::rules(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
use ely_gpui_component::canvas::{
|
||||
AlignmentToolbar, AssetPanel, Corner, Frame, InspectorPanel, LayerPanel, Shape, ShapeKind,
|
||||
aligned,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use super::tools::studio;
|
||||
use crate::ui::{change, section};
|
||||
|
||||
fn asset(
|
||||
key: &'static str,
|
||||
name: &'static str,
|
||||
kind: ShapeKind,
|
||||
w: f32,
|
||||
h: f32,
|
||||
hue: usize,
|
||||
) -> Shape {
|
||||
Shape::new(key, name, kind, Frame::new(0.0, 0.0, w, h)).hue(hue)
|
||||
}
|
||||
|
||||
fn shapes() -> Vec<Shape> {
|
||||
vec![
|
||||
asset("button", "Button", ShapeKind::Rect, 120.0, 40.0, 0),
|
||||
asset("dot", "Dot", ShapeKind::Ellipse, 40.0, 40.0, 1),
|
||||
asset("hexagon", "Hexagon", ShapeKind::Polygon(6), 60.0, 60.0, 2),
|
||||
asset(
|
||||
"pointer",
|
||||
"Pointer",
|
||||
ShapeKind::Arrow(Corner::BottomRight),
|
||||
80.0,
|
||||
40.0,
|
||||
3,
|
||||
),
|
||||
asset(
|
||||
"divider",
|
||||
"Divider",
|
||||
ShapeKind::Line(Corner::TopRight),
|
||||
120.0,
|
||||
0.0,
|
||||
4,
|
||||
),
|
||||
asset(
|
||||
"heading",
|
||||
"Heading",
|
||||
ShapeKind::Text("Heading".into()),
|
||||
160.0,
|
||||
32.0,
|
||||
5,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn notes() -> Vec<Shape> {
|
||||
vec![
|
||||
asset(
|
||||
"idea",
|
||||
"Idea",
|
||||
ShapeKind::Note("An idea".into()),
|
||||
120.0,
|
||||
120.0,
|
||||
6,
|
||||
),
|
||||
asset(
|
||||
"todo",
|
||||
"To do",
|
||||
ShapeKind::Note("To do".into()),
|
||||
120.0,
|
||||
120.0,
|
||||
7,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn arrange(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let studio = studio(window, cx);
|
||||
let now = studio.read(cx);
|
||||
let (shapes_now, selected) = (now.shapes.clone(), now.selected.clone());
|
||||
let chosen: Vec<Shape> = shapes_now
|
||||
.iter()
|
||||
.filter(|shape| selected.contains(&shape.key))
|
||||
.cloned()
|
||||
.collect();
|
||||
let [picked, shown, named, stacked, lined, edited, placed] = [(); 7].map(|_| studio.clone());
|
||||
let layers = LayerPanel::new("canvas-layers", shapes_now)
|
||||
.selected(selected.clone())
|
||||
.on_select(move |keys, _, cx| change(&picked, cx, |studio| studio.selected = keys.to_vec()))
|
||||
.on_show(move |keys, _, cx| {
|
||||
change(&shown, cx, |studio| {
|
||||
for shape in &mut studio.shapes {
|
||||
shape.hidden = !keys.contains(&shape.key);
|
||||
}
|
||||
studio.record("Show or hide".into());
|
||||
})
|
||||
})
|
||||
.on_rename(move |key, name, _, cx| {
|
||||
change(&named, cx, |studio| {
|
||||
let shape = studio
|
||||
.shapes
|
||||
.iter_mut()
|
||||
.find(|shape| shape.key == *key)
|
||||
.expect("a listed shape");
|
||||
shape.name = name.clone();
|
||||
studio.record(format!("Rename {name}"));
|
||||
})
|
||||
})
|
||||
.on_restack(move |order, _, cx| {
|
||||
change(&stacked, cx, |studio| {
|
||||
studio.shapes.sort_by_key(|shape| {
|
||||
order
|
||||
.iter()
|
||||
.position(|key| *key == shape.key)
|
||||
.expect("every layer restacked")
|
||||
});
|
||||
studio.record("Restack".into());
|
||||
})
|
||||
});
|
||||
let toolbar =
|
||||
AlignmentToolbar::new("canvas-align", selected.len()).on_align(move |align, _, cx| {
|
||||
change(&lined, cx, |studio| {
|
||||
let keys: Vec<SharedString> = studio.selected.clone();
|
||||
let frames: Vec<Frame> = studio
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|shape| keys.contains(&shape.key))
|
||||
.map(|shape| shape.frame)
|
||||
.collect();
|
||||
let moved = aligned(&frames, align);
|
||||
studio
|
||||
.shapes
|
||||
.iter_mut()
|
||||
.filter(|shape| keys.contains(&shape.key))
|
||||
.zip(moved)
|
||||
.for_each(|(shape, frame)| shape.frame = frame);
|
||||
studio.record(align.words().into());
|
||||
})
|
||||
});
|
||||
let inspector =
|
||||
InspectorPanel::new("canvas-inspector", chosen).on_change(move |shape, _, cx| {
|
||||
change(&edited, cx, |studio| {
|
||||
let at = studio
|
||||
.shapes
|
||||
.iter()
|
||||
.position(|each| each.key == shape.key)
|
||||
.expect("a listed shape");
|
||||
let words = format!("Edit {}", shape.name);
|
||||
studio.shapes[at] = shape;
|
||||
studio.record(words);
|
||||
})
|
||||
});
|
||||
let assets = AssetPanel::new("canvas-assets")
|
||||
.group("Shapes", shapes())
|
||||
.group("Notes", notes())
|
||||
.on_pick(move |shape, _, cx| {
|
||||
change(&placed, cx, |studio| {
|
||||
studio.made += 1;
|
||||
let key = SharedString::from(format!("made-{}", studio.made));
|
||||
let mut copy = shape.clone();
|
||||
copy.key = key.clone();
|
||||
copy.frame.x = 280.0 + 12.0 * studio.made as f32;
|
||||
copy.frame.y = 180.0 + 12.0 * studio.made as f32;
|
||||
studio.shapes.push(copy);
|
||||
studio.selected = vec![key];
|
||||
studio.record(format!("Place {}", shape.name));
|
||||
})
|
||||
});
|
||||
section(
|
||||
"LayerPanel · InspectorPanel · AlignmentToolbar · AssetPanel",
|
||||
"Panels on the drawing above. The layers list it topmost first: a box shows or hides a layer, F2 renames it, a drag restacks it. The inspector edits the one selected; the toolbar lines up or spreads several; an asset pressed lands on the canvas. Each change is a step in the history.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(div().w(px(240.)).h(px(280.)).child(layers))
|
||||
.child(
|
||||
div()
|
||||
.w(px(300.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(toolbar)
|
||||
.child(inspector),
|
||||
)
|
||||
.child(div().w(px(256.)).child(assets)),
|
||||
)
|
||||
}
|
||||
@@ -13,15 +13,15 @@ use crate::{
|
||||
};
|
||||
|
||||
/// 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>,
|
||||
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,
|
||||
made: usize,
|
||||
pub(super) made: usize,
|
||||
}
|
||||
|
||||
impl Studio {
|
||||
@@ -61,14 +61,14 @@ impl Studio {
|
||||
}
|
||||
|
||||
/// Keeps the shapes as a new step, dropping any steps undone.
|
||||
fn record(&mut self, words: String) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
fn studio(window: &mut Window, cx: &mut App) -> Entity<Studio> {
|
||||
pub(super) fn studio(window: &mut Window, cx: &mut App) -> Entity<Studio> {
|
||||
keep("canvas-studio", Studio::new, window, cx)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user