diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 5bf4dbe4f..63ab393e9 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -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::>(), 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]); } diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 6b4afd9c4..ef8845957 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -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| { diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index c22fdc5e0..71df96572 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -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::*;