Merge main into Select-Color-Redesign

Keep both sides: main's ViewBackground colour target and PREVIEW icon
next to this branch's Interface target and LIST/SEARCH icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-14 19:10:33 +00:00
commit 451b0f5d78
231 files changed
+32217 -3423

No files matched your search

+52 -20
View File
@@ -9,7 +9,7 @@ use super::widgets::{
make_icon, make_tip, muted_text_style, popup_panel_style, popup_row_style, tip_style,
tool_btn_style, LARGE_DROPDOWN_ARROW_SIZE,
};
use super::{dropdown_backdrop, position_ribbon_dropdown, Ribbon};
use super::{dropdown_backdrop, keytips, position_ribbon_dropdown, Ribbon};
use crate::app::Message;
use crate::modules::IconKind;
use crate::t;
@@ -248,6 +248,32 @@ fn panel_for_group(title: &str, group_ids: &[&'static str]) -> Option<Panel> {
})
}
/// The slide-out under the group `title`, if it has one.
pub(super) fn slide_out_id(title: &str, group_ids: &[&'static str]) -> Option<&'static str> {
panel_for_group(title, group_ids).map(|panel| panel.id)
}
/// What the open slide-out or tool submenu `id` offers, as (dropdown id,
/// command, opens a submenu) in display order.
pub(super) fn keytip_items(id: &str) -> Vec<(&'static str, &'static str, bool)> {
let Some(panel) = panel_for_dropdown(id) else {
return Vec::new();
};
if panel.id == id {
return panel
.tools
.iter()
.map(|tool| (panel.id, tool.command, !tool.options.is_empty()))
.collect();
}
panel
.tools
.iter()
.filter(|tool| tool.command == id)
.flat_map(|tool| tool.options.iter().map(|(cmd, _)| (tool.command, *cmd, false)))
.collect()
}
/// Every command the slide-out of the group `title` offers: each tool and
/// each of its submenu options.
pub(super) fn slide_out_commands(
@@ -311,7 +337,8 @@ pub(super) fn group_title<'a>(
if panel.id != REFERENCE_PANEL_ID {
return title_button;
}
// The corner arrow opens the External References palette.
// The corner arrow opens the External References palette, or closes it
// when it is open.
let launcher = tooltip(
// A right arrow turned to point into the corner.
button(
@@ -323,10 +350,7 @@ pub(super) fn group_title<'a>(
color: Some(theme.palette().background.base.text),
}),
)
.on_press(Message::RibbonToolClick {
tool_id: "EXTERNALREFERENCES".to_string(),
event: crate::modules::ModuleEvent::Command("EXTERNALREFERENCES".to_string()),
})
.on_press(Message::ToggleXrefManager)
.style(move |theme: &Theme, status| tool_btn_style(theme, false, status))
.padding([0, 3]),
make_tip(format!(
@@ -339,6 +363,7 @@ pub(super) fn group_title<'a>(
.delay(Duration::from_millis(400))
.style(tip_style);
// Shrink-sized: a Fill here would make the panel claim the whole row.
let launcher = keytips::anchor(keytips::XREF_LAUNCHER, launcher);
row![title_button, launcher]
.spacing(6)
.align_y(iced::Center)
@@ -364,6 +389,7 @@ fn tool_button(tool: &Tool, active: bool, panel_id: &'static str) -> Element<'st
.delay(Duration::from_millis(400))
.style(tip_style)
.into();
let face = keytips::anchor_with(|| keytips::item_anchor(panel_id, tool.command), face);
if !tool.options.is_empty() {
row![
face,
@@ -403,6 +429,7 @@ fn reference_overlay<'a>(ribbon: &Ribbon, panel: Panel, win: (f32, f32)) -> Elem
.style(popup_row_style)
.width(Fill)
.padding([3, 6]);
let edit = keytips::anchor_with(|| keytips::item_anchor(panel.id, tool.command), edit);
let fade = ribbon.xref_fade;
let on = fade > 0;
let switch = button(make_icon(
@@ -413,18 +440,23 @@ fn reference_overlay<'a>(ribbon: &Ribbon, panel: Panel, win: (f32, f32)) -> Elem
.style(move |theme: &Theme, status| tool_btn_style(theme, on, status))
.padding(3);
let amount = fade.unsigned_abs().min(90) as u8;
let switch = tooltip(
switch,
make_tip(format!(
"{}\n{} XDWGFADECTL",
t!("Xref fading"),
t!("Command:")
)),
tooltip::Position::Bottom,
)
.delay(Duration::from_millis(400))
.style(tip_style);
let switch = keytips::anchor_with(
|| keytips::item_anchor(panel.id, keytips::XREF_FADE),
switch,
);
let fading = row![
tooltip(
switch,
make_tip(format!(
"{}\n{} XDWGFADECTL",
t!("Xref fading"),
t!("Command:")
)),
tooltip::Position::Bottom
)
.delay(Duration::from_millis(400))
.style(tip_style),
switch,
text(t!("Xref fading")).size(11),
iced::widget::slider(0..=90u8, amount, Message::XrefFadeSlide)
.on_release(Message::XrefFadeCommit)
@@ -470,7 +502,7 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element
tool.options
.iter()
.map(|&(cmd, label)| {
button(text(t!(label)).size(11))
let item = button(text(t!(label)).size(11))
.on_press(Message::DropdownSelectItem {
dropdown_id: tool.command,
cmd,
@@ -478,8 +510,8 @@ pub(super) fn overlay<'a>(ribbon: &Ribbon, id: &str, win: (f32, f32)) -> Element
.style(popup_row_style)
.width(Fill)
.height(OPTION_HEIGHT)
.padding([3, 7])
.into()
.padding([3, 7]);
keytips::anchor_with(|| keytips::item_anchor(tool.command, cmd), item)
})
.collect::<Vec<Element<'static, Message>>>(),
)
File diff suppressed because it is too large. Load diff
+6 -15
View File
@@ -68,15 +68,11 @@ impl Ribbon {
fn entries(&self) -> impl Iterator<Item = (usize, Entry<'_>)> {
let order = std::iter::once(self.active)
.chain((0..self.modules.len()).filter(move |i| *i != self.active));
order
.filter_map(|i| self.modules.get(i).map(|m| (i, m)))
.flat_map(|(i, module)| {
module
.ribbon_groups()
.iter()
.flat_map(group_entries)
.map(move |entry| (i, entry))
})
order.flat_map(|i| {
self.groups_for(i)
.flat_map(group_entries)
.map(move |entry| (i, entry))
})
}
/// The button id that runs the command line `cmd`: an exact match first,
@@ -111,12 +107,7 @@ impl Ribbon {
/// module has no tab (its tools are on the side toolbar).
pub(super) fn tab_holding_active_tool(&self) -> Option<usize> {
let id = self.active_tool.as_deref()?;
let holds = |i: usize| {
self.modules[i]
.ribbon_groups()
.iter()
.any(|group| group_holds(group, id))
};
let holds = |i: usize| self.groups_for(i).any(|group| group_holds(group, id));
if self
.modules
.get(self.active)
+153 -42
View File
@@ -31,6 +31,12 @@ use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
mod collapse;
mod locate;
mod keytips;
pub use keytips::{
active as keytips_active, key_pressed as keytip_key_pressed,
key_released as keytip_key_released, pointer_pressed as keytip_pointer_pressed, KeyTipInput,
KeyTipStep,
};
use crate::t;
use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow};
pub use collapse::CollapseMode;
@@ -120,8 +126,14 @@ pub struct Ribbon {
xref_ctx: bool,
/// Only point clouds are selected: whether the first shows its crops.
point_cloud_ctx: Option<bool>,
/// A block editor session is open: the tab that was active before the
/// Block Editor tab took over.
block_editing: Option<usize>,
block_testing: Option<usize>,
/// XDWGFADECTL as the Reference slide-out shows it (negative = off).
pub xref_fade: i32,
/// Open key tips (Alt).
keytips: Option<keytips::State>,
}
/// Per-layer display data shown in the ribbon layer dropdown.
@@ -240,7 +252,10 @@ impl Ribbon {
underlay_ctx: None,
xref_ctx: false,
point_cloud_ctx: None,
block_editing: None,
block_testing: None,
xref_fade: 50,
keytips: None,
}
}
@@ -314,6 +329,62 @@ impl Ribbon {
self.available_linetypes = items;
}
/// Show the Block Editor tab while a block is being edited, and return to
/// the previous tab when the editing ends.
pub fn set_block_editing(&mut self, editing: bool) {
let Some(index) = self.modules.iter().position(|module| module.id() == "block_editor")
else {
return;
};
match (editing, self.block_editing) {
(true, None) => {
self.block_editing = Some(self.active);
self.active = index;
}
(false, Some(previous)) => {
self.block_editing = None;
if self.active == index {
self.active = previous.min(self.modules.len().saturating_sub(1));
}
}
_ => {}
}
}
pub fn set_block_testing(&mut self, testing: bool) {
if testing != self.block_testing.is_some() {
self.close_dropdown();
let _ = self.hide_keytips();
}
let Some(index) = self.modules.iter().position(|module| module.id() == "draw") else {
return;
};
match (testing, self.block_testing) {
(true, None) => {
self.block_testing = Some(self.active);
self.active = index;
}
(false, Some(previous)) => {
self.block_testing = None;
if self.active == index {
self.active = previous.min(self.modules.len().saturating_sub(1));
}
}
_ => {}
}
}
/// Contextual panels share their normal tab with the drawing tools.
fn groups_for(&self, index: usize) -> impl Iterator<Item = &RibbonGroup> {
let testing = self.block_testing.is_some();
self.modules.get(index).into_iter().flat_map(move |module| {
module
.ribbon_groups()
.iter()
.filter(move |group| module.id() != "draw" || group.title != "Close" || testing)
})
}
pub fn select(&mut self, index: usize) {
if index < self.modules.len() {
self.active = index;
@@ -571,12 +642,14 @@ impl Ribbon {
|mut acc, (i, module)| {
// The Layout module no longer has a ribbon tab — its paper-space
// tools live in the right-edge side toolbar (see ui::side_toolbar).
if module.id() == "layout" {
if module.id() == "layout"
|| (module.id() == "block_editor" && self.block_editing.is_none())
{
return acc;
}
let is_active = i == self.active;
let is_contextual = module.id() == "layout";
let is_contextual = matches!(module.id(), "layout" | "block_editor");
let holds_tool = holding_tab == Some(i);
let btn = container(
button(
@@ -655,7 +728,7 @@ impl Ribbon {
},
..Default::default()
});
acc.push(btn.into());
acc.push(keytips::anchor_with(|| format!("tab:{}", module.id()), btn));
acc
},
);
@@ -717,38 +790,36 @@ impl Ribbon {
} else {
self.active
};
let tool_area: Element<'_, Message> =
if let Some(module) = self.modules.get(effective_active) {
let groups = module.ribbon_groups();
let style_ctx = StyleContext {
text_style_names: &self.text_style_names,
active_text_style: &self.active_text_style,
dim_style_names: &self.dim_style_names,
active_dim_style: &self.active_dim_style,
mleader_style_names: &self.mleader_style_names,
active_mleader_style: &self.active_mleader_style,
table_style_names: &self.table_style_names,
active_table_style: &self.active_table_style,
};
// Adaptive tool area: panels sit on one row; when they don't all
// fit they degrade from the right — a panel's large buttons first
// shrink to compact icon columns, then it collapses to a ▾ flyout
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = groups
.iter()
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
.into()
} else {
text("").into()
let tool_area: Element<'_, Message> = if self.modules.get(effective_active).is_some() {
let groups = self.groups_for(effective_active);
let style_ctx = StyleContext {
text_style_names: &self.text_style_names,
active_text_style: &self.active_text_style,
dim_style_names: &self.dim_style_names,
active_dim_style: &self.active_dim_style,
mleader_style_names: &self.mleader_style_names,
active_mleader_style: &self.active_mleader_style,
table_style_names: &self.table_style_names,
active_table_style: &self.active_table_style,
};
// Adaptive tool area: panels sit on one row; when they don't all
// fit they degrade from the right — a panel's large buttons first
// shrink to compact icon columns, then it collapses to a ▾ flyout
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = groups
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
.mode(self.collapse_mode)
.into()
} else {
text("").into()
};
let tool_bar: Element<'_, Message> = container(tool_area)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weakest.color)),
@@ -793,7 +864,7 @@ impl Ribbon {
tool_bar
};
column![tab_bar, tool_bar].into()
keytips::ribbon_area(column![tab_bar, tool_bar])
}
// ── Dropdown overlay ──────────────────────────────────────────────────
@@ -915,8 +986,7 @@ impl Ribbon {
return Some(ov);
}
let module = self.modules.get(self.active)?;
let groups = module.ribbon_groups();
let groups = self.groups_for(self.active);
let mut items_list: Option<Vec<(&'static str, &'static str, IconKind)>> = None;
let mut dd_default = "";
let mut dd_id: &'static str = "";
@@ -964,7 +1034,7 @@ impl Ribbon {
.then_some(theme.palette().background.base.text.scale_alpha(0.72)),
});
button(
let item = button(
row![checkmark, icon_el, label_el]
.spacing(4)
.align_y(iced::Center),
@@ -975,8 +1045,8 @@ impl Ribbon {
}))
.style(popup_row_style)
.width(Fill)
.padding([4, 10])
.into()
.padding([4, 10]);
keytips::anchor_with(|| keytips::item_anchor(dd_id, cmd), item)
})
.collect();
@@ -1145,7 +1215,7 @@ impl Ribbon {
/// Draw-tab dropdowns — so the list grows to fit its entries instead of
/// being clipped to the ribbon-row height. (#153)
fn style_combo_overlay(&self, open_id: &str, win: (f32, f32)) -> Option<Element<'_, Message>> {
let groups = self.modules.get(self.active)?.ribbon_groups();
let groups = self.groups_for(self.active);
// Locate the open style combo; capture its style key + manager command.
let mut found: Option<(crate::modules::StyleKey, Option<&'static str>)> = None;
@@ -1568,7 +1638,10 @@ fn collapse_button<'a>(
.style(opener_style(holds))
.width(Fill)
.padding([3, 5]);
return automatic_large_button(localized_title, content.into());
return keytips::anchor_with(
|| keytips::collapsed_anchor(title),
automatic_large_button(localized_title, content.into()),
);
}
// Collapsed (not yet tight): a large representative-tool face — a live button
@@ -1633,7 +1706,10 @@ fn collapse_button<'a>(
.style(opener_style(holds && !face_lit))
.width(Fill)
.padding([1, 4]);
let opener = automatic_large_button(localized_title, opener.into());
let opener = keytips::anchor_with(
|| keytips::collapsed_anchor(title),
automatic_large_button(localized_title, opener.into()),
);
// The large face fills a fixed slot so a collapsed panel is shorter than a full
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
@@ -1659,6 +1735,41 @@ mod tests {
use super::widgets::is_active_tool;
use super::Ribbon;
#[test]
fn block_test_close_panel_extends_draw_only_while_testing() {
let mut ribbon = Ribbon::default();
let draw = ribbon
.modules
.iter()
.position(|module| module.id() == "draw")
.unwrap();
assert!(!ribbon
.modules
.iter()
.any(|module| module.id() == "block_test"));
let ordinary: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect();
assert!(!ordinary.contains(&"Close"));
ribbon.set_block_editing(true);
ribbon.set_block_editing(false);
ribbon.set_block_testing(true);
assert_eq!(ribbon.active, draw);
let testing: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect();
assert_eq!(&testing[..ordinary.len()], ordinary.as_slice());
assert_eq!(testing.last(), Some(&"Close"));
ribbon.toggle_collapsed_panel("Close");
ribbon.set_block_testing(false);
assert!(ribbon.collapsed_open.is_none());
assert_eq!(
ribbon
.groups_for(draw)
.map(|group| group.title)
.collect::<Vec<_>>(),
ordinary
);
ribbon.set_block_editing(true);
assert_eq!(ribbon.modules[ribbon.active].id(), "block_editor");
}
#[test]
fn block_palette_highlight_follows_the_app_state_passed_to_the_view() {
// The Block Palette button highlight is derived from the app's
+35 -27
View File
@@ -99,7 +99,7 @@ thread_local! {
/// Dropdown items that report a state and cannot be picked.
pub(super) fn is_disabled_item(cmd: &str) -> bool {
cmd == "FRAMES3"
matches!(cmd, "FRAMES3" | "UOSNAP2")
}
fn ribbon_label_bounds(
@@ -543,11 +543,13 @@ pub(super) fn render_small<'a>(
.width(Length::Fixed(SMALL_W))
.height(ROW_H)
.padding([4, 4]);
tooltip(btn, make_tip(tip_text), TipPos::Right)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style)
.into()
super::keytips::anchor(
t.id,
tooltip(btn, make_tip(tip_text), TipPos::Right)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style),
)
}
RibbonItem::LabeledTool(t) => {
@@ -568,11 +570,13 @@ pub(super) fn render_small<'a>(
.width(Length::Fixed(LABELED_SMALL_W + ARROW_W))
.height(ROW_H)
.padding([3, 4]);
tooltip(btn, make_tip(tip_text), TipPos::Right)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style)
.into()
super::keytips::anchor(
t.id,
tooltip(btn, make_tip(tip_text), TipPos::Right)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style),
)
}
RibbonItem::Dropdown {
@@ -895,7 +899,7 @@ fn tool_row<'a>(tools: &[ToolDef], active_tool: &Option<String>) -> Element<'a,
let event = t.event.clone();
let icon_el: Element<Message> = make_icon(t.icon, 16.0);
let msg = module_event_to_message(event);
tooltip(
let btn = tooltip(
button(icon_el)
.on_press(msg)
.style(move |theme: &Theme, status| tool_btn_style(theme, is_active, status))
@@ -905,8 +909,8 @@ fn tool_row<'a>(tools: &[ToolDef], active_tool: &Option<String>) -> Element<'a,
)
.gap(4.0)
.delay(Duration::from_millis(400))
.style(tip_style)
.into()
.style(tip_style);
super::keytips::anchor(t.id, btn)
})
.collect();
row(btns).spacing(2).align_y(iced::Center).into()
@@ -974,15 +978,17 @@ pub(super) fn render_large<'a>(item: &RibbonItem, ctx: &RenderCtx<'_>) -> Elemen
bottom: 4.0,
left: 4.0,
});
tooltip(
automatic_large_button(label, btn.into()),
make_tip(tip_text),
TipPos::Right,
super::keytips::anchor(
t.id,
tooltip(
automatic_large_button(label, btn.into()),
make_tip(tip_text),
TipPos::Right,
)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style),
)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style)
.into()
}
RibbonItem::LargeDropdown {
@@ -1313,11 +1319,13 @@ pub(super) fn quick_access_btn<'a>(
.width(Length::Fixed(TOP_HIST_W))
.height(24)
.padding([2, 0]);
tooltip(btn, make_tip(t!(label).into_owned()), TipPos::Bottom)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style)
.into()
super::keytips::anchor_with(
|| super::keytips::quick_access_anchor(cmd),
tooltip(btn, make_tip(t!(label).into_owned()), TipPos::Bottom)
.gap(6.0)
.delay(Duration::from_millis(400))
.style(tip_style),
)
}
pub(super) fn render_history_control<'a>(