mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
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:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
e7babf364e
commit
4c8508cd93
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user