Files
Ely-GPUI-Components/src/canvas/style.rs
T
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

373 lines
12 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window,
div, prelude::*,
};
use super::panel::{OnEdit, caption, color_well, editing, heading, pair};
use crate::{
buttons::{IconButton, SegmentedControl, ToggleButton, ToggleItem},
forms::{ColorPalette, ColorPicker, ScrubInput},
primitives::IconName,
theme::{ActiveTheme, Elevation},
typography::Ellipsis,
};
/// The most a shadow or a border measures, in pixels.
const FAR: f64 = 1_000.0;
/// A color to edit in full, over the document's colors to pick from.
#[derive(IntoElement)]
pub struct ColorPanel {
id: ElementId,
color: Hsla,
swatches: Vec<(SharedString, Hsla)>,
on_change: Option<OnEdit<Hsla>>,
}
impl ColorPanel {
pub fn new(id: impl Into<ElementId>, color: impl Into<Hsla>) -> Self {
Self {
id: id.into(),
color: color.into(),
swatches: Vec::new(),
on_change: None,
}
}
/// The document's colors, by name.
pub fn swatches(
mut self,
swatches: impl IntoIterator<Item = (impl Into<SharedString>, impl Into<Hsla>)>,
) -> Self {
self.swatches = swatches
.into_iter()
.map(|(name, color)| (name.into(), color.into()))
.collect();
self
}
pub fn on_change(mut self, handler: impl Fn(Hsla, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ColorPanel {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let (picked, chosen) = (self.on_change.clone(), self.on_change);
let report = |on_change: Option<OnEdit<Hsla>>| {
move |color: Hsla, window: &mut Window, cx: &mut App| {
log::info!("color panel: {color:?}");
if let Some(on_change) = &on_change {
on_change(color, window, cx);
}
}
};
div()
.flex()
.flex_col()
.gap_3()
.child(
ColorPicker::new((self.id.clone(), "picker"), self.color).on_change(report(picked)),
)
.when(!self.swatches.is_empty(), |panel| {
panel.child(caption("Document colors", cx)).child(
ColorPalette::new((self.id, "swatches"), self.swatches)
.selected(self.color)
.on_change(report(chosen)),
)
})
}
}
/// A drop shadow: its offset, blur and spread in pixels, and its color.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Shadow {
pub x: f32,
pub y: f32,
pub blur: f32,
pub spread: f32,
pub color: Hsla,
}
/// Shadows in a list. Each sets its color, offset, blur and spread, and minus drops it; plus adds the theme's floating shadow.
#[derive(IntoElement)]
pub struct ShadowEditor {
id: ElementId,
shadows: Vec<Shadow>,
on_change: Option<OnEdit<Vec<Shadow>>>,
}
impl ShadowEditor {
pub fn new(id: impl Into<ElementId>, shadows: impl IntoIterator<Item = Shadow>) -> Self {
Self {
id: id.into(),
shadows: shadows.into_iter().collect(),
on_change: None,
}
}
pub fn on_change(
mut self,
handler: impl Fn(Vec<Shadow>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
impl RenderOnce for ShadowEditor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let lift = cx
.theme()
.elevation(Elevation::Floating)
.last()
.map(|shadow| Shadow {
x: f32::from(shadow.offset.x),
y: f32::from(shadow.offset.y),
blur: f32::from(shadow.blur_radius),
spread: f32::from(shadow.spread_radius),
color: shadow.color,
})
.expect("a floating elevation casts a shadow");
let (id, shadows, on_change) = (self.id, self.shadows, &self.on_change);
let add = editing("shadow editor", &shadows, on_change, move |all, _: ()| {
all.push(lift)
});
let rows = shadows.iter().enumerate().map(|(ix, shadow)| {
let at = |edit: fn(&mut Shadow, f64)| {
editing(
"shadow editor",
&shadows,
on_change,
move |all: &mut Vec<Shadow>, value| edit(&mut all[ix], value),
)
};
let scrub = |name: &'static str, value: f32, least: f64| {
ScrubInput::new((id.clone(), format!("{name}-{ix}")), name, value as f64)
.range(least, FAR)
.precision(0)
};
let tint = editing(
"shadow editor",
&shadows,
on_change,
move |all, color: Hsla| all[ix].color = color,
);
let remove = editing("shadow editor", &shadows, on_change, move |all, _: ()| {
all.remove(ix);
});
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(color_well(
(id.clone(), format!("color-{ix}")).into(),
shadow.color,
tint,
))
.child(
div()
.flex_1()
.min_w_0()
.child(Ellipsis::new(format!("Shadow {}", ix + 1))),
)
.child(
IconButton::new((id.clone(), format!("remove-{ix}")), IconName::Minus)
.tooltip("Remove the shadow")
.on_click(move |_, window, cx| remove((), window, cx)),
),
)
.child(pair(
scrub("X", shadow.x, -FAR).on_change(at(|s, v| s.x = v as f32)),
scrub("Y", shadow.y, -FAR).on_change(at(|s, v| s.y = v as f32)),
))
.child(pair(
scrub("Blur", shadow.blur, 0.0).on_change(at(|s, v| s.blur = v as f32)),
scrub("Spread", shadow.spread, -FAR).on_change(at(|s, v| s.spread = v as f32)),
))
});
div()
.flex()
.flex_col()
.gap_3()
.child(heading(
"Shadows",
IconButton::new((id.clone(), "add"), IconName::Plus)
.tooltip("Add a shadow")
.on_click(move |_, window, cx| add((), window, cx)),
cx,
))
.children(rows)
}
}
/// How a border's line is drawn.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dash {
Solid,
Dashed,
Dotted,
}
impl Dash {
pub const ALL: [Dash; 3] = [Dash::Solid, Dash::Dashed, Dash::Dotted];
pub fn word(self) -> &'static str {
match self {
Dash::Solid => "Solid",
Dash::Dashed => "Dashed",
Dash::Dotted => "Dotted",
}
}
}
/// A border: its width in pixels, its line, its color, and each corner's radius from the top left around.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Border {
pub width: f32,
pub dash: Dash,
pub color: Hsla,
pub radii: [f32; 4],
}
/// A border's color, width and line, and its corners: one radius for all, or each its own once split. Uneven corners always show split; joining them gives all the first one's radius.
#[derive(IntoElement)]
pub struct BorderEditor {
id: ElementId,
border: Border,
on_change: Option<OnEdit<Border>>,
}
impl BorderEditor {
pub fn new(id: impl Into<ElementId>, border: Border) -> Self {
Self {
id: id.into(),
border,
on_change: None,
}
}
pub fn on_change(mut self, handler: impl Fn(Border, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
}
const CORNERS: [&str; 4] = ["TL", "TR", "BR", "BL"];
impl RenderOnce for BorderEditor {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let (id, border, on_change) = (self.id, self.border, &self.on_change);
let parted = window.use_keyed_state((id.clone(), "split"), cx, |_, _| false);
let uneven = border.radii.iter().any(|radius| *radius != border.radii[0]);
let split = *parted.read(cx) || uneven;
let at = |edit: fn(&mut Border, f64)| editing("border editor", &border, on_change, edit);
let scrub = |name: &'static str, value: f32| {
ScrubInput::new((id.clone(), name), name, value as f64)
.range(0.0, FAR)
.precision(0)
};
let dash = {
let set = editing("border editor", &border, on_change, |border, dash: Dash| {
border.dash = dash
});
Dash::ALL
.iter()
.fold(
SegmentedControl::new((id.clone(), "dash"), border.dash.word()),
|control, dash| control.segment(dash.word(), dash.word(), None),
)
.on_change(move |word, window, cx| {
let dash = *Dash::ALL
.iter()
.find(|dash| dash.word() == word.as_ref())
.expect("a dash the control offers");
set(dash, window, cx)
})
};
let join = editing("border editor", &border, on_change, |border, _: ()| {
border.radii = [border.radii[0]; 4]
});
let corners = ToggleButton::new(
(id.clone(), "corners"),
ToggleItem::new("corners")
.icon(IconName::SquareRoundCorner)
.tooltip("Each corner its own"),
split,
)
.on_toggle(move |on, window, cx| {
log::info!(
"border editor: corners {}",
if on { "split" } else { "joined" }
);
parted.update(cx, |parted, cx| {
*parted = on;
cx.notify();
});
if !on && uneven {
join((), window, cx);
}
});
let corner = |ix: usize| {
let edit = editing(
"border editor",
&border,
on_change,
move |border, value: f64| border.radii[ix] = value as f32,
);
scrub(CORNERS[ix], border.radii[ix]).on_change(edit)
};
div()
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.items_center()
.gap_2()
.child(color_well(
(id.clone(), "color").into(),
border.color,
editing(
"border editor",
&border,
on_change,
|border, color: Hsla| border.color = color,
),
))
.child(div().flex_1().min_w_0().child(
scrub("Width", border.width).on_change(at(|b, v| b.width = v as f32)),
)),
)
.child(dash)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(div().flex_1().min_w_0().child(if split {
Ellipsis::new("Corners").into_any_element()
} else {
scrub("Radius", border.radii[0])
.on_change(at(|b, v| b.radii = [v as f32; 4]))
.into_any_element()
}))
.child(corners),
)
.when(split, |editor| {
editor
.child(pair(corner(0), corner(1)))
.child(pair(corner(3), corner(2)))
})
}
}