Merge #275: fix(ui): gradient type icons that show their gradient (#256) (@Nael-Nathanael)

This commit is contained in:
bflatastic
2026-10-15 15:08:48 +07:00
2 changed files with 75 additions and 4 deletions
+4 -4
View File
@@ -438,10 +438,10 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
) {
let kinds = [
(Icon::Fill, tr("Solid")),
(Icon::LayoutH, tr("Linear")),
(Icon::Ellipse, tr("Radial")),
(Icon::Rotate, tr("Angular")),
(Icon::Instance, tr("Diamond")),
(Icon::GradientLinear, tr("Linear")),
(Icon::GradientRadial, tr("Radial")),
(Icon::GradientAngular, tr("Angular")),
(Icon::GradientDiamond, tr("Diamond")),
(Icon::Image, tr("Image")),
(Icon::ViewGrid, tr("Pattern")),
];
+71
View File
@@ -120,6 +120,11 @@ icons! {
Export,
StrokeIcon,
Fill,
/// Gradient kinds: a swatch outline holding a fade of that kind.
GradientLinear,
GradientRadial,
GradientAngular,
GradientDiamond,
Sun,
Moon,
Undo,
@@ -311,6 +316,32 @@ impl P<'_> {
// A hairline around the mesh gives it the same soft edge as the convex fills.
self.painter.add(Shape::closed_line(v, Stroke::new(self.px, self.color)));
}
/// Triangles `tris` over `verts` (x, y, alpha): the colour fades between the vertices.
fn shade(&self, verts: &[(f32, f32, f32)], tris: &[u32]) {
let mut mesh = Mesh::default();
for &(x, y, a) in verts {
mesh.colored_vertex(self.raw(x, y), self.color.gamma_multiply(a));
}
mesh.indices = tris.to_vec();
self.painter.add(Shape::mesh(mesh));
}
/// A disc of radius `r` at the centre, as a fan from the top, clockwise: each slice's rim
/// alphas from `rim` and its centre's from `centre`, both of how far round it is (0..1).
fn shade_fan(&self, r: f32, centre: impl Fn(f32) -> f32, rim: impl Fn(f32) -> f32) {
const SLICES: u32 = 32;
let at = |t: f32| {
let a = -FRAC_PI_2 + t * TAU;
(8.0 + r * a.cos(), 8.0 + r * a.sin())
};
let mut verts = Vec::new();
for i in 0..SLICES {
let (t0, t1) = (i as f32 / SLICES as f32, (i + 1) as f32 / SLICES as f32);
let ((x0, y0), (x1, y1)) = (at(t0), at(t1));
verts.extend([(8.0, 8.0, centre((t0 + t1) / 2.0)), (x0, y0, rim(t0)), (x1, y1, rim(t1))]);
}
let tris: Vec<u32> = (0..SLICES * 3).collect();
self.shade(&verts, &tris);
}
fn rect(&self, x0: f32, y0: f32, x1: f32, y1: f32, r: f32) {
let rr = Rect::from_min_max(self.sp(x0, y0), self.sp(x1, y1));
self.painter.rect_stroke(rr, egui::CornerRadius::same((r * self.s).round() as u8), self.stroke, egui::StrokeKind::Middle);
@@ -833,6 +864,24 @@ fn draw(p: &P, icon: Icon) {
}
StrokeIcon => p.rect(2.5, 2.5, 13.5, 13.5, 2.0),
Fill => p.fill_rect(2.0, 2.0, 14.0, 14.0, 2.0),
GradientLinear => {
p.rect(2.5, 2.5, 13.5, 13.5, 2.0);
p.shade(&[(4.0, 4.0, 1.0), (12.0, 4.0, 0.0), (12.0, 12.0, 0.0), (4.0, 12.0, 1.0)], &[0, 1, 2, 0, 2, 3]);
}
GradientRadial => {
p.rect(2.5, 2.5, 13.5, 13.5, 2.0);
p.shade_fan(5.5, |_| 1.0, |_| 0.0);
}
GradientAngular => {
// Once round from the top: the seam shows where the sweep starts.
p.rect(2.5, 2.5, 13.5, 13.5, 2.0);
p.shade_fan(4.0, |t| 1.0 - t, |t| 1.0 - t);
}
GradientDiamond => {
p.rect(2.5, 2.5, 13.5, 13.5, 2.0);
let v = [(8.0, 8.0, 1.0), (8.0, 2.5, 0.0), (13.5, 8.0, 0.0), (8.0, 13.5, 0.0), (2.5, 8.0, 0.0)];
p.shade(&v, &[0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1]);
}
Moon => {
// A crescent: the outer rim, then the shadowing circle's edge back. Both arcs stop short
// of where the circles meet, so each tip is two blunt joins (a single cusp join folds).
@@ -1102,6 +1151,28 @@ mod tests {
assert!(out.shapes.len() > Icon::ALL.len() * 10);
}
/// The gradient kinds' icons show a fade: a mesh whose vertices run from opaque to clear.
#[test]
fn gradient_icons_fade() {
let ctx = egui::Context::default();
for icon in [Icon::GradientLinear, Icon::GradientRadial, Icon::GradientAngular, Icon::GradientDiamond] {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
paint(ui.painter(), Rect::from_min_size(Pos2::ZERO, Vec2::splat(16.0)), icon, Color32::WHITE);
});
out.textures_delta.clear();
let alphas: Vec<u8> = out
.shapes
.iter()
.filter_map(|c| match &c.shape {
Shape::Mesh(m) => Some(m.vertices.iter().map(|v| v.color.a()).collect::<Vec<_>>()),
_ => None,
})
.flatten()
.collect();
assert!(alphas.contains(&255) && alphas.contains(&0), "{icon:?} fades from opaque to clear: {alphas:?}");
}
}
#[test]
fn concave_polygons_triangulate() {
let arrow = [pos2(4.0, 2.0), pos2(4.0, 13.6), pos2(6.9, 10.9), pos2(8.8, 14.8), pos2(10.6, 14.0), pos2(8.7, 10.2), pos2(12.6, 10.2)];