diff --git a/.gitignore b/.gitignore index af430a81..0a9913f0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ *.swp /.claude/worktrees/ /target-snapshot +__pycache__/ diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 7415df25..1de6dc60 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -252,6 +252,11 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) -> _ => err("no dialog input with that index"), } } + "ui.handles" => { + // The handles drawn last frame (toolbar buttons `toolbar:`, panels, browser rows…). + let list = crate::scenario::handles(s("prefix").unwrap_or("")); + ok(json!(list.into_iter().map(|(n, p)| json!({"name": n, "x": p.x, "y": p.y})).collect::>())) + } "ui.at" => { // A scenario point: {world: [x,y,z]} | {sketch: [x,y]} | {plane: "XY"} (the middle of an // origin plane's square) → screen point. diff --git a/crates/ui-egui/src/scenario.rs b/crates/ui-egui/src/scenario.rs index 49a924a2..4ff05bcc 100644 --- a/crates/ui-egui/src/scenario.rs +++ b/crates/ui-egui/src/scenario.rs @@ -62,6 +62,11 @@ pub fn handle_at(name: &str) -> Option { HANDLES.with(|h| h.borrow().iter().rfind(|(n, _)| n == name).map(|x| x.1)) } +/// Every handle drawn last frame whose name starts with `prefix`. +pub fn handles(prefix: &str) -> Vec<(String, egui::Pos2)> { + HANDLES.with(|h| h.borrow().iter().filter(|(n, _)| n.starts_with(prefix)).cloned().collect()) +} + /// A painted part's rect this frame, by name ("toolbar", "workspace_switcher"). pub fn publish_rect(name: &str, r: egui::Rect) { RECTS.with(|h| h.borrow_mut().push((name.to_string(), r))); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index ed4f5588..88ed4780 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -297,6 +297,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let br = Rect::from_min_size(pos2(px + 4.0 + i as f32 * 40.0, top + 2.0), vec2(38.0, 38.0)); let info = c.info(&app.session); let resp = ui.interact(br, ui.id().with(("cmd", c.id)), Sense::click()); + crate::scenario::publish_handle(&format!("toolbar:{}", c.id), br.center()); if resp.hovered() && info.enabled { painter.rect_filled(br, 4.0, t.hover); } @@ -324,6 +325,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let lr = Rect::from_min_size(pos2(px, top + 42.0), vec2(width, 18.0)); let label = panel.to_string(); let lresp = ui.interact(lr, ui.id().with(("panel", panel)), Sense::click()); + crate::scenario::publish_handle(&format!("panel:{tab}:{panel}"), lr.center()); if lresp.hovered() && enabled_panel { painter.rect_filled(lr, 3.0, t.hover); } @@ -360,6 +362,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let (ir, _) = ui.allocate_exact_size(vec2(18.0, 18.0), Sense::hover()); icons::paint(ui.painter(), ir, c.icon, t.icon, t.icon_fill, t.accent); let b = ui.add_enabled(info.enabled, egui::Button::new(c.label).frame(false).shortcut_text(c.shortcut.unwrap_or(""))); + crate::scenario::publish_handle(&format!("toolbar:{}", c.id), b.rect.center()); if b.clicked() { let id = c.id; app.start(id); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index f547051c..b0543a43 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -37,6 +37,7 @@ Commands never open dialogs when run this way. `solvecraft-cli commands` prints | `ui.sketchToScreen` | `points`: `[[x, y]…]` in active-sketch coordinates | their screen points (null when behind the camera) | | `ui.dialogInput` | `index` | what clicking an input's Select chip does: picks go to that input | | `ui.at` | `world: [x,y,z]` \| `sketch: [x,y]` \| `plane: "XY"` \| `axis: "Z"` \| `dimension: "d1"` (its value text) \| `handle: "arrow"` (a manipulator handle, see `scenario::publish_handle`) | the screen point (a scenario step's target; see `crates/ui-egui/src/scenario.rs`) | +| `ui.handles` | `prefix?` | the handles drawn last frame, `[{name, x, y}]`: toolbar buttons and drop-down items `toolbar:`, panel labels `panel::`, browser rows `row: