forked from 1b-agent/opencadstudio-upstream
Refactor src/main.rs into modular src/app/ structure
Split the 4500-line main.rs into focused submodules under src/app/: - mod.rs: H7CAD struct, Message enum, boot/new/run - document.rs: DocumentTab, HistorySnapshot, HistoryState - helpers.rs: pure helper functions - history.rs: undo/redo logic - properties.rs: properties panel logic - layers.rs: layer sync - commands.rs: dispatch_command - cmd_result.rs: apply_cmd_result - view.rs: view, subscription, tab bar - update.rs: update message handler Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
854f07517e
commit
ed9cbc2092
19 files changed
+4150
-4519
No files matched your search
+369
@@ -0,0 +1,369 @@
|
||||
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, Row};
|
||||
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()
|
||||
)
|
||||
]
|
||||
.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());
|
||||
|
||||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_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()
|
||||
}
|
||||
Reference in new issue
Block a user