fix(block): open nested editor tabs

Closes #538
This commit is contained in:
Hakan Seven 2026-07-28 19:13:27 +03:00
commit d77eb8f419
8 changed files with 296 additions and 79 deletions

View file

@ -335,11 +335,7 @@ impl OpenCADStudio {
"BEDIT" => { "BEDIT" => {
use crate::modules::draw::modify::block_edit::BlockEditPickCommand; use crate::modules::draw::modify::block_edit::BlockEditPickCommand;
if self.tabs[i].block_edit.is_some() { if self.tabs[i].refedit_session.is_some() {
self.command_line.push_error(
"BEDIT: a block editor is already open. Save or discard it first.",
);
} else if self.tabs[i].refedit_session.is_some() {
self.command_line self.command_line
.push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first."); .push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first.");
} else { } else {
@ -399,6 +395,16 @@ impl OpenCADStudio {
.push_error("BEDIT: cannot edit an external reference (xref)."); .push_error("BEDIT: cannot edit an external reference (xref).");
return Some(Task::none()); return Some(Task::none());
} }
if self.tabs[i]
.block_edits
.iter()
.any(|session| session.br_handle == br_handle)
{
self.tabs[i].active_cmd = None;
return Some(Task::done(Message::BlockEditSwitch(
insert.block_name.clone(),
)));
}
// Snapshot the block's block-local entities so Discard can restore // Snapshot the block's block-local entities so Discard can restore
// them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT). // them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT).
@ -425,23 +431,42 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "BEDIT"); self.push_undo_snapshot(i, "BEDIT");
let return_layout = self.tabs[i].scene.current_layout.clone();
// Capture the camera before the editor reframes it, so leaving // Capture the camera before the editor reframes it, so leaving
// the block editor returns the view exactly where it was (#425). // the block editor returns the view exactly where it was (#425).
let return_camera = self.tabs[i].scene.camera.borrow().clone(); let current_camera = self.tabs[i].scene.camera.borrow().clone();
let (return_layout, return_block, return_camera) =
if let Some(parent_index) = self.tabs[i].active_block_edit {
let parent = &mut self.tabs[i].block_edits[parent_index];
parent.editor_camera = current_camera.clone();
(
parent.return_layout.clone(),
Some(parent.block_name.clone()),
parent.return_camera.clone(),
)
} else {
self.tabs[i].scene.sync_camera_to_document();
(
self.tabs[i].scene.current_layout.clone(),
None,
current_camera.clone(),
)
};
// A block editor renders in model style; switch to Model first so // A block editor renders in model style; switch to Model first so
// the paper-space code paths stay off, then scope to the block. // the paper-space code paths stay off, then scope to the block.
if return_layout != "Model" { if self.tabs[i].scene.current_layout != "Model" {
self.tabs[i].scene.set_current_layout("Model".to_string()); self.tabs[i].scene.set_current_layout("Model".to_string());
} }
self.tabs[i].scene.block_edit_block = Some(br_handle); self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].block_edit = Some(BlockEditSession { self.tabs[i].block_edits.push(BlockEditSession {
block_name: insert.block_name.clone(), block_name: insert.block_name.clone(),
br_handle, br_handle,
return_layout, return_layout,
return_block,
snapshot, snapshot,
return_camera, return_camera,
editor_camera: current_camera,
}); });
self.tabs[i].active_block_edit = Some(self.tabs[i].block_edits.len() - 1);
self.tabs[i].scene.deselect_all(); self.tabs[i].scene.deselect_all();
// The first click of a double-click selects the model-space // The first click of a double-click selects the model-space
@ -461,6 +486,11 @@ impl OpenCADStudio {
// it already scopes to the edited block. Without this the view // it already scopes to the edited block. Without this the view
// stays wherever model/paper space was. (#261) // stays wherever model/paper space was. (#261)
self.tabs[i].scene.fit_all(); self.tabs[i].scene.fit_all();
let editor_camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
session.editor_camera = editor_camera;
}
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.refresh_properties(); self.refresh_properties();
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
@ -471,27 +501,28 @@ impl OpenCADStudio {
} }
"BEDIT_SAVE" => { "BEDIT_SAVE" => {
let session = match self.tabs[i].block_edit.take() { let session = match self.tabs[i].active_block_edit.take() {
Some(s) => s, Some(index) if index < self.tabs[i].block_edits.len() => {
self.tabs[i].block_edits.remove(index)
}
None => { None => {
self.command_line self.command_line
.push_error("BEDIT_SAVE: no block editor is open."); .push_error("BEDIT_SAVE: no block editor is open.");
return Some(Task::none()); return Some(Task::none());
} }
Some(_) => {
self.command_line
.push_error("BEDIT_SAVE: invalid block editor state.");
return Some(Task::none());
}
}; };
// Edits are live on the block record — just leave the block space. // Edits are live on the block record — just leave the block space.
self.tabs[i].scene.block_edit_block = None; self.restore_after_block_edit_close(
self.tabs[i].scene.deselect_all(); i,
self.tabs[i].active_grip = None; session.return_block.as_deref(),
self.grip_hover = None; &session.return_layout,
self.grip_popup = None; &session.return_camera,
self.visibility_popup = None; );
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
// Return the view to where it was when BEDIT began (#425).
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.rebuild_derived_caches();
self.refresh_properties();
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line.push_output(&format!( self.command_line.push_output(&format!(
"BEDIT: Block \"{}\" saved. All references updated.", "BEDIT: Block \"{}\" saved. All references updated.",
@ -500,13 +531,20 @@ impl OpenCADStudio {
} }
"BEDIT_DISCARD" => { "BEDIT_DISCARD" => {
let session = match self.tabs[i].block_edit.take() { let session = match self.tabs[i].active_block_edit.take() {
Some(s) => s, Some(index) if index < self.tabs[i].block_edits.len() => {
self.tabs[i].block_edits.remove(index)
}
None => { None => {
self.command_line self.command_line
.push_error("BEDIT_DISCARD: no block editor is open."); .push_error("BEDIT_DISCARD: no block editor is open.");
return Some(Task::none()); return Some(Task::none());
} }
Some(_) => {
self.command_line
.push_error("BEDIT_DISCARD: invalid block editor state.");
return Some(Task::none());
}
}; };
// Restore the block definition to its on-entry snapshot: remove the // Restore the block definition to its on-entry snapshot: remove the
// block's current entities, then re-add the snapshot ones (mirrors // block's current entities, then re-add the snapshot ones (mirrors
@ -531,27 +569,25 @@ impl OpenCADStudio {
{ {
br.entity_handles.clear(); br.entity_handles.clear();
} }
let block_name = session.block_name.clone();
let return_block = session.return_block.clone();
let return_layout = session.return_layout.clone();
let return_camera = session.return_camera.clone();
for mut entity in session.snapshot { for mut entity in session.snapshot {
entity.common_mut().handle = acadrust::Handle::NULL; entity.common_mut().handle = acadrust::Handle::NULL;
entity.common_mut().owner_handle = session.br_handle; entity.common_mut().owner_handle = session.br_handle;
let _ = self.tabs[i].scene.document.add_entity(entity); let _ = self.tabs[i].scene.document.add_entity(entity);
} }
self.tabs[i].scene.block_edit_block = None; self.restore_after_block_edit_close(
self.tabs[i].scene.deselect_all(); i,
self.tabs[i].active_grip = None; return_block.as_deref(),
self.grip_hover = None; &return_layout,
self.grip_popup = None; &return_camera,
self.visibility_popup = None; );
self.tabs[i].scene.set_current_layout(session.return_layout.clone());
// Return the view to where it was when BEDIT began (#425).
*self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone();
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.rebuild_derived_caches();
self.refresh_properties();
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line.push_output(&format!( self.command_line.push_output(&format!(
"BEDIT: Block \"{}\" edit discarded.", "BEDIT: Block \"{}\" edit discarded.",
session.block_name block_name
)); ));
} }
@ -1150,6 +1186,60 @@ impl OpenCADStudio {
Some(self.finish_dispatch(cmd)) Some(self.finish_dispatch(cmd))
} }
fn restore_after_block_edit_close(
&mut self,
i: usize,
return_block: Option<&str>,
return_layout: &str,
return_camera: &crate::scene::view::camera::Camera,
) {
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
let parent_index = return_block.and_then(|parent_name| {
self.tabs[i]
.block_edits
.iter()
.position(|candidate| candidate.block_name == parent_name)
});
if let Some(parent_index) = parent_index {
let (br_handle, editor_camera) = {
let parent = &self.tabs[i].block_edits[parent_index];
(parent.br_handle, parent.editor_camera.clone())
};
self.tabs[i].scene.set_current_layout("Model".to_string());
self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].active_block_edit = Some(parent_index);
*self.tabs[i].scene.camera.borrow_mut() = editor_camera;
} else {
let return_layout = if self.tabs[i]
.scene
.layout_names()
.iter()
.any(|name| name == return_layout)
{
return_layout.to_string()
} else {
"Model".to_string()
};
self.tabs[i].active_block_edit = None;
self.tabs[i].scene.set_current_layout(return_layout);
*self.tabs[i].scene.camera.borrow_mut() = return_camera.clone();
}
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.tabs[i].scene.rebuild_derived_caches();
self.tabs[i].refresh_active_ucs();
self.refresh_properties();
self.adopt_view_display(i);
self.sync_dyn_fields();
}
/// ADDSELECTED — start the draw command that creates the same kind of /// ADDSELECTED — start the draw command that creates the same kind of
/// object as the currently-selected one, adopting its general properties /// object as the currently-selected one, adopting its general properties
/// (layer, colour, linetype, lineweight, linetype scale) as the current /// (layer, colour, linetype, lineweight, linetype scale) as the current

View file

@ -231,8 +231,11 @@ pub(super) struct DocumentTab {
pub(super) paper_bg_color: Option<[f32; 4]>, pub(super) paper_bg_color: Option<[f32; 4]>,
/// Active REFEDIT session, if any. /// Active REFEDIT session, if any.
pub(super) refedit_session: Option<RefEditSession>, pub(super) refedit_session: Option<RefEditSession>,
/// Active BEDIT block-editor space session, if any (issue #261). /// Open BEDIT block tabs. Definitions are edited live; each tab owns its
pub(super) block_edit: Option<BlockEditSession>, /// entry snapshot and camera so nested blocks can remain open independently.
pub(super) block_edits: Vec<BlockEditSession>,
/// Index of the block tab currently shown, or `None` for Model/Paper space.
pub(super) active_block_edit: Option<usize>,
/// Currently active MLeader style name. /// Currently active MLeader style name.
pub(super) active_mleader_style: String, pub(super) active_mleader_style: String,
/// Last camera_generation value written back to the document. /// Last camera_generation value written back to the document.
@ -258,6 +261,16 @@ pub(super) struct DocumentTab {
} }
impl DocumentTab { impl DocumentTab {
pub(super) fn active_block_edit_session(&self) -> Option<&BlockEditSession> {
self.active_block_edit
.and_then(|index| self.block_edits.get(index))
}
pub(super) fn active_block_edit_session_mut(&mut self) -> Option<&mut BlockEditSession> {
self.active_block_edit
.and_then(|index| self.block_edits.get_mut(index))
}
/// The active WCS↔UCS converter for this tab — identity when no UCS is set. /// The active WCS↔UCS converter for this tab — identity when no UCS is set.
/// Every consumer that needs UCS-relative coordinates goes through this. /// Every consumer that needs UCS-relative coordinates goes through this.
pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform { pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform {
@ -478,7 +491,8 @@ impl DocumentTab {
bg_color: None, bg_color: None,
paper_bg_color: None, paper_bg_color: None,
refedit_session: None, refedit_session: None,
block_edit: None, block_edits: Vec::new(),
active_block_edit: None,
active_mleader_style: "Standard".to_string(), active_mleader_style: "Standard".to_string(),
last_synced_camera_gen: 0, last_synced_camera_gen: 0,
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View file

@ -1957,6 +1957,8 @@ pub enum Message {
PspaceCommand, PspaceCommand,
/// Switch to a named layout ("Model" or paper space layout name). /// Switch to a named layout ("Model" or paper space layout name).
LayoutSwitch(String), LayoutSwitch(String),
/// Switch to an already-open BEDIT block tab.
BlockEditSwitch(String),
/// Move a paper layout before/after another paper layout. /// Move a paper layout before/after another paper layout.
LayoutReorder { LayoutReorder {
from: String, from: String,

View file

@ -3358,6 +3358,11 @@ impl OpenCADStudio {
self.on_layout_switch(name) self.on_layout_switch(name)
} }
Message::BlockEditSwitch(name) => {
self.layout_list_open = false;
self.on_block_edit_switch(name)
}
Message::LayoutReorder { from, to, after } => { Message::LayoutReorder { from, to, after } => {
let i = self.active_tab; let i = self.active_tab;
if self.tabs[i].is_start { if self.tabs[i].is_start {

View file

@ -370,7 +370,12 @@ impl OpenCADStudio {
let gen = self.tabs[i].scene.camera_generation; let gen = self.tabs[i].scene.camera_generation;
if gen != self.tabs[i].last_synced_camera_gen { if gen != self.tabs[i].last_synced_camera_gen {
self.tabs[i].last_synced_camera_gen = gen; self.tabs[i].last_synced_camera_gen = gen;
if self.tabs[i].scene.sync_camera_to_document() { if self.tabs[i].active_block_edit.is_some() {
let camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
session.editor_camera = camera;
}
} else if self.tabs[i].scene.sync_camera_to_document() {
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
} }
} }
@ -3134,17 +3139,14 @@ impl OpenCADStudio {
self.tabs[i].scene.document.get_entity(handle), self.tabs[i].scene.document.get_entity(handle),
Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty() Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty()
); );
if insert_has_attrs { if insert_has_attrs && self.tabs[i].active_block_edit.is_none() {
return Task::done(Message::AttrEditorOpen(handle)); return Task::done(Message::AttrEditorOpen(handle));
} }
let is_insert = matches!( let is_insert = matches!(
self.tabs[i].scene.document.get_entity(handle), self.tabs[i].scene.document.get_entity(handle),
Some(AcadEntityType::Insert(_)) Some(AcadEntityType::Insert(_))
); );
if is_insert if is_insert && self.tabs[i].refedit_session.is_none() {
&& self.tabs[i].refedit_session.is_none()
&& self.tabs[i].block_edit.is_none()
{
return Task::done(Message::Command(format!( return Task::done(Message::Command(format!(
"BEDIT_BEGIN:{}", "BEDIT_BEGIN:{}",
handle.value() handle.value()
@ -3679,6 +3681,59 @@ impl OpenCADStudio {
self.on_layout_switch_inner(name, false) self.on_layout_switch_inner(name, false)
} }
pub(crate) fn on_block_edit_switch(&mut self, name: String) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].is_start {
return Task::none();
}
let Some(target_index) = self.tabs[i]
.block_edits
.iter()
.position(|session| session.block_name == name)
else {
self.command_line
.push_error(&format!("BEDIT: block tab \"{name}\" is not open."));
return Task::none();
};
if self.tabs[i].active_block_edit == Some(target_index) {
return Task::none();
}
let cancel_task = self.cancel_active_command_for_space_change();
let current_camera = self.tabs[i].scene.camera.borrow().clone();
if let Some(active_index) = self.tabs[i].active_block_edit {
if let Some(session) = self.tabs[i].block_edits.get_mut(active_index) {
session.editor_camera = current_camera;
}
} else {
self.tabs[i].scene.sync_camera_to_document();
}
let (br_handle, editor_camera) = {
let session = &self.tabs[i].block_edits[target_index];
(session.br_handle, session.editor_camera.clone())
};
self.layout_rename_state = None;
self.tabs[i].scene.active_viewport = None;
self.tabs[i].scene.set_current_layout("Model".to_string());
self.tabs[i].scene.block_edit_block = Some(br_handle);
self.tabs[i].active_block_edit = Some(target_index);
*self.tabs[i].scene.camera.borrow_mut() = editor_camera;
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
self.tabs[i].scene.deselect_all();
self.tabs[i].active_grip = None;
self.grip_hover = None;
self.grip_popup = None;
self.visibility_popup = None;
self.tabs[i].refresh_active_ucs();
self.tabs[i].scene.bump_geometry_no_blocks();
self.refresh_properties();
self.adopt_view_display(i);
self.sync_dyn_fields();
cancel_task
}
/// MVIEW's "Insert view > New" flow deliberately visits Model space to /// MVIEW's "Insert view > New" flow deliberately visits Model space to
/// define a view and then returns to its paper layout. This is the only /// define a view and then returns to its paper layout. This is the only
/// layout transition allowed to preserve an active command. /// layout transition allowed to preserve an active command.
@ -3700,19 +3755,16 @@ impl OpenCADStudio {
.push_info("Open or create a drawing to switch layouts."); .push_info("Open or create a drawing to switch layouts.");
return Task::none(); return Task::none();
} }
// A BEDIT block editor locks the active space; finish it with
// Save Block or Discard before switching spaces. (#261)
if self.tabs[i].block_edit.is_some() {
self.command_line.push_info(
"Finish the block editor (Save Block or Discard) before switching spaces.",
);
return Task::none();
}
let perf = crate::perf::enabled(); let perf = crate::perf::enabled();
let perf_total = Instant::now(); let perf_total = Instant::now();
let perf_from = self.tabs[i].scene.current_layout.clone(); let leaving_block_edit = self.tabs[i].active_block_edit.is_some();
let context_changed = let perf_from = self.tabs[i]
self.tabs[i].scene.current_layout != name || self.tabs[i].scene.active_viewport.is_some(); .active_block_edit_session()
.map(|session| session.block_name.clone())
.unwrap_or_else(|| self.tabs[i].scene.current_layout.clone());
let context_changed = leaving_block_edit
|| self.tabs[i].scene.current_layout != name
|| self.tabs[i].scene.active_viewport.is_some();
let preserve_active_command = preserve_active_command let preserve_active_command = preserve_active_command
&& self.tabs[i] && self.tabs[i]
.active_cmd .active_cmd
@ -3730,6 +3782,24 @@ impl OpenCADStudio {
} else { } else {
Task::none() Task::none()
}; };
if let Some(active_index) = self.tabs[i].active_block_edit.take() {
let camera = self.tabs[i].scene.camera.borrow().clone();
let (return_layout, return_camera) = {
let session = &mut self.tabs[i].block_edits[active_index];
session.editor_camera = camera;
(session.return_layout.clone(), session.return_camera.clone())
};
let return_layout = if self.tabs[i].scene.layout_names().contains(&return_layout) {
return_layout
} else {
"Model".to_string()
};
self.tabs[i].scene.block_edit_block = None;
self.tabs[i].scene.set_current_layout(return_layout);
*self.tabs[i].scene.camera.borrow_mut() = return_camera;
self.tabs[i].scene.camera_generation += 1;
self.tabs[i].scene.bump_geometry_no_blocks();
}
let going_to_paper = name != "Model"; let going_to_paper = name != "Model";
// Persist the camera of the layout we're leaving BEFORE switching // Persist the camera of the layout we're leaving BEFORE switching
// so returning to it restores where the user left off (the // so returning to it restores where the user left off (the
@ -3849,10 +3919,8 @@ impl OpenCADStudio {
// BEDIT block tab: renaming it renames the BLOCK itself — // BEDIT block tab: renaming it renames the BLOCK itself —
// its record, marker and every INSERT reference. (#261) // its record, marker and every INSERT reference. (#261)
let is_block_tab = self.tabs[i] let is_block_tab = self.tabs[i]
.block_edit .active_block_edit_session()
.as_ref() .is_some_and(|session| session.block_name == orig);
.map(|be| be.block_name == orig)
.unwrap_or(false);
if is_block_tab { if is_block_tab {
if self.tabs[i] if self.tabs[i]
.scene .scene
@ -3866,8 +3934,15 @@ impl OpenCADStudio {
} else { } else {
self.push_undo_snapshot(i, "BLOCK RENAME"); self.push_undo_snapshot(i, "BLOCK RENAME");
if self.tabs[i].scene.rename_block(&orig, &new_name) { if self.tabs[i].scene.rename_block(&orig, &new_name) {
if let Some(be) = self.tabs[i].block_edit.as_mut() { if let Some(session) =
be.block_name = new_name.clone(); self.tabs[i].active_block_edit_session_mut()
{
session.block_name = new_name.clone();
}
for session in &mut self.tabs[i].block_edits {
if session.return_block.as_deref() == Some(orig.as_str()) {
session.return_block = Some(new_name.clone());
}
} }
self.tabs[i].dirty = true; self.tabs[i].dirty = true;
self.command_line self.command_line

View file

@ -1084,7 +1084,7 @@ impl OpenCADStudio {
} }
// BEDIT block editor: right-edge Save Block / Discard toolbar (#261). // BEDIT block editor: right-edge Save Block / Discard toolbar (#261).
if tab.block_edit.is_some() && !tab.is_start { if tab.active_block_edit.is_some() && !tab.is_start {
if let Some(tb) = crate::ui::side_toolbar::view( if let Some(tb) = crate::ui::side_toolbar::view(
&crate::modules::draw::modify::block_edit::block_edit_tools(), &crate::modules::draw::modify::block_edit::block_edit_tools(),
) { ) {
@ -1375,10 +1375,14 @@ impl OpenCADStudio {
let coords_mode = tab.scene.document.header.coords_mode; let coords_mode = tab.scene.document.header.coords_mode;
let picking = tab.active_cmd.is_some(); let picking = tab.active_cmd.is_some();
let layout_names = tab.scene.layout_names(); let layout_names = tab.scene.layout_names();
let mut displayed_layouts = layout_names.clone(); let block_tabs = tab
if let Some(be) = &tab.block_edit { .block_edits
displayed_layouts.push(be.block_name.clone()); .iter()
} .map(|session| session.block_name.clone())
.collect();
let active_block = tab
.active_block_edit_session()
.map(|session| session.block_name.clone());
let status_menu_data = crate::ui::statusbar::StatusMenuData { let status_menu_data = crate::ui::statusbar::StatusMenuData {
layout_names: layout_names.clone(), layout_names: layout_names.clone(),
polar_custom_input: &self.polar_custom_input, polar_custom_input: &self.polar_custom_input,
@ -1401,12 +1405,11 @@ impl OpenCADStudio {
self.polar_increment_deg, self.polar_increment_deg,
self.dyn_input, self.dyn_input,
self.snapper.otrack_enabled, self.snapper.otrack_enabled,
displayed_layouts, layout_names.clone(),
block_tabs,
layout_names.into_iter().skip(1).collect(), layout_names.into_iter().skip(1).collect(),
tab.block_edit tab.scene.current_layout.clone(),
.as_ref() active_block,
.map(|be| be.block_name.clone())
.unwrap_or_else(|| tab.scene.current_layout.clone()),
tab.is_start, tab.is_start,
self.layout_rename_state.as_ref(), self.layout_rename_state.as_ref(),
tab.scene.first_viewport_scale(), tab.scene.first_viewport_scale(),

View file

@ -42,7 +42,7 @@ pub fn block_edit_tools() -> Vec<ToolDef> {
// ── Session state (held in DocumentTab) ─────────────────────────────────── // ── Session state (held in DocumentTab) ───────────────────────────────────
/// Active BEDIT block-editor session. Lives in `DocumentTab::block_edit`. /// One open BEDIT block-editor tab. Lives in `DocumentTab::block_edits`.
#[derive(Clone)] #[derive(Clone)]
pub struct BlockEditSession { pub struct BlockEditSession {
/// Name of the block being edited (also the space-tab label). /// Name of the block being edited (also the space-tab label).
@ -52,12 +52,18 @@ pub struct BlockEditSession {
/// Space to return to when the block editor closes (the layout that was /// Space to return to when the block editor closes (the layout that was
/// active when BEDIT began). /// active when BEDIT began).
pub return_layout: String, pub return_layout: String,
/// Parent block tab to return to for a nested BEDIT session. `None` returns
/// to `return_layout`.
pub return_block: Option<String>,
/// The block's block-local entities captured on entry, so Discard can /// The block's block-local entities captured on entry, so Discard can
/// restore the definition (structural Block/BlockEnd/AttDef excluded). /// restore the definition (structural Block/BlockEnd/AttDef excluded).
pub snapshot: Vec<EntityType>, pub snapshot: Vec<EntityType>,
/// Camera state of the space that was active when BEDIT began, restored /// Camera state of the space that was active when BEDIT began, restored
/// on Save/Discard so the view returns exactly where it was (#425). /// on Save/Discard so the view returns exactly where it was (#425).
pub return_camera: crate::scene::view::camera::Camera, pub return_camera: crate::scene::view::camera::Camera,
/// Camera owned by this block tab. Updated whenever another space tab is
/// activated, then restored when the user returns to this block.
pub editor_camera: crate::scene::view::camera::Camera,
} }
// ── BEDIT pick command ───────────────────────────────────────────────────── // ── BEDIT pick command ─────────────────────────────────────────────────────

View file

@ -60,8 +60,10 @@ impl StatusBar {
dyn_input: bool, dyn_input: bool,
otrack: bool, otrack: bool,
layouts: Vec<String>, layouts: Vec<String>,
block_tabs: Vec<String>,
reorderable_layouts: Vec<String>, reorderable_layouts: Vec<String>,
current_layout: String, current_layout: String,
active_block: Option<String>,
// Start/welcome view has no drawing to own layouts. // Start/welcome view has no drawing to own layouts.
is_start: bool, is_start: bool,
// If `Some((original, edit_value))`, the named tab shows a text input. // If `Some((original, edit_value))`, the named tab shows a text input.
@ -413,10 +415,11 @@ impl StatusBar {
if show_layout_tabs { if show_layout_tabs {
let reorderable_layouts: Arc<[String]> = reorderable_layouts.into(); let reorderable_layouts: Arc<[String]> = reorderable_layouts.into();
for name in layouts { for name in layouts {
let is_active = name == current_layout; let is_active = active_block.is_none() && name == current_layout;
let renaming = rename_state let renaming = rename_state
.filter(|(orig, _)| *orig == name) .filter(|(orig, _)| *orig == name)
.map(|(_, edit)| edit.as_str()); .map(|(_, edit)| edit.as_str());
let switch_msg = Message::LayoutSwitch(name.clone());
left.push( left.push(
space_tab( space_tab(
name, name,
@ -424,6 +427,24 @@ impl StatusBar {
renaming, renaming,
!is_start, !is_start,
reorderable_layouts.clone(), reorderable_layouts.clone(),
switch_msg,
"SB_LAYOUT_TAB",
)
.into(),
);
}
for name in block_tabs {
let is_active = active_block.as_deref() == Some(name.as_str());
let switch_msg = Message::BlockEditSwitch(name.clone());
left.push(
space_tab(
name,
is_active,
None,
!is_start,
Arc::from(Vec::<String>::new()),
switch_msg,
"SB_BLOCK_TAB",
) )
.into(), .into(),
); );
@ -760,6 +781,8 @@ fn space_tab<'a>(
rename_edit: Option<&'a str>, rename_edit: Option<&'a str>,
enabled: bool, enabled: bool,
reorderable_layouts: Arc<[String]>, reorderable_layouts: Arc<[String]>,
switch_msg: Message,
report_key_prefix: &'static str,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let tab_style = move |theme: &Theme| { let tab_style = move |theme: &Theme| {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
@ -791,7 +814,7 @@ fn space_tab<'a>(
.style(tab_style) .style(tab_style)
.padding([4, 10]); .padding([4, 10]);
crate::ui::wrap_bar::PosReport::owned( crate::ui::wrap_bar::PosReport::owned(
format!("SB_LAYOUT_TAB:{label}"), format!("{report_key_prefix}:{label}"),
tip( tip(
display.into(), display.into(),
"Open or create a drawing to switch layouts.", "Open or create a drawing to switch layouts.",
@ -827,9 +850,8 @@ fn space_tab<'a>(
.style(tab_style) .style(tab_style)
.padding([4, 10]); .padding([4, 10]);
let switch_msg = Message::LayoutSwitch(label.clone());
let has_context_menu = reorderable_layouts.contains(&label); let has_context_menu = reorderable_layouts.contains(&label);
let report_key = format!("SB_LAYOUT_TAB:{label}"); let report_key = format!("{report_key_prefix}:{label}");
let tab = mouse_area(display).on_press(switch_msg); let tab = mouse_area(display).on_press(switch_msg);
let tab: Element<'a, Message> = if has_context_menu { let tab: Element<'a, Message> = if has_context_menu {
crate::ui::wrap_bar::ReorderTab::layout( crate::ui::wrap_bar::ReorderTab::layout(