forked from 1b-agent/opencadstudio-upstream
feat(ui): show ribbon command icons in menus and suggestions
The ribbon definition beside each command stays the one source of its label and icon. One cached lookup (modules::registry::ribbon_commands) walks the core ribbon and the expanded Draw/Modify panels; the shortcut list, the right-click menu, the grip menu and the command-line suggestions read it. - Context menu: command rows carry their ribbon icon; Undo/Redo and Isolate use interface glyphs, Draw Order its ribbon icon; icons and the caret fade on disabled rows. - Grip menu: Stretch, Move, Rotate, Scale, Mirror and Copy icons. - Suggestions: an icon column, aligned when a command has none. - Shortcut list: dropdown entries were read as (label, command) though the ribbon stores (command, label), so they never got a label or icon. Reworked from #1449. Co-Authored-By: MarcelP102 <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6b37476687
commit
7ed6a362dc
8 files changed
+195
-81
No files matched your search
+6
-46
@@ -299,7 +299,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn actions() -> Vec<Action> {
|
||||
use crate::modules::{IconKind, ModuleEvent, RibbonItem, ToolDef};
|
||||
use crate::modules::IconKind;
|
||||
use std::collections::BTreeMap;
|
||||
let mut names = crate::command::all_registered_command_names();
|
||||
names.extend([
|
||||
@@ -342,56 +342,16 @@ pub(super) fn actions() -> Vec<Action> {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
fn describe(
|
||||
actions: &mut BTreeMap<String, Action>,
|
||||
command: &str,
|
||||
label: &str,
|
||||
icon: IconKind,
|
||||
) {
|
||||
if let Some(action) = actions.get_mut(command) {
|
||||
action.label = crate::t!(label).into_owned();
|
||||
for (command, (label, icon)) in crate::modules::registry::ribbon_commands() {
|
||||
if let Some(action) = actions.get_mut(command.as_str()) {
|
||||
action.label = crate::t!(*label).into_owned();
|
||||
action.description = format!("{} ({command})", action.label);
|
||||
action.icon = match icon {
|
||||
IconKind::Svg(bytes) => Some(bytes),
|
||||
_ => None,
|
||||
IconKind::Svg(bytes) => Some(*bytes),
|
||||
IconKind::Glyph(_) => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
fn tool(actions: &mut BTreeMap<String, Action>, tool: &ToolDef) {
|
||||
if let ModuleEvent::Command(command) = &tool.event {
|
||||
describe(actions, command, tool.label, tool.icon);
|
||||
}
|
||||
}
|
||||
for module in crate::modules::registry::all_modules() {
|
||||
for group in module.ribbon_groups() {
|
||||
for item in &group.tools {
|
||||
match item {
|
||||
RibbonItem::Tool(t) | RibbonItem::LabeledTool(t) | RibbonItem::LargeTool(t) => {
|
||||
tool(&mut actions, t)
|
||||
}
|
||||
RibbonItem::Dropdown { items, .. }
|
||||
| RibbonItem::LabeledDropdown { items, .. }
|
||||
| RibbonItem::LargeDropdown { items, .. } => {
|
||||
for (label, command, icon) in items {
|
||||
describe(&mut actions, command, label, *icon);
|
||||
}
|
||||
}
|
||||
RibbonItem::ToolGrid { columns }
|
||||
| RibbonItem::StyleComboGroup { rows: columns, .. } => {
|
||||
for t in columns.iter().flatten() {
|
||||
tool(&mut actions, t);
|
||||
}
|
||||
}
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
for t in row2.iter().chain(row3) {
|
||||
tool(&mut actions, t);
|
||||
}
|
||||
}
|
||||
RibbonItem::PropertiesGroup { match_prop } => tool(&mut actions, match_prop),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actions.into_values().collect()
|
||||
}
|
||||
|
||||
|
||||
+25
-15
@@ -1124,20 +1124,27 @@ impl Widget<Message, Theme, iced::Renderer> for ClampedPin<'_> {
|
||||
/// panel's border, when the menu opens.
|
||||
const MENU_CURSOR_INSET_X: f32 = 24.0;
|
||||
/// Width of the icon gutter every row reserves so labels line up whether or
|
||||
/// not the row carries a glyph (object snaps, Pan / Zoom).
|
||||
/// not the row carries a glyph (object snaps, command icons).
|
||||
const MENU_GUTTER_W: f32 = 18.0;
|
||||
const MENU_ICON_SIZE: f32 = 14.0;
|
||||
|
||||
/// The gutter cell: the row's glyph, or empty space of the same width.
|
||||
fn context_menu_gutter(icon: Option<MenuIcon>) -> Element<'static, Message> {
|
||||
let bytes = icon.map(|icon| match icon {
|
||||
MenuIcon::Snap(t) => crate::ui::icons::osnap(t),
|
||||
MenuIcon::Mtp => crate::ui::icons::mtp_icon(),
|
||||
MenuIcon::Pan => crate::ui::icons::pan_icon(),
|
||||
MenuIcon::Zoom => crate::ui::icons::zoom_icon(),
|
||||
});
|
||||
let cell: Element<'static, Message> = match bytes {
|
||||
Some(bytes) => crate::ui::icons::themed::<Message>(bytes, MENU_ICON_SIZE),
|
||||
/// The gutter cell: the row's glyph, faded with a disabled row, or empty
|
||||
/// space of the same width.
|
||||
fn context_menu_gutter(icon: Option<MenuIcon>, enabled: bool) -> Element<'static, Message> {
|
||||
use crate::ui::icons;
|
||||
let chrome = |bytes| {
|
||||
if enabled {
|
||||
icons::themed::<Message>(bytes, MENU_ICON_SIZE)
|
||||
} else {
|
||||
icons::themed_disabled::<Message>(bytes, MENU_ICON_SIZE)
|
||||
}
|
||||
};
|
||||
let cell: Element<'static, Message> = match icon {
|
||||
Some(MenuIcon::Snap(t)) => chrome(icons::osnap(t)),
|
||||
Some(MenuIcon::Mtp) => chrome(icons::mtp_icon()),
|
||||
Some(MenuIcon::Chrome(bytes)) => chrome(bytes),
|
||||
Some(MenuIcon::Tool(bytes)) if enabled => icons::semantic(bytes, MENU_ICON_SIZE),
|
||||
Some(MenuIcon::Tool(bytes)) => icons::semantic_disabled(bytes, MENU_ICON_SIZE),
|
||||
None => iced::widget::Space::new().width(MENU_ICON_SIZE).height(MENU_ICON_SIZE).into(),
|
||||
};
|
||||
container(cell)
|
||||
@@ -1188,24 +1195,27 @@ pub(super) fn viewport_context_menu_overlay(
|
||||
MenuRow::Submenu {
|
||||
id,
|
||||
label,
|
||||
icon,
|
||||
items: children,
|
||||
open,
|
||||
} => {
|
||||
let is_hl = highlighted == Some(sel_idx);
|
||||
let caret = if *open {
|
||||
let enabled = !children.is_empty();
|
||||
let caret = if !enabled {
|
||||
crate::ui::icons::themed_disabled_arrow_right(9.0)
|
||||
} else if *open {
|
||||
crate::ui::icons::themed_arrow_down(9.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_arrow_right(9.0)
|
||||
};
|
||||
let content = row![
|
||||
context_menu_gutter(None),
|
||||
context_menu_gutter(*icon, enabled),
|
||||
text(label.clone()).size(12),
|
||||
iced::widget::Space::new().width(Fill),
|
||||
caret,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center);
|
||||
let enabled = !children.is_empty();
|
||||
let mut btn = button(content)
|
||||
.padding(iced::Padding {
|
||||
top: 3.0,
|
||||
@@ -1271,7 +1281,7 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element<
|
||||
});
|
||||
}
|
||||
let enabled = item.enabled;
|
||||
let mut content = row![context_menu_gutter(item.icon), label_text]
|
||||
let mut content = row![context_menu_gutter(item.icon, enabled), label_text]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center);
|
||||
if let Some(hint) = item.hint.as_ref() {
|
||||
|
||||
Reference in new issue
Block a user