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.
316 lines
10 KiB
Rust
316 lines
10 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
App, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
|
SharedString, Styled, Window, div,
|
|
};
|
|
|
|
use super::{
|
|
panel::{OnEdit, caption, editing},
|
|
view::Frame,
|
|
};
|
|
use crate::{
|
|
buttons::SegmentedControl,
|
|
forms::ScrubInput,
|
|
primitives::{IconName, tab_stop},
|
|
theme::{ActiveTheme, Radius},
|
|
};
|
|
|
|
/// Which way auto layout runs its children.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Flow {
|
|
Row,
|
|
Column,
|
|
}
|
|
|
|
/// Where children sit along a side of their frame.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Place {
|
|
Start,
|
|
Center,
|
|
End,
|
|
}
|
|
|
|
const PLACES: [Place; 3] = [Place::Start, Place::Center, Place::End];
|
|
|
|
impl Place {
|
|
/// How far into `free` room it puts what it places.
|
|
fn offset(self, free: f32) -> f32 {
|
|
match self {
|
|
Place::Start => 0.0,
|
|
Place::Center => free / 2.0,
|
|
Place::End => free,
|
|
}
|
|
}
|
|
|
|
/// The place a step of `by` reaches, stopping at either end.
|
|
fn stepped(self, by: isize) -> Place {
|
|
let at = PLACES
|
|
.iter()
|
|
.position(|place| *place == self)
|
|
.expect("a place") as isize;
|
|
PLACES[(at + by).clamp(0, 2) as usize]
|
|
}
|
|
}
|
|
|
|
/// Children in a row or a column, a gap apart inside padding, placed across and down.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct AutoLayout {
|
|
pub flow: Flow,
|
|
pub gap: f32,
|
|
/// Across and down, in pixels.
|
|
pub padding: (f32, f32),
|
|
/// Across and down.
|
|
pub place: (Place, Place),
|
|
}
|
|
|
|
impl AutoLayout {
|
|
/// Children of `sizes` laid inside `frame`.
|
|
pub fn laid(&self, frame: Frame, sizes: &[(f32, f32)]) -> Vec<Frame> {
|
|
let row = self.flow == Flow::Row;
|
|
let (x, y) = (frame.x + self.padding.0, frame.y + self.padding.1);
|
|
let room = (
|
|
frame.w - 2.0 * self.padding.0,
|
|
frame.h - 2.0 * self.padding.1,
|
|
);
|
|
let (along, wide) = if row { room } else { (room.1, room.0) };
|
|
let (lead, side) = if row {
|
|
self.place
|
|
} else {
|
|
(self.place.1, self.place.0)
|
|
};
|
|
let long = |(w, h): (f32, f32)| if row { w } else { h };
|
|
let run: f32 = sizes.iter().map(|size| long(*size)).sum::<f32>()
|
|
+ self.gap * sizes.len().saturating_sub(1) as f32;
|
|
let mut next = lead.offset(along - run);
|
|
sizes
|
|
.iter()
|
|
.map(|&(w, h)| {
|
|
let off = side.offset(wide - if row { h } else { w });
|
|
let laid = if row {
|
|
Frame::new(x + next, y + off, w, h)
|
|
} else {
|
|
Frame::new(x + off, y + next, w, h)
|
|
};
|
|
next += long((w, h)) + self.gap;
|
|
laid
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Auto layout's flow, gap and padding, and where children sit: nine places in a grid, one Tab stop that the arrows move through.
|
|
#[derive(IntoElement)]
|
|
pub struct AutoLayoutControls {
|
|
id: ElementId,
|
|
layout: AutoLayout,
|
|
on_change: Option<OnEdit<AutoLayout>>,
|
|
}
|
|
|
|
impl AutoLayoutControls {
|
|
pub fn new(id: impl Into<ElementId>, layout: AutoLayout) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
layout,
|
|
on_change: None,
|
|
}
|
|
}
|
|
|
|
pub fn on_change(
|
|
mut self,
|
|
handler: impl Fn(AutoLayout, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.on_change = Some(Rc::new(handler));
|
|
self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for AutoLayoutControls {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let (id, layout, on_change) = (self.id, self.layout, &self.on_change);
|
|
let place = Rc::new(editing(
|
|
"auto layout",
|
|
&layout,
|
|
on_change,
|
|
|layout, place| layout.place = place,
|
|
));
|
|
let flow = editing("auto layout", &layout, on_change, |layout, flow: Flow| {
|
|
layout.flow = flow
|
|
});
|
|
let at = |edit: fn(&mut AutoLayout, f64)| editing("auto layout", &layout, on_change, edit);
|
|
let scrub = |name: &'static str, value: f32| {
|
|
ScrubInput::new((id.clone(), name), name, value as f64)
|
|
.range(0.0, 1_000.0)
|
|
.precision(0)
|
|
};
|
|
let focus = tab_stop((id.clone(), "places").into(), true, window, cx);
|
|
let focused = focus.is_focused(window);
|
|
let theme = cx.theme();
|
|
let sizes = theme.canvas();
|
|
let (accent, subtle, hover) = (
|
|
theme.colors.accent,
|
|
theme.colors.fg_subtle,
|
|
theme.colors.hover,
|
|
);
|
|
let keys = place.clone();
|
|
let grid = div()
|
|
.id((id.clone(), "places"))
|
|
.debug_selector(|| "places".into())
|
|
.track_focus(&focus)
|
|
.flex_none()
|
|
.flex()
|
|
.flex_col()
|
|
.p_0p5()
|
|
.rounded(theme.radius(Radius::Md))
|
|
.border_1()
|
|
.border_color(if focused {
|
|
theme.colors.focus
|
|
} else {
|
|
theme.colors.border
|
|
})
|
|
.on_key_down(move |event, window, cx| {
|
|
let (across, down) = layout.place;
|
|
let next = match event.keystroke.key.as_str() {
|
|
"left" => (across.stepped(-1), down),
|
|
"right" => (across.stepped(1), down),
|
|
"up" => (across, down.stepped(-1)),
|
|
"down" => (across, down.stepped(1)),
|
|
_ => return,
|
|
};
|
|
cx.stop_propagation();
|
|
if next != layout.place {
|
|
keys(next, window, cx);
|
|
}
|
|
})
|
|
.children(PLACES.iter().map(|down| {
|
|
div().flex().children(PLACES.iter().map(|across| {
|
|
let spot = (*across, *down);
|
|
let chosen = spot == layout.place;
|
|
let press = place.clone();
|
|
div()
|
|
.id((id.clone(), format!("place-{across:?}-{down:?}")))
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.size(sizes.place)
|
|
.rounded(theme.radius(Radius::Sm))
|
|
.cursor_pointer()
|
|
.hover(move |style| style.bg(hover))
|
|
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
press(spot, window, cx)
|
|
})
|
|
.child(
|
|
div()
|
|
.size(if chosen {
|
|
sizes.place_mark
|
|
} else {
|
|
sizes.place_pip
|
|
})
|
|
.rounded(theme.radius(Radius::Sm))
|
|
.bg(if chosen { accent } else { subtle }),
|
|
)
|
|
}))
|
|
}));
|
|
div()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_3()
|
|
.child(
|
|
SegmentedControl::new(
|
|
(id.clone(), "flow"),
|
|
if layout.flow == Flow::Row {
|
|
"row"
|
|
} else {
|
|
"column"
|
|
},
|
|
)
|
|
.segment("row", "Row", Some(IconName::ArrowRight))
|
|
.segment("column", "Column", Some(IconName::ArrowDown))
|
|
.on_change(move |value: &SharedString, window, cx| {
|
|
let chosen = if value.as_ref() == "row" {
|
|
Flow::Row
|
|
} else {
|
|
Flow::Column
|
|
};
|
|
flow(chosen, window, cx)
|
|
}),
|
|
)
|
|
.child(
|
|
div().flex().items_start().gap_3().child(grid).child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.flex()
|
|
.flex_col()
|
|
.gap_2()
|
|
.child(scrub("Gap", layout.gap).on_change(at(|l, v| l.gap = v as f32)))
|
|
.child(caption("Padding", cx))
|
|
.child(
|
|
scrub("Across", layout.padding.0)
|
|
.on_change(at(|l, v| l.padding.0 = v as f32)),
|
|
)
|
|
.child(
|
|
scrub("Down", layout.padding.1)
|
|
.on_change(at(|l, v| l.padding.1 = v as f32)),
|
|
),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{AutoLayout, Flow, Place};
|
|
use crate::canvas::Frame;
|
|
|
|
#[test]
|
|
fn a_row_runs_its_children_a_gap_apart_and_places_the_run() {
|
|
let row = AutoLayout {
|
|
flow: Flow::Row,
|
|
gap: 10.0,
|
|
padding: (20.0, 10.0),
|
|
place: (Place::Center, Place::End),
|
|
};
|
|
let laid = row.laid(
|
|
Frame::new(0.0, 0.0, 200.0, 100.0),
|
|
&[(40.0, 20.0), (60.0, 40.0)],
|
|
);
|
|
assert_eq!(
|
|
laid,
|
|
[
|
|
Frame::new(45.0, 70.0, 40.0, 20.0),
|
|
Frame::new(95.0, 50.0, 60.0, 40.0)
|
|
],
|
|
"the run of 110 sits in the middle of 160, each child on the bottom padding"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_column_runs_down_from_its_top() {
|
|
let column = AutoLayout {
|
|
flow: Flow::Column,
|
|
gap: 8.0,
|
|
padding: (0.0, 0.0),
|
|
place: (Place::Start, Place::Start),
|
|
};
|
|
let laid = column.laid(
|
|
Frame::new(10.0, 10.0, 100.0, 100.0),
|
|
&[(30.0, 10.0), (50.0, 20.0)],
|
|
);
|
|
assert_eq!(
|
|
laid,
|
|
[
|
|
Frame::new(10.0, 10.0, 30.0, 10.0),
|
|
Frame::new(10.0, 28.0, 50.0, 20.0)
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_step_stops_at_either_end() {
|
|
assert_eq!(Place::Start.stepped(-1), Place::Start);
|
|
assert_eq!(Place::Start.stepped(1), Place::Center);
|
|
assert_eq!(Place::End.stepped(1), Place::End);
|
|
}
|
|
}
|