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.
223 lines
7.3 KiB
Rust
223 lines
7.3 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{App, ElementId, IntoElement, ParentElement, RenderOnce, Window};
|
|
|
|
use super::view::Frame;
|
|
use crate::{
|
|
buttons::IconButton,
|
|
primitives::IconName,
|
|
shell::{Toolbar, ToolbarGroup, ToolbarSeparator},
|
|
};
|
|
|
|
type OnAlign = Rc<dyn Fn(Align, &mut Window, &mut App)>;
|
|
|
|
/// A way to line up selected shapes, or to spread them so the gaps between them match.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Align {
|
|
Left,
|
|
Center,
|
|
Right,
|
|
Top,
|
|
Middle,
|
|
Bottom,
|
|
SpreadAcross,
|
|
SpreadDown,
|
|
}
|
|
|
|
impl Align {
|
|
pub const ALL: [Align; 8] = [
|
|
Align::Left,
|
|
Align::Center,
|
|
Align::Right,
|
|
Align::Top,
|
|
Align::Middle,
|
|
Align::Bottom,
|
|
Align::SpreadAcross,
|
|
Align::SpreadDown,
|
|
];
|
|
|
|
pub fn words(self) -> &'static str {
|
|
match self {
|
|
Align::Left => "Align left edges",
|
|
Align::Center => "Align centers",
|
|
Align::Right => "Align right edges",
|
|
Align::Top => "Align top edges",
|
|
Align::Middle => "Align middles",
|
|
Align::Bottom => "Align bottom edges",
|
|
Align::SpreadAcross => "Spread across",
|
|
Align::SpreadDown => "Spread down",
|
|
}
|
|
}
|
|
|
|
fn icon(self) -> IconName {
|
|
match self {
|
|
Align::Left => IconName::AlignStartVertical,
|
|
Align::Center => IconName::AlignCenterVertical,
|
|
Align::Right => IconName::AlignEndVertical,
|
|
Align::Top => IconName::AlignStartHorizontal,
|
|
Align::Middle => IconName::AlignCenterHorizontal,
|
|
Align::Bottom => IconName::AlignEndHorizontal,
|
|
Align::SpreadAcross => IconName::AlignHorizontalDistributeCenter,
|
|
Align::SpreadDown => IconName::AlignVerticalDistributeCenter,
|
|
}
|
|
}
|
|
|
|
/// The fewest shapes it moves.
|
|
pub fn least(self) -> usize {
|
|
match self {
|
|
Align::SpreadAcross | Align::SpreadDown => 3,
|
|
_ => 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `frames` lined up on the edge or center line they span together, or spread so the gaps between them match while the first and last stay.
|
|
pub fn aligned(frames: &[Frame], align: Align) -> Vec<Frame> {
|
|
assert!(
|
|
frames.len() >= align.least(),
|
|
"{} takes {} shapes, not {}",
|
|
align.words(),
|
|
align.least(),
|
|
frames.len()
|
|
);
|
|
let whole = frames
|
|
.iter()
|
|
.copied()
|
|
.reduce(|whole, frame| whole.union(&frame))
|
|
.expect("frames to align");
|
|
let at = |frame: &Frame, x: f32, y: f32| Frame::new(x, y, frame.w, frame.h);
|
|
let each = |place: &dyn Fn(&Frame) -> Frame| frames.iter().map(place).collect::<Vec<_>>();
|
|
match align {
|
|
Align::Left => each(&|f| at(f, whole.x, f.y)),
|
|
Align::Center => each(&|f| at(f, whole.x + (whole.w - f.w) / 2.0, f.y)),
|
|
Align::Right => each(&|f| at(f, whole.right() - f.w, f.y)),
|
|
Align::Top => each(&|f| at(f, f.x, whole.y)),
|
|
Align::Middle => each(&|f| at(f, f.x, whole.y + (whole.h - f.h) / 2.0)),
|
|
Align::Bottom => each(&|f| at(f, f.x, whole.bottom() - f.h)),
|
|
Align::SpreadAcross => spread(frames, |f| (f.x, f.w), |f, x| at(f, x, f.y)),
|
|
Align::SpreadDown => spread(frames, |f| (f.y, f.h), |f, y| at(f, f.x, y)),
|
|
}
|
|
}
|
|
|
|
/// Frames placed along one axis in the order they start, the gaps between them equal.
|
|
fn spread(
|
|
frames: &[Frame],
|
|
span: impl Fn(&Frame) -> (f32, f32),
|
|
to: impl Fn(&Frame, f32) -> Frame,
|
|
) -> Vec<Frame> {
|
|
let mut order: Vec<usize> = (0..frames.len()).collect();
|
|
order.sort_by(|a, b| span(&frames[*a]).0.total_cmp(&span(&frames[*b]).0));
|
|
let first = span(&frames[order[0]]);
|
|
let last = span(&frames[order[order.len() - 1]]);
|
|
let used: f32 = frames.iter().map(|frame| span(frame).1).sum();
|
|
let gap = (last.0 + last.1 - first.0 - used) / (frames.len() - 1) as f32;
|
|
let mut out = frames.to_vec();
|
|
let mut next = first.0;
|
|
for ix in order {
|
|
out[ix] = to(&frames[ix], next);
|
|
next += span(&frames[ix]).1 + gap;
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Buttons that line up or spread the selection. Each asks the owner, who moves the shapes with `aligned`; a way rests disabled until enough shapes are selected for it.
|
|
#[derive(IntoElement)]
|
|
pub struct AlignmentToolbar {
|
|
id: ElementId,
|
|
count: usize,
|
|
on_align: Option<OnAlign>,
|
|
}
|
|
|
|
impl AlignmentToolbar {
|
|
/// `count` shapes are selected.
|
|
pub fn new(id: impl Into<ElementId>, count: usize) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
count,
|
|
on_align: None,
|
|
}
|
|
}
|
|
|
|
pub fn on_align(mut self, handler: impl Fn(Align, &mut Window, &mut App) + 'static) -> Self {
|
|
self.on_align = Some(Rc::new(handler));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for AlignmentToolbar {
|
|
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
|
let button = |align: Align| {
|
|
let on_align = self.on_align.clone();
|
|
IconButton::new((self.id.clone(), align.words()), align.icon())
|
|
.tooltip(align.words())
|
|
.disabled(self.count < align.least())
|
|
.on_click(move |_, window, cx| {
|
|
log::info!("alignment toolbar: {}", align.words());
|
|
if let Some(on_align) = &on_align {
|
|
on_align(align, window, cx);
|
|
}
|
|
})
|
|
};
|
|
let (lines, spreads) = Align::ALL.split_at(6);
|
|
Toolbar::new()
|
|
.child(ToolbarGroup::new().children(lines.iter().map(|align| button(*align))))
|
|
.child(ToolbarSeparator)
|
|
.child(ToolbarGroup::new().children(spreads.iter().map(|align| button(*align))))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{Align, aligned};
|
|
use crate::canvas::Frame;
|
|
|
|
fn three() -> Vec<Frame> {
|
|
vec![
|
|
Frame::new(0.0, 10.0, 20.0, 20.0),
|
|
Frame::new(100.0, 0.0, 40.0, 60.0),
|
|
Frame::new(40.0, 30.0, 10.0, 10.0),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn a_line_up_meets_the_edge_or_center_the_frames_span() {
|
|
let xs = |align| {
|
|
aligned(&three(), align)
|
|
.iter()
|
|
.map(|f| f.x)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(xs(Align::Left), [0.0, 0.0, 0.0]);
|
|
assert_eq!(xs(Align::Right), [120.0, 100.0, 130.0]);
|
|
assert_eq!(xs(Align::Center), [60.0, 50.0, 65.0]);
|
|
let ys = |align| {
|
|
aligned(&three(), align)
|
|
.iter()
|
|
.map(|f| f.y)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(ys(Align::Top), [0.0, 0.0, 0.0]);
|
|
assert_eq!(ys(Align::Middle), [20.0, 0.0, 25.0]);
|
|
assert_eq!(ys(Align::Bottom), [40.0, 0.0, 50.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_spread_keeps_the_ends_and_evens_the_gaps() {
|
|
let spread = aligned(&three(), Align::SpreadAcross);
|
|
assert_eq!(spread[0].x, 0.0, "the first stays");
|
|
assert_eq!(spread[1].x, 100.0, "the last stays");
|
|
assert_eq!(spread[2].x, 55.0, "35 either side of the middle one");
|
|
assert_eq!(
|
|
spread.iter().map(|f| f.y).collect::<Vec<_>>(),
|
|
[10.0, 0.0, 30.0],
|
|
"a spread across leaves heights alone"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "takes 3 shapes, not 2")]
|
|
fn a_spread_takes_three() {
|
|
aligned(&three()[..2], Align::SpreadDown);
|
|
}
|
|
}
|