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 <30177694+MarcelP102@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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() {
|
||||
|
||||
+64
-1
@@ -7,7 +7,9 @@
|
||||
// 2. Add `pub mod my_name;` to src/modules/mod.rs
|
||||
// 3. Add one `Box::new(super::my_name::MyNameModule)` line below, in the
|
||||
// position you want its tab to appear.
|
||||
use crate::modules::CadModule;
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonItem, ToolDef};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Returns one boxed instance of every registered core CAD module.
|
||||
/// Called once at startup by `Ribbon::new()`.
|
||||
@@ -23,3 +25,64 @@ pub fn all_modules() -> Vec<Box<dyn CadModule>> {
|
||||
Box::new(super::layout::LayoutModule),
|
||||
]
|
||||
}
|
||||
|
||||
/// Label and icon of every core ribbon command, keyed by its upper-case
|
||||
/// command line (`LINE`, `ZOOM EXTENTS`). The ribbon definition beside each
|
||||
/// command stays the one source; the shortcut list, the context menus and the
|
||||
/// command-line suggestions read it from here.
|
||||
pub fn ribbon_commands() -> &'static HashMap<String, (&'static str, IconKind)> {
|
||||
static COMMANDS: OnceLock<HashMap<String, (&'static str, IconKind)>> = OnceLock::new();
|
||||
COMMANDS.get_or_init(|| {
|
||||
let mut out = HashMap::new();
|
||||
let tool = |out: &mut HashMap<_, _>, t: &ToolDef| {
|
||||
if let ModuleEvent::Command(command) = &t.event {
|
||||
out.insert(command.to_ascii_uppercase(), (t.label, t.icon));
|
||||
}
|
||||
};
|
||||
for module in 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 out, t),
|
||||
RibbonItem::Dropdown { items, .. }
|
||||
| RibbonItem::LabeledDropdown { items, .. }
|
||||
| RibbonItem::LargeDropdown { items, .. } => {
|
||||
for (command, label, icon) in items {
|
||||
out.insert(command.to_ascii_uppercase(), (*label, *icon));
|
||||
}
|
||||
}
|
||||
RibbonItem::ToolGrid { columns }
|
||||
| RibbonItem::StyleComboGroup { rows: columns, .. } => {
|
||||
for t in columns.iter().flatten() {
|
||||
tool(&mut out, t);
|
||||
}
|
||||
}
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
for t in row2.iter().chain(row3) {
|
||||
tool(&mut out, t);
|
||||
}
|
||||
}
|
||||
RibbonItem::PropertiesGroup { match_prop } => tool(&mut out, match_prop),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (command, label, icon) in crate::ui::ribbon::panel_tools() {
|
||||
out.entry(command.to_string())
|
||||
.or_insert((label, IconKind::Svg(icon)));
|
||||
}
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
/// The ribbon icon of a command line, for surfaces that show the command
|
||||
/// outside the ribbon. A leading `'` (transparent use) is ignored.
|
||||
pub fn command_icon(command: &str) -> Option<&'static [u8]> {
|
||||
let key = command.trim().trim_start_matches('\'').to_ascii_uppercase();
|
||||
match ribbon_commands().get(&key)?.1 {
|
||||
IconKind::Svg(bytes) => Some(bytes),
|
||||
IconKind::Glyph(_) => None,
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -798,7 +798,16 @@ impl CommandLine {
|
||||
let mut col = column![].spacing(0).width(Length::Fill);
|
||||
for (idx, cmd) in matches.iter().enumerate() {
|
||||
let is_selected = idx == cursor;
|
||||
let row = button(text(cmd.clone()).size(11))
|
||||
// Every row keeps the icon's width so names line up.
|
||||
let icon: Element<'_, Message> =
|
||||
match crate::modules::registry::command_icon(cmd) {
|
||||
Some(bytes) => crate::ui::icons::semantic(bytes, 14.0),
|
||||
None => Space::new().width(14.0).into(),
|
||||
};
|
||||
let label = row![icon, text(cmd.clone()).size(11)]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
let row = button(label)
|
||||
.on_press(Message::CommandSuggestionPick(cmd.clone()))
|
||||
.width(Length::Fill)
|
||||
.padding([2, 8])
|
||||
|
||||
@@ -314,6 +314,15 @@ pub fn semantic<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
||||
})
|
||||
}
|
||||
|
||||
/// [`semantic`] faded like disabled menu text.
|
||||
pub fn semantic_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
||||
Element::new(SemanticIcon {
|
||||
bytes,
|
||||
size,
|
||||
opacity: 0.42,
|
||||
})
|
||||
}
|
||||
|
||||
fn semantic_handle(bytes: &'static [u8], theme: &Theme) -> svg::Handle {
|
||||
let key = SemanticCacheKey {
|
||||
address: bytes.as_ptr() as usize,
|
||||
@@ -654,6 +663,14 @@ pub fn zoom_icon() -> &'static [u8] {
|
||||
NAV_ZOOM
|
||||
}
|
||||
|
||||
pub fn undo_icon() -> &'static [u8] {
|
||||
UNDO
|
||||
}
|
||||
|
||||
pub fn redo_icon() -> &'static [u8] {
|
||||
REDO
|
||||
}
|
||||
|
||||
/// Layer visibility icon bytes (on / off).
|
||||
pub fn layer_visible(visible: bool) -> &'static [u8] {
|
||||
if visible {
|
||||
@@ -709,6 +726,10 @@ pub fn themed_disabled_arrow_down<'a, M: 'a>(size: f32) -> Element<'a, M> {
|
||||
themed_disabled(TRI_DOWN, size)
|
||||
}
|
||||
|
||||
pub fn themed_disabled_arrow_right<'a, M: 'a>(size: f32) -> Element<'a, M> {
|
||||
themed_disabled(TRI_RIGHT, size)
|
||||
}
|
||||
|
||||
pub fn themed_home<'a, M: 'a>(size: f32) -> Element<'a, M> {
|
||||
themed(HOME, size)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,10 @@ pub enum SubmenuId {
|
||||
pub enum MenuIcon {
|
||||
Snap(SnapType),
|
||||
Mtp,
|
||||
Pan,
|
||||
Zoom,
|
||||
/// Monochrome interface glyph, drawn in the text colour.
|
||||
Chrome(&'static [u8]),
|
||||
/// A command's ribbon icon, drawn in its own colours.
|
||||
Tool(&'static [u8]),
|
||||
}
|
||||
|
||||
/// What a grip-menu row does (the grip-mode shortcut menu of commercial solutions).
|
||||
@@ -146,6 +148,12 @@ impl MenuItem {
|
||||
self
|
||||
}
|
||||
|
||||
/// The command's ribbon icon, when the ribbon has one.
|
||||
fn command_icon(mut self, command: &str) -> Self {
|
||||
self.icon = command_icon(command);
|
||||
self
|
||||
}
|
||||
|
||||
fn hint(mut self, hint: impl Into<String>) -> Self {
|
||||
self.hint = Some(hint.into());
|
||||
self
|
||||
@@ -179,11 +187,16 @@ pub enum MenuRow {
|
||||
Submenu {
|
||||
id: SubmenuId,
|
||||
label: String,
|
||||
icon: Option<MenuIcon>,
|
||||
items: Vec<MenuItem>,
|
||||
open: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn command_icon(command: &str) -> Option<MenuIcon> {
|
||||
crate::modules::registry::command_icon(command).map(MenuIcon::Tool)
|
||||
}
|
||||
|
||||
/// A keyboard-navigable row in display order: either an item or a submenu
|
||||
/// header. Separators and the children of collapsed submenus are skipped.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -470,6 +483,7 @@ fn command_rows(
|
||||
rows.push(MenuRow::Submenu {
|
||||
id: SubmenuId::SnapOverrides,
|
||||
label: t!("Snap Overrides").into_owned(),
|
||||
icon: None,
|
||||
items: snap_override_items(),
|
||||
open: open_submenu == Some(SubmenuId::SnapOverrides),
|
||||
});
|
||||
@@ -486,6 +500,7 @@ fn recent_input_submenu(items: Vec<MenuItem>, open_submenu: Option<SubmenuId>) -
|
||||
MenuRow::Submenu {
|
||||
id: SubmenuId::RecentInput,
|
||||
label: t!("Recent Input").into_owned(),
|
||||
icon: None,
|
||||
items,
|
||||
open: open_submenu == Some(SubmenuId::RecentInput),
|
||||
}
|
||||
@@ -528,14 +543,14 @@ fn navigation_rows(transparent: bool) -> Vec<MenuRow> {
|
||||
vec![
|
||||
MenuRow::Item(
|
||||
MenuItem::new(t!("Pan").into_owned(), MenuAction::Command(format!("{prefix}PAN")))
|
||||
.icon(MenuIcon::Pan),
|
||||
.icon(MenuIcon::Chrome(crate::ui::icons::pan_icon())),
|
||||
),
|
||||
MenuRow::Item(
|
||||
MenuItem::new(
|
||||
t!("Zoom").into_owned(),
|
||||
MenuAction::Command(format!("{prefix}ZOOM DYNAMIC")),
|
||||
)
|
||||
.icon(MenuIcon::Zoom),
|
||||
.icon(MenuIcon::Chrome(crate::ui::icons::zoom_icon())),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -552,7 +567,9 @@ fn idle_rows(
|
||||
props_open: bool,
|
||||
open_submenu: Option<SubmenuId>,
|
||||
) -> Vec<MenuRow> {
|
||||
let cmd = |label: String, name: &str| MenuItem::new(label, MenuAction::Command(name.to_string()));
|
||||
let cmd = |label: String, name: &str| {
|
||||
MenuItem::new(label, MenuAction::Command(name.to_string())).command_icon(name)
|
||||
};
|
||||
let mut rows = Vec::new();
|
||||
|
||||
// ── Repeat / Recent Input ──────────────────────────────────────────
|
||||
@@ -562,6 +579,7 @@ fn idle_rows(
|
||||
t!("Repeat %{last}", last = last.clone()).into_owned(),
|
||||
MenuAction::Command(last.to_uppercase()),
|
||||
)
|
||||
.command_icon(last)
|
||||
.default(),
|
||||
));
|
||||
}
|
||||
@@ -569,7 +587,9 @@ fn idle_rows(
|
||||
recent_cmds
|
||||
.iter()
|
||||
.take(RECENT_LIMIT)
|
||||
.map(|c| MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase())))
|
||||
.map(|c| {
|
||||
MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase())).command_icon(c)
|
||||
})
|
||||
.collect(),
|
||||
open_submenu,
|
||||
));
|
||||
@@ -579,6 +599,7 @@ fn idle_rows(
|
||||
rows.push(MenuRow::Submenu {
|
||||
id: SubmenuId::Clipboard,
|
||||
label: t!("Clipboard").into_owned(),
|
||||
icon: None,
|
||||
items: vec![
|
||||
cmd(t!("Cut").into_owned(), "CUTCLIP").enabled(has_selection),
|
||||
cmd(t!("Copy").into_owned(), "COPYCLIP").enabled(has_selection),
|
||||
@@ -602,19 +623,25 @@ fn idle_rows(
|
||||
None => t!("Redo").into_owned(),
|
||||
};
|
||||
rows.push(MenuRow::Item(
|
||||
MenuItem::new(undo, MenuAction::Undo).hint("Ctrl+Z").enabled(undo_label.is_some()),
|
||||
MenuItem::new(undo, MenuAction::Undo)
|
||||
.icon(MenuIcon::Chrome(crate::ui::icons::undo_icon()))
|
||||
.hint("Ctrl+Z")
|
||||
.enabled(undo_label.is_some()),
|
||||
));
|
||||
rows.push(MenuRow::Item(
|
||||
MenuItem::new(redo, MenuAction::Redo).hint("Ctrl+Y").enabled(redo_label.is_some()),
|
||||
MenuItem::new(redo, MenuAction::Redo)
|
||||
.icon(MenuIcon::Chrome(crate::ui::icons::redo_icon()))
|
||||
.hint("Ctrl+Y")
|
||||
.enabled(redo_label.is_some()),
|
||||
));
|
||||
rows.push(MenuRow::Separator);
|
||||
|
||||
// ── Selection edit block (the "Edit" shortcut menu of commercial solutions) ──
|
||||
if has_selection {
|
||||
rows.push(MenuRow::Item(MenuItem::new(
|
||||
t!("Erase").into_owned(),
|
||||
MenuAction::DeleteSelected,
|
||||
)));
|
||||
rows.push(MenuRow::Item(
|
||||
MenuItem::new(t!("Erase").into_owned(), MenuAction::DeleteSelected)
|
||||
.command_icon("ERASE"),
|
||||
));
|
||||
rows.push(MenuRow::Item(cmd(t!("Move").into_owned(), "MOVE")));
|
||||
rows.push(MenuRow::Item(cmd(t!("Copy Selection").into_owned(), "COPY")));
|
||||
rows.push(MenuRow::Item(cmd(t!("Scale").into_owned(), "SCALE")));
|
||||
@@ -623,6 +650,7 @@ fn idle_rows(
|
||||
rows.push(MenuRow::Submenu {
|
||||
id: SubmenuId::DrawOrder,
|
||||
label: t!("Draw Order").into_owned(),
|
||||
icon: command_icon("DRAWORDER_FRONT"),
|
||||
items: vec![
|
||||
cmd(t!("Bring to Front").into_owned(), "DRAWORDER F"),
|
||||
cmd(t!("Send to Back").into_owned(), "DRAWORDER B"),
|
||||
@@ -652,6 +680,7 @@ fn idle_rows(
|
||||
rows.push(MenuRow::Submenu {
|
||||
id: SubmenuId::Isolate,
|
||||
label: t!("Isolate").into_owned(),
|
||||
icon: Some(MenuIcon::Chrome(crate::ui::icons::ST_ISOLATE)),
|
||||
items: vec![
|
||||
cmd(t!("Isolate Objects").into_owned(), "ISOLATEOBJECTS").enabled(has_selection),
|
||||
cmd(t!("Hide Objects").into_owned(), "HIDEOBJECTS").enabled(has_selection),
|
||||
@@ -697,17 +726,29 @@ fn idle_rows(
|
||||
|
||||
// ── Panels ─────────────────────────────────────────────────────────
|
||||
rows.push(MenuRow::Item(
|
||||
MenuItem::new(t!("Properties").into_owned(), MenuAction::Properties).checked(props_open),
|
||||
MenuItem::new(t!("Properties").into_owned(), MenuAction::Properties)
|
||||
.command_icon("PROPERTIES")
|
||||
.checked(props_open),
|
||||
));
|
||||
rows.push(MenuRow::Item(
|
||||
MenuItem::new(t!("Options...").into_owned(), MenuAction::Options).command_icon("OPTIONS"),
|
||||
));
|
||||
rows.push(MenuRow::Item(MenuItem::new(
|
||||
t!("Options...").into_owned(),
|
||||
MenuAction::Options,
|
||||
)));
|
||||
rows
|
||||
}
|
||||
|
||||
fn grip_rows(grip: &GripMenuContext) -> Vec<MenuRow> {
|
||||
let g = |label: String, cmd: GripMenuCmd| MenuItem::new(label, MenuAction::Grip(cmd));
|
||||
let g = |label: String, cmd: GripMenuCmd| {
|
||||
let command = match cmd {
|
||||
GripMenuCmd::Stretch => "STRETCH",
|
||||
GripMenuCmd::Move => "MOVE",
|
||||
GripMenuCmd::Rotate => "ROTATE",
|
||||
GripMenuCmd::Scale => "SCALE",
|
||||
GripMenuCmd::Mirror => "MIRROR",
|
||||
GripMenuCmd::CopyToggle => "COPY",
|
||||
GripMenuCmd::BasePoint | GripMenuCmd::Undo | GripMenuCmd::Exit => "",
|
||||
};
|
||||
MenuItem::new(label, MenuAction::Grip(cmd)).command_icon(command)
|
||||
};
|
||||
vec![
|
||||
MenuRow::Item(MenuItem::new(t!("Enter").into_owned(), MenuAction::Enter).hint("⏎").default()),
|
||||
MenuRow::Separator,
|
||||
|
||||
@@ -126,6 +126,15 @@ const PANELS: &[Panel] = &[
|
||||
},
|
||||
];
|
||||
|
||||
/// Command, label and icon of every tool in the expanded Draw and Modify
|
||||
/// panels.
|
||||
pub(crate) fn tools() -> impl Iterator<Item = (&'static str, &'static str, &'static [u8])> {
|
||||
PANELS
|
||||
.iter()
|
||||
.flat_map(|panel| panel.tools)
|
||||
.map(|tool| (tool.command, tool.label, tool.icon))
|
||||
}
|
||||
|
||||
fn panel_for_dropdown(id: &str) -> Option<Panel> {
|
||||
PANELS.iter().copied().find(|panel| {
|
||||
panel.id == id
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::ui::properties::{linetype_display_name, lw_options, LinetypeItem};
|
||||
|
||||
mod widgets;
|
||||
mod draw_panel;
|
||||
pub(crate) use draw_panel::tools as panel_tools;
|
||||
mod modify_panel;
|
||||
mod color_dropdown;
|
||||
use widgets::{StyleContext, *};
|
||||
|
||||
Reference in new issue
Block a user