forked from 1b-agent/opencadstudio-upstream
Viewport right-click now shows a proper widget-based context menu: - When a command is active: Cancel / Enter - Otherwise: Repeat last commands, Delete/Move/Copy (if selected), Select All, Zoom Extents CONSTRUCTIONLINE added as an alias for XLINE (XL). Canvas-drawn placeholder removed; context menu closes on Escape, click-outside, or when any action is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1756 lines
76 KiB
Rust
1756 lines
76 KiB
Rust
use super::{H7CAD, Message};
|
||
use super::document::DocumentTab;
|
||
use super::history::history_dropdown_labels;
|
||
use super::helpers::grid_plane_from_camera;
|
||
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
|
||
use crate::scene::grip::grips_to_screen;
|
||
use crate::ui::overlay;
|
||
use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, text_input, Row, Space};
|
||
use iced::window;
|
||
use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme};
|
||
|
||
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
||
|
||
impl H7CAD {
|
||
pub fn view(&self, window_id: window::Id) -> Element<'_, Message> {
|
||
// ── Layer Properties Manager window ───────────────────────────────
|
||
if Some(window_id) == self.layer_window {
|
||
let tab = &self.tabs[self.active_tab];
|
||
return tab.layers.view_window();
|
||
}
|
||
|
||
let i = self.active_tab;
|
||
let tab = &self.tabs[i];
|
||
let is_paper = tab.scene.current_layout != "Model";
|
||
let viewport_3d = shader(&tab.scene).width(Fill).height(Fill);
|
||
|
||
let selection_overlay = {
|
||
let sel = tab.scene.selection.borrow().clone();
|
||
let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type));
|
||
|
||
let grips: Vec<overlay::GripMarker> =
|
||
if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() {
|
||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||
let bounds = iced::Rectangle {
|
||
x: 0.0, y: 0.0, width: vw, height: vh,
|
||
};
|
||
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
||
let sel_h = tab.selected_handle;
|
||
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
||
.into_iter()
|
||
.filter(|(_, screen, _, _)| {
|
||
screen.x.is_finite()
|
||
&& screen.y.is_finite()
|
||
&& screen.x >= -bounds.width
|
||
&& screen.x <= bounds.width * 2.0
|
||
&& screen.y >= -bounds.height
|
||
&& screen.y <= bounds.height * 2.0
|
||
})
|
||
.map(|(grip_id, screen, _is_midpoint, shape)| {
|
||
let is_hot = tab
|
||
.active_grip
|
||
.as_ref()
|
||
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id);
|
||
overlay::GripMarker { pos: screen, shape, is_hot }
|
||
})
|
||
.collect()
|
||
} else {
|
||
vec![]
|
||
};
|
||
|
||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||
let vp_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||
|
||
let grid = if self.show_grid {
|
||
let cam = tab.scene.camera.borrow();
|
||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||
Some(overlay::GridParams {
|
||
view_proj: cam.view_proj(vp_bounds),
|
||
bounds: vp_bounds,
|
||
plane,
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
||
let cam = tab.scene.camera.borrow();
|
||
Some(overlay::UcsIconParams {
|
||
view_proj: cam.view_proj(vp_bounds),
|
||
bounds: vp_bounds,
|
||
})
|
||
} else {
|
||
None
|
||
};
|
||
|
||
// OST tracking points → screen positions.
|
||
let ost_points: Vec<overlay::OstTrackPoint> = if self.snapper.otrack_enabled {
|
||
let vp_mat = tab.scene.camera.borrow().view_proj(vp_bounds);
|
||
self.snapper.tracking_points.iter().map(|&wp| {
|
||
let ndc = vp_mat.project_point3(wp);
|
||
overlay::OstTrackPoint {
|
||
screen: iced::Point::new(
|
||
(ndc.x + 1.0) * 0.5 * vp_bounds.width,
|
||
(1.0 - ndc.y) * 0.5 * vp_bounds.height,
|
||
),
|
||
}
|
||
}).collect()
|
||
} else {
|
||
vec![]
|
||
};
|
||
|
||
overlay::selection_overlay(sel, snap_info, grips, grid, ucs_icon, ost_points, tab.last_cursor_screen)
|
||
};
|
||
|
||
let nav = container(overlay::nav_toolbar())
|
||
.align_right(Fill)
|
||
.align_top(Fill)
|
||
.padding(iced::Padding { top: 148.0, right: 8.0, bottom: 0.0, left: 0.0 });
|
||
|
||
let info = container(overlay::info_bar(
|
||
if is_paper { &tab.scene.current_layout } else { "Custom View" },
|
||
&tab.visual_style,
|
||
))
|
||
.padding([4, 6]);
|
||
|
||
let viewport_mouse = mouse_area(container(
|
||
iced::widget::Space::new().width(Fill).height(Fill),
|
||
))
|
||
.on_move(Message::ViewportMove)
|
||
.on_press(Message::ViewportLeftPress)
|
||
.on_release(Message::ViewportLeftRelease)
|
||
.on_right_press(Message::ViewportRightPress)
|
||
.on_right_release(Message::ViewportRightRelease)
|
||
.on_middle_press(Message::ViewportMiddlePress)
|
||
.on_middle_release(Message::ViewportMiddleRelease)
|
||
.on_scroll(Message::ViewportScroll)
|
||
.on_exit(Message::ViewportExit);
|
||
|
||
let cube_click = mouse_area(container(
|
||
iced::widget::Space::new()
|
||
.width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE))
|
||
.height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)),
|
||
))
|
||
.on_move(Message::CursorMoved)
|
||
.on_press(Message::ViewportClick);
|
||
|
||
let cube_click = container(cube_click)
|
||
.align_right(Fill)
|
||
.align_top(Fill)
|
||
.padding(iced::Padding { top: VIEWCUBE_PAD, right: VIEWCUBE_PAD, bottom: 0.0, left: 0.0 })
|
||
.width(Fill)
|
||
.height(Fill);
|
||
|
||
let bg_color = if is_paper {
|
||
tab.paper_bg_color
|
||
.map(|[r, g, b, a]| Color { r, g, b, a })
|
||
.unwrap_or(Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 })
|
||
} else {
|
||
tab.bg_color
|
||
.map(|[r, g, b, a]| Color { r, g, b, a })
|
||
.unwrap_or(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })
|
||
};
|
||
|
||
// Dynamic input overlay — shown when a command is active and DYN is on.
|
||
let dyn_input_overlay: Option<Element<'_, Message>> =
|
||
if self.dyn_input && tab.active_cmd.is_some() {
|
||
let w = tab.last_cursor_world;
|
||
let label = if let Some(base) = self.last_point {
|
||
// Show relative distance + angle when we have a base point.
|
||
let dx = (w.x - base.x) as f64;
|
||
let dy = (w.z - base.z) as f64;
|
||
let dist = (dx * dx + dy * dy).sqrt();
|
||
let ang = dy.atan2(dx).to_degrees();
|
||
format!("d={:.3} <{:.1}°", dist, ang)
|
||
} else {
|
||
format!("X:{:.3} Y:{:.3}", w.x, w.z)
|
||
};
|
||
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, label))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let mut viewport_stack = stack![
|
||
container(viewport_3d)
|
||
.style(move |_: &Theme| container::Style {
|
||
background: Some(Background::Color(bg_color)),
|
||
..Default::default()
|
||
})
|
||
.width(Fill)
|
||
.height(Fill),
|
||
container(info).width(Fill).height(Fill),
|
||
selection_overlay,
|
||
viewport_mouse,
|
||
nav,
|
||
cube_click,
|
||
]
|
||
.width(Fill)
|
||
.height(Fill);
|
||
if let Some(dyn_ol) = dyn_input_overlay {
|
||
viewport_stack = viewport_stack.push(dyn_ol);
|
||
}
|
||
|
||
let center_stack = iced::widget::stack![
|
||
row![tab.properties.view(), viewport_stack]
|
||
.width(Fill)
|
||
.height(Fill),
|
||
]
|
||
.width(Fill)
|
||
.height(Fill);
|
||
|
||
let tab_bar = doc_tab_bar(&self.tabs, self.active_tab);
|
||
|
||
let main_ui = container(
|
||
column![
|
||
self.ribbon.view(
|
||
is_paper,
|
||
self.tabs[self.active_tab].history.undo_stack.len(),
|
||
self.tabs[self.active_tab].history.redo_stack.len(),
|
||
),
|
||
tab_bar,
|
||
center_stack,
|
||
self.command_line.view(),
|
||
self.status_bar.view(
|
||
&self.snapper,
|
||
self.snap_popup_open,
|
||
self.ortho_mode,
|
||
self.polar_mode,
|
||
self.polar_increment_deg,
|
||
self.show_grid,
|
||
self.dyn_input,
|
||
self.snapper.otrack_enabled,
|
||
tab.scene.layout_names(),
|
||
tab.scene.current_layout.clone(),
|
||
self.layout_rename_state.as_ref(),
|
||
tab.scene.first_viewport_scale(),
|
||
tab.scene.viewport_count(),
|
||
)
|
||
]
|
||
.width(Fill)
|
||
.height(Fill),
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })),
|
||
..Default::default()
|
||
})
|
||
.width(Fill)
|
||
.height(Fill);
|
||
|
||
let snap_layer: Element<'_, Message> = if self.snap_popup_open {
|
||
crate::ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
let dropdown_layer: Element<'_, Message> = self
|
||
.ribbon
|
||
.dropdown_overlay(
|
||
&history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack),
|
||
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
||
)
|
||
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).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()
|
||
};
|
||
|
||
let page_setup_layer: Element<'_, Message> = if self.page_setup_open {
|
||
page_setup_overlay(
|
||
&self.page_setup_w,
|
||
&self.page_setup_h,
|
||
&self.page_setup_plot_area,
|
||
self.page_setup_center,
|
||
&self.page_setup_offset_x,
|
||
&self.page_setup_offset_y,
|
||
&self.page_setup_rotation,
|
||
&self.page_setup_scale,
|
||
)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
let textstyle_layer: Element<'_, Message> = if self.textstyle_open {
|
||
let tab = &self.tabs[self.active_tab];
|
||
let styles: Vec<String> = tab.scene.document.text_styles
|
||
.iter().map(|s| s.name.clone()).collect();
|
||
textstyle_overlay(styles, &self.textstyle_selected, &self.textstyle_font, &self.textstyle_width, &self.textstyle_oblique)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
let tablestyle_layer: Element<'_, Message> = if self.tablestyle_open {
|
||
use acadrust::objects::ObjectType;
|
||
let tab = &self.tabs[self.active_tab];
|
||
let styles: Vec<String> = tab.scene.document.objects.values()
|
||
.filter_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None })
|
||
.collect();
|
||
let selected_style = tab.scene.document.objects.values()
|
||
.find_map(|o| if let ObjectType::TableStyle(s) = o {
|
||
if s.name == self.tablestyle_selected { Some(s) } else { None }
|
||
} else { None });
|
||
tablestyle_overlay(styles, &self.tablestyle_selected, selected_style)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
let mlstyle_layer: Element<'_, Message> = if self.mlstyle_open {
|
||
use acadrust::objects::ObjectType;
|
||
let tab = &self.tabs[self.active_tab];
|
||
let styles: Vec<String> = tab.scene.document.objects.values()
|
||
.filter_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None })
|
||
.collect();
|
||
let selected_style = tab.scene.document.objects.values()
|
||
.find_map(|o| if let ObjectType::MLineStyle(s) = o {
|
||
if s.name == self.mlstyle_selected { Some(s) } else { None }
|
||
} else { None });
|
||
mlstyle_overlay(styles, &self.mlstyle_selected, selected_style, tab.scene.document.header.multiline_style.clone())
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
let dimstyle_layer: Element<'_, Message> = if self.dimstyle_open {
|
||
let tab = &self.tabs[self.active_tab];
|
||
let styles: Vec<String> = tab.scene.document.dim_styles
|
||
.iter().map(|s| s.name.clone()).collect();
|
||
dimstyle_overlay(
|
||
styles,
|
||
&self.dimstyle_selected,
|
||
self.dimstyle_tab,
|
||
self,
|
||
)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
};
|
||
|
||
// ── Viewport right-click context menu ─────────────────────────────
|
||
let viewport_ctx_layer: Element<'_, Message> = {
|
||
let ctx_pos = tab.scene.selection.borrow().context_menu;
|
||
if let Some(p) = ctx_pos {
|
||
let has_cmd = tab.active_cmd.is_some();
|
||
let has_selection = !tab.scene.selected.is_empty();
|
||
let last_cmds: Vec<String> = self.command_line.cmd_recall
|
||
.iter().rev().take(3).cloned().collect();
|
||
viewport_context_menu_overlay(p, has_cmd, has_selection, last_cmds)
|
||
} else {
|
||
iced::widget::Space::new().width(0).height(0).into()
|
||
}
|
||
};
|
||
|
||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer, viewport_ctx_layer].into()
|
||
}
|
||
|
||
pub fn subscription(&self) -> Subscription<Message> {
|
||
use iced::event;
|
||
iced::Subscription::batch([
|
||
window::frames().map(Message::Tick),
|
||
event::listen_with(|ev, status, win_id| {
|
||
use iced::event::Status;
|
||
match ev {
|
||
iced::Event::Window(window::Event::Closed) => {
|
||
Some(Message::OsWindowClosed(win_id))
|
||
}
|
||
iced::Event::Window(window::Event::Resized(sz)) => {
|
||
Some(Message::WindowResized(sz.width as f32, sz.height as f32))
|
||
}
|
||
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||
key, modifiers, ..
|
||
}) => {
|
||
let ctrl = modifiers.control();
|
||
let shift = modifiers.shift();
|
||
match key {
|
||
keyboard::Key::Named(keyboard::key::Named::Enter)
|
||
| keyboard::Key::Named(keyboard::key::Named::Space)
|
||
if status == Status::Ignored =>
|
||
{
|
||
Some(Message::CommandFinalize)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::Escape) => {
|
||
Some(Message::CommandEscape)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::Delete)
|
||
if status == Status::Ignored =>
|
||
{
|
||
Some(Message::DeleteSelected)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
|
||
Some(Message::CommandHistoryPrev)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
|
||
Some(Message::CommandHistoryNext)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F3) => {
|
||
Some(Message::ToggleSnapEnabled)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F7) => {
|
||
Some(Message::ToggleGrid)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F8) => {
|
||
Some(Message::ToggleOrtho)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F9) => {
|
||
Some(Message::ToggleGridSnap)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
||
Some(Message::TogglePolar)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F11) => {
|
||
Some(Message::ToggleOTrack)
|
||
}
|
||
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
||
Some(Message::ToggleDynInput)
|
||
}
|
||
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
||
"n" => Some(Message::ClearScene),
|
||
"o" => Some(Message::OpenFile),
|
||
"s" if !shift => Some(Message::SaveFile),
|
||
"s" if shift => Some(Message::SaveAs),
|
||
"z" if !shift => Some(Message::Undo),
|
||
"z" if shift => Some(Message::Redo),
|
||
"y" => Some(Message::Redo),
|
||
"c" => Some(Message::Command("COPYCLIP".to_string())),
|
||
"x" => Some(Message::Command("CUTCLIP".to_string())),
|
||
"v" => Some(Message::Command("PASTECLIP".to_string())),
|
||
_ => None,
|
||
},
|
||
_ => None,
|
||
}
|
||
}
|
||
_ => None,
|
||
}
|
||
}),
|
||
])
|
||
}
|
||
|
||
pub(super) fn focus_cmd_input(&self) -> Task<Message> {
|
||
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
||
}
|
||
|
||
pub(super) fn blur_cmd_input(&self) -> Task<Message> {
|
||
let op = iced::advanced::widget::operation::focusable::unfocus::<Message>();
|
||
iced::advanced::widget::operate(op)
|
||
}
|
||
}
|
||
|
||
// ── Document tab bar ───────────────────────────────────────────────────────
|
||
|
||
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
|
||
const BAR_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
|
||
const TAB_ACTIVE: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 };
|
||
const TAB_HOVER: Color = Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 };
|
||
const TAB_INACTIVE: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.20, g: 0.55, b: 0.90, a: 1.0 };
|
||
const TEXT_ACTIVE: Color = Color::WHITE;
|
||
const TEXT_INACTIVE: Color = Color { r: 0.60, g: 0.60, b: 0.60, a: 1.0 };
|
||
const CLOSE_HOVER: Color = Color { r: 0.70, g: 0.22, b: 0.22, a: 1.0 };
|
||
const BORDER_COLOR: Color = Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0 };
|
||
|
||
let mut bar = Row::new().spacing(0).align_y(iced::Center);
|
||
|
||
for (idx, tab) in tabs.iter().enumerate() {
|
||
let is_active = idx == active_tab;
|
||
let name = tab.tab_display_name();
|
||
let label = if tab.dirty { format!("● {}", name) } else { name };
|
||
|
||
let title_btn = button(text(label).size(12))
|
||
.on_press(Message::TabSwitch(idx))
|
||
.padding([5, 12])
|
||
.style(move |_: &Theme, status| button::Style {
|
||
background: Some(Background::Color(match (is_active, status) {
|
||
(true, _) => TAB_ACTIVE,
|
||
(false, button::Status::Hovered) => TAB_HOVER,
|
||
_ => TAB_INACTIVE,
|
||
})),
|
||
text_color: if is_active { TEXT_ACTIVE } else { TEXT_INACTIVE },
|
||
border: Border {
|
||
color: if is_active { ACCENT } else { Color::TRANSPARENT },
|
||
width: if is_active { 1.0 } else { 0.0 },
|
||
radius: 0.0.into(),
|
||
},
|
||
shadow: iced::Shadow::default(),
|
||
snap: false,
|
||
});
|
||
|
||
let close_btn = button(text("×").size(11).color(Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }))
|
||
.on_press(Message::TabClose(idx))
|
||
.padding([3, 5])
|
||
.style(move |_: &Theme, status| button::Style {
|
||
background: Some(Background::Color(match status {
|
||
button::Status::Hovered => CLOSE_HOVER,
|
||
_ => if is_active { TAB_ACTIVE } else { TAB_INACTIVE },
|
||
})),
|
||
border: Border { radius: 3.0.into(), ..Default::default() },
|
||
..Default::default()
|
||
});
|
||
|
||
bar = bar.push(
|
||
container(row![title_btn, close_btn].spacing(0).align_y(iced::Center))
|
||
.style(move |_: &Theme| container::Style {
|
||
border: Border {
|
||
color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT },
|
||
width: if is_active { 1.0 } else { 0.0 },
|
||
radius: 0.0.into(),
|
||
},
|
||
..Default::default()
|
||
}),
|
||
);
|
||
}
|
||
|
||
let new_btn = button(text("+").size(14).color(Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0 }))
|
||
.on_press(Message::TabNew)
|
||
.padding([4, 10])
|
||
.style(|_: &Theme, status| button::Style {
|
||
background: Some(Background::Color(match status {
|
||
button::Status::Hovered => TAB_HOVER,
|
||
_ => Color::TRANSPARENT,
|
||
})),
|
||
border: Border { radius: 0.0.into(), ..Default::default() },
|
||
..Default::default()
|
||
});
|
||
|
||
bar = bar.push(new_btn);
|
||
bar = bar.push(iced::widget::Space::new().width(Fill));
|
||
|
||
container(bar)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(BAR_BG)),
|
||
border: Border {
|
||
color: BORDER_COLOR,
|
||
width: 1.0,
|
||
radius: 0.0.into(),
|
||
},
|
||
..Default::default()
|
||
})
|
||
.height(30)
|
||
.width(Fill)
|
||
.padding([0, 2])
|
||
.into()
|
||
}
|
||
|
||
// ── Layout context-menu overlay ────────────────────────────────────────────
|
||
|
||
// ── Viewport right-click context menu ──────────────────────────────────────
|
||
|
||
fn viewport_context_menu_overlay(
|
||
pos: iced::Point,
|
||
has_cmd: bool,
|
||
has_selection: bool,
|
||
last_cmds: Vec<String>,
|
||
) -> Element<'static, 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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const SEP_COL: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
||
|
||
let item = |label: String, msg: Message| -> Element<'static, Message> {
|
||
button(text(label).size(12).color(TEXT_COL))
|
||
.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_COL,
|
||
border: Border::default(),
|
||
shadow: iced::Shadow::default(),
|
||
snap: false,
|
||
})
|
||
.padding([4, 12])
|
||
.width(Fill)
|
||
.into()
|
||
};
|
||
|
||
let sep = || -> Element<'static, Message> {
|
||
container(iced::widget::Space::new().width(Fill).height(1))
|
||
.style(move |_: &Theme| container::Style {
|
||
background: Some(Background::Color(SEP_COL)),
|
||
..Default::default()
|
||
})
|
||
.width(Fill)
|
||
.height(1)
|
||
.padding([0, 6])
|
||
.into()
|
||
};
|
||
|
||
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
||
|
||
if has_cmd {
|
||
items.push(item("Cancel".to_string(), Message::CommandEscape));
|
||
items.push(item("Enter".to_string(), Message::CommandFinalize));
|
||
} else {
|
||
if !last_cmds.is_empty() {
|
||
let last = last_cmds[0].clone();
|
||
items.push(item(
|
||
format!("Repeat {last}"),
|
||
Message::Command(last.to_uppercase()),
|
||
));
|
||
if last_cmds.len() > 1 {
|
||
for cmd in last_cmds.iter().skip(1) {
|
||
let c = cmd.clone();
|
||
items.push(item(c.clone(), Message::Command(c.to_uppercase())));
|
||
}
|
||
}
|
||
items.push(sep());
|
||
}
|
||
if has_selection {
|
||
items.push(item("Delete".to_string(), Message::DeleteSelected));
|
||
items.push(item("Move".to_string(), Message::Command("MOVE".to_string())));
|
||
items.push(item("Copy".to_string(), Message::Command("COPY".to_string())));
|
||
items.push(sep());
|
||
}
|
||
items.push(item("Select All".to_string(), Message::Command("SELECTALL".to_string())));
|
||
items.push(item("Zoom Extents".to_string(), Message::Command("ZOOM".to_string())));
|
||
}
|
||
|
||
let menu_col = column(items).spacing(0).width(180);
|
||
|
||
let menu = container(menu_col)
|
||
.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 to close the menu when clicking outside.
|
||
let catcher = mouse_area(
|
||
container(Space::new()).width(Fill).height(Fill),
|
||
)
|
||
.on_press(Message::ViewportContextMenuClose)
|
||
.on_right_press(Message::ViewportContextMenuClose);
|
||
|
||
// Position using top/left spacing.
|
||
let positioned = column![
|
||
Space::new().height(pos.y),
|
||
row![Space::new().width(pos.x), menu],
|
||
]
|
||
.width(Fill)
|
||
.height(Fill);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|
||
|
||
/// 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("Rename", Message::LayoutRenameStart(rename_name)),
|
||
item("Delete", 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()
|
||
}
|
||
|
||
// ── Page Setup overlay ──────────────────────────────────────────────────────
|
||
|
||
/// Modal panel for editing paper width / height of the current layout.
|
||
// ── Paper size presets ────────────────────────────────────────────────────
|
||
|
||
#[allow(dead_code)]
|
||
const PAPER_PRESETS: &[(&str, f64, f64)] = &[
|
||
("A4 Portrait", 210.0, 297.0),
|
||
("A4 Landscape", 297.0, 210.0),
|
||
("A3 Portrait", 297.0, 420.0),
|
||
("A3 Landscape", 420.0, 297.0),
|
||
("A2 Portrait", 420.0, 594.0),
|
||
("A2 Landscape", 594.0, 420.0),
|
||
("A1 Portrait", 594.0, 841.0),
|
||
("A1 Landscape", 841.0, 594.0),
|
||
("A0 Portrait", 841.0, 1189.0),
|
||
("A0 Landscape", 1189.0, 841.0),
|
||
("Letter Portrait", 215.9, 279.4),
|
||
("Letter Landscape", 279.4, 215.9),
|
||
("Custom", 0.0, 0.0),
|
||
];
|
||
|
||
fn page_setup_overlay<'a>(
|
||
w_buf: &'a str,
|
||
h_buf: &'a str,
|
||
plot_area: &'a str,
|
||
center: bool,
|
||
offset_x: &'a str,
|
||
offset_y: &'a str,
|
||
rotation: &'a str,
|
||
scale: &'a str,
|
||
) -> Element<'a, Message> {
|
||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||
const BORDER_COL: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||
const TEXT_COLOR: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const DIM_COLOR: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||
|
||
let lbl = |s: &'static str| text(s).size(11).color(DIM_COLOR).width(110);
|
||
|
||
let field_style = |_: &Theme, _: text_input::Status| text_input::Style {
|
||
background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }),
|
||
border: Border { color: BORDER_COL, width: 1.0, radius: 3.0.into() },
|
||
icon: TEXT_COLOR,
|
||
placeholder: Color { r: 0.45, g: 0.45, b: 0.45, a: 1.0 },
|
||
value: TEXT_COLOR,
|
||
selection: ACCENT,
|
||
};
|
||
|
||
let btn_style = |accent: bool| {
|
||
move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match status {
|
||
button::Status::Hovered | button::Status::Pressed if accent => {
|
||
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }
|
||
}
|
||
button::Status::Hovered | button::Status::Pressed => {
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
|
||
}
|
||
_ if accent => ACCENT,
|
||
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COLOR,
|
||
border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() },
|
||
shadow: iced::Shadow::default(),
|
||
snap: false,
|
||
}
|
||
};
|
||
|
||
let pill_style = |active: bool| {
|
||
move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match (active, status) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) => {
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
|
||
}
|
||
_ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COLOR,
|
||
border: Border { color: BORDER_COL, width: 1.0, radius: 3.0.into() },
|
||
shadow: iced::Shadow::default(),
|
||
snap: false,
|
||
}
|
||
};
|
||
|
||
let divider = || container(Space::new().width(Fill).height(1))
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(BORDER_COL)),
|
||
..Default::default()
|
||
})
|
||
.width(Fill)
|
||
.height(1);
|
||
|
||
// ── Paper size presets ────────────────────────────────────────────────
|
||
let preset_row1 = row![
|
||
button(text("A4 P").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A4 Portrait".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("A4 L").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A4 Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("A3 P").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A3 Portrait".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("A3 L").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A3 Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
].spacing(4);
|
||
|
||
let preset_row2 = row![
|
||
button(text("A2 L").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A2 Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("A1 L").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A1 Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("A0 L").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("A0 Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
button(text("Letter").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPreset("Letter Landscape".into()))
|
||
.style(pill_style(false)).padding([3, 6]),
|
||
].spacing(4);
|
||
|
||
// ── Plot area buttons ─────────────────────────────────────────────────
|
||
let area_row = row![
|
||
button(text("Layout").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPlotArea("Layout".into()))
|
||
.style(pill_style(plot_area == "Layout")).padding([3, 8]),
|
||
button(text("Extents").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupPlotArea("Extents".into()))
|
||
.style(pill_style(plot_area == "Extents")).padding([3, 8]),
|
||
].spacing(6);
|
||
|
||
// ── Center toggle ─────────────────────────────────────────────────────
|
||
let center_btn = button(
|
||
text(if center { "✓ Center on page" } else { " Center on page" }).size(11).color(TEXT_COLOR)
|
||
)
|
||
.on_press(Message::PageSetupCenterToggle)
|
||
.style(pill_style(center))
|
||
.padding([4, 10]);
|
||
|
||
// ── Rotation buttons ──────────────────────────────────────────────────
|
||
let rot_row = row![
|
||
button(text("0°").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupRotation("0".into()))
|
||
.style(pill_style(rotation == "0")).padding([3, 8]),
|
||
button(text("90°").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupRotation("90".into()))
|
||
.style(pill_style(rotation == "90")).padding([3, 8]),
|
||
button(text("180°").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupRotation("180".into()))
|
||
.style(pill_style(rotation == "180")).padding([3, 8]),
|
||
button(text("270°").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupRotation("270".into()))
|
||
.style(pill_style(rotation == "270")).padding([3, 8]),
|
||
].spacing(4);
|
||
|
||
let panel = container(
|
||
column![
|
||
text("Page Setup").size(14).color(TEXT_COLOR),
|
||
divider(),
|
||
// Paper size
|
||
text("Paper Size").size(11).color(DIM_COLOR),
|
||
preset_row1,
|
||
preset_row2,
|
||
row![
|
||
lbl("Width (mm)"),
|
||
text_input("297", w_buf)
|
||
.on_input(Message::PageSetupWidthEdit)
|
||
.on_submit(Message::PageSetupCommit)
|
||
.style(field_style)
|
||
.width(80).size(12),
|
||
].spacing(6).align_y(iced::Alignment::Center),
|
||
row![
|
||
lbl("Height (mm)"),
|
||
text_input("210", h_buf)
|
||
.on_input(Message::PageSetupHeightEdit)
|
||
.on_submit(Message::PageSetupCommit)
|
||
.style(field_style)
|
||
.width(80).size(12),
|
||
].spacing(6).align_y(iced::Alignment::Center),
|
||
divider(),
|
||
// Plot area
|
||
text("Plot Area").size(11).color(DIM_COLOR),
|
||
area_row,
|
||
divider(),
|
||
// Position
|
||
text("Position").size(11).color(DIM_COLOR),
|
||
center_btn,
|
||
row![
|
||
lbl("Offset X (mm)"),
|
||
text_input("0", offset_x)
|
||
.on_input(Message::PageSetupOffsetXEdit)
|
||
.style(field_style)
|
||
.width(80).size(12),
|
||
].spacing(6).align_y(iced::Alignment::Center),
|
||
row![
|
||
lbl("Offset Y (mm)"),
|
||
text_input("0", offset_y)
|
||
.on_input(Message::PageSetupOffsetYEdit)
|
||
.style(field_style)
|
||
.width(80).size(12),
|
||
].spacing(6).align_y(iced::Alignment::Center),
|
||
divider(),
|
||
// Rotation
|
||
text("Rotation").size(11).color(DIM_COLOR),
|
||
rot_row,
|
||
divider(),
|
||
// Plot Scale
|
||
text("Plot Scale").size(11).color(DIM_COLOR),
|
||
row![
|
||
button(text("Fit").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("Fit".into()))
|
||
.style(pill_style(scale == "Fit")).padding([3, 8]),
|
||
button(text("1:1").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:1".into()))
|
||
.style(pill_style(scale == "1:1")).padding([3, 8]),
|
||
button(text("1:2").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:2".into()))
|
||
.style(pill_style(scale == "1:2")).padding([3, 8]),
|
||
button(text("1:5").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:5".into()))
|
||
.style(pill_style(scale == "1:5")).padding([3, 8]),
|
||
button(text("1:10").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:10".into()))
|
||
.style(pill_style(scale == "1:10")).padding([3, 8]),
|
||
].spacing(4),
|
||
row![
|
||
button(text("1:20").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:20".into()))
|
||
.style(pill_style(scale == "1:20")).padding([3, 8]),
|
||
button(text("1:50").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:50".into()))
|
||
.style(pill_style(scale == "1:50")).padding([3, 8]),
|
||
button(text("1:100").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("1:100".into()))
|
||
.style(pill_style(scale == "1:100")).padding([3, 8]),
|
||
button(text("2:1").size(10).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupScale("2:1".into()))
|
||
.style(pill_style(scale == "2:1")).padding([3, 8]),
|
||
].spacing(4),
|
||
divider(),
|
||
// Buttons
|
||
row![
|
||
button(text("Cancel").size(12).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupClose)
|
||
.style(btn_style(false))
|
||
.padding([5, 14]),
|
||
Space::new().width(Fill).height(0),
|
||
button(text("OK").size(12).color(TEXT_COLOR))
|
||
.on_press(Message::PageSetupCommit)
|
||
.style(btn_style(true))
|
||
.padding([5, 20]),
|
||
]
|
||
.align_y(iced::Alignment::Center),
|
||
]
|
||
.spacing(8)
|
||
.padding(16)
|
||
.width(290),
|
||
)
|
||
.style(move |_: &Theme| container::Style {
|
||
background: Some(Background::Color(PANEL_BG)),
|
||
border: Border { color: BORDER_COL, width: 1.0, radius: 6.0.into() },
|
||
..Default::default()
|
||
});
|
||
|
||
// Click-catcher to close on outside click.
|
||
let catcher = mouse_area(
|
||
container(Space::new().width(Fill).height(Fill))
|
||
.width(Fill)
|
||
.height(Fill),
|
||
)
|
||
.on_press(Message::PageSetupClose);
|
||
|
||
// Center the panel on screen.
|
||
let positioned = container(panel)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.align_x(iced::Alignment::Center)
|
||
.align_y(iced::Alignment::Center);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|
||
|
||
// ── DimStyle Dialog overlay ─────────────────────────────────────────────────
|
||
|
||
fn dimstyle_overlay<'a>(
|
||
styles: Vec<String>,
|
||
selected: &'a str,
|
||
tab: u8,
|
||
app: &'a super::H7CAD,
|
||
) -> Element<'a, Message> {
|
||
use super::DsField;
|
||
use iced::widget::checkbox;
|
||
use iced::Length::Shrink;
|
||
|
||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||
|
||
let lbl = |s: &'static str| text(s).size(11).color(DIM_COL).width(150);
|
||
|
||
let field_style = |_: &Theme, _: text_input::Status| text_input::Style {
|
||
background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }),
|
||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||
icon: TEXT_COL, placeholder: DIM_COL, value: TEXT_COL, selection: ACCENT,
|
||
};
|
||
|
||
let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> {
|
||
text_input("", val)
|
||
.on_input(move |s| Message::DsEdit(fld.clone(), s))
|
||
.style(field_style)
|
||
.size(11)
|
||
.width(90)
|
||
.into()
|
||
};
|
||
|
||
let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match (accent, status) {
|
||
(true, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 },
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
(true, _) => ACCENT,
|
||
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
};
|
||
|
||
let tab_btn = |label: &'static str, idx: u8| {
|
||
let active = tab == idx;
|
||
button(text(label).size(11).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogTab(idx))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (active, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
_ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding([4, 10])
|
||
};
|
||
|
||
// ── Style list ────────────────────────────────────────────────────────
|
||
let style_list: Element<'_, Message> = {
|
||
let mut col = column![].spacing(2);
|
||
for name in styles {
|
||
let active = name == selected;
|
||
col = col.push(
|
||
button(text(name.clone()).size(11).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogSelect(name))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (active, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
_ => Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 0.0, radius: 3.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding([3, 8])
|
||
.width(Fill)
|
||
);
|
||
}
|
||
iced::widget::scrollable(col).height(160).into()
|
||
};
|
||
|
||
let style_panel = column![
|
||
text("Styles").size(11).color(DIM_COL),
|
||
container(style_list)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(150)
|
||
.padding(2),
|
||
row![
|
||
button(text("New").size(10).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogNew)
|
||
.style(btn_style(false)).padding([3, 8]),
|
||
button(text("Delete").size(10).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogDelete)
|
||
.style(btn_style(false)).padding([3, 8]),
|
||
].spacing(4),
|
||
button(text("Set Current").size(10).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogSetCurrent)
|
||
.style(btn_style(false)).padding([3, 8]).width(Fill),
|
||
].spacing(6).width(150);
|
||
|
||
// ── Tab bar ───────────────────────────────────────────────────────────
|
||
let tabs = row![
|
||
tab_btn("Lines", 0),
|
||
tab_btn("Arrows", 1),
|
||
tab_btn("Text", 2),
|
||
tab_btn("Scale/Units", 3),
|
||
tab_btn("Tolerances", 4),
|
||
].spacing(2);
|
||
|
||
// ── Checkbox helper ───────────────────────────────────────────────────
|
||
let chk = |label: &'static str, val: bool, fld: DsField| -> Element<'a, Message> {
|
||
checkbox(val)
|
||
.label(label)
|
||
.on_toggle(move |_| Message::DsToggle(fld.clone()))
|
||
.size(14)
|
||
.text_size(11)
|
||
.into()
|
||
};
|
||
|
||
// ── Tab content ───────────────────────────────────────────────────────
|
||
let tab_content: Element<'_, Message> = match tab {
|
||
0 => column![
|
||
text("Dimension Line").size(11).color(ACCENT),
|
||
row![lbl("Extension (DIMDLE)"), mk_field(DsField::Dimdle, &app.ds_dimdle)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Spacing (DIMDLI)"), mk_field(DsField::Dimdli, &app.ds_dimdli)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Text gap (DIMGAP)"), mk_field(DsField::Dimgap, &app.ds_dimgap)].spacing(8).align_y(iced::Center),
|
||
chk("Suppress 1st line (DIMSD1)", app.ds_dimsd1, DsField::Dimsd1),
|
||
chk("Suppress 2nd line (DIMSD2)", app.ds_dimsd2, DsField::Dimsd2),
|
||
text("Extension Line").size(11).color(ACCENT),
|
||
row![lbl("Extension (DIMEXE)"), mk_field(DsField::Dimexe, &app.ds_dimexe)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Offset (DIMEXO)"), mk_field(DsField::Dimexo, &app.ds_dimexo)].spacing(8).align_y(iced::Center),
|
||
chk("Suppress 1st line (DIMSE1)", app.ds_dimse1, DsField::Dimse1),
|
||
chk("Suppress 2nd line (DIMSE2)", app.ds_dimse2, DsField::Dimse2),
|
||
].spacing(6).into(),
|
||
|
||
1 => column![
|
||
text("Arrows").size(11).color(ACCENT),
|
||
row![lbl("Arrow size (DIMASZ)"), mk_field(DsField::Dimasz, &app.ds_dimasz)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Center mark (DIMCEN)"), mk_field(DsField::Dimcen, &app.ds_dimcen)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Tick size (DIMTSZ)"), mk_field(DsField::Dimtsz, &app.ds_dimtsz)].spacing(8).align_y(iced::Center),
|
||
].spacing(6).into(),
|
||
|
||
2 => column![
|
||
text("Text").size(11).color(ACCENT),
|
||
row![lbl("Height (DIMTXT)"), mk_field(DsField::Dimtxt, &app.ds_dimtxt)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Style (DIMTXSTY)"), mk_field(DsField::Dimtxsty, &app.ds_dimtxsty)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Vertical pos (DIMTAD)"), mk_field(DsField::Dimtad, &app.ds_dimtad)].spacing(8).align_y(iced::Center),
|
||
chk("Horizontal inside (DIMTIH)", app.ds_dimtih, DsField::Dimtih),
|
||
chk("Horizontal outside (DIMTOH)", app.ds_dimtoh, DsField::Dimtoh),
|
||
].spacing(6).into(),
|
||
|
||
3 => column![
|
||
text("Scale").size(11).color(ACCENT),
|
||
row![lbl("Overall scale (DIMSCALE)"), mk_field(DsField::Dimscale, &app.ds_dimscale)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Linear factor (DIMLFAC)"), mk_field(DsField::Dimlfac, &app.ds_dimlfac)].spacing(8).align_y(iced::Center),
|
||
text("Units").size(11).color(ACCENT),
|
||
row![lbl("Format (DIMLUNIT)"), mk_field(DsField::Dimlunit, &app.ds_dimlunit)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Decimals (DIMDEC)"), mk_field(DsField::Dimdec, &app.ds_dimdec)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Suffix (DIMPOST)"), mk_field(DsField::Dimpost, &app.ds_dimpost)].spacing(8).align_y(iced::Center),
|
||
].spacing(6).into(),
|
||
|
||
_ => column![
|
||
text("Tolerances").size(11).color(ACCENT),
|
||
chk("Generate tolerances (DIMTOL)", app.ds_dimtol, DsField::Dimtol),
|
||
chk("Limits generation (DIMLIM)", app.ds_dimlim, DsField::Dimlim),
|
||
row![lbl("Plus tolerance (DIMTP)"), mk_field(DsField::Dimtp, &app.ds_dimtp)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Minus tolerance (DIMTM)"), mk_field(DsField::Dimtm, &app.ds_dimtm)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Tol. decimals (DIMTDEC)"), mk_field(DsField::Dimtdec, &app.ds_dimtdec)].spacing(8).align_y(iced::Center),
|
||
row![lbl("Tol. scale (DIMTFAC)"), mk_field(DsField::Dimtfac, &app.ds_dimtfac)].spacing(8).align_y(iced::Center),
|
||
].spacing(6).into(),
|
||
};
|
||
|
||
// ── Right panel ───────────────────────────────────────────────────────
|
||
let right_panel = column![
|
||
text(format!("Editing: {selected}")).size(12).color(TEXT_COL),
|
||
tabs,
|
||
container(
|
||
iced::widget::scrollable(
|
||
container(tab_content).padding(8)
|
||
).height(220)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(Fill),
|
||
row![
|
||
button(text("Apply").size(11).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogApply)
|
||
.style(btn_style(true)).padding([5, 16]),
|
||
button(text("Close").size(11).color(TEXT_COL))
|
||
.on_press(Message::DimStyleDialogClose)
|
||
.style(btn_style(false)).padding([5, 12]),
|
||
].spacing(8),
|
||
].spacing(8).width(Fill);
|
||
|
||
// ── Main panel ────────────────────────────────────────────────────────
|
||
let panel = container(
|
||
column![
|
||
row![
|
||
text("Dimension Style Manager").size(14).color(TEXT_COL),
|
||
Space::new().width(Fill),
|
||
button(text("✕").size(12).color(DIM_COL))
|
||
.on_press(Message::DimStyleDialogClose)
|
||
.style(|_: &Theme, _| button::Style {
|
||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||
text_color: DIM_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([2, 6]),
|
||
].align_y(iced::Center),
|
||
row![style_panel, right_panel].spacing(12).align_y(iced::Top),
|
||
].spacing(10).padding(16)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(PANEL_BG)),
|
||
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(Shrink);
|
||
|
||
let catcher = mouse_area(
|
||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||
).on_press(Message::DimStyleDialogClose);
|
||
|
||
let positioned = container(panel)
|
||
.width(Fill).height(Fill)
|
||
.align_x(iced::Alignment::Center)
|
||
.align_y(iced::Alignment::Center);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|
||
|
||
// ── TextStyle Font Browser overlay ─────────────────────────────────────────
|
||
|
||
/// Built-in CXF font file names (relative to assets/fonts/).
|
||
const BUILTIN_FONTS: &[&str] = &[
|
||
"CourierCad.cxf", "Cursive.cxf", "GothGBT.cxf", "GothGRT.cxf", "GothITT.cxf",
|
||
"GreekC.cxf", "GreekS.cxf", "ItalicC.cxf", "ItalicT.cxf",
|
||
"RomanC.cxf", "RomanD.cxf", "RomanS.cxf", "RomanT.cxf",
|
||
"SansND.cxf", "SansNS.cxf", "ScriptC.cxf", "ScriptS.cxf",
|
||
"Standard.cxf", "Unicode.cxf", "SymbolCad.cxf",
|
||
];
|
||
|
||
fn textstyle_overlay<'a>(
|
||
styles: Vec<String>,
|
||
selected: &'a str,
|
||
font_buf: &'a str,
|
||
width_buf: &'a str,
|
||
oblique_buf: &'a str,
|
||
) -> Element<'a, Message> {
|
||
use iced::Length::Shrink;
|
||
|
||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||
|
||
let field_style = |_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
|
||
background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }),
|
||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||
icon: TEXT_COL, placeholder: DIM_COL, value: TEXT_COL, selection: ACCENT,
|
||
};
|
||
|
||
let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match (accent, status) {
|
||
(true, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 },
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
(true, _) => ACCENT,
|
||
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
};
|
||
|
||
// Left: style list.
|
||
let style_items: Vec<Element<'_, Message>> = styles.iter().map(|name| {
|
||
let is_sel = name.as_str() == selected;
|
||
button(text(name.clone()).size(11).color(TEXT_COL))
|
||
.on_press(Message::TextStyleDialogSelect(name.clone()))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (is_sel, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 },
|
||
_ => Color::TRANSPARENT,
|
||
})),
|
||
text_color: TEXT_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([3, 8])
|
||
.width(Fill)
|
||
.into()
|
||
}).collect();
|
||
|
||
let style_panel = container(
|
||
column(style_items).spacing(2)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding(4)
|
||
.width(150)
|
||
.height(280);
|
||
|
||
// Middle: font file list (built-in CXF fonts).
|
||
let font_items: Vec<Element<'_, Message>> = BUILTIN_FONTS.iter().map(|&f| {
|
||
let is_sel = font_buf == f;
|
||
button(text(f).size(10).color(TEXT_COL))
|
||
.on_press(Message::TextStyleFontPick(f.to_string()))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (is_sel, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 },
|
||
_ => Color::TRANSPARENT,
|
||
})),
|
||
text_color: TEXT_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([3, 8])
|
||
.width(Fill)
|
||
.into()
|
||
}).collect();
|
||
|
||
let font_panel = column![
|
||
text("Font File:").size(11).color(DIM_COL),
|
||
container(
|
||
iced::widget::scrollable(column(font_items).spacing(1))
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding(4)
|
||
.width(180)
|
||
.height(160),
|
||
text_input("font file…", font_buf)
|
||
.on_input(|v| Message::TextStyleEdit { field: "font", value: v })
|
||
.style(field_style)
|
||
.size(11)
|
||
.width(180),
|
||
]
|
||
.spacing(4);
|
||
|
||
// Right: properties + preview.
|
||
let props = column![
|
||
text("Properties").size(12).color(ACCENT),
|
||
row![
|
||
text("Width Factor:").size(11).color(DIM_COL).width(110),
|
||
text_input("1.0", width_buf)
|
||
.on_input(|v| Message::TextStyleEdit { field: "width", value: v })
|
||
.style(field_style)
|
||
.size(11)
|
||
.width(80),
|
||
].spacing(6).align_y(iced::Center),
|
||
row![
|
||
text("Oblique (°):").size(11).color(DIM_COL).width(110),
|
||
text_input("0.0", oblique_buf)
|
||
.on_input(|v| Message::TextStyleEdit { field: "oblique", value: v })
|
||
.style(field_style)
|
||
.size(11)
|
||
.width(80),
|
||
].spacing(6).align_y(iced::Center),
|
||
Space::new().height(8),
|
||
text("Preview:").size(11).color(DIM_COL),
|
||
container(
|
||
text("AaBbCc 0123").size(20).color(TEXT_COL)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding(10)
|
||
.width(Fill),
|
||
Space::new().height(Fill),
|
||
row![
|
||
button(text("Apply").size(11))
|
||
.on_press(Message::TextStyleApply)
|
||
.style(btn_style(true))
|
||
.padding([5, 10]),
|
||
button(text("Set Current").size(11))
|
||
.on_press(Message::TextStyleDialogSetCurrent)
|
||
.style(btn_style(false))
|
||
.padding([5, 10]),
|
||
].spacing(6),
|
||
]
|
||
.spacing(8)
|
||
.width(220);
|
||
|
||
let panel = container(
|
||
column![
|
||
row![
|
||
text("Text Style Font Browser").size(13).color(TEXT_COL),
|
||
Space::new().width(Fill),
|
||
row![
|
||
button(text("New").size(11))
|
||
.on_press(Message::TextStyleDialogNew)
|
||
.style(btn_style(false))
|
||
.padding([3, 8]),
|
||
button(text("Delete").size(11))
|
||
.on_press(Message::TextStyleDialogDelete)
|
||
.style(btn_style(false))
|
||
.padding([3, 8]),
|
||
button(text("✕").size(12).color(DIM_COL))
|
||
.on_press(Message::TextStyleDialogClose)
|
||
.style(|_: &Theme, _| button::Style {
|
||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||
text_color: DIM_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([2, 6]),
|
||
].spacing(4),
|
||
].align_y(iced::Center),
|
||
row![style_panel, font_panel, props].spacing(12).align_y(iced::Top),
|
||
].spacing(10).padding(16)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(PANEL_BG)),
|
||
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(Shrink);
|
||
|
||
let catcher = mouse_area(
|
||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||
).on_press(Message::TextStyleDialogClose);
|
||
|
||
let positioned = container(panel)
|
||
.width(Fill).height(Fill)
|
||
.align_x(iced::Alignment::Center)
|
||
.align_y(iced::Alignment::Center);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|
||
|
||
// ── TableStyle Dialog overlay ───────────────────────────────────────────────
|
||
|
||
fn tablestyle_overlay<'a>(
|
||
styles: Vec<String>,
|
||
selected: &'a str,
|
||
selected_style: Option<&'a acadrust::objects::TableStyle>,
|
||
) -> Element<'a, Message> {
|
||
use iced::Length::Shrink;
|
||
|
||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||
|
||
let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match (accent, status) {
|
||
(true, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 },
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
(true, _) => ACCENT,
|
||
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
};
|
||
|
||
// Style list.
|
||
let style_items: Vec<Element<'_, Message>> = styles.iter().map(|name| {
|
||
let is_sel = name.as_str() == selected;
|
||
button(text(name.clone()).size(11).color(TEXT_COL))
|
||
.on_press(Message::TableStyleDialogSelect(name.clone()))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (is_sel, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 },
|
||
_ => Color::TRANSPARENT,
|
||
})),
|
||
text_color: TEXT_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([3, 8])
|
||
.width(Fill)
|
||
.into()
|
||
}).collect();
|
||
|
||
let style_panel = container(
|
||
column(style_items).spacing(2)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding(4)
|
||
.width(160)
|
||
.height(240);
|
||
|
||
// Right panel: details.
|
||
let details: Element<'_, Message> = if let Some(s) = selected_style {
|
||
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
|
||
row![
|
||
text(label).size(11).color(DIM_COL).width(140),
|
||
text(val).size(11).color(TEXT_COL),
|
||
].spacing(8).align_y(iced::Center).into()
|
||
};
|
||
let row_info = |row_label: &'static str, rs: &acadrust::objects::RowCellStyle| -> Element<'_, Message> {
|
||
column![
|
||
text(row_label).size(11).color(ACCENT),
|
||
info_row(" Text Style:", rs.text_style_name.clone()),
|
||
info_row(" Text Height:", format!("{:.4}", rs.text_height)),
|
||
info_row(" Alignment:", format!("{:?}", rs.alignment)),
|
||
].spacing(3).into()
|
||
};
|
||
column![
|
||
info_row("Name:", s.name.clone()),
|
||
info_row("H Margin:", format!("{:.4}", s.horizontal_margin)),
|
||
info_row("V Margin:", format!("{:.4}", s.vertical_margin)),
|
||
info_row("Title Suppressed:", s.title_suppressed.to_string()),
|
||
info_row("Header Suppressed:", s.header_suppressed.to_string()),
|
||
row_info("Data Row:", &s.data_row_style),
|
||
row_info("Header Row:", &s.header_row_style),
|
||
row_info("Title Row:", &s.title_row_style),
|
||
].spacing(5).into()
|
||
} else {
|
||
text("No style selected.").size(11).color(DIM_COL).into()
|
||
};
|
||
|
||
let right_panel = column![
|
||
details,
|
||
Space::new().height(Fill),
|
||
row![
|
||
button(text("New").size(11))
|
||
.on_press(Message::TableStyleDialogNew)
|
||
.style(btn_style(true))
|
||
.padding([5, 10]),
|
||
button(text("Delete").size(11))
|
||
.on_press(Message::TableStyleDialogDelete)
|
||
.style(btn_style(false))
|
||
.padding([5, 10]),
|
||
].spacing(6),
|
||
]
|
||
.spacing(10)
|
||
.width(280)
|
||
.height(240);
|
||
|
||
let panel = container(
|
||
column![
|
||
row![
|
||
text("Table Style Manager").size(13).color(TEXT_COL),
|
||
Space::new().width(Fill),
|
||
button(text("✕").size(12).color(DIM_COL))
|
||
.on_press(Message::TableStyleDialogClose)
|
||
.style(|_: &Theme, _| button::Style {
|
||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||
text_color: DIM_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([2, 6]),
|
||
].align_y(iced::Center),
|
||
row![style_panel, right_panel].spacing(12).align_y(iced::Top),
|
||
].spacing(10).padding(16)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(PANEL_BG)),
|
||
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(Shrink);
|
||
|
||
let catcher = mouse_area(
|
||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||
).on_press(Message::TableStyleDialogClose);
|
||
|
||
let positioned = container(panel)
|
||
.width(Fill).height(Fill)
|
||
.align_x(iced::Alignment::Center)
|
||
.align_y(iced::Alignment::Center);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|
||
|
||
// ── MLineStyle Dialog overlay ───────────────────────────────────────────────
|
||
|
||
fn mlstyle_overlay<'a>(
|
||
styles: Vec<String>,
|
||
selected: &'a str,
|
||
selected_style: Option<&'a acadrust::objects::MLineStyle>,
|
||
current_style: String,
|
||
) -> Element<'a, Message> {
|
||
use iced::Length::Shrink;
|
||
|
||
const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||
const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||
const ACTIVE_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||
|
||
let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style {
|
||
background: Some(Background::Color(match (accent, status) {
|
||
(true, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 },
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 },
|
||
(true, _) => ACCENT,
|
||
_ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 },
|
||
})),
|
||
text_color: TEXT_COL,
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
};
|
||
|
||
// Style list.
|
||
let style_items: Vec<Element<'_, Message>> = styles.iter().map(|name| {
|
||
let is_sel = name.as_str() == selected;
|
||
let is_cur = *name == current_style;
|
||
let label = if is_cur {
|
||
format!("{} ◀", name)
|
||
} else {
|
||
name.clone()
|
||
};
|
||
button(text(label).size(11).color(TEXT_COL))
|
||
.on_press(Message::MlStyleDialogSelect(name.clone()))
|
||
.style(move |_: &Theme, st| button::Style {
|
||
background: Some(Background::Color(match (is_sel, st) {
|
||
(true, _) => ACTIVE_BG,
|
||
(false, button::Status::Hovered | button::Status::Pressed) =>
|
||
Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 },
|
||
_ => Color::TRANSPARENT,
|
||
})),
|
||
text_color: TEXT_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([3, 8])
|
||
.width(Fill)
|
||
.into()
|
||
}).collect();
|
||
|
||
let style_panel = container(
|
||
column(style_items).spacing(2)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||
..Default::default()
|
||
})
|
||
.padding(4)
|
||
.width(160)
|
||
.height(240);
|
||
|
||
// Right panel: details for selected style.
|
||
let details: Element<'_, Message> = if let Some(s) = selected_style {
|
||
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
|
||
row![
|
||
text(label).size(11).color(DIM_COL).width(110),
|
||
text(val).size(11).color(TEXT_COL),
|
||
].spacing(8).align_y(iced::Center).into()
|
||
};
|
||
let elem_rows: Vec<Element<'_, Message>> = s.elements.iter().enumerate().map(|(idx, e)| {
|
||
let color_str: String = match &e.color {
|
||
acadrust::types::Color::ByLayer => "ByLayer".into(),
|
||
acadrust::types::Color::ByBlock => "ByBlock".into(),
|
||
acadrust::types::Color::Index(i) => format!("ACI {}", i),
|
||
acadrust::types::Color::Rgb { r, g, b } => format!("#{:02X}{:02X}{:02X}", r, g, b),
|
||
};
|
||
let lt = if e.linetype.is_empty() { "ByLayer" } else { e.linetype.as_str() };
|
||
row![
|
||
text(format!(" {}:", idx)).size(10).color(DIM_COL).width(20),
|
||
text(format!("{:+.3}", e.offset)).size(10).color(TEXT_COL).width(60),
|
||
text(color_str).size(10).color(TEXT_COL).width(80),
|
||
text(lt).size(10).color(TEXT_COL),
|
||
].spacing(4).align_y(iced::Center).into()
|
||
}).collect();
|
||
|
||
let mut col_items: Vec<Element<'_, Message>> = vec![
|
||
info_row("Name:", s.name.clone()),
|
||
info_row("Elements:", s.elements.len().to_string()),
|
||
text(" Off Color Ltype").size(10).color(DIM_COL).into(),
|
||
];
|
||
col_items.extend(elem_rows);
|
||
column(col_items).spacing(6).into()
|
||
} else {
|
||
text("No style selected.").size(11).color(DIM_COL).into()
|
||
};
|
||
|
||
let right_panel = column![
|
||
details,
|
||
Space::new().height(Fill),
|
||
row![
|
||
button(text("Set Current").size(11))
|
||
.on_press(Message::MlStyleDialogSetCurrent)
|
||
.style(btn_style(true))
|
||
.padding([5, 10]),
|
||
button(text("New").size(11))
|
||
.on_press(Message::MlStyleDialogNew)
|
||
.style(btn_style(false))
|
||
.padding([5, 10]),
|
||
button(text("Delete").size(11))
|
||
.on_press(Message::MlStyleDialogDelete)
|
||
.style(btn_style(false))
|
||
.padding([5, 10]),
|
||
].spacing(6),
|
||
]
|
||
.spacing(10)
|
||
.width(280)
|
||
.height(240);
|
||
|
||
let panel = container(
|
||
column![
|
||
row![
|
||
text("Multiline Style Manager").size(13).color(TEXT_COL),
|
||
Space::new().width(Fill),
|
||
button(text("✕").size(12).color(DIM_COL))
|
||
.on_press(Message::MlStyleDialogClose)
|
||
.style(|_: &Theme, _| button::Style {
|
||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||
text_color: DIM_COL,
|
||
..Default::default()
|
||
})
|
||
.padding([2, 6]),
|
||
].align_y(iced::Center),
|
||
row![style_panel, right_panel].spacing(12).align_y(iced::Top),
|
||
].spacing(10).padding(16)
|
||
)
|
||
.style(|_: &Theme| container::Style {
|
||
background: Some(Background::Color(PANEL_BG)),
|
||
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
|
||
..Default::default()
|
||
})
|
||
.width(Shrink);
|
||
|
||
let catcher = mouse_area(
|
||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||
).on_press(Message::MlStyleDialogClose);
|
||
|
||
let positioned = container(panel)
|
||
.width(Fill).height(Fill)
|
||
.align_x(iced::Alignment::Center)
|
||
.align_y(iced::Alignment::Center);
|
||
|
||
stack![catcher, positioned].into()
|
||
}
|