Toolbar: drag commands from drop-downs onto the toolbar; pin icon; Customize Toolbar… (#295)

* Toolbar drop-downs highlight the row under the pointer and the keyboard focus (#288)

A panel drop-down's command rows are full-width rows that take the theme's hover background
(icon, label and shortcut on it) when hovered or focused. Up/Down move the focus through the
enabled rows, Enter starts the command, Escape closes the drop-down. Rows publish
`dropdown:<id>` and the highlighted one `dropdown_hot:<id>`; scenario s46 checks pointer and
keyboard highlight.

Fixes #288

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* Toolbar: drag commands from drop-downs onto the toolbar; pin icon; Customize Toolbar… (#289)

- A panel drop-down row drags onto the toolbar (a button or a panel's free space, with the
  drop line) and is pinned there; the dragged command's icon follows the pointer.
- A toolbar button dragged off the toolbar, or back into the open drop-down, leaves it (its
  icon shows a cross while releasing would remove it).
- A hovered drop-down row shows a pin at its end: click to pin or unpin.
- Right-click on the toolbar's empty space: Customize Toolbar… (a window listing the tab's
  commands with check boxes, and Reset Toolbar) and Reset Toolbar; command menus have
  Customize Toolbar… too.
- Still per workspace and tab, kept with the preferences. Toolbar buttons also publish
  `button:<id>`; scenario s47 drags a row to the toolbar, unpins by dragging off and into the
  drop-down, uses the pin icon and Customize Toolbar…, and checks it survives a restart.

Fixes #289

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

* Toolbar drop-down pin: a push pin drawn in code (upright and filled when pinned); control protocol lists the toolbar handles

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 10:27:25 -04:00
1 parent 3d99ab4bb1
commit 2cac9cad6a
7 files changed
+490 -27

No files matched your search

+7 -1
View File
@@ -54,6 +54,8 @@ pub enum Target {
ToolbarCommand {
id: String,
},
/// The toolbar's empty space.
Toolbar,
/// The workspace switcher.
Workspace,
/// The Named Views folder, and one view (saved or standard) in it.
@@ -240,6 +242,7 @@ pub fn target_bodies(app: &SolveApp, target: &Target) -> Vec<String> {
| Target::Units
| Target::Joint { .. }
| Target::ToolbarCommand { .. }
| Target::Toolbar
| Target::Workspace
| Target::NamedViews
| Target::NamedView { .. } => Vec::new(),
@@ -298,6 +301,7 @@ pub fn items(app: &SolveApp, target: &Target) -> Vec<Item> {
Target::NamedViews => vec![act("ui.newView", "New Named View", "perspective")],
Target::Joint { id } => joint_items(app, *id),
Target::ToolbarCommand { id } => crate::toolbar_custom::command_items(app, id),
Target::Toolbar => crate::toolbar_custom::toolbar_items(app),
Target::Workspace => {
let mut v = vec![act("ui.workspace", "Design", "box").key("current")];
for w in ["Generative Design", "Render", "Animation", "Simulation", "Manufacture", "Drawing"] {
@@ -879,7 +883,9 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
let _ = app.run("browser.visibility", json!({"visible": true, "paths": [p["path"]], "isolate": true}));
}
"ui.canvasVisible" => drop(app.run("canvas.edit", p)),
"ui.toolbarPin" | "ui.toolbarRemove" | "ui.toolbarReset" | "ui.shortcutPin" => crate::toolbar_custom::run(app, &item.id, &p),
"ui.toolbarPin" | "ui.toolbarRemove" | "ui.toolbarReset" | "ui.toolbarCustomize" | "ui.shortcutPin" => {
crate::toolbar_custom::run(app, &item.id, &p)
}
"ui.workspace" => {}
"ui.findJoint" => {
if let Some(id) = id_of(&p, "joint") {
+1
View File
@@ -1007,6 +1007,7 @@ impl SolveApp {
}
shortcut_box::show(self, ui.ctx());
prefs::show(self, ui.ctx());
toolbar_custom::window(self, ui.ctx());
}
fn drain_control(&mut self, ctx: &egui::Context) {
+197 -24
View File
@@ -359,6 +359,12 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
egui::Panel::top("sc_toolbar").exact_size(92.0).frame(egui::Frame::NONE.fill(t.toolbar)).show(ui, |ui| {
let r = ui.max_rect();
let painter = ui.painter().clone();
// Right-click on the toolbar's empty space: its own menu. Interacted first, so every
// button, tab and panel label is above it.
let bg = ui.interact(r, ui.id().with("toolbar_bg"), Sense::click());
if bg.secondary_clicked() {
crate::context_menu::open_for(app, bg.interact_pointer_pos().unwrap_or(r.left_bottom()), crate::context_menu::Target::Toolbar);
}
// Workspace selector and tabs.
let tabs_y = r.top() + 2.0;
// The workspace switcher spans the toolbar's height (the tab row and the panel row).
@@ -426,6 +432,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
let top = r.top() + 26.0;
let mut drop: Option<(&'static str, &'static str, Option<&'static str>)> = None;
let mut menu: Option<(egui::Pos2, &'static str)> = None;
// A drop-down row's pin icon clicked: pin or unpin that command.
let mut toggle: Option<&'static str> = None;
// The open drop-down (a button dropped on it leaves the toolbar).
let mut popup_rect: Option<Rect> = None;
let mut overflow = Vec::new();
for (panel, cmds, promote) in crate::toolbar_custom::panel_lists(&tab) {
let panel: &&str = &panel;
@@ -447,26 +457,28 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
painter.line_segment([pos2(free.left(), free.top() + 2.0), pos2(free.left(), free.bottom() - 2.0)], Stroke::new(2.0, t.accent));
}
if let Some(d) = fresp.dnd_release_payload::<ToolbarDrag>() {
drop = Some((d.0, *panel, None));
drop = Some((d.id, *panel, None));
}
for (i, c) in shown.iter().enumerate() {
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_and_drag());
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), br.center());
// The button itself (`toolbar:<id>` is also a drop-down row's while one is open).
crate::scenario::publish_handle(&format!("button:{}", c.id), br.center());
if resp.drag_started() {
resp.dnd_set_drag_payload(ToolbarDrag(c.id));
resp.dnd_set_drag_payload(ToolbarDrag { id: c.id, from_menu: false });
}
if let Some(d) = resp.dnd_hover_payload::<ToolbarDrag>()
&& d.0 != c.id
&& d.id != c.id
{
painter
.line_segment([pos2(br.left() - 1.0, br.top() + 2.0), pos2(br.left() - 1.0, br.bottom() - 2.0)], Stroke::new(2.0, t.accent));
}
if let Some(d) = resp.dnd_release_payload::<ToolbarDrag>()
&& d.0 != c.id
&& d.id != c.id
{
drop = Some((d.0, *panel, Some(c.id)));
drop = Some((d.id, *panel, Some(c.id)));
}
if resp.secondary_clicked() {
menu = Some((resp.interact_pointer_pos().unwrap_or(br.left_bottom()), c.id));
@@ -542,29 +554,47 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
} else if cmds.is_empty() {
ui.label(egui::RichText::new("Not available yet").color(t.text_dim));
}
let row_w = dropdown_width(app, ui, &cmds);
// Up and Down move the keyboard focus through the enabled rows (from none
// focused: the first or the last), instead of egui's own spatial move.
let (down, up) = ui.input(|i| (i.key_pressed(egui::Key::ArrowDown), i.key_pressed(egui::Key::ArrowUp)));
if down || up {
let enabled: Vec<&str> = cmds.iter().filter(|c| c.info(&app.session).enabled).map(|c| c.id).collect();
let focused = ui.ctx().memory(|m| m.focused());
let at = enabled.iter().position(|id| focused == Some(dropdown_row_id(ui, id)));
let n = enabled.len();
let next = match (at, down) {
(None, true) => Some(0),
(None, false) => n.checked_sub(1),
(Some(i), true) => Some((i + 1) % n.max(1)),
(Some(i), false) => Some((i + n.max(1) - 1) % n.max(1)),
};
if let Some(id) = next.and_then(|i| enabled.get(i)) {
ui.ctx().memory_mut(|m| {
m.request_focus(dropdown_row_id(ui, id));
m.move_focus(egui::FocusDirection::None);
});
}
}
for c in &cmds {
let info = c.info(&app.session);
ui.horizontal(|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(app.tr_label(c.label)).frame(false).shortcut_text(c.shortcut.unwrap_or("")),
);
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), b.rect.center());
if b.secondary_clicked() {
menu = Some((b.interact_pointer_pos().unwrap_or(b.rect.left_bottom()), c.id));
}
if b.clicked() {
let id = c.id;
app.start(id);
let pinned = crate::toolbar_custom::pinned_on(app, &tab, c.id).is_some();
match dropdown_row(app, ui, c, row_w, pinned) {
RowAction::None => {}
RowAction::Menu(at) => menu = Some((at, c.id)),
RowAction::TogglePin => toggle = Some(c.id),
RowAction::Start => {
app.start(c.id);
ui.ctx().data_mut(|d| d.remove::<egui::Id>(egui::Id::new("sc_panel_open")));
}
});
}
}
});
});
if area.response.clicked_elsewhere() && !lresp.clicked() {
popup_rect = Some(area.response.rect);
crate::scenario::publish_rect("toolbar_dropdown", area.response.rect);
// A drag out of the drop-down ends with a release elsewhere: not a click elsewhere.
let dragging = egui::DragAndDrop::has_payload_of_type::<ToolbarDrag>(ui.ctx());
if (area.response.clicked_elsewhere() && !lresp.clicked() && !dragging) || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
ui.ctx().data_mut(|d| d.remove::<egui::Id>(egui::Id::new("sc_panel_open")));
}
}
@@ -598,9 +628,27 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
});
}
painter.line_segment([pos2(r.left(), r.bottom() - 0.5), pos2(r.right(), r.bottom() - 0.5)], Stroke::new(1.0, t.border));
// Dragging a command: its icon follows the pointer; a toolbar button dragged off the
// toolbar (or into the open drop-down) shows it will be removed.
let pointer = ui.input(|i| i.pointer.latest_pos());
let over_popup = |p: egui::Pos2| popup_rect.is_some_and(|pr| pr.contains(p));
let leaves = |p: egui::Pos2| !r.contains(p) || over_popup(p);
if let (Some(d), Some(p)) = (egui::DragAndDrop::payload::<ToolbarDrag>(ui.ctx()), pointer) {
let removing = !d.from_menu && leaves(p);
drag_ghost(ui.ctx(), d.id, p, removing);
if ui.input(|i| i.pointer.any_released()) && drop.is_none() {
egui::DragAndDrop::clear_payload(ui.ctx());
if removing {
crate::toolbar_custom::remove(app, &tab, d.id);
}
}
}
if let Some((id, panel, before)) = drop {
crate::toolbar_custom::reorder(app, &tab, id, panel, before);
}
if let Some(id) = toggle {
crate::toolbar_custom::toggle(app, &tab, id);
}
if let Some((at, id)) = menu {
ui.ctx().data_mut(|d| d.remove::<egui::Id>(egui::Id::new("sc_panel_open")));
crate::context_menu::open_for(app, at, crate::context_menu::Target::ToolbarCommand { id: id.to_string() });
@@ -608,9 +656,134 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
});
}
/// A toolbar button being dragged (its command id).
/// What a click on a panel drop-down row asks for.
enum RowAction {
None,
Start,
Menu(egui::Pos2),
TogglePin,
}
const ROW_H: f32 = 24.0;
/// The id of a drop-down row (for keyboard focus).
fn dropdown_row_id(ui: &egui::Ui, id: &str) -> egui::Id {
ui.id().with(("dropdown_row", id))
}
/// How wide a panel drop-down's rows are: the widest label and shortcut, at least 200.
fn dropdown_width(app: &SolveApp, ui: &egui::Ui, cmds: &[&'static solvecraft_engine::CommandSpec]) -> f32 {
let t = Tokens::get();
let font = FontId::proportional(13.0);
cmds.iter()
.map(|c| {
let label = ui.painter().layout_no_wrap(app.tr_label(c.label).to_string(), font.clone(), t.text).size().x;
let key = c.shortcut.map(|k| ui.painter().layout_no_wrap(k.to_string(), font.clone(), t.text).size().x + 24.0).unwrap_or(0.0);
6.0 + 18.0 + 8.0 + label + key + 30.0
})
.fold(200.0, f32::max)
}
/// One command row of a panel drop-down: icon, label and shortcut, with the hover background
/// under the pointer or keyboard focus (like every menu).
fn dropdown_row(app: &SolveApp, ui: &mut egui::Ui, c: &'static solvecraft_engine::CommandSpec, width: f32, pinned: bool) -> RowAction {
let t = Tokens::get();
let info = c.info(&app.session);
let (rect, _) = ui.allocate_exact_size(vec2(width, ROW_H), Sense::hover());
// Every row drags onto the toolbar (to pin it there); enabled rows also start their command.
let sense = if info.enabled { Sense::click_and_drag() } else { Sense::drag() };
let resp = ui.interact(rect, dropdown_row_id(ui, c.id), sense);
if resp.drag_started() {
resp.dnd_set_drag_payload(ToolbarDrag { id: c.id, from_menu: true });
}
// The pin at the row's end (above the row, so it takes its own clicks).
let pr = Rect::from_center_size(pos2(rect.right() - 13.0, rect.center().y), vec2(20.0, 20.0));
let pin = ui.interact(pr, ui.id().with(("dropdown_pin", c.id)), Sense::click());
let pointer_on = (resp.hovered() || pin.hovered()) && !egui::DragAndDrop::has_any_payload(ui.ctx());
let label = app.tr_label(c.label).to_string();
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled() && info.enabled, &label));
crate::scenario::publish_handle(&format!("toolbar:{}", c.id), rect.center());
crate::scenario::publish_handle(&format!("dropdown:{}", c.id), rect.center());
let hot = info.enabled && (pointer_on || resp.has_focus());
let p = ui.painter();
if hot {
p.rect_filled(rect, 3.0, t.hover);
crate::scenario::publish_handle(&format!("dropdown_hot:{}", c.id), rect.center());
}
let (ink, fill, acc, text) =
if info.enabled { (t.icon, t.icon_fill, t.accent, t.text) } else { (t.border, t.panel_header, t.border, t.text_dim) };
let ir = Rect::from_center_size(pos2(rect.left() + 6.0 + 9.0, rect.center().y), vec2(18.0, 18.0));
icons::paint(p, ir, c.icon, ink, fill, acc);
p.text(pos2(ir.right() + 8.0, rect.center().y), Align2::LEFT_CENTER, &label, FontId::proportional(13.0), text);
if let Some(k) = c.shortcut {
p.text(pos2(pr.left() - 4.0, rect.center().y), Align2::RIGHT_CENTER, k, FontId::proportional(13.0), t.text_dim);
}
if pointer_on || resp.has_focus() {
if pin.hovered() {
p.rect_filled(pr, 3.0, t.border.gamma_multiply(0.6));
}
pin_icon(p, pr, pinned, if pinned { t.accent } else { t.text_dim });
crate::scenario::publish_handle(&format!("dropdown_pin:{}", c.id), pr.center());
}
let tip = if pinned { "Remove from Toolbar" } else { "Pin to Toolbar" };
if pin.on_hover_text(tip).clicked() {
return RowAction::TogglePin;
}
let resp = match &info.disabled_reason {
Some(why) if !info.enabled => resp.on_hover_text(why),
_ => resp,
};
if resp.secondary_clicked() {
return RowAction::Menu(resp.interact_pointer_pos().unwrap_or(rect.left_bottom()));
}
if resp.clicked() && info.enabled {
return RowAction::Start;
}
RowAction::None
}
/// A command being dragged: a toolbar button, or a row of a panel drop-down (`from_menu`).
#[derive(Clone, Copy, Debug)]
struct ToolbarDrag(&'static str);
struct ToolbarDrag {
id: &'static str,
from_menu: bool,
}
/// The dragged command's icon under the pointer, crossed out when releasing removes it.
fn drag_ghost(ctx: &egui::Context, id: &str, at: egui::Pos2, removing: bool) {
let t = Tokens::get();
let Some(c) = solvecraft_engine::find_command(id) else { return };
let p = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("sc_toolbar_drag")));
let r = Rect::from_center_size(at + vec2(14.0, 16.0), vec2(30.0, 30.0));
p.rect(r, 4.0, t.toolbar.gamma_multiply(0.92), Stroke::new(1.0, if removing { t.error } else { t.accent }), egui::StrokeKind::Inside);
icons::paint(&p, r.shrink(5.0), c.icon, t.icon, t.icon_fill, t.accent);
if removing {
let s = Stroke::new(2.0, t.error);
let x = Rect::from_center_size(r.right_top(), vec2(9.0, 9.0));
p.circle_filled(x.center(), 7.0, t.toolbar);
p.line_segment([x.left_top(), x.right_bottom()], s);
p.line_segment([x.right_top(), x.left_bottom()], s);
}
crate::scenario::publish_handle(if removing { "toolbar_drag:remove" } else { "toolbar_drag" }, at);
}
/// A push pin (drawn in code): upright and filled when the command is on the toolbar, tilted and
/// outlined when not.
fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
let c = r.center();
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
// Local coordinates: the pin points down, its head on top.
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
let stroke = Stroke::new(1.4, col);
if on {
p.add(egui::Shape::convex_polygon(head, col, stroke));
} else {
p.add(egui::Shape::closed_line(head, stroke));
}
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
}
/// The SELECT panel: what a click in the view picks.
fn selection_filter(app: &mut SolveApp, ui: &mut egui::Ui) {
+90 -1
View File
@@ -1,7 +1,11 @@
//! Toolbar customisation: which commands each panel shows as buttons, per workspace and tab.
//! A command's menu (right-click on a toolbar button, a panel drop-down row or an S box row)
//! pins it to the toolbar, removes it, or pins it to the S box's favourites; buttons are dragged
//! to reorder them within and between panels; Reset Toolbar restores the tab. Kept with the
//! to reorder them within and between panels; a drop-down row dragged onto the toolbar pins it
//! there, a button dragged off the toolbar (or back into the drop-down) leaves it; the pin icon
//! on a hovered drop-down row pins or unpins; the toolbar's own menu (right-click its empty
//! space) has Customize Toolbar… (a window listing every command of the tab with a check box)
//! and Reset Toolbar; Reset Toolbar restores the tab. Kept with the
//! preferences and exported or imported with the keyboard shortcuts.
use std::collections::BTreeMap;
@@ -20,6 +24,8 @@ pub struct ToolbarCustom {
/// `"<workspace>/<tab>/<panel>"` → the panel's buttons, in order (only panels the user
/// changed).
pub panels: BTreeMap<String, Vec<String>>,
/// The Customize Toolbar window is open (not kept).
pub customizing: bool,
}
impl ToolbarCustom {
@@ -130,6 +136,15 @@ pub fn reorder(app: &mut SolveApp, tab: &str, id: &str, panel: &str, before: Opt
app.toolbar_custom.panels.insert(key(tab, panel), ids);
}
/// The pin icon of a drop-down row: pin the command, or remove it when it is pinned.
pub fn toggle(app: &mut SolveApp, tab: &str, id: &str) {
if pinned_on(app, tab, id).is_some() {
remove(app, tab, id);
} else if let Err(e) = pin(app, tab, id) {
app.set_status(e, true);
}
}
/// Reset Toolbar: a tab's panels back to their defaults.
pub fn reset(app: &mut SolveApp, tab: &str) {
let prefix = format!("{WORKSPACE}/{tab}/");
@@ -149,10 +164,19 @@ pub fn command_items(app: &SolveApp, id: &str) -> Vec<Item> {
});
v.push(Item::action("ui.shortcutPin", if fav { "Unpin from Shortcuts" } else { "Pin to Shortcuts" }, "").with(json!({ "command": id })));
v.push(Item::sep());
v.push(Item::action("ui.toolbarCustomize", "Customize Toolbar…", ""));
v.push(Item::action("ui.toolbarReset", "Reset Toolbar", "").with(json!({ "tab": tab })));
v
}
/// The toolbar's own menu (right-click on its empty space).
pub fn toolbar_items(app: &SolveApp) -> Vec<Item> {
vec![
Item::action("ui.toolbarCustomize", "Customize Toolbar…", ""),
Item::action("ui.toolbarReset", "Reset Toolbar", "").with(json!({ "tab": app.ui.tab })),
]
}
/// Run a toolbar item from a menu.
pub fn run(app: &mut SolveApp, action: &str, p: &Value) {
let tab = p.get("tab").and_then(Value::as_str).map(str::to_string).unwrap_or_else(|| app.ui.tab.clone());
@@ -165,6 +189,7 @@ pub fn run(app: &mut SolveApp, action: &str, p: &Value) {
}
"ui.toolbarRemove" => remove(app, &tab, id),
"ui.toolbarReset" => reset(app, &tab),
"ui.toolbarCustomize" => app.toolbar_custom.customizing = true,
"ui.shortcutPin" => app.sbox.toggle_pin(id),
_ => {}
}
@@ -190,3 +215,67 @@ pub fn import(app: &mut SolveApp, v: &Value) -> Result<(), String> {
}
Ok(())
}
/// Customize Toolbar: every command of the tab's panels with a check box (on the toolbar or
/// not), how to drag them, and Reset Toolbar.
pub fn window(app: &mut SolveApp, ctx: &egui::Context) {
if !app.toolbar_custom.customizing {
return;
}
let t = crate::theme::Tokens::get();
let tab = app.ui.tab.clone();
let mut open = true;
let mut changes: Vec<(&'static str, bool)> = Vec::new();
let mut reset_tab = false;
crate::frame::window(ctx, "Customize Toolbar", crate::frame::Width::Normal)
.id(egui::Id::new("sc_toolbar_customize"))
.open(&mut open)
.default_height(520.0)
.show(ctx, |ui| {
ui.label(
egui::RichText::new(format!(
"{} tab. Tick a command to give it a toolbar button. You can also drag a row of a panel's drop-down onto the toolbar, drag a button off the toolbar to remove it, or use the pin on a drop-down row.",
app.tr_label(&tab)
))
.color(t.text_dim),
);
ui.separator();
egui::ScrollArea::vertical().max_height(400.0).show(ui, |ui| {
for (panel, cmds, n) in panel_lists(&tab) {
if cmds.is_empty() {
continue;
}
ui.label(egui::RichText::new(app.tr_label(panel)).strong().size(11.0));
let shown = buttons(app, &tab, panel, &cmds, n);
for c in &cmds {
let mut on = shown.iter().any(|b| b.id == c.id) || pinned_on(app, &tab, c.id).is_some();
let r = ui.checkbox(&mut on, app.tr_label(c.label));
crate::scenario::publish_handle(&format!("customize:{}", c.id), r.rect.left_center() + egui::vec2(8.0, 0.0));
if r.changed() {
changes.push((c.id, on));
}
}
ui.add_space(4.0);
}
});
ui.separator();
ui.horizontal(|ui| {
if ui.button("Reset Toolbar").clicked() {
reset_tab = true;
}
});
});
for (id, on) in changes {
if on {
if let Err(e) = pin(app, &tab, id) {
app.set_status(e, true);
}
} else {
remove(app, &tab, id);
}
}
if reset_tab {
reset(app, &tab);
}
app.toolbar_custom.customizing = open;
}
@@ -0,0 +1,75 @@
[
{
"note": "#288: a panel drop-down highlights the row under the pointer with the hover background, and the row with keyboard focus. Drop-down rows publish `dropdown:<command id>`, the highlighted one also `dropdown_hot:<command id>`."
},
{
"click": {
"handle": "panel:SOLID:CREATE"
}
},
{
"expect": {
"shown_handles": ["dropdown:solid.box", "dropdown:solid.sphere"],
"hidden_handles": ["dropdown_hot:solid.box", "dropdown_hot:solid.sphere"]
}
},
{
"move": {
"handle": "dropdown:solid.box"
}
},
{
"expect": {
"shown_handles": ["dropdown_hot:solid.box"],
"hidden_handles": ["dropdown_hot:solid.sphere", "dropdown_hot:solid.cylinder"]
}
},
{
"move": {
"handle": "dropdown:solid.sphere"
}
},
{
"expect": {
"shown_handles": ["dropdown_hot:solid.sphere"],
"hidden_handles": ["dropdown_hot:solid.box"]
}
},
{
"move": [700, 600]
},
{
"expect": {
"shown_handles": ["dropdown:solid.box"],
"hidden_handles": ["dropdown_hot:solid.sphere", "dropdown_hot:solid.box"]
}
},
{
"note": "The keyboard: Down focuses the first row, Down again the next; the highlight follows."
},
{
"key": "ArrowDown"
},
{
"expect": {
"shown_handles": ["dropdown_hot:sketch.create"]
}
},
{
"key": "ArrowDown"
},
{
"expect": {
"shown_handles": ["dropdown_hot:solid.extrude"],
"hidden_handles": ["dropdown_hot:sketch.create"]
}
},
{
"key": "Enter"
},
{
"expect": {
"hidden_handles": ["dropdown:solid.box"]
}
}
]
@@ -0,0 +1,119 @@
[
{
"note": "#289: drag a command from a panel's drop-down onto the toolbar to pin it (a drop line shows where), drag a button off the toolbar or back into the drop-down to unpin it, the pin icon on a hovered drop-down row, and Customize Toolbar… in the toolbar's own menu. Kept with the preferences."
},
{
"expect": {
"shown_handles": ["toolbar:solid.extrude"],
"hidden_handles": ["toolbar:solid.sphere", "toolbar:solid.torus"]
}
},
{
"click": {"handle": "panel:SOLID:CREATE"}
},
{
"drag": [{"handle": "dropdown:solid.sphere"}, {"handle": "button:solid.extrude"}],
"steps": 12
},
{
"key": "Escape"
},
{
"expect": {
"hidden_handles": ["dropdown:solid.sphere"],
"handle_order": ["toolbar:solid.sphere", "toolbar:solid.extrude"]
}
},
{
"note": "A button dragged off the toolbar leaves it."
},
{
"drag": [{"handle": "toolbar:solid.sphere"}, [700, 600]],
"steps": 12
},
{
"expect": {
"hidden_handles": ["toolbar:solid.sphere"],
"shown_handles": ["toolbar:solid.extrude"]
}
},
{
"note": "The pin icon on a hovered drop-down row pins the command."
},
{
"click": {"handle": "panel:SOLID:CREATE"}
},
{
"move": {"handle": "dropdown:solid.torus"}
},
{
"expect": {
"shown_handles": ["dropdown_pin:solid.torus"],
"hidden_handles": ["dropdown_pin:solid.box"]
}
},
{
"click": {"handle": "dropdown_pin:solid.torus"}
},
{
"expect": {
"shown_handles": ["dropdown:solid.box"],
"tool": null,
"dialog": null
}
},
{
"key": "Escape"
},
{
"expect": {
"shown_handles": ["toolbar:solid.torus"]
}
},
{
"note": "A button dragged back into its open drop-down leaves the toolbar."
},
{
"click": {"handle": "panel:SOLID:CREATE"}
},
{
"drag": [{"handle": "button:solid.torus"}, {"handle": "dropdown:solid.box"}],
"steps": 12
},
{
"key": "Escape"
},
{
"expect": {
"hidden_handles": ["toolbar:solid.torus", "dropdown:solid.box"]
}
},
{
"note": "Customize Toolbar… in the toolbar's own menu: tick a command."
},
{
"call": "ui.menu",
"params": {"x": 1000, "y": 60, "target": {"type": "toolbar"}}
},
{
"call": "ui.menuPick",
"params": {"item": "Customize Toolbar…"}
},
{
"click": {"handle": "customize:solid.sphere"}
},
{
"expect": {
"shown_handles": ["toolbar:solid.sphere"]
}
},
{
"restart": "close"
},
{
"expect": {
"shown_handles": ["toolbar:solid.sphere", "toolbar:solid.extrude"],
"hidden_handles": ["toolbar:solid.torus"]
}
}
]
+1 -1
View File
@@ -63,7 +63,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o
| `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:<command id>`, panel labels `panel:<tab>:<panel>`, browser rows `row:<label>` and fold arrows `fold:<label>`, manipulator handles; orientation tips `axis_view:x`, `axis_view:-x` (and y/z) |
| `ui.handles` | `prefix?` | the handles drawn last frame, `[{name, x, y}]`: toolbar buttons and drop-down items `toolbar:<command id>` (the button alone: `button:<command id>`; a drop-down row: `dropdown:<command id>`, highlighted `dropdown_hot:<command id>`, its pin `dropdown_pin:<command id>`; Customize Toolbar's check boxes `customize:<command id>`), panel labels `panel:<tab>:<panel>`, browser rows `row:<label>` and fold arrows `fold:<label>`, manipulator handles; orientation tips `axis_view:x`, `axis_view:-x` (and y/z) |
| `ui.editFeature` | `feature` (id or name) | what double-clicking a timeline item does: rolls back to it and opens its dialog filled in (sketches open in sketch mode) |
| `ui.scroll` | `x`, `y`, `delta?` | wheel zoom at the cursor |
| `ui.key` | `key` (egui key name, `Enter`, `Escape`…), `cmd?`, `shift?` | |