diff --git a/ROADMAP.md b/ROADMAP.md index dccfb34..991a2ee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -239,7 +239,7 @@ Scores by behaviour, 0–100%. Hours are what remains (one agent). | Layers panel | 5 | 83% | Tab to rename the next layer, visual QA of long lists | 1 | | Shapes & vector | 8 | 74% | live boolean editing, arc handles on canvas, shape builder on strokes and open paths, one-sided widths | 4–8 | | Paint | 7 | 78% | a wide-gamut canvas (egui draws sRGB, so Display P3 colours outside sRGB clip on screen; exports keep them), images' own embedded colour profiles (images are taken as sRGB) | 1–3 | -| Strokes | 3 | 80% | stroke styles, arrowhead previews in the menus | 2–3 | +| Strokes | 3 | 82% | stroke styles | 1–2 | | Effects | 4 | 70% | glass/texture fidelity, shadow spread on non-rects exactness | 4–7 | | Frames, groups, sections, masks | 5 | 75% | hiding section contents UI, section-level prototype/dev affordances | 2–4 | | Auto layout & constraints | 9 | 72% | visual QA of nested edge cases, arrow-key moves across nested auto layout frames | 2–4 | diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 3ed8e94..e68a97f 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -223,6 +223,14 @@ pub fn handle(app: &mut FigApp, ctx: &Context, method: &str, p: &Value) -> Resul } } else if let Some(t) = e.get("text").and_then(Value::as_str) { events.push(egui::Event::Text(t.to_string())); + } else if let Some(d) = e.get("scroll").and_then(pos) { + // Content moves by `d` points (negative y scrolls down, like a wheel turned towards you). + events.push(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: d.to_vec2(), + phase: egui::TouchPhase::Move, + modifiers: mods, + }); } else if b(e, "leave") { events.push(egui::Event::PointerGone); } else { diff --git a/crates/ui-egui/src/right.rs b/crates/ui-egui/src/right.rs index f7da398..809c88d 100644 --- a/crates/ui-egui/src/right.rs +++ b/crates/ui-egui/src/right.rs @@ -1,6 +1,6 @@ //! The right panel: Design, Prototype and Inspect tabs. -use egui::{Align2, Color32, CornerRadius, Id, Rect, Sense, Stroke, Ui, pos2, vec2}; +use egui::{Align2, Color32, CornerRadius, Id, Pos2, Rect, Sense, Stroke, Ui, pos2, vec2}; use figcraft_color::{BlendMode, Paint, PaintKind}; use figcraft_doc::*; use serde_json::{Value, json}; @@ -1080,7 +1080,7 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId] let resp = widgets::dropdown(ui, cap.map(StrokeCap::label).unwrap_or("Mixed"), half, None); egui::Popup::menu(&resp).show(|ui| { for c in StrokeCap::ALL { - if menu_item(ui, c.label(), "") { + if menu_item_with(ui, c.label(), "", |p, r, col| cap_art(p, r, col, c, key == "startCap")) { app.set_props(&ids, json!({key: format!("{c:?}")})); ui.close(); } @@ -1092,6 +1092,50 @@ fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId] ui.add_space(4.0); } +/// A short stroke ending in `cap` (on its left end for a start point), drawn at the right of a menu +/// row. +fn cap_art(p: &egui::Painter, r: Rect, col: Color32, cap: StrokeCap, start: bool) { + let y = r.center().y; + let (left, right) = (r.max.x - 58.0, r.max.x - 14.0); + // The capped end, the plain end, and the direction from the stroke into its cap. + let (tip, tail, d) = if start { (left, right, -1.0) } else { (right, left, 1.0) }; + let at = |back: f32, dy: f32| pos2(tip - d * back, y + dy); + let line = |to: Pos2, w: f32| { + p.line_segment([pos2(tail, y), to], Stroke::new(w, col)); + }; + let fill = |pts: Vec| { + p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE)); + }; + match cap { + StrokeCap::None => line(at(0.0, 0.0), 4.0), + StrokeCap::Round => { + line(at(2.0, 0.0), 4.0); + p.circle_filled(at(2.0, 0.0), 2.0, col); + } + StrokeCap::Square => line(at(-2.0, 0.0), 4.0), + StrokeCap::LineArrow => { + line(at(0.0, 0.0), 1.5); + p.add(egui::Shape::line(vec![at(5.5, -5.5), at(0.0, 0.0), at(5.5, 5.5)], Stroke::new(1.5, col))); + } + StrokeCap::TriangleArrow => { + line(at(7.0, 0.0), 1.5); + fill(vec![at(0.0, 0.0), at(8.0, -4.5), at(8.0, 4.5)]); + } + StrokeCap::ReversedTriangle => { + line(at(8.0, 0.0), 1.5); + fill(vec![at(8.0, 0.0), at(0.0, -4.5), at(0.0, 4.5)]); + } + StrokeCap::CircleFilled => { + line(at(4.0, 0.0), 1.5); + p.circle_filled(at(4.0, 0.0), 4.0, col); + } + StrokeCap::DiamondFilled => { + line(at(5.0, 0.0), 1.5); + fill(vec![at(0.0, 0.0), at(5.0, -5.0), at(10.0, 0.0), at(5.0, 5.0)]); + } + } +} + fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint, fills: bool) { let t = Tokens::get(ui.ctx()); let ids = ids.to_vec(); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 7a284ca..eb85e0e 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -105,8 +105,9 @@ Raw window input for the next frame, as if from the mouse and keyboard: it reach popovers and fields (`ui.pointer` and `ui.key` drive the canvas tools and shortcuts instead). `events` is a list of `{move: [x, y]}`, `{down: [x, y], button?: "secondary"}`, `{up: [x, y], button?}`, `{key, shift?, alt?, cmd?}` (pressed and released; egui key names such as -`ArrowLeft`, `Escape`, `A`), `{text}` (typed text) and `{leave: true}` (the pointer left the -window); positions are window points. Events of one call arrive in the same frame, so send a press +`ArrowLeft`, `Escape`, `A`), `{text}` (typed text), `{scroll: [dx, dy], shift?, alt?, cmd?}` (a +mouse wheel at the pointer: the content moves by that many points, so a negative `dy` scrolls down) +and `{leave: true}` (the pointer left the window); positions are window points. Events of one call arrive in the same frame, so send a press and its release (or a double-click's two clicks) as separate calls. Returns `{queued}`. ```json