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

+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