feat(block-editor): add functional Visibility ribbon controls

Add the current-state selector, Visibility States dialog and mode/show/hide
controls to the contextual ribbon, using shared icons, tooltips, modal UI
and adaptive flyouts. Disable the controls until a Visibility parameter exists.

Stage state creation, rename, deletion, current-state changes and reordering
until OK. Support copied, show-all and hide-all new states in one undo step,
preserve native graph metadata, and restore current-state identity on undo/redo.

Implement nonsaved BVMODE and display-only reveal during BVSHOW selection.
Cancelling selection leaves saved visibility flags and history unchanged.
Keep the existing command-line state workflows available.

Validation: 6 targeted tests; 277 block, 31 app dock, 35 ribbon and 12 history
checks passed (overlapping filters). All 17 local DWG fixtures passed in-memory
DWG/DXF preservation checks with unchanged original checksums.
This commit is contained in:
Karim Jerbi committed 2026-10-15 15:08:21 +01:00
1 parent 308990e672
commit daa26007f9
24 files changed
+1341 -34

No files matched your search

+11 -1
View File
@@ -26,6 +26,7 @@ pub(crate) use draw_panel::tools as panel_tools;
mod color_dropdown;
mod context_tools;
mod modify_panel;
mod visibility_panel;
pub use context_tools::{pdf_underlay_tools, point_cloud_tools, xref_tools, UnderlayContext};
use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
@@ -630,6 +631,7 @@ impl Ribbon {
undo_count: usize,
redo_count: usize,
show_block_palette: bool,
visibility: Option<crate::ui::window::block_visibility::RibbonContext>,
) -> Element<'_, Message> {
// ── Quick-access file commands + undo/redo, one merged flow ────────
let lead = iced::widget::Row::with_children(vec![
@@ -825,7 +827,15 @@ impl Ribbon {
// 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()))
.map(|g| {
if self.modules[effective_active].id() == "block_editor"
&& g.title == "Visibility"
{
visibility_panel::panel(visibility.clone())
} else {
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())
+165
View File
@@ -0,0 +1,165 @@
//! The contextual visibility controls share the ribbon's buttons, tips and collapse layout.
use iced::widget::{button, column, container, pick_list, row, text, tooltip};
use iced::{Element, Fill, Theme};
use super::collapse::Panel;
use super::widgets::{make_icon, tool_btn_style, LARGE_ICON, LARGE_W, SMALL_ICON, TOOL_BAR_H};
use crate::app::Message;
use crate::modules::IconKind;
use crate::ui::window::block_visibility::{Action, RibbonContext};
use crate::ui::ROW_H;
fn tool(
id: &'static str,
label: &'static str,
icon: &'static [u8],
large: bool,
active: bool,
message: Option<Message>,
) -> Element<'static, Message> {
let enabled = message.is_some();
let size = if large { LARGE_ICON } else { SMALL_ICON };
let image = if enabled {
crate::ui::icons::semantic(icon, size)
} else {
crate::ui::icons::semantic_disabled(icon, size)
};
let face: Element<'static, Message> = if large {
column![
image,
text(crate::t!(label).into_owned())
.size(10)
.align_x(iced::Center)
]
.align_x(iced::Center)
.spacing(3)
.into()
} else {
image
};
let mut control = button(face)
.on_press_maybe(message)
.style(move |theme: &Theme, status| {
let mut style = tool_btn_style(theme, active, status);
if !enabled {
style.text_color = style.text_color.scale_alpha(0.42);
style.background = None;
}
style
})
.padding([3, 5]);
control = if large {
control.width(LARGE_W + 8.0).height(3.0 * ROW_H)
} else {
control.width(ROW_H + 4.0).height(ROW_H)
};
tooltip(
super::keytips::anchor(id, container(control).id(id)),
super::tooltip_content(crate::t!(label).into_owned()),
iced::widget::tooltip::Position::Right,
)
.gap(6)
.delay(std::time::Duration::from_millis(400))
.style(super::tooltip_style)
.into()
}
fn view(context: Option<RibbonContext>, compact: bool) -> Element<'static, Message> {
let target = context.as_ref().map(|context| context.target);
let mode = context.as_ref().is_some_and(|context| context.mode);
let manager = tool(
"BVSTATE_MANAGER",
"Visibility States",
include_bytes!("../../../assets/icons/ui/list.svg"),
!compact,
false,
target.map(|target| Message::BlockVisibility(Action::Open(target))),
);
let toggles = row![
tool(
"BVMODE_TOGGLE",
"Visibility Mode",
include_bytes!("../../../assets/icons/constrain/show_all.svg"),
false,
mode,
target.map(|target| Message::BlockVisibility(Action::Mode(target)))
),
tool(
"BVSHOW",
"Make Visible",
include_bytes!("../../../assets/icons/constrain/show.svg"),
false,
false,
target.map(|_| Message::Command("BVSHOW".into()))
),
tool(
"BVHIDE",
"Make Invisible",
include_bytes!("../../../assets/icons/constrain/hide_all.svg"),
false,
false,
target.map(|_| Message::Command("BVHIDE".into()))
),
]
.spacing(2);
let names = context
.as_ref()
.map(|context| context.names.clone())
.unwrap_or_default();
let current = context.as_ref().and_then(|context| context.current.clone());
let mut selector = pick_list(current, names, |name: &String| name.clone())
.placeholder(crate::t!("Visibility state").into_owned())
.text_size(11)
.padding([3, 6])
.width(Fill);
if let Some(target) = target {
selector =
selector.on_select(move |name| Message::BlockVisibility(Action::Current(target, name)));
}
let selector = container(selector).id("BVSTATE_CURRENT").width(Fill);
let controls: Element<'static, Message> = if compact {
column![row![manager, toggles].spacing(3), selector]
.spacing(4)
.width(160)
.height(Fill)
.into()
} else {
row![manager, column![toggles, selector].spacing(6).width(170)]
.spacing(4)
.height(Fill)
.into()
};
column![controls, text(crate::t!("Visibility").into_owned()).size(9)]
.align_x(iced::Center)
.padding([3, 4])
.height(TOOL_BAR_H)
.into()
}
fn collapsed(tight: bool) -> Element<'static, Message> {
let icon = make_icon(
IconKind::Svg(include_bytes!("../../../assets/icons/ui/list.svg")),
if tight { SMALL_ICON } else { LARGE_ICON },
);
button(
column![icon, text(crate::t!("Visibility ▾").into_owned()).size(10)]
.align_x(iced::Center)
.spacing(4),
)
.on_press(Message::ToggleRibbonPanel("Visibility".into()))
.style(|theme, status| tool_btn_style(theme, false, status))
.padding([4, 6])
.into()
}
pub(super) fn panel(context: Option<RibbonContext>) -> Panel<'static> {
Panel {
id: "Visibility".into(),
elements: [
view(context.clone(), false),
view(context, true),
collapsed(false),
collapsed(true),
],
}
}