Aşama 1: Layout yönetim UI — yeniden adlandırma, silme, context menüsü

- Layout sekme sağ-tıklamasında context menüsü (Yeniden Adlandır / Sil)
- Inline rename: sekme metin-giriş alanına dönüşür, Enter ile commit, ✕ ile iptal
- LayoutDelete: layout ve sahip olduğu tüm entity'leri kaldırır
- LayoutRename: Layout nesnesinin adını yerinde günceller
- LayoutCreate çakışma koruması: mevcut isimler arasında benzersiz ad bulur
- Escape tuşu açık rename/context menüsünü kapatır

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-03-31 07:13:36 +03:00
commit 9b14719ec5
5 changed files with 341 additions and 36 deletions

View file

@ -57,6 +57,10 @@ pub(super) struct H7CAD {
clipboard: Vec<acadrust::EntityType>,
/// Centroid of the clipboard entities (XZ plane, Y-up).
clipboard_centroid: glam::Vec3,
/// Which layout tab has its context menu open (None = closed).
layout_context_menu: Option<String>,
/// Inline rename state: (original_name, current_edit_value).
layout_rename_state: Option<(String, String)>,
}
#[derive(Debug, Clone)]
@ -210,6 +214,20 @@ pub enum Message {
LayoutSwitch(String),
/// Create a new paper space layout.
LayoutCreate,
/// Delete the named paper space layout (Model cannot be deleted).
LayoutDelete(String),
/// Begin inline rename for the given layout tab.
LayoutRenameStart(String),
/// Live-update the rename text input buffer.
LayoutRenameEdit(String),
/// Commit the rename (Enter pressed in the text input).
LayoutRenameCommit,
/// Cancel an in-progress rename (Escape).
LayoutRenameCancel,
/// Open the right-click context menu for the given layout tab.
LayoutContextMenu(String),
/// Close the layout context menu.
LayoutContextMenuClose,
/// A window was closed by the OS (e.g. the user clicked the title-bar ✕).
OsWindowClosed(window::Id),
/// No-op — used as a fallback when a TabEvent has no host mapping.
@ -241,6 +259,8 @@ impl H7CAD {
main_window: None,
clipboard: Vec::new(),
clipboard_centroid: glam::Vec3::ZERO,
layout_context_menu: None,
layout_rename_state: None,
};
app.sync_ribbon_layers();
app

View file

@ -302,6 +302,10 @@ impl H7CAD {
}
Message::CommandEscape => {
// Cancel layout rename / context menu first, then fall through.
if self.layout_rename_state.take().is_some() || self.layout_context_menu.take().is_some() {
return Task::none();
}
let i = self.active_tab;
if self.tabs[i].active_cmd.is_some() {
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape());
@ -1427,6 +1431,9 @@ impl H7CAD {
Message::LayoutSwitch(name) => {
let i = self.active_tab;
let going_to_paper = name != "Model";
// Cancel any pending rename/context-menu when switching layouts.
self.layout_rename_state = None;
self.layout_context_menu = None;
self.tabs[i].scene.current_layout = name;
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.fit_all();
@ -1442,8 +1449,16 @@ impl H7CAD {
Message::LayoutCreate => {
let i = self.active_tab;
let count = self.tabs[i].scene.layout_names().len();
let new_name = format!("Layout{}", count);
// Find a unique name (e.g. Layout2, Layout3, ...).
let existing = self.tabs[i].scene.layout_names();
let mut idx = existing.len();
let new_name = loop {
let candidate = format!("Layout{}", idx);
if !existing.contains(&candidate) {
break candidate;
}
idx += 1;
};
self.push_undo_snapshot(i, "LAYOUT");
match self.tabs[i].scene.document.add_layout(&new_name) {
Ok(_) => {
@ -1463,6 +1478,87 @@ impl H7CAD {
Task::none()
}
Message::LayoutDelete(name) => {
let i = self.active_tab;
self.push_undo_snapshot(i, "LAYOUT DEL");
if self.tabs[i].scene.delete_layout(&name) {
self.layout_context_menu = None;
self.layout_rename_state = None;
// If we fell back to Model space, update ribbon.
if self.tabs[i].scene.current_layout == "Model"
&& self.ribbon.active_is_layout()
{
self.ribbon.select(0);
}
self.command_line.push_output(&format!("Layout \"{name}\" silindi"));
self.tabs[i].dirty = true;
}
Task::none()
}
Message::LayoutRenameStart(name) => {
if name != "Model" {
self.layout_rename_state = Some((name.clone(), name));
self.layout_context_menu = None;
}
Task::none()
}
Message::LayoutRenameEdit(val) => {
if let Some((orig, _)) = &self.layout_rename_state {
let orig = orig.clone();
self.layout_rename_state = Some((orig, val));
}
Task::none()
}
Message::LayoutRenameCommit => {
if let Some((orig, new_name)) = self.layout_rename_state.take() {
let new_name = new_name.trim().to_string();
if !new_name.is_empty() && new_name != orig {
let i = self.active_tab;
let exists = self.tabs[i]
.scene
.layout_names()
.iter()
.any(|n| *n == new_name);
if exists {
self.command_line.push_error(&format!(
"\"{}\" adı zaten kullanımda",
new_name
));
} else {
self.push_undo_snapshot(i, "LAYOUT RENAME");
self.tabs[i].scene.rename_layout(&orig, &new_name);
if self.tabs[i].scene.current_layout == orig {
self.tabs[i].scene.current_layout = new_name.clone();
}
self.tabs[i].dirty = true;
self.command_line
.push_output(&format!("Layout \"{orig}\" → \"{new_name}\""));
}
}
}
Task::none()
}
Message::LayoutRenameCancel => {
self.layout_rename_state = None;
Task::none()
}
Message::LayoutContextMenu(name) => {
if name != "Model" {
self.layout_context_menu = Some(name);
}
Task::none()
}
Message::LayoutContextMenuClose => {
self.layout_context_menu = None;
Task::none()
}
Message::Undo => { self.undo_active_tab(); Task::none() }
Message::Redo => { self.redo_active_tab(); Task::none() }

View file

@ -163,7 +163,8 @@ impl H7CAD {
self.polar_mode,
self.show_grid,
tab.scene.layout_names(),
tab.scene.current_layout.clone()
tab.scene.current_layout.clone(),
self.layout_rename_state.as_ref(),
)
]
.width(Fill)
@ -190,7 +191,14 @@ impl H7CAD {
)
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer].into()
let layout_ctx_layer: Element<'_, Message> =
if let Some(name) = &self.layout_context_menu {
layout_context_menu_overlay(name)
} else {
iced::widget::Space::new().width(0).height(0).into()
};
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer].into()
}
pub fn subscription(&self) -> Subscription<Message> {
@ -367,3 +375,70 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
.padding([0, 2])
.into()
}
// ── Layout context-menu overlay ────────────────────────────────────────────
/// A small right-click context menu rendered above the status bar.
/// The `name` is the layout tab that was right-clicked.
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
const MENU_BG: Color = Color { r: 0.17, g: 0.17, b: 0.17, a: 1.0 };
const MENU_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
const ITEM_HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 };
const TEXT_COLOR: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
let item = |label: &'static str, msg: Message| {
button(text(label).size(12).color(TEXT_COLOR))
.on_press(msg)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
_ => Color::TRANSPARENT,
})),
text_color: TEXT_COLOR,
border: Border::default(),
shadow: iced::Shadow::default(),
snap: false,
})
.padding([4, 12])
.width(Fill)
};
let rename_name = name.to_string();
let delete_name = name.to_string();
let menu = container(
column![
item("Yeniden Adlandır", Message::LayoutRenameStart(rename_name)),
item("Sil", Message::LayoutDelete(delete_name)),
]
.spacing(0)
.width(160),
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(MENU_BG)),
border: Border {
color: MENU_BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.padding([4, 0]);
// Click-catcher fills the whole screen to close the menu when clicking outside.
let catcher = mouse_area(
container(iced::widget::Space::new().width(Fill).height(Fill))
.width(Fill)
.height(Fill),
)
.on_press(Message::LayoutContextMenuClose)
.on_right_press(Message::LayoutContextMenuClose);
// Position the menu above the status bar at the left.
let positioned = container(menu)
.align_bottom(Fill)
.align_left(Fill)
.padding(iced::Padding { top: 0.0, right: 0.0, bottom: 30.0, left: 8.0 });
stack![catcher, positioned].into()
}