Merge pull request #1778 from ramox81/codex/refedit-toolbar

REFEDIT session toolbar: definition name, working set count and REFSET buttons
This commit is contained in:
Hakan Seven committed 2026-10-14 12:05:19 +03:00
commit dec3ba2743
29 files changed
+218 -7

No files matched your search

+7 -2
View File
@@ -21,7 +21,7 @@ impl OpenCADStudio {
pub(super) fn dispatch_refedit(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")),
"REFSET" => {
"REFSET" | "REFSET_ADD" | "REFSET_REMOVE" => {
if self.tabs[i].refedit_session.is_none() {
self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref());
return Some(Task::none());
@@ -30,7 +30,12 @@ impl OpenCADStudio {
crate::t!("Transfer objects between the RefEdit working set and host drawing...")
.as_ref(),
);
let command = RefSetCommand::new();
// The side toolbar's Add / Remove go straight to the selection.
let command = match cmd {
"REFSET_ADD" => RefSetCommand::selecting(true),
"REFSET_REMOVE" => RefSetCommand::selecting(false),
_ => RefSetCommand::new(),
};
use crate::command::CadCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
+31 -3
View File
@@ -1687,9 +1687,18 @@ bg={bg_ms:.1}ms n={view_count}"
// In-place block edit (REFEDIT): right-edge toolbar with Save / Discard
// so the edit can be finished by clicking. (#136)
if tab.refedit_session.is_some() && !tab.is_start {
if let Some(tb) = crate::ui::side_toolbar::view(
&crate::modules::draw::modify::refedit::refedit_tools(),
if let (Some(session), false) = (tab.refedit_session.as_ref(), tab.is_start) {
use crate::modules::draw::modify::refedit::{refedit_set_tools, refedit_tools};
let set_tools = refedit_set_tools();
let close_tools = refedit_tools();
let header = crate::ui::side_toolbar::refedit_header(
&session.block_name,
refedit_working_set_count(session, &tab.scene),
);
if let Some(tb) = crate::ui::side_toolbar::view_sections(
Some(header),
&[&set_tools, &close_tools],
&|_| false,
) {
viewport_stack = viewport_stack.push(tb);
}
@@ -4183,3 +4192,22 @@ mod tests {
);
}
}
/// The objects in a REFEDIT session's working set: the checked-out copies and
/// objects added with REFSET still in it, and everything created in the
/// space since.
// ponytail: walks the document per frame while a session is open; keep a
// running count on the session if large drawings feel it.
fn refedit_working_set_count(
session: &crate::modules::draw::modify::refedit::RefEditSession,
scene: &crate::scene::Scene,
) -> usize {
let space = scene.current_layout_block_handle_pub();
scene
.document
.entities()
.filter(|entity| {
entity.common().owner_handle == space && session.in_working_set(entity.common().handle)
})
.count()
}
+2
View File
@@ -3,6 +3,8 @@
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
match source {
"Add to Working Set" => Some(("ref-edit", "add-to-working-set")),
"Remove from Working Set" => Some(("ref-edit", "remove-from-working-set")),
"Select reference:" => Some(("ref-edit", "select-reference")),
"Reference not found." => Some(("ref-edit", "reference-not-found")),
"Object is not a valid reference" => Some(("ref-edit", "not-valid-reference")),
+28
View File
@@ -20,6 +20,25 @@ use std::collections::HashSet;
use crate::command::{CadCommand, CmdOption, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
/// Right-edge side-toolbar buttons that move objects into or out of the
/// working set while a REFEDIT session is active (REFSET Add / Remove).
pub fn refedit_set_tools() -> Vec<ToolDef> {
vec![
ToolDef {
id: "REFSET_ADD",
label: "Add to Working Set",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/refset_add.svg")),
event: ModuleEvent::Command("REFSET_ADD".to_string()),
},
ToolDef {
id: "REFSET_REMOVE",
label: "Remove from Working Set",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/refset_remove.svg")),
event: ModuleEvent::Command("REFSET_REMOVE".to_string()),
},
]
}
/// Right-edge side-toolbar buttons shown while a REFEDIT session is active:
/// save the in-place block edit, or discard it. (#136)
pub fn refedit_tools() -> Vec<ToolDef> {
@@ -479,6 +498,15 @@ impl RefSetCommand {
selected: Vec::new(),
}
}
/// Straight to `Select objects:` for Add (`add`) or Remove.
pub fn selecting(add: bool) -> Self {
Self {
add,
gathering: true,
selected: Vec::new(),
}
}
}
impl CadCommand for RefSetCommand {
+76 -2
View File
@@ -49,6 +49,53 @@ fn tip_panel(label: &'static str) -> Element<'static, Message> {
.into()
}
/// The REFEDIT session toolbar's head: the definition being edited as an
/// accent pill (REFEDITNAME) and how many objects the working set holds.
pub fn refedit_header(name: &str, count: usize) -> Element<'static, Message> {
// Long names are cut so the bar stays narrow; the full name is in the tip.
let short: String = if name.chars().count() > 7 {
format!("{}…", name.chars().take(6).collect::<String>())
} else {
name.to_string()
};
let pill = container(text(short).size(10))
.padding([1, 6])
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.primary.base.color)),
border: Border {
radius: 8.0.into(),
..Default::default()
},
text_color: Some(palette.primary.base.text),
..Default::default()
}
});
let tip = container(text(name.to_string()).size(11)).padding([2, 6]).style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(palette.background.strong.text),
..Default::default()
}
});
column![
tooltip(pill, tip, tooltip::Position::Left).gap(6),
text(format!("{} {}", count, crate::tr!("ref-edit", "toolbar-objects")))
.size(9)
.style(crate::ui::style::common::muted_style),
]
.spacing(2)
.align_x(iced::Center)
.into()
}
/// Build the floating right-edge vertical toolbar from `tools`, vertically
/// centred over the canvas. Returns `None` when `tools` is empty so the caller
/// can skip pushing an overlay.
@@ -61,12 +108,39 @@ pub fn view_with_active(
tools: &[ToolDef],
active: &dyn Fn(&str) -> bool,
) -> Option<Element<'static, Message>> {
if tools.is_empty() {
view_sections(None, &[tools], active)
}
/// As [`view_with_active`], with `header` on top and a rule between the
/// groups of tools.
pub fn view_sections(
header: Option<Element<'static, Message>>,
groups: &[&[ToolDef]],
active: &dyn Fn(&str) -> bool,
) -> Option<Element<'static, Message>> {
if groups.iter().all(|g| g.is_empty()) {
return None;
}
let mut col = column![].spacing(4).align_x(iced::Center);
for t in tools {
if let Some(header) = header {
col = col.push(header);
}
let tools = groups.iter().filter(|g| !g.is_empty()).enumerate().flat_map(|(group, tools)| {
tools.iter().enumerate().map(move |(k, t)| (group > 0 && k == 0, t))
});
for (starts_group, t) in tools {
if starts_group {
col = col.push(
container(iced::widget::Space::new())
.width(Length::Fixed(BTN_SIZE - 4.0))
.height(Length::Fixed(1.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}),
);
}
let on = active(t.id);
let btn = button(icon_el(t.icon))
.on_press(Message::RibbonToolClick {