feat(ribbon): highlight the running command's button

A command started from the command line, an alias, a shortcut or Repeat
now lights the same ribbon button a click on it would, so users learn
where the tools they type live. A button inside a dropdown lights the
dropdown, a slide-out tool lights its group title, a tool hidden in a
collapsed panel lights the panel's opener, and a button on another tab
lights that tab's header. Always on, in the existing active-button blue.

Buttons are matched by the command they dispatch, so ids that differ
from their command still resolve; the tab on show wins when a command
appears twice, and a clicked button keeps its highlight. Transparent
commands leave the running command lit. The #355 rule that a highlight
lasts only while something runs moves from the ribbon click handler into
dispatch_command, so typed commands are cleared the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Christopher OverbeckandClaude Opus 5.5 committed 2026-10-02 15:38:47 -05:00
1 parent 3f54f2f494
commit 1093900a49
11 files changed
+425 -33

No files matched your search

+39 -9
View File
@@ -207,22 +207,52 @@ pub(super) fn parent_panel(id: &str) -> Option<&'static str> {
panel_for_dropdown(id).map(|panel| panel.id)
}
pub(super) fn group_title<'a>(
title: &'static str,
group_ids: &[&'static str],
open: &Option<String>,
) -> Element<'a, Message> {
let Some(panel) = PANELS.iter().copied().find(|panel| {
/// The slide-out under the title of the ribbon group `title`, whose items
/// are `group_ids`.
fn panel_for_group(title: &str, group_ids: &[&'static str]) -> Option<Panel> {
PANELS.iter().copied().find(|panel| {
panel.title == title
&& !panel.tools.is_empty()
&& panel.anchor.is_none_or(|anchor| group_ids.contains(&anchor))
})
else {
}
/// Every command the slide-out of the group `title` offers: each tool and
/// each of its submenu options.
pub(super) fn slide_out_commands(
title: &str,
group_ids: &[&'static str],
) -> impl Iterator<Item = &'static str> {
panel_for_group(title, group_ids)
.into_iter()
.flat_map(|panel| panel.tools)
.flat_map(|tool| {
std::iter::once(tool.command).chain(tool.options.iter().map(|(cmd, _)| *cmd))
})
}
/// A slide-out tool is lit while it or one of its submenu options runs.
fn tool_is_active(tool: &Tool, active_tool: Option<&str>) -> bool {
active_tool.is_some_and(|active| {
tool.command == active || tool.options.iter().any(|(cmd, _)| *cmd == active)
})
}
pub(super) fn group_title<'a>(
title: &'static str,
group_ids: &[&'static str],
open: &Option<String>,
active_tool: Option<&str>,
) -> Element<'a, Message> {
let Some(panel) = panel_for_group(title, group_ids) else {
return container(text(t!(title)).size(9).style(muted_text_style))
.padding([1, 4])
.into();
};
let expanded = open.as_deref().and_then(parent_panel) == Some(panel.id);
// A command running from the closed slide-out lights its title, the way a
// dropdown lights while one of its items runs.
let lit = expanded || panel.tools.iter().any(|tool| tool_is_active(tool, active_tool));
let arrow = if expanded {
icons::themed_arrow_up(GROUP_TITLE_ARROW_SIZE)
} else {
@@ -239,7 +269,7 @@ pub(super) fn group_title<'a>(
.align_y(iced::Center),
)
.on_press(Message::ToggleRibbonDropdown(panel.id.to_string()))
.style(move |theme: &Theme, status| tool_btn_style(theme, expanded, status))
.style(move |theme: &Theme, status| tool_btn_style(theme, lit, status))
.padding([1, 4]),
)
.into();
@@ -421,7 +451,7 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element
.map(|tool| {
tool_button(
tool,
ribbon.active_tool.as_deref() == Some(tool.command),
tool_is_active(tool, ribbon.active_tool.as_deref()),
panel.id,
)
})