mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-12 00:40:36 -06:00
Merge pull request #826 from slyim/fix/toolbar-icon-rhythm
M4: toolbar marks share one size and stroke weight; a simpler Slice icon (#737, #738, #739)
This commit is contained in:
+73
-11
@@ -421,6 +421,7 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) {
|
||||
draw(&p, icon);
|
||||
}
|
||||
|
||||
/// Draw `icon` on the 16-unit grid through `p`, which maps grid units to the target box.
|
||||
fn draw(p: &P, icon: Icon) {
|
||||
use Icon::*;
|
||||
match icon {
|
||||
@@ -446,18 +447,23 @@ fn draw(p: &P, icon: Icon) {
|
||||
p.path(&h, false);
|
||||
}
|
||||
Frame => {
|
||||
p.line(5.5, 2.0, 5.5, 14.0);
|
||||
p.line(10.5, 2.0, 10.5, 14.0);
|
||||
p.line(2.0, 5.5, 14.0, 5.5);
|
||||
p.line(2.0, 10.5, 14.0, 10.5);
|
||||
// Inset a unit from the box: four long hinted strokes read heavier than the
|
||||
// anti-aliased outlines beside them at full size (#737).
|
||||
p.line(5.5, 3.0, 5.5, 13.0);
|
||||
p.line(10.5, 3.0, 10.5, 13.0);
|
||||
p.line(3.0, 5.5, 13.0, 5.5);
|
||||
p.line(3.0, 10.5, 13.0, 10.5);
|
||||
}
|
||||
Section | Section2 => {
|
||||
p.rect(2.5, 5.5, 13.5, 13.5, 1.0);
|
||||
p.fill_rect(2.0, 2.0, 9.0, 6.0, 1.0);
|
||||
}
|
||||
Slice => {
|
||||
p.rect(2.5, 2.5, 13.5, 13.5, 0.0);
|
||||
p.line(2.5, 13.5, 13.5, 2.5);
|
||||
// A knife, one silhouette (#739): a slim blade to the top right, outlined like the
|
||||
// rest of the row, and a solid handle whose end overlaps the blade's base so the two
|
||||
// join at every size.
|
||||
p.poly(&[(13.5, 2.5), (5.0, 8.0), (8.0, 11.0)], true);
|
||||
p.fill(&capsule((2.8, 13.2), (5.9, 10.1), 1.3));
|
||||
}
|
||||
Bend => {
|
||||
// A curve with a smooth point and its handle line.
|
||||
@@ -523,11 +529,12 @@ fn draw(p: &P, icon: Icon) {
|
||||
p.line(9.0, 4.5, 11.5, 7.0);
|
||||
}
|
||||
Text => {
|
||||
p.line(2.0, 3.5, 13.0, 3.5);
|
||||
p.line(7.5, 3.0, 7.5, 13.0);
|
||||
p.line(2.5, 3.0, 2.5, 6.0);
|
||||
p.line(12.5, 3.0, 12.5, 6.0);
|
||||
p.line(5.0, 12.5, 10.0, 12.5);
|
||||
// Inset like the Frame mark, so its long strokes don't outweigh the outlines (#737).
|
||||
p.line(3.0, 3.5, 12.0, 3.5);
|
||||
p.line(7.5, 3.0, 7.5, 12.5);
|
||||
p.line(3.5, 3.0, 3.5, 5.5);
|
||||
p.line(11.5, 3.0, 11.5, 5.5);
|
||||
p.line(5.5, 12.5, 9.5, 12.5);
|
||||
}
|
||||
Comment => {
|
||||
let mut b: Vec<(f32, f32)> = arc_pts(8.5, 7.5, 5.0, 2.75, TAU + 1.95).collect();
|
||||
@@ -1226,4 +1233,59 @@ mod tests {
|
||||
assert!(!rotation.is_empty() && !radius.is_empty(), "both draw an icon");
|
||||
assert_ne!(rotation, radius);
|
||||
}
|
||||
|
||||
/// The shapes `icon` paints in a 32-point box at the origin: two points per grid unit.
|
||||
fn shapes_at_32(icon: Icon) -> Vec<Shape> {
|
||||
let ctx = egui::Context::default();
|
||||
let mut out =
|
||||
ctx.run_ui(egui::RawInput::default(), |ui| paint(ui.painter(), Rect::from_min_size(Pos2::ZERO, Vec2::splat(32.0)), icon, Color32::WHITE));
|
||||
out.textures_delta.clear();
|
||||
out.shapes.into_iter().map(|c| c.shape).collect()
|
||||
}
|
||||
|
||||
/// Frame and Text stay a grid unit inside the box, so their long hinted strokes don't
|
||||
/// outweigh the outlines beside them in the toolbar (#737).
|
||||
#[test]
|
||||
fn frame_and_text_marks_are_inset() {
|
||||
for icon in [Icon::Frame, Icon::Text] {
|
||||
let pts: Vec<Pos2> = shapes_at_32(icon)
|
||||
.iter()
|
||||
.flat_map(|s| match s {
|
||||
Shape::LineSegment { points, .. } => points.to_vec(),
|
||||
other => panic!("{icon:?} draws straight strokes only: {other:?}"),
|
||||
})
|
||||
.collect();
|
||||
assert!(!pts.is_empty());
|
||||
for p in pts {
|
||||
// Grid units 3..13, two points each.
|
||||
assert!((6.0..=26.0).contains(&p.x) && (6.0..=26.0).contains(&p.y), "{icon:?} reaches {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice is one knife (#739): an outlined blade and a solid handle that covers the middle of
|
||||
/// the blade's base, so the two read as one mark rather than separate pieces.
|
||||
#[test]
|
||||
fn slice_is_one_connected_knife() {
|
||||
let shapes = shapes_at_32(Icon::Slice);
|
||||
let paths: Vec<_> = shapes
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
Shape::Path(p) => p,
|
||||
other => panic!("Slice draws paths only: {other:?}"),
|
||||
})
|
||||
.collect();
|
||||
let blade = paths.iter().find(|p| p.closed && p.fill == Color32::TRANSPARENT).expect("an outlined blade");
|
||||
let handle = paths.iter().find(|p| p.fill != Color32::TRANSPARENT).expect("a solid handle");
|
||||
assert_eq!((paths.len(), blade.points.len()), (2, 3), "a blade and a handle, nothing else");
|
||||
// The blade's base runs between its second and third points; the handle is convex.
|
||||
let base = blade.points[1] + (blade.points[2] - blade.points[1]) / 2.0;
|
||||
let h = &handle.points;
|
||||
let side = |i: usize| {
|
||||
let (a, b) = (h[i], h[(i + 1) % h.len()]);
|
||||
(b - a).x * (base - a).y - (b - a).y * (base - a).x
|
||||
};
|
||||
let inside = (0..h.len()).all(|i| side(i) >= 0.0) || (0..h.len()).all(|i| side(i) <= 0.0);
|
||||
assert!(inside, "the handle covers the blade's base at {base:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ use crate::i18n::{tr, tr_noop, trf};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{ACCENT, Tokens, regular};
|
||||
|
||||
/// Every mark in the toolbar row is painted in a box this size, so they share one optical size
|
||||
/// and one stroke width (#737, #738).
|
||||
const GLYPH: f32 = 18.0;
|
||||
/// The flyout carets: smaller than the marks, but on a box whose stroke rounds to the same width
|
||||
/// as theirs at 1×, 1.5× and 2× (a 10-point box drew them one physical pixel thinner).
|
||||
const CARET: f32 = 14.0;
|
||||
|
||||
/// `GLYPH`'s box centred in `r`.
|
||||
fn glyph_rect(r: Rect) -> Rect {
|
||||
Rect::from_center_size(r.center(), vec2(GLYPH, GLYPH))
|
||||
}
|
||||
|
||||
struct Group {
|
||||
key: &'static str,
|
||||
/// The flyout button's name for screen readers.
|
||||
@@ -82,6 +94,8 @@ pub(crate) fn main_width() -> f32 {
|
||||
GROUPS.len() as f32 * (cell + gap) + gap * 10.0 + 2.0 * (cell + gap) + 16.0 + 4.0 * 34.0 + 24.0
|
||||
}
|
||||
|
||||
/// The main toolbar pill: the tool groups with their flyouts, Resources and Actions, then the
|
||||
/// Draw · Design · Prototype · Inspect mode switcher.
|
||||
fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
let (cell, gap) = (32.0, 4.0);
|
||||
let width = main_width();
|
||||
@@ -121,7 +135,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
if cresp.hovered() {
|
||||
ui.painter().rect_filled(cr, CornerRadius::same(4), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(cr.center(), vec2(10.0, 10.0)), Icon::Chevron, t.icon_secondary);
|
||||
icons::paint(ui.painter(), Rect::from_center_size(cr.center(), vec2(CARET, CARET)), Icon::Chevron, t.icon_secondary);
|
||||
if cresp.clicked() {
|
||||
app.ui.tool_flyout = if app.ui.tool_flyout == Some(g.key) { None } else { Some(g.key) };
|
||||
}
|
||||
@@ -149,7 +163,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), r.shrink(7.0), icon, if actions_open { Color32::WHITE } else { t.icon });
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if actions_open { Color32::WHITE } else { t.icon });
|
||||
if resp.clicked() {
|
||||
if id == "actions" {
|
||||
crate::menus::toggle_palette(&mut app.ui);
|
||||
@@ -181,7 +195,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(6), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), r.shrink(9.0), *icon, if i == active { t.accent_text } else { t.icon_secondary });
|
||||
icons::paint(ui.painter(), glyph_rect(r), *icon, if i == active { t.accent_text } else { t.icon_secondary });
|
||||
if resp.clicked() {
|
||||
match i {
|
||||
0 => {
|
||||
@@ -379,7 +393,7 @@ fn tool_cell(ui: &mut Ui, t: &Tokens, r: Rect, id: impl egui::AsIdSalt, icon: Ic
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), bg);
|
||||
icons::paint(ui.painter(), r.shrink(7.0), icon, if active { Color32::WHITE } else { t.icon });
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if active { Color32::WHITE } else { t.icon });
|
||||
resp
|
||||
}
|
||||
|
||||
@@ -452,3 +466,52 @@ fn flyout_id(key: &str) -> egui::Id {
|
||||
pub fn pretty_shortcut(s: &str) -> String {
|
||||
figcraft_engine::keymap::display(s, crate::shortcuts::platform())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::Shape;
|
||||
|
||||
/// The stroke widths `icon` paints with in a `size` box at `ppp` pixels per point.
|
||||
fn widths(ppp: f32, icon: Icon, size: f32) -> Vec<f32> {
|
||||
let ctx = egui::Context::default();
|
||||
let input = || {
|
||||
let mut i = egui::RawInput::default();
|
||||
i.viewports.entry(egui::ViewportId::ROOT).or_default().native_pixels_per_point = Some(ppp);
|
||||
i
|
||||
};
|
||||
ctx.run_ui(input(), |_| {}).textures_delta.clear();
|
||||
let mut out =
|
||||
ctx.run_ui(input(), |ui| icons::paint(ui.painter(), Rect::from_center_size(pos2(50.0, 50.0), vec2(size, size)), icon, Color32::WHITE));
|
||||
out.textures_delta.clear();
|
||||
assert_eq!(ctx.pixels_per_point(), ppp);
|
||||
let mut w: Vec<f32> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|c| match &c.shape {
|
||||
Shape::LineSegment { stroke, .. } => Some(stroke.width),
|
||||
Shape::Path(p) => Some(p.stroke.width),
|
||||
Shape::Rect(r) => Some(r.stroke.width),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|w| *w > 0.0)
|
||||
.collect();
|
||||
w.dedup();
|
||||
w
|
||||
}
|
||||
|
||||
/// The flyout carets and the marks beside them draw with one stroke width (#737), and the
|
||||
/// mode switcher's marks are the tool marks' size (#738).
|
||||
#[test]
|
||||
fn toolbar_marks_share_one_stroke_width() {
|
||||
for ppp in [1.0, 1.5, 2.0] {
|
||||
let mark = widths(ppp, Icon::Rectangle, GLYPH);
|
||||
assert_eq!(mark.len(), 1, "one width at {ppp}×: {mark:?}");
|
||||
assert_eq!(widths(ppp, Icon::Chevron, CARET), mark, "caret at {ppp}×");
|
||||
assert_eq!(widths(ppp, Icon::Code, GLYPH), mark, "mode mark at {ppp}×");
|
||||
for icon in [Icon::Frame, Icon::Text] {
|
||||
assert_eq!(widths(ppp, icon, GLYPH), mark, "{icon:?} at {ppp}×");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user