From d77eb8f4191d7f68034cb27729f3a2b5e7d6bc38 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 28 Jul 2026 19:13:27 +0300 Subject: [PATCH] fix(block): open nested editor tabs Closes #538 --- src/app/commands/inquiry.rs | 166 ++++++++++++++++++++------ src/app/document.rs | 20 +++- src/app/mod.rs | 2 + src/app/update/mod.rs | 5 + src/app/update/viewport.rs | 121 +++++++++++++++---- src/app/view/mod.rs | 23 ++-- src/modules/draw/modify/block_edit.rs | 8 +- src/ui/statusbar/mod.rs | 30 ++++- 8 files changed, 296 insertions(+), 79 deletions(-) diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 7415ce45..4114d0c9 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -335,11 +335,7 @@ impl OpenCADStudio { "BEDIT" => { use crate::modules::draw::modify::block_edit::BlockEditPickCommand; - if self.tabs[i].block_edit.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() { + if self.tabs[i].refedit_session.is_some() { self.command_line .push_error("BEDIT: finish the active REFEDIT (REFCLOSE) first."); } else { @@ -399,6 +395,16 @@ impl OpenCADStudio { .push_error("BEDIT: cannot edit an external reference (xref)."); 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 // them (skip structural Block/BlockEnd/AttDef, mirroring REFEDIT). @@ -425,23 +431,42 @@ impl OpenCADStudio { 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 // 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 // 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.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(), br_handle, return_layout, + return_block, snapshot, 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(); // 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 // stays wherever model/paper space was. (#261) 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.tabs[i].active_cmd = None; self.tabs[i].dirty = true; @@ -471,27 +501,28 @@ impl OpenCADStudio { } "BEDIT_SAVE" => { - let session = match self.tabs[i].block_edit.take() { - Some(s) => s, + let session = match self.tabs[i].active_block_edit.take() { + Some(index) if index < self.tabs[i].block_edits.len() => { + self.tabs[i].block_edits.remove(index) + } None => { self.command_line .push_error("BEDIT_SAVE: no block editor is open."); 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. - 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; - 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.restore_after_block_edit_close( + i, + session.return_block.as_deref(), + &session.return_layout, + &session.return_camera, + ); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "BEDIT: Block \"{}\" saved. All references updated.", @@ -500,13 +531,20 @@ impl OpenCADStudio { } "BEDIT_DISCARD" => { - let session = match self.tabs[i].block_edit.take() { - Some(s) => s, + let session = match self.tabs[i].active_block_edit.take() { + Some(index) if index < self.tabs[i].block_edits.len() => { + self.tabs[i].block_edits.remove(index) + } None => { self.command_line .push_error("BEDIT_DISCARD: no block editor is open."); 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 // block's current entities, then re-add the snapshot ones (mirrors @@ -531,27 +569,25 @@ impl OpenCADStudio { { 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 { entity.common_mut().handle = acadrust::Handle::NULL; entity.common_mut().owner_handle = session.br_handle; let _ = self.tabs[i].scene.document.add_entity(entity); } - 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; - 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.restore_after_block_edit_close( + i, + return_block.as_deref(), + &return_layout, + &return_camera, + ); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "BEDIT: Block \"{}\" edit discarded.", - session.block_name + block_name )); } @@ -1150,6 +1186,60 @@ impl OpenCADStudio { 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 /// object as the currently-selected one, adopting its general properties /// (layer, colour, linetype, lineweight, linetype scale) as the current diff --git a/src/app/document.rs b/src/app/document.rs index 62a90921..157a5b8a 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -231,8 +231,11 @@ pub(super) struct DocumentTab { pub(super) paper_bg_color: Option<[f32; 4]>, /// Active REFEDIT session, if any. pub(super) refedit_session: Option, - /// Active BEDIT block-editor space session, if any (issue #261). - pub(super) block_edit: Option, + /// Open BEDIT block tabs. Definitions are edited live; each tab owns its + /// entry snapshot and camera so nested blocks can remain open independently. + pub(super) block_edits: Vec, + /// Index of the block tab currently shown, or `None` for Model/Paper space. + pub(super) active_block_edit: Option, /// Currently active MLeader style name. pub(super) active_mleader_style: String, /// Last camera_generation value written back to the document. @@ -258,6 +261,16 @@ pub(super) struct 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. /// Every consumer that needs UCS-relative coordinates goes through this. pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform { @@ -478,7 +491,8 @@ impl DocumentTab { bg_color: None, paper_bg_color: None, refedit_session: None, - block_edit: None, + block_edits: Vec::new(), + active_block_edit: None, active_mleader_style: "Standard".to_string(), last_synced_camera_gen: 0, #[cfg(not(target_arch = "wasm32"))] diff --git a/src/app/mod.rs b/src/app/mod.rs index 7edbb070..dcd84150 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1957,6 +1957,8 @@ pub enum Message { PspaceCommand, /// Switch to a named layout ("Model" or paper space layout name). LayoutSwitch(String), + /// Switch to an already-open BEDIT block tab. + BlockEditSwitch(String), /// Move a paper layout before/after another paper layout. LayoutReorder { from: String, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c13a2ae3..80e3397d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3358,6 +3358,11 @@ impl OpenCADStudio { self.on_layout_switch(name) } + Message::BlockEditSwitch(name) => { + self.layout_list_open = false; + self.on_block_edit_switch(name) + } + Message::LayoutReorder { from, to, after } => { let i = self.active_tab; if self.tabs[i].is_start { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 1433a629..bad9aef2 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -370,7 +370,12 @@ impl OpenCADStudio { let gen = self.tabs[i].scene.camera_generation; if gen != self.tabs[i].last_synced_camera_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; } } @@ -3134,17 +3139,14 @@ impl OpenCADStudio { self.tabs[i].scene.document.get_entity(handle), 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)); } let is_insert = matches!( self.tabs[i].scene.document.get_entity(handle), Some(AcadEntityType::Insert(_)) ); - if is_insert - && self.tabs[i].refedit_session.is_none() - && self.tabs[i].block_edit.is_none() - { + if is_insert && self.tabs[i].refedit_session.is_none() { return Task::done(Message::Command(format!( "BEDIT_BEGIN:{}", handle.value() @@ -3679,6 +3681,59 @@ impl OpenCADStudio { self.on_layout_switch_inner(name, false) } + pub(crate) fn on_block_edit_switch(&mut self, name: String) -> Task { + 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 /// define a view and then returns to its paper layout. This is the only /// layout transition allowed to preserve an active command. @@ -3700,19 +3755,16 @@ impl OpenCADStudio { .push_info("Open or create a drawing to switch layouts."); 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_total = Instant::now(); - let perf_from = self.tabs[i].scene.current_layout.clone(); - let context_changed = - self.tabs[i].scene.current_layout != name || self.tabs[i].scene.active_viewport.is_some(); + let leaving_block_edit = self.tabs[i].active_block_edit.is_some(); + let perf_from = self.tabs[i] + .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 && self.tabs[i] .active_cmd @@ -3730,6 +3782,24 @@ impl OpenCADStudio { } else { 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"; // Persist the camera of the layout we're leaving BEFORE switching // 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 — // its record, marker and every INSERT reference. (#261) let is_block_tab = self.tabs[i] - .block_edit - .as_ref() - .map(|be| be.block_name == orig) - .unwrap_or(false); + .active_block_edit_session() + .is_some_and(|session| session.block_name == orig); if is_block_tab { if self.tabs[i] .scene @@ -3866,8 +3934,15 @@ impl OpenCADStudio { } else { self.push_undo_snapshot(i, "BLOCK RENAME"); if self.tabs[i].scene.rename_block(&orig, &new_name) { - if let Some(be) = self.tabs[i].block_edit.as_mut() { - be.block_name = new_name.clone(); + if let Some(session) = + 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.command_line diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index a80def45..382afb8a 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1084,7 +1084,7 @@ impl OpenCADStudio { } // 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( &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 picking = tab.active_cmd.is_some(); let layout_names = tab.scene.layout_names(); - let mut displayed_layouts = layout_names.clone(); - if let Some(be) = &tab.block_edit { - displayed_layouts.push(be.block_name.clone()); - } + let block_tabs = tab + .block_edits + .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 { layout_names: layout_names.clone(), polar_custom_input: &self.polar_custom_input, @@ -1401,12 +1405,11 @@ impl OpenCADStudio { self.polar_increment_deg, self.dyn_input, self.snapper.otrack_enabled, - displayed_layouts, + layout_names.clone(), + block_tabs, layout_names.into_iter().skip(1).collect(), - tab.block_edit - .as_ref() - .map(|be| be.block_name.clone()) - .unwrap_or_else(|| tab.scene.current_layout.clone()), + tab.scene.current_layout.clone(), + active_block, tab.is_start, self.layout_rename_state.as_ref(), tab.scene.first_viewport_scale(), diff --git a/src/modules/draw/modify/block_edit.rs b/src/modules/draw/modify/block_edit.rs index 2bb271e4..71666173 100644 --- a/src/modules/draw/modify/block_edit.rs +++ b/src/modules/draw/modify/block_edit.rs @@ -42,7 +42,7 @@ pub fn block_edit_tools() -> Vec { // ── 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)] pub struct BlockEditSession { /// 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 /// active when BEDIT began). pub return_layout: String, + /// Parent block tab to return to for a nested BEDIT session. `None` returns + /// to `return_layout`. + pub return_block: Option, /// The block's block-local entities captured on entry, so Discard can /// restore the definition (structural Block/BlockEnd/AttDef excluded). pub snapshot: Vec, /// Camera state of the space that was active when BEDIT began, restored /// on Save/Discard so the view returns exactly where it was (#425). 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 ───────────────────────────────────────────────────── diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index 6ab82a43..a7be7daa 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -60,8 +60,10 @@ impl StatusBar { dyn_input: bool, otrack: bool, layouts: Vec, + block_tabs: Vec, reorderable_layouts: Vec, current_layout: String, + active_block: Option, // Start/welcome view has no drawing to own layouts. is_start: bool, // If `Some((original, edit_value))`, the named tab shows a text input. @@ -413,10 +415,11 @@ impl StatusBar { if show_layout_tabs { let reorderable_layouts: Arc<[String]> = reorderable_layouts.into(); for name in layouts { - let is_active = name == current_layout; + let is_active = active_block.is_none() && name == current_layout; let renaming = rename_state .filter(|(orig, _)| *orig == name) .map(|(_, edit)| edit.as_str()); + let switch_msg = Message::LayoutSwitch(name.clone()); left.push( space_tab( name, @@ -424,6 +427,24 @@ impl StatusBar { renaming, !is_start, 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::::new()), + switch_msg, + "SB_BLOCK_TAB", ) .into(), ); @@ -760,6 +781,8 @@ fn space_tab<'a>( rename_edit: Option<&'a str>, enabled: bool, reorderable_layouts: Arc<[String]>, + switch_msg: Message, + report_key_prefix: &'static str, ) -> Element<'a, Message> { let tab_style = move |theme: &Theme| { let palette = theme.extended_palette(); @@ -791,7 +814,7 @@ fn space_tab<'a>( .style(tab_style) .padding([4, 10]); crate::ui::wrap_bar::PosReport::owned( - format!("SB_LAYOUT_TAB:{label}"), + format!("{report_key_prefix}:{label}"), tip( display.into(), "Open or create a drawing to switch layouts.", @@ -827,9 +850,8 @@ fn space_tab<'a>( .style(tab_style) .padding([4, 10]); - let switch_msg = Message::LayoutSwitch(label.clone()); 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: Element<'a, Message> = if has_context_menu { crate::ui::wrap_bar::ReorderTab::layout(