Insert › Shapes menu: an icon for each shape and connector (#2142)

Each item shows a small drawn outline of what it inserts (rectangle,
oval, triangle, star, elbow connector...). The icons are drawn in code in
icons.rs as shape.<kind>; widgets::icon_menu_item draws the item.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Joseph Anglada
2026-10-18 11:36:27 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent e7babf364e
commit 4c8508cd93
3 changed files with 137 additions and 3 deletions
+98
View File
@@ -74,6 +74,11 @@ impl Pen<'_> {
/// Paint icon `name` into `r`. `c` is the line colour; the accent comes from the theme.
pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32, accent: Color32) {
let pen = Pen { p, r, c, a: accent, w: 1.25 };
if let Some(kind) = name.strip_prefix("shape.")
&& shape(&pen, kind)
{
return;
}
let a = accent;
let red = Color32::from_rgb(0xD1, 0x3B, 0x3B);
let yellow = Color32::from_rgb(0xF2, 0xC8, 0x11);
@@ -1443,6 +1448,99 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32, accent: Color32) {
}
}
/// The Insert › Shapes menu's icons: each shape kind (`insert.shape` ids) as a small outline.
fn shape(pen: &Pen, kind: &str) -> bool {
let (c, a) = (pen.c, pen.a);
let tint = a.linear_multiply(0.25);
let solid = |pts: &[(f32, f32)]| {
pen.fill(pts, tint);
pen.closed(pts, c);
};
let head = |tip: (f32, f32), from: (f32, f32)| {
let (dx, dy) = (tip.0 - from.0, tip.1 - from.1);
let len = (dx * dx + dy * dy).sqrt().max(0.01);
let (ux, uy) = (dx / len, dy / len);
let back = (tip.0 - ux * 4.5, tip.1 - uy * 4.5);
pen.fill(&[tip, (back.0 - uy * 2.5, back.1 + ux * 2.5), (back.0 + uy * 2.5, back.1 - ux * 2.5)], c);
};
let ends = |pts: &[(f32, f32)], arrow: bool| {
pen.line(pts);
if let Some(&first) = pts.first() {
pen.fcircle(first.0, first.1, 1.6, a);
}
if let [.., from, tip] = pts {
if arrow {
head(*tip, *from);
} else {
pen.fcircle(tip.0, tip.1, 1.6, a);
}
}
};
let curve = || -> Vec<(f32, f32)> {
let (p0, p1, p2, p3) = ((4.0, 15.0), (12.0, 15.0), (8.0, 5.0), (16.0, 5.0));
(0..=16)
.map(|i| {
let s = i as f32 / 16.0;
let m = 1.0 - s;
let w = [m * m * m, 3.0 * m * m * s, 3.0 * m * s * s, s * s * s];
(w[0] * p0.0 + w[1] * p1.0 + w[2] * p2.0 + w[3] * p3.0, w[0] * p0.1 + w[1] * p1.1 + w[2] * p2.1 + w[3] * p3.1)
})
.collect()
};
match kind {
"rectangle" => solid(&[(3.0, 5.0), (17.0, 5.0), (17.0, 15.0), (3.0, 15.0)]),
"roundedRectangle" => {
let r = Rect::from_min_max(pen.pt(3.0, 5.0), pen.pt(17.0, 15.0));
let radius = 3.0 * pen.s();
pen.p.rect_filled(r, radius, tint);
pen.p.rect_stroke(r, radius, pen.stroke(c), egui::StrokeKind::Middle);
}
"ellipse" => {
let (center, radius) = (pen.pt(10.0, 10.0), vec2(7.5, 5.5) * pen.s());
pen.p.add(Shape::ellipse_filled(center, radius, tint));
pen.p.add(Shape::ellipse_stroke(center, radius, pen.stroke(c)));
}
"triangle" => solid(&[(10.0, 3.5), (17.0, 16.5), (3.0, 16.5)]),
"diamond" => solid(&[(10.0, 3.0), (17.0, 10.0), (10.0, 17.0), (3.0, 10.0)]),
"line" => pen.line(&[(4.0, 16.0), (16.0, 4.0)]),
"arrow" => {
pen.line(&[(4.0, 16.0), (14.5, 5.5)]);
head((16.5, 3.5), (4.0, 16.0));
}
"star" => {
let pts: Vec<(f32, f32)> = (0..10)
.map(|i| {
let ang = -std::f32::consts::FRAC_PI_2 + i as f32 * std::f32::consts::PI / 5.0;
let rad = if i % 2 == 0 { 8.0 } else { 3.3 };
(10.0 + rad * ang.cos(), 10.8 + rad * ang.sin())
})
.collect();
// Concave: the tinted core, then the outline.
pen.fill(&pts.iter().skip(1).step_by(2).copied().collect::<Vec<_>>(), tint);
pen.closed(&pts, c);
}
"heart" => {
let pts: Vec<(f32, f32)> = (0..32)
.map(|i| {
let s = i as f32 / 32.0 * std::f32::consts::TAU;
let x = 16.0 * s.sin().powi(3);
let y = 13.0 * s.cos() - 5.0 * (2.0 * s).cos() - 2.0 * (3.0 * s).cos() - (4.0 * s).cos();
(10.0 + x * 0.44, 9.6 - y * 0.44)
})
.collect();
pen.closed(&pts, c);
}
"straightConnector" => ends(&[(4.0, 15.0), (16.0, 5.0)], false),
"straightArrowConnector" => ends(&[(4.0, 15.0), (16.0, 5.0)], true),
"elbowConnector" => ends(&[(4.0, 15.0), (10.0, 15.0), (10.0, 5.0), (16.0, 5.0)], false),
"elbowArrowConnector" => ends(&[(4.0, 15.0), (10.0, 15.0), (10.0, 5.0), (16.0, 5.0)], true),
"curvedConnector" => ends(&curve(), false),
"curvedArrowConnector" => ends(&curve(), true),
_ => return false,
}
true
}
trait ThenLines {
fn then_lines(self, pen: &Pen, x0: f32, x1: f32, ys: &[f32]);
}
+15 -3
View File
@@ -628,6 +628,17 @@ pub(crate) fn mi(ui: &mut Ui, app: &mut WordApp, label: &str, id: &str, params:
mi_text(ui, app, tl!(label), id, params);
}
/// [`mi`] with a drawn icon before the label.
fn mi_icon(ui: &mut Ui, app: &mut WordApp, icon: &str, label: &str, id: &str, params: Value) {
let sc = crate::widgets::shortcut_text(app, id);
let enabled = crate::widgets::enabled(app, id);
let resp = ui.add_enabled_ui(enabled, |ui| crate::widgets::icon_menu_item(ui, icon, tl!(label), &sc, 200.0)).inner;
if resp.clicked() {
crate::widgets::run_from_ribbon(app, id, params);
ui.close();
}
}
/// A menu item whose text is already in the interface language.
fn mi_text(ui: &mut Ui, app: &mut WordApp, text: &str, id: &str, params: Value) {
let sc = crate::widgets::shortcut_text(app, id);
@@ -1223,10 +1234,10 @@ fn insert(app: &mut WordApp, ui: &mut Ui) {
("Star", "star"),
("Heart", "heart"),
] {
mi(ui, app, l, "insert.shape", json!({"kind": k}));
mi_icon(ui, app, &format!("shape.{k}"), l, "insert.shape", json!({"kind": k}));
}
ui.separator();
mi(ui, app, "New Drawing Canvas", "insert.canvas", json!({}));
mi_icon(ui, app, "canvas", "New Drawing Canvas", "insert.canvas", json!({}));
// Connectors: glue their ends to shapes by dragging them (shape.connect).
for (l, k, arrow) in [
("Straight Connector", "straightConnector", "none"),
@@ -1236,7 +1247,8 @@ fn insert(app: &mut WordApp, ui: &mut Ui) {
("Curved Connector", "curvedConnector", "none"),
("Curved Arrow Connector", "curvedConnector", "end"),
] {
mi(ui, app, l, "insert.shape", json!({"kind": k, "arrow": arrow}));
let icon = if arrow == "end" { k.replace("Connector", "ArrowConnector") } else { k.to_string() };
mi_icon(ui, app, &format!("shape.{icon}"), l, "insert.shape", json!({"kind": k, "arrow": arrow}));
}
});
stack(ui, |ui| {
+24
View File
@@ -1022,6 +1022,30 @@ fn icon_text_button_with(ui: &mut Ui, icon: &str, text: &str, font: egui::FontId
resp
}
/// A drop-down menu item with `icon` before `text`, at least `min_w` wide.
pub fn icon_menu_item(ui: &mut Ui, icon: &str, text: &str, shortcut: &str, min_w: f32) -> Response {
let t = Tokens::get(ui.ctx());
let font = egui::TextStyle::Button.resolve(ui.style());
let galley = ui.painter().layout_no_wrap(text.to_string(), font.clone(), Color32::PLACEHOLDER);
let sc = (!shortcut.is_empty()).then(|| ui.painter().layout_no_wrap(shortcut.to_string(), font, Color32::PLACEHOLDER));
let pad = ui.spacing().button_padding;
let icon_w = 16.0;
let sc_w = sc.as_ref().map_or(0.0, |g| g.size().x + 3.0 * pad.x);
let w = (pad.x + icon_w + 6.0 + galley.size().x + sc_w + pad.x).max(min_w).max(ui.available_width());
let (r, resp) = ui.allocate_exact_size(vec2(w, galley.size().y.max(icon_w) + 2.0 * pad.y), Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), text));
let st = drawn_state(ui, &resp, false);
ui.painter().rect_filled(r, ui.visuals().widgets.inactive.corner_radius, st.fill);
let (line, accent) = if ui.is_enabled() { (t.icon, t.accent) } else { (t.text_disabled, t.text_disabled) };
let text_c = if ui.is_enabled() { st.text } else { t.text_disabled };
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + pad.x + icon_w / 2.0, r.center().y), vec2(icon_w, icon_w)), icon, line, accent);
ui.painter().galley(pos2(r.min.x + pad.x + icon_w + 6.0, r.center().y - galley.size().y / 2.0), galley, text_c);
if let Some(g) = sc {
ui.painter().galley(pos2(r.max.x - pad.x - g.size().x, r.center().y - g.size().y / 2.0), g, t.text_disabled);
}
resp
}
#[cfg(test)]
mod tests {
use super::*;