cad-editor/src/app/view.rs
Hakan Seven 1572d0c38f Feat: Phase 1.5 — viewport count in status bar + REVCLOUD command
Status bar: shows "N VP" pill (with tooltip) when in a paper layout that has
user viewports (id > 1). New Scene::viewport_count() method.

REVCLOUD: closed LwPolyline with arc bumps (bulge = 0.5) along each edge.
Click polygon corners, Enter to close into a revision cloud shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 23:52:35 +03:00

710 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 grid = if self.show_grid {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
let cam = tab.scene.camera.borrow();
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
Some(overlay::GridParams {
view_proj: cam.view_proj(bounds),
bounds,
plane,
})
} else {
None
};
overlay::selection_overlay(sel, snap_info, grips, grid)
};
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 {
Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 }
} else {
Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 }
};
let 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);
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.show_grid,
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,
)
} 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].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::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::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 ────────────────────────────────────────────
/// 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,
) -> 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(),
// 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()
}