forked from 1b-agent/opencadstudio-upstream
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
2baabdf4a0
commit
bddc050b3e
39 files changed
+26674
-26113
No files matched your search
@@ -0,0 +1,191 @@
|
||||
use super::*;
|
||||
use super::super::document::{DynComponent, DynFieldEntry};
|
||||
use super::super::Message;
|
||||
use iced::widget::{
|
||||
button, container, row,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Theme};
|
||||
|
||||
pub(super) fn viewport_controls<'a>(
|
||||
render_mode: acadrust::entities::ViewportRenderMode,
|
||||
show_grid: bool,
|
||||
snap_on: bool,
|
||||
include_split: bool,
|
||||
tile_count: usize,
|
||||
) -> Element<'a, Message> {
|
||||
use acadrust::entities::ViewportRenderMode as M;
|
||||
let render_modes: Vec<RenderModeChoice> = vec![
|
||||
RenderModeChoice(M::Wireframe2D),
|
||||
RenderModeChoice(M::Wireframe3D),
|
||||
RenderModeChoice(M::HiddenLine),
|
||||
RenderModeChoice(M::FlatShaded),
|
||||
RenderModeChoice(M::GouraudShaded),
|
||||
RenderModeChoice(M::FlatShadedWithEdges),
|
||||
RenderModeChoice(M::GouraudShadedWithEdges),
|
||||
];
|
||||
let light = Color { r: 0.85, g: 0.85, b: 0.85, a: 1.0 };
|
||||
let accent = Color { r: 0.45, g: 0.70, b: 1.0, a: 1.0 };
|
||||
|
||||
// Borderless icon button; an `active` toggle gets an accent tint + fill.
|
||||
let icon_btn = move |bytes: &'static [u8], active: bool, msg: Message| {
|
||||
let tint = if active { accent } else { light };
|
||||
button(crate::ui::icons::tinted(bytes, 15.0, tint))
|
||||
.on_press(msg)
|
||||
.padding([4, 6])
|
||||
.style(move |_: &Theme, status| iced::widget::button::Style {
|
||||
background: Some(Background::Color(match (active, status) {
|
||||
(_, iced::widget::button::Status::Hovered) => Color {
|
||||
r: 0.25,
|
||||
g: 0.25,
|
||||
b: 0.25,
|
||||
a: 0.9,
|
||||
},
|
||||
(true, _) => Color {
|
||||
r: 0.16,
|
||||
g: 0.22,
|
||||
b: 0.32,
|
||||
a: 0.9,
|
||||
},
|
||||
(false, _) => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
text_color: tint,
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
|
||||
// Render-mode picker, restyled borderless so the outer chip frames it.
|
||||
let picker = iced::widget::pick_list(
|
||||
render_modes,
|
||||
Some(RenderModeChoice(render_mode)),
|
||||
|c| Message::SetRenderMode(c.0),
|
||||
)
|
||||
.text_size(11)
|
||||
.padding([4, 6])
|
||||
.style(move |_: &Theme, _| iced::widget::pick_list::Style {
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
text_color: light,
|
||||
placeholder_color: light,
|
||||
handle_color: light,
|
||||
});
|
||||
|
||||
// Thin vertical divider between control groups.
|
||||
let sep = || {
|
||||
container(iced::widget::Space::new().width(1.0).height(16.0)).style(|_: &Theme| {
|
||||
iced::widget::container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
r: 0.45,
|
||||
g: 0.45,
|
||||
b: 0.45,
|
||||
a: 0.7,
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let mut bar = row![]
|
||||
.spacing(3)
|
||||
.align_y(iced::alignment::Vertical::Center);
|
||||
bar = bar
|
||||
.push(icon_btn(crate::ui::icons::GRID, show_grid, Message::ToggleGrid))
|
||||
.push(sep())
|
||||
.push(icon_btn(crate::ui::icons::SNAP, snap_on, Message::ToggleGridSnap))
|
||||
.push(sep())
|
||||
.push(picker);
|
||||
if include_split {
|
||||
bar = bar
|
||||
.push(sep())
|
||||
.push(icon_btn(crate::ui::icons::SPLIT_V, false, Message::SplitModelViewport(false)))
|
||||
.push(sep())
|
||||
.push(icon_btn(crate::ui::icons::SPLIT_H, false, Message::SplitModelViewport(true)));
|
||||
// Close button: only meaningful with more than one model tile.
|
||||
if tile_count > 1 {
|
||||
bar = bar
|
||||
.push(sep())
|
||||
.push(icon_btn(crate::ui::icons::CLOSE, false, Message::CloseModelViewport));
|
||||
}
|
||||
}
|
||||
|
||||
container(bar)
|
||||
.padding(2)
|
||||
.style(|_: &Theme| iced::widget::container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
r: 0.10,
|
||||
g: 0.10,
|
||||
b: 0.10,
|
||||
a: 0.75,
|
||||
})),
|
||||
border: Border {
|
||||
color: Color {
|
||||
r: 0.35,
|
||||
g: 0.35,
|
||||
b: 0.35,
|
||||
a: 1.0,
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Dynamic-input field formatting ─────────────────────────────────────────
|
||||
|
||||
/// Short prefix shown before a dynamic-input box's value.
|
||||
/// The string shown inside a dynamic-input box: the typed buffer when the
|
||||
/// field is locked, otherwise the live value derived from the cursor
|
||||
/// world position (and the base point for polar quantities).
|
||||
pub(super) fn dyn_component_value(
|
||||
f: &DynFieldEntry,
|
||||
w: glam::Vec3,
|
||||
base: Option<glam::Vec3>,
|
||||
xf: &super::super::helpers::UcsXform,
|
||||
) -> String {
|
||||
if let Some(b) = &f.buffer {
|
||||
return b.clone();
|
||||
}
|
||||
let b = base.unwrap_or(glam::Vec3::ZERO);
|
||||
// Relative deltas and the polar angle read in the active UCS plane. The
|
||||
// delta is offset-invariant, so only the axis rotation matters (identity
|
||||
// xf reproduces the world-frame deltas).
|
||||
let d = xf.vec_to_ucs(w - b);
|
||||
let dx = d.x as f64;
|
||||
let dy = d.y as f64;
|
||||
// When a base point exists (DYN-on after the first pick) the cartesian
|
||||
// fields show relative deltas — matching the typed-value convention
|
||||
// in `dyn_resolve_point` so the live preview and the committed
|
||||
// coordinate use the same frame. See #35.
|
||||
let has_base = base.is_some();
|
||||
// Width / Height read as unsigned magnitudes (the sign is taken from the
|
||||
// cursor side on commit), matching the rectangle's two-edge entry.
|
||||
let wh = matches!(f.role, crate::command::DynRole::Width | crate::command::DynRole::Height);
|
||||
match f.component {
|
||||
DynComponent::X if has_base => format!("{:.4}", if wh { dx.abs() } else { dx }),
|
||||
DynComponent::Y if has_base => format!("{:.4}", if wh { dy.abs() } else { dy }),
|
||||
DynComponent::Z if has_base => "0.0000".to_string(),
|
||||
DynComponent::X => format!("{:.4}", w.x),
|
||||
DynComponent::Y => format!("{:.4}", w.y),
|
||||
DynComponent::Z => format!("{:.4}", b.z),
|
||||
// Scaled by the role so a diameter box reads twice the radius.
|
||||
DynComponent::Distance => {
|
||||
format!("{:.4}", (dx * dx + dy * dy).sqrt() * f.role.value_scale() as f64)
|
||||
}
|
||||
// Shared rule: unsigned magnitude of the short angle, so CW (below the
|
||||
// reference axis) reads positive (e.g. 30°, not -30°/330°). The
|
||||
// committed value stays signed (see dyn_resolve_point).
|
||||
DynComponent::Angle => {
|
||||
format!("{:.1}", crate::command::dyn_display_angle_deg(dy.atan2(dx) as f32))
|
||||
}
|
||||
// Typed-only scalar — no geometric value to track when empty.
|
||||
DynComponent::Scalar => String::new(),
|
||||
}
|
||||
}
|
||||
+2091
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,985 @@
|
||||
use super::super::{Message, OpenCADStudio};
|
||||
use iced::widget::{
|
||||
button, column, container, mouse_area, pick_list, row, text, text_input,
|
||||
Space,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Fill, Theme};
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Build the currently-open modal dialog's content (Plan B), or `None`.
|
||||
/// Each former pop-up window is constructed here and given a bounded size
|
||||
/// (About shrinks to its content). Rendered as an overlay by `view_main`.
|
||||
pub(super) fn modal_content(&self) -> Option<Element<'_, Message>> {
|
||||
fn sized<'a>(e: Element<'a, Message>, w: u16, h: u16) -> Element<'a, Message> {
|
||||
iced::widget::container(e)
|
||||
.width(iced::Length::Fixed(w as f32))
|
||||
.height(iced::Length::Fixed(h as f32))
|
||||
.into()
|
||||
}
|
||||
Some(match self.active_modal? {
|
||||
super::super::ModalKind::About => crate::ui::about::view_window(),
|
||||
super::super::ModalKind::Shortcuts => {
|
||||
sized(crate::ui::shortcuts::view_window(&self.shortcut_overrides), 720, 520)
|
||||
}
|
||||
super::super::ModalKind::PluginManager => sized(
|
||||
crate::ui::plugin_manager::view_window(
|
||||
&self.disabled_plugins,
|
||||
&self.external_plugins,
|
||||
&self.loaded_plugin_ids,
|
||||
crate::ui::plugin_manager::MarketView {
|
||||
registry: &self.plugin_registry,
|
||||
input: &self.plugin_repo_input,
|
||||
repos: &self.plugin_repos,
|
||||
release_tags: &self.repo_release_tags,
|
||||
selected_tag: &self.repo_selected_tag,
|
||||
status: &self.marketplace_status,
|
||||
},
|
||||
),
|
||||
520,
|
||||
460,
|
||||
),
|
||||
super::super::ModalKind::UpdateNotice => {
|
||||
let latest = self.update_notice_version.as_deref().unwrap_or("?");
|
||||
let body = self.update_notice_body.as_deref().unwrap_or("");
|
||||
sized(crate::ui::update_notice::view_window(latest, body), 560, 460)
|
||||
}
|
||||
super::super::ModalKind::Layers => {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
sized(tab.layers.view_window(), 900, 360)
|
||||
}
|
||||
super::super::ModalKind::PageSetup => sized(
|
||||
crate::ui::page_setup::view_window(
|
||||
&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,
|
||||
),
|
||||
520,
|
||||
460,
|
||||
),
|
||||
super::super::ModalKind::LayoutManager => {
|
||||
let i = self.active_tab;
|
||||
let layouts = self.tabs[i].scene.layout_names();
|
||||
let current = self.tabs[i].scene.current_layout.clone();
|
||||
sized(
|
||||
crate::ui::layout_manager::view_window(
|
||||
layouts,
|
||||
&self.layout_manager_selected,
|
||||
&self.layout_manager_rename_buf,
|
||||
current,
|
||||
),
|
||||
640,
|
||||
320,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::Plotstyle => sized(
|
||||
crate::ui::plotstyle::view_window(
|
||||
self.active_plot_style.as_ref(),
|
||||
self.plotstyle_panel_aci,
|
||||
&self.ps_color_buf,
|
||||
&self.ps_lineweight_buf,
|
||||
&self.ps_screening_buf,
|
||||
),
|
||||
780,
|
||||
540,
|
||||
),
|
||||
super::super::ModalKind::TextStyle => {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
let (backward, upside_down, annotative) = tab
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.get(&self.textstyle_selected)
|
||||
.map(|s| (s.flags.backward, s.flags.upside_down, s.annotative))
|
||||
.unwrap_or((false, false, false));
|
||||
sized(
|
||||
crate::ui::textstyle::view_window(crate::ui::textstyle::TextStyleView {
|
||||
styles,
|
||||
selected: &self.textstyle_selected,
|
||||
current: &tab.scene.document.header.current_text_style_name,
|
||||
font_buf: &self.textstyle_font,
|
||||
width_buf: &self.textstyle_width,
|
||||
oblique_buf: &self.textstyle_oblique,
|
||||
height_buf: &self.textstyle_height,
|
||||
bigfont_buf: &self.textstyle_bigfont,
|
||||
ttf_buf: &self.textstyle_ttf,
|
||||
backward,
|
||||
upside_down,
|
||||
annotative,
|
||||
rename_active: self.style_rename.as_deref(),
|
||||
rename_buf: &self.style_rename_buf,
|
||||
}),
|
||||
// Wider than the old 620 window: the TTF system-font panel
|
||||
// (Plan B / web fonts) added a column.
|
||||
860,
|
||||
480,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::MlStyle => {
|
||||
use acadrust::objects::ObjectType;
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| match o {
|
||||
ObjectType::MLineStyle(s) => Some(s.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
||||
ObjectType::MLineStyle(s) if s.name == self.mlstyle_selected => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
sized(
|
||||
crate::ui::mlstyle::view_window(
|
||||
styles,
|
||||
&self.mlstyle_selected,
|
||||
selected_style,
|
||||
tab.scene.document.header.multiline_style.clone(),
|
||||
self.style_rename.as_deref(),
|
||||
&self.style_rename_buf,
|
||||
),
|
||||
620,
|
||||
420,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::TableStyle => {
|
||||
use acadrust::objects::ObjectType;
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| match o {
|
||||
ObjectType::TableStyle(s) => Some(s.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
||||
ObjectType::TableStyle(s) if s.name == self.tablestyle_selected => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
sized(
|
||||
crate::ui::tablestyle::view_window(
|
||||
styles,
|
||||
&self.tablestyle_selected,
|
||||
&self.ribbon.active_table_style,
|
||||
selected_style,
|
||||
&self.ts_hmargin,
|
||||
&self.ts_vmargin,
|
||||
&self.ts_description,
|
||||
&self.ts_cell_textstyle,
|
||||
&self.ts_cell_height,
|
||||
&self.ts_cell_textcolor,
|
||||
&self.ts_cell_fillcolor,
|
||||
&self.ts_cell_datatype,
|
||||
&self.ts_cell_unittype,
|
||||
&self.ts_cell_format,
|
||||
&self.ts_border_lw,
|
||||
&self.ts_border_color,
|
||||
&self.ts_border_spacing,
|
||||
self.style_rename.as_deref(),
|
||||
&self.style_rename_buf,
|
||||
self.ts_color_open,
|
||||
),
|
||||
620,
|
||||
420,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::MLeaderStyle => {
|
||||
use acadrust::objects::ObjectType;
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.filter_map(|o| match o {
|
||||
ObjectType::MultiLeaderStyle(s) => Some(s.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
|
||||
ObjectType::MultiLeaderStyle(s) if s.name == self.mleaderstyle_selected => {
|
||||
Some(s)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let doc = &tab.scene.document;
|
||||
let mut block_opts: Vec<String> = vec!["None".to_string()];
|
||||
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
||||
let mut lt_opts: Vec<String> = vec!["None".to_string()];
|
||||
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
||||
let mut textstyle_opts: Vec<String> = vec!["None".to_string()];
|
||||
textstyle_opts.extend(doc.text_styles.iter().map(|t| t.name.clone()));
|
||||
let opt_block = |h: Option<acadrust::types::Handle>| -> String {
|
||||
match h {
|
||||
Some(h) => doc
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|b| b.handle == h)
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_else(|| "None".to_string()),
|
||||
None => "None".to_string(),
|
||||
}
|
||||
};
|
||||
let opt_lt = |h: Option<acadrust::types::Handle>| -> String {
|
||||
match h {
|
||||
Some(h) => doc
|
||||
.line_types
|
||||
.iter()
|
||||
.find(|lt| lt.handle == h)
|
||||
.map(|lt| lt.name.clone())
|
||||
.unwrap_or_else(|| "None".to_string()),
|
||||
None => "None".to_string(),
|
||||
}
|
||||
};
|
||||
let opt_ts = |h: Option<acadrust::types::Handle>| -> String {
|
||||
match h {
|
||||
Some(h) => doc
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|t| t.handle == h)
|
||||
.map(|t| t.name.clone())
|
||||
.unwrap_or_else(|| "None".to_string()),
|
||||
None => "None".to_string(),
|
||||
}
|
||||
};
|
||||
let (line_type_name, arrowhead_name, text_style_name, block_content_name) =
|
||||
match selected_style {
|
||||
Some(s) => (
|
||||
opt_lt(s.line_type_handle),
|
||||
opt_block(s.arrowhead_handle),
|
||||
opt_ts(s.text_style_handle),
|
||||
opt_block(s.block_content_handle),
|
||||
),
|
||||
None => Default::default(),
|
||||
};
|
||||
sized(
|
||||
crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView {
|
||||
styles,
|
||||
selected: &self.mleaderstyle_selected,
|
||||
style: selected_style,
|
||||
current: tab.active_mleader_style.clone(),
|
||||
landing_distance: &self.mls_landing_distance,
|
||||
landing_gap: &self.mls_landing_gap,
|
||||
arrowhead_size: &self.mls_arrowhead_size,
|
||||
text_height: &self.mls_text_height,
|
||||
scale_factor: &self.mls_scale_factor,
|
||||
break_gap: &self.mls_break_gap,
|
||||
first_seg_angle: &self.mls_first_seg_angle,
|
||||
second_seg_angle: &self.mls_second_seg_angle,
|
||||
max_points: &self.mls_max_points,
|
||||
default_text: &self.mls_default_text,
|
||||
line_color: &self.mls_line_color,
|
||||
text_color: &self.mls_text_color,
|
||||
description: &self.mls_description,
|
||||
line_weight: &self.mls_line_weight,
|
||||
align_space: &self.mls_align_space,
|
||||
block_color: &self.mls_block_color,
|
||||
block_rotation: &self.mls_block_rotation,
|
||||
block_scale_x: &self.mls_block_scale_x,
|
||||
block_scale_y: &self.mls_block_scale_y,
|
||||
block_scale_z: &self.mls_block_scale_z,
|
||||
block_opts,
|
||||
lt_opts,
|
||||
textstyle_opts,
|
||||
line_type_name,
|
||||
arrowhead_name,
|
||||
text_style_name,
|
||||
block_content_name,
|
||||
rename_active: self.style_rename.as_deref(),
|
||||
rename_buf: &self.style_rename_buf,
|
||||
color_open: self.mls_color_open,
|
||||
}),
|
||||
560,
|
||||
560,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::DimStyle => {
|
||||
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
let doc = &tab.scene.document;
|
||||
// Dropdown options (names must match the records exactly so the
|
||||
// selection can be resolved back to a handle on the update side).
|
||||
let mut block_opts: Vec<String> = vec!["Default".to_string()];
|
||||
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
|
||||
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
|
||||
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
|
||||
let blk_name = |h: acadrust::types::Handle| -> String {
|
||||
if h.is_null() {
|
||||
"Default".to_string()
|
||||
} else {
|
||||
doc.block_records
|
||||
.iter()
|
||||
.find(|b| b.handle == h)
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_else(|| "Default".to_string())
|
||||
}
|
||||
};
|
||||
let lt_name = |h: acadrust::types::Handle| -> String {
|
||||
if h.is_null() {
|
||||
"ByBlock".to_string()
|
||||
} else {
|
||||
doc.line_types
|
||||
.iter()
|
||||
.find(|lt| lt.handle == h)
|
||||
.map(|lt| lt.name.clone())
|
||||
.unwrap_or_else(|| "ByBlock".to_string())
|
||||
}
|
||||
};
|
||||
let ds_sel = doc.dim_styles.get(&self.dimstyle_selected);
|
||||
let (
|
||||
dimblk_name,
|
||||
dimblk1_name,
|
||||
dimblk2_name,
|
||||
dimldrblk_name,
|
||||
dimltex_name,
|
||||
dimltex1_name,
|
||||
dimltex2_name,
|
||||
) = match ds_sel {
|
||||
Some(d) => (
|
||||
blk_name(d.dimblk),
|
||||
blk_name(d.dimblk1),
|
||||
blk_name(d.dimblk2),
|
||||
blk_name(d.dimldrblk),
|
||||
lt_name(d.dimltex_handle),
|
||||
lt_name(d.dimltex1_handle),
|
||||
lt_name(d.dimltex2_handle),
|
||||
),
|
||||
None => Default::default(),
|
||||
};
|
||||
sized(crate::ui::dimstyle::view_window(
|
||||
styles,
|
||||
&self.dimstyle_selected,
|
||||
&self.tabs[self.active_tab]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_dimstyle_name,
|
||||
self.dimstyle_tab,
|
||||
crate::ui::dimstyle::DimStyleValues {
|
||||
dimdle: &self.ds_dimdle,
|
||||
dimdli: &self.ds_dimdli,
|
||||
dimgap: &self.ds_dimgap,
|
||||
dimexe: &self.ds_dimexe,
|
||||
dimexo: &self.ds_dimexo,
|
||||
dimsd1: self.ds_dimsd1,
|
||||
dimsd2: self.ds_dimsd2,
|
||||
dimse1: self.ds_dimse1,
|
||||
dimse2: self.ds_dimse2,
|
||||
dimasz: &self.ds_dimasz,
|
||||
dimcen: &self.ds_dimcen,
|
||||
dimtsz: &self.ds_dimtsz,
|
||||
dimtxt: &self.ds_dimtxt,
|
||||
dimtxsty: &self.ds_dimtxsty,
|
||||
dimtad: &self.ds_dimtad,
|
||||
dimtih: self.ds_dimtih,
|
||||
dimtoh: self.ds_dimtoh,
|
||||
dimscale: &self.ds_dimscale,
|
||||
dimlfac: &self.ds_dimlfac,
|
||||
dimlunit: &self.ds_dimlunit,
|
||||
dimdec: &self.ds_dimdec,
|
||||
dimpost: &self.ds_dimpost,
|
||||
dimtol: self.ds_dimtol,
|
||||
dimlim: self.ds_dimlim,
|
||||
dimtp: &self.ds_dimtp,
|
||||
dimtm: &self.ds_dimtm,
|
||||
dimtdec: &self.ds_dimtdec,
|
||||
dimtfac: &self.ds_dimtfac,
|
||||
annotative: self.ds_annotative,
|
||||
dimclrd: &self.ds_dimclrd,
|
||||
dimlwd: &self.ds_dimlwd,
|
||||
dimclre: &self.ds_dimclre,
|
||||
dimlwe: &self.ds_dimlwe,
|
||||
dimfxl: &self.ds_dimfxl,
|
||||
dimfxlon: self.ds_dimfxlon,
|
||||
dimsah: self.ds_dimsah,
|
||||
dimarcsym: &self.ds_dimarcsym,
|
||||
dimjogang: &self.ds_dimjogang,
|
||||
dimclrt: &self.ds_dimclrt,
|
||||
dimjust: &self.ds_dimjust,
|
||||
dimtvp: &self.ds_dimtvp,
|
||||
dimtfill: &self.ds_dimtfill,
|
||||
dimtfillclr: &self.ds_dimtfillclr,
|
||||
dimtxtdirection: self.ds_dimtxtdirection,
|
||||
dimatfit: &self.ds_dimatfit,
|
||||
dimtix: self.ds_dimtix,
|
||||
dimsoxd: self.ds_dimsoxd,
|
||||
dimtmove: &self.ds_dimtmove,
|
||||
dimupt: self.ds_dimupt,
|
||||
dimtofl: self.ds_dimtofl,
|
||||
dimfit: &self.ds_dimfit,
|
||||
dimdsep: &self.ds_dimdsep,
|
||||
dimrnd: &self.ds_dimrnd,
|
||||
dimzin: &self.ds_dimzin,
|
||||
dimfrac: &self.ds_dimfrac,
|
||||
dimaunit: &self.ds_dimaunit,
|
||||
dimadec: &self.ds_dimadec,
|
||||
dimunit: &self.ds_dimunit,
|
||||
dimazin: &self.ds_dimazin,
|
||||
dimalt: self.ds_dimalt,
|
||||
dimaltf: &self.ds_dimaltf,
|
||||
dimaltd: &self.ds_dimaltd,
|
||||
dimaltu: &self.ds_dimaltu,
|
||||
dimalttd: &self.ds_dimalttd,
|
||||
dimaltrnd: &self.ds_dimaltrnd,
|
||||
dimapost: &self.ds_dimapost,
|
||||
dimaltz: &self.ds_dimaltz,
|
||||
dimalttz: &self.ds_dimalttz,
|
||||
dimtolj: &self.ds_dimtolj,
|
||||
dimtzin: &self.ds_dimtzin,
|
||||
dimblk_name,
|
||||
dimblk1_name,
|
||||
dimblk2_name,
|
||||
dimldrblk_name,
|
||||
dimltex_name,
|
||||
dimltex1_name,
|
||||
dimltex2_name,
|
||||
block_opts,
|
||||
lt_opts,
|
||||
color_open: self.ds_color_open.clone(),
|
||||
},
|
||||
self.style_rename.as_deref(),
|
||||
&self.style_rename_buf,
|
||||
), 720, 560)
|
||||
}
|
||||
super::super::ModalKind::AssocPrompt => sized(default_assoc_dialog_window(), 440, 210),
|
||||
super::super::ModalKind::Unsaved => {
|
||||
let tab_name = match &self.pending_close {
|
||||
Some(super::super::PendingClose::Tab(idx)) => self
|
||||
.tabs
|
||||
.get(*idx)
|
||||
.map(|t| t.tab_display_name())
|
||||
.unwrap_or_default(),
|
||||
Some(super::super::PendingClose::Quit) => self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|t| t.dirty)
|
||||
.map(|t| t.tab_display_name())
|
||||
.unwrap_or_default(),
|
||||
None => String::new(),
|
||||
};
|
||||
sized(unsaved_changes_dialog_window(&tab_name), 420, 160)
|
||||
}
|
||||
super::super::ModalKind::PointStyle => sized(
|
||||
crate::ui::point_style::view_window(
|
||||
self.tabs[self.active_tab].scene.document.header.point_display_mode,
|
||||
self.point_size_relative,
|
||||
&self.point_size_buf,
|
||||
),
|
||||
360,
|
||||
470,
|
||||
),
|
||||
super::super::ModalKind::SaveDialog => sized(
|
||||
save_as_dialog_window(
|
||||
&self.save_dialog_filename,
|
||||
&self.save_dialog_folder,
|
||||
&self.save_dialog_entries,
|
||||
&self.save_dialog_format,
|
||||
),
|
||||
560,
|
||||
480,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_FORMAT_OPTIONS: &[&str] = &[
|
||||
"DWG 2018", "DWG 2013", "DWG 2010", "DWG 2007", "DWG 2004", "DWG 2000", "DWG R14", "DXF 2018",
|
||||
"DXF 2013", "DXF 2010", "DXF 2007", "DXF 2004", "DXF 2000", "DXF R14",
|
||||
];
|
||||
|
||||
fn save_as_dialog_window<'a>(
|
||||
filename: &'a str,
|
||||
folder: &'a std::path::Path,
|
||||
entries: &'a [(String, bool, std::path::PathBuf)],
|
||||
format: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
const BG: Color = Color {
|
||||
r: 0.15,
|
||||
g: 0.15,
|
||||
b: 0.17,
|
||||
a: 1.0,
|
||||
};
|
||||
const LIST_BG: Color = Color {
|
||||
r: 0.11,
|
||||
g: 0.11,
|
||||
b: 0.13,
|
||||
a: 1.0,
|
||||
};
|
||||
const BORDER: Color = Color {
|
||||
r: 0.32,
|
||||
g: 0.32,
|
||||
b: 0.36,
|
||||
a: 1.0,
|
||||
};
|
||||
const TEXT: Color = Color {
|
||||
r: 0.90,
|
||||
g: 0.90,
|
||||
b: 0.90,
|
||||
a: 1.0,
|
||||
};
|
||||
const DIM: Color = Color {
|
||||
r: 0.58,
|
||||
g: 0.58,
|
||||
b: 0.62,
|
||||
a: 1.0,
|
||||
};
|
||||
const INPUT_BG: Color = Color {
|
||||
r: 0.10,
|
||||
g: 0.10,
|
||||
b: 0.12,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_OK: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.46,
|
||||
b: 0.80,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_HOV: Color = Color {
|
||||
r: 0.26,
|
||||
g: 0.55,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_GREY: Color = Color {
|
||||
r: 0.26,
|
||||
g: 0.26,
|
||||
b: 0.29,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_GHOV: Color = Color {
|
||||
r: 0.34,
|
||||
g: 0.34,
|
||||
b: 0.38,
|
||||
a: 1.0,
|
||||
};
|
||||
const DIR_COL: Color = Color {
|
||||
r: 0.75,
|
||||
g: 0.85,
|
||||
b: 1.00,
|
||||
a: 1.0,
|
||||
};
|
||||
const FILE_COL: Color = TEXT;
|
||||
const ROW_HOV: Color = Color {
|
||||
r: 0.22,
|
||||
g: 0.24,
|
||||
b: 0.28,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let input_sty =
|
||||
|_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
|
||||
background: Background::Color(INPUT_BG),
|
||||
border: Border {
|
||||
color: BORDER,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
icon: TEXT,
|
||||
placeholder: DIM,
|
||||
value: TEXT,
|
||||
selection: Color {
|
||||
r: 0.20,
|
||||
g: 0.46,
|
||||
b: 0.80,
|
||||
a: 0.45,
|
||||
},
|
||||
};
|
||||
|
||||
let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| {
|
||||
button(text(lbl).size(12).color(TEXT))
|
||||
.on_press(msg)
|
||||
.style(move |_: &Theme, st| button::Style {
|
||||
background: Some(Background::Color(
|
||||
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
||||
hov
|
||||
} else {
|
||||
base
|
||||
},
|
||||
)),
|
||||
text_color: TEXT,
|
||||
border: Border {
|
||||
color: BORDER,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([4, 12])
|
||||
};
|
||||
|
||||
// ── Path bar ─────────────────────────────────────────────────────────
|
||||
let path_str = if crate::io::is_drives_root(folder) {
|
||||
crate::io::drives_root_label().to_string()
|
||||
} else {
|
||||
folder.to_string_lossy().into_owned()
|
||||
};
|
||||
let up_path = crate::io::parent_folder(folder);
|
||||
let path_bar = row![
|
||||
{
|
||||
let up_msg = up_path.map(Message::SaveDialogNavigate);
|
||||
let b = button(crate::ui::icons::tinted(crate::ui::icons::UP, 14.0, TEXT))
|
||||
.style(|_: &Theme, st| button::Style {
|
||||
background: Some(Background::Color(
|
||||
if matches!(st, button::Status::Hovered | button::Status::Pressed) {
|
||||
BTN_GHOV
|
||||
} else {
|
||||
BTN_GREY
|
||||
},
|
||||
)),
|
||||
text_color: TEXT,
|
||||
border: Border {
|
||||
color: BORDER,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 10]);
|
||||
if let Some(msg) = up_msg {
|
||||
b.on_press(msg)
|
||||
} else {
|
||||
b
|
||||
}
|
||||
},
|
||||
Space::new().width(8),
|
||||
container(text(path_str.clone()).size(12).color(DIM))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(INPUT_BG)),
|
||||
border: Border {
|
||||
color: BORDER,
|
||||
width: 1.0,
|
||||
radius: 4.0.into()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([4, 8])
|
||||
.width(Fill),
|
||||
]
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
// ── File list ─────────────────────────────────────────────────────────
|
||||
let file_list: Element<'_, Message> = {
|
||||
let rows: Vec<Element<'_, Message>> = entries
|
||||
.iter()
|
||||
.map(|(name, is_dir, path)| {
|
||||
let icon_bytes = if *is_dir {
|
||||
crate::ui::icons::FOLDER
|
||||
} else {
|
||||
crate::ui::icons::DOC
|
||||
};
|
||||
let color = if *is_dir { DIR_COL } else { FILE_COL };
|
||||
let p = path.clone();
|
||||
let d = *is_dir;
|
||||
mouse_area(
|
||||
container(
|
||||
row![
|
||||
crate::ui::icons::tinted(icon_bytes, 13.0, color),
|
||||
Space::new().width(6),
|
||||
text(crate::ui::text_util::elide(name.as_str(), 48))
|
||||
.size(13)
|
||||
.color(color),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 8])
|
||||
.width(Fill),
|
||||
)
|
||||
.on_press(Message::SaveDialogEntryClicked(p, d))
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
container(iced::widget::scrollable(
|
||||
column(rows).spacing(1).width(Fill),
|
||||
))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(LIST_BG)),
|
||||
border: Border {
|
||||
color: BORDER,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
};
|
||||
let _ = ROW_HOV; // used conceptually, suppress warning
|
||||
|
||||
let sel_fmt = SAVE_FORMAT_OPTIONS.iter().copied().find(|&s| s == format);
|
||||
let label = |s: &'static str| text(s).size(11).color(DIM);
|
||||
|
||||
// ── Bottom controls ───────────────────────────────────────────────────
|
||||
let bottom = column![
|
||||
row![
|
||||
label("File name:").width(90),
|
||||
text_input("drawing.dwg", filename)
|
||||
.on_input(Message::SaveDialogFilenameChanged)
|
||||
.style(input_sty)
|
||||
.size(13)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(6),
|
||||
Space::new().height(6),
|
||||
row![
|
||||
label("Format:").width(90),
|
||||
pick_list(SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| {
|
||||
Message::SaveDialogFormatChanged(s.to_string())
|
||||
})
|
||||
.width(Fill),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(6),
|
||||
Space::new().height(12),
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
btn("Save", Message::SaveDialogConfirm, BTN_OK, BTN_HOV),
|
||||
Space::new().width(8),
|
||||
btn("Cancel", Message::SaveDialogCancel, BTN_GREY, BTN_GHOV),
|
||||
],
|
||||
]
|
||||
.spacing(0);
|
||||
|
||||
container(
|
||||
column![
|
||||
path_bar,
|
||||
Space::new().height(8),
|
||||
file_list,
|
||||
Space::new().height(10),
|
||||
bottom,
|
||||
]
|
||||
.spacing(0),
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BG)),
|
||||
..Default::default()
|
||||
})
|
||||
.padding([14, 16])
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> {
|
||||
const BG: Color = Color {
|
||||
r: 0.18,
|
||||
g: 0.18,
|
||||
b: 0.20,
|
||||
a: 1.0,
|
||||
};
|
||||
const BORDER_COL: Color = Color {
|
||||
r: 0.38,
|
||||
g: 0.38,
|
||||
b: 0.42,
|
||||
a: 1.0,
|
||||
};
|
||||
const TEXT_COL: Color = Color {
|
||||
r: 0.90,
|
||||
g: 0.90,
|
||||
b: 0.90,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_SAVE: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.46,
|
||||
b: 0.80,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_HOVER: Color = Color {
|
||||
r: 0.26,
|
||||
g: 0.55,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_DISC: Color = Color {
|
||||
r: 0.28,
|
||||
g: 0.28,
|
||||
b: 0.30,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_DHOV: Color = Color {
|
||||
r: 0.36,
|
||||
g: 0.36,
|
||||
b: 0.40,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let body_text = format!("Do you want to save changes to \"{}\"?", name);
|
||||
|
||||
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
||||
button(text(label).size(13).color(TEXT_COL))
|
||||
.on_press(msg)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered | button::Status::Pressed => hov,
|
||||
_ => base,
|
||||
})),
|
||||
text_color: TEXT_COL,
|
||||
border: Border {
|
||||
color: BORDER_COL,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([6, 18])
|
||||
};
|
||||
|
||||
container(
|
||||
column![
|
||||
text(body_text).size(13).color(TEXT_COL),
|
||||
iced::widget::Space::new().height(20),
|
||||
row![
|
||||
btn("Save", Message::UnsavedDialogSave, BTN_SAVE, BTN_HOVER),
|
||||
iced::widget::Space::new().width(8),
|
||||
btn("Discard", Message::UnsavedDialogDiscard, BTN_DISC, BTN_DHOV),
|
||||
iced::widget::Space::new().width(8),
|
||||
btn("Cancel", Message::UnsavedDialogCancel, BTN_DISC, BTN_DHOV),
|
||||
],
|
||||
]
|
||||
.spacing(0),
|
||||
)
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BG)),
|
||||
..Default::default()
|
||||
})
|
||||
.center(Fill)
|
||||
.padding([24, 28])
|
||||
.into()
|
||||
}
|
||||
|
||||
/// First-launch prompt offering to register Open CAD Studio as the default
|
||||
/// handler for .dwg / .dxf. "Yes" runs the platform association call; "Not now"
|
||||
/// just dismisses. Either answer flips the persisted `default_assoc_prompted`
|
||||
/// flag so the dialog never reappears.
|
||||
fn default_assoc_dialog_window() -> Element<'static, Message> {
|
||||
const BG: Color = Color {
|
||||
r: 0.18,
|
||||
g: 0.18,
|
||||
b: 0.20,
|
||||
a: 1.0,
|
||||
};
|
||||
const BORDER_COL: Color = Color {
|
||||
r: 0.38,
|
||||
g: 0.38,
|
||||
b: 0.42,
|
||||
a: 1.0,
|
||||
};
|
||||
const TEXT_COL: Color = Color {
|
||||
r: 0.90,
|
||||
g: 0.90,
|
||||
b: 0.90,
|
||||
a: 1.0,
|
||||
};
|
||||
const DIM_COL: Color = Color {
|
||||
r: 0.62,
|
||||
g: 0.62,
|
||||
b: 0.66,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_YES: Color = Color {
|
||||
r: 0.20,
|
||||
g: 0.46,
|
||||
b: 0.80,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_YHOV: Color = Color {
|
||||
r: 0.26,
|
||||
g: 0.55,
|
||||
b: 0.92,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_NO: Color = Color {
|
||||
r: 0.28,
|
||||
g: 0.28,
|
||||
b: 0.30,
|
||||
a: 1.0,
|
||||
};
|
||||
const BTN_NHOV: Color = Color {
|
||||
r: 0.36,
|
||||
g: 0.36,
|
||||
b: 0.40,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
let btn = |label: &'static str, msg: Message, base: Color, hov: Color| {
|
||||
button(text(label).size(13).color(TEXT_COL))
|
||||
.on_press(msg)
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered | button::Status::Pressed => hov,
|
||||
_ => base,
|
||||
})),
|
||||
text_color: TEXT_COL,
|
||||
border: Border {
|
||||
color: BORDER_COL,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
})
|
||||
.padding([6, 18])
|
||||
};
|
||||
|
||||
container(
|
||||
column![
|
||||
text("Make Open CAD Studio your default CAD app?")
|
||||
.size(15)
|
||||
.color(TEXT_COL),
|
||||
iced::widget::Space::new().height(10),
|
||||
text("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings.")
|
||||
.size(12)
|
||||
.color(DIM_COL),
|
||||
iced::widget::Space::new().height(22),
|
||||
row![
|
||||
iced::widget::Space::new().width(Fill),
|
||||
btn("Not now", Message::AssocPromptNo, BTN_NO, BTN_NHOV),
|
||||
iced::widget::Space::new().width(8),
|
||||
btn("Yes, set as default", Message::AssocPromptYes, BTN_YES, BTN_YHOV),
|
||||
]
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(0),
|
||||
)
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BG)),
|
||||
..Default::default()
|
||||
})
|
||||
.center(Fill)
|
||||
.padding([24, 28])
|
||||
.into()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,195 @@
|
||||
use super::super::Message;
|
||||
use crate::scene::{VIEWCUBE_PX, VIEWCUBE_REGION_PX};
|
||||
use iced::widget::{
|
||||
button, column, container, mouse_area, pick_list, row, stack,
|
||||
Space,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Fill, Theme};
|
||||
|
||||
// ── Render-mode picker ──────────────────────────────────────────────────────
|
||||
|
||||
/// Top-left viewport control bar: a single dark chip holding (optionally) the
|
||||
/// horizontal/vertical split buttons, the render-mode picker, and the grid /
|
||||
/// grid-snap toggles. `include_split` is off for paper-space viewports, which
|
||||
/// have no model-tile splitting. Grid / snap reflect the active viewport's
|
||||
/// state and emit `ToggleGrid` / `ToggleGridSnap`.
|
||||
// ── ViewCube navigation controls (home / roll / nudge / UCS) ───────────────
|
||||
|
||||
/// Place `el` at pixel offset (x, y) inside a Fill layer (top-left origin).
|
||||
fn vc_place<'a>(x: f32, y: f32, el: Element<'a, Message>) -> Element<'a, Message> {
|
||||
column![
|
||||
Space::new().height(iced::Length::Fixed(y.max(0.0))),
|
||||
row![Space::new().width(iced::Length::Fixed(x.max(0.0))), el],
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Borderless square icon button used by the ViewCube nav controls.
|
||||
fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element<'a, Message> {
|
||||
button(
|
||||
container(content)
|
||||
.width(iced::Length::Fixed(size))
|
||||
.height(iced::Length::Fixed(size))
|
||||
.center_x(iced::Length::Fixed(size))
|
||||
.center_y(iced::Length::Fixed(size)),
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(msg)
|
||||
.style(|_: &Theme, status| iced::widget::button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
iced::widget::button::Status::Hovered | iced::widget::button::Status::Pressed => Color {
|
||||
r: 0.45,
|
||||
g: 0.62,
|
||||
b: 0.95,
|
||||
a: 0.30,
|
||||
},
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Overlay of home / roll / nudge controls sized to the whole nav region, so
|
||||
/// the caller can position it exactly like the cube hit area.
|
||||
pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
|
||||
use crate::scene::NudgeDir;
|
||||
use crate::ui::icons;
|
||||
let tint = Color {
|
||||
r: 0.86,
|
||||
g: 0.89,
|
||||
b: 0.96,
|
||||
a: 1.0,
|
||||
};
|
||||
let r = VIEWCUBE_REGION_PX;
|
||||
let c = r * 0.5;
|
||||
let cube_half = VIEWCUBE_PX as f32 * 0.36; // VIEWCUBE_PX * VIEWCUBE_SCALE
|
||||
let nr = cube_half + 8.0; // nudge triangle distance from centre
|
||||
const BTN: f32 = 18.0;
|
||||
const TRI: f32 = 13.0;
|
||||
let ctr = |cx: f32, cy: f32, s: f32| (cx - s * 0.5, cy - s * 0.5);
|
||||
|
||||
// Home top-left, roll arrows top-right.
|
||||
let (rax, ray) = (r - 2.0 * BTN - 4.0, 2.0);
|
||||
let (rbx, rby) = (r - BTN - 2.0, 2.0);
|
||||
// Nudge triangles pointing inward at the four cube faces.
|
||||
let (tux, tuy) = ctr(c, c - nr, TRI);
|
||||
let (tdx, tdy) = ctr(c, c + nr, TRI);
|
||||
let (tlx, tly) = ctr(c - nr, c, TRI);
|
||||
let (trx, try_) = ctr(c + nr, c, TRI);
|
||||
|
||||
// Bottom layer: the cube/cardinal hit area covering the whole region. The
|
||||
// control buttons sit ABOVE it in the same stack, so a click on a button is
|
||||
// caught by the button while a click on the cube (or empty space) falls
|
||||
// through to this mouse_area → ViewportClick. Moves keep cursor_pos current.
|
||||
let cube_hit = mouse_area(
|
||||
Space::new()
|
||||
.width(iced::Length::Fixed(r))
|
||||
.height(iced::Length::Fixed(r)),
|
||||
)
|
||||
.on_move(Message::CursorMoved)
|
||||
.on_press(Message::ViewportClick);
|
||||
|
||||
let controls = stack![
|
||||
cube_hit,
|
||||
vc_place(
|
||||
3.0,
|
||||
3.0,
|
||||
vc_btn(icons::home(15.0, tint), BTN, Message::ViewCubeHome)
|
||||
),
|
||||
vc_place(
|
||||
rax,
|
||||
ray,
|
||||
vc_btn(icons::undo(14.0, tint), BTN, Message::ViewCubeRoll(false))
|
||||
),
|
||||
vc_place(
|
||||
rbx,
|
||||
rby,
|
||||
vc_btn(icons::redo(14.0, tint), BTN, Message::ViewCubeRoll(true))
|
||||
),
|
||||
vc_place(
|
||||
tux,
|
||||
tuy,
|
||||
vc_btn(
|
||||
icons::arrow_down(11.0, tint),
|
||||
TRI,
|
||||
Message::ViewCubeNudge(NudgeDir::Up)
|
||||
)
|
||||
),
|
||||
vc_place(
|
||||
tdx,
|
||||
tdy,
|
||||
vc_btn(
|
||||
icons::arrow_up(11.0, tint),
|
||||
TRI,
|
||||
Message::ViewCubeNudge(NudgeDir::Down)
|
||||
)
|
||||
),
|
||||
vc_place(
|
||||
tlx,
|
||||
tly,
|
||||
vc_btn(
|
||||
icons::arrow_right(11.0, tint),
|
||||
TRI,
|
||||
Message::ViewCubeNudge(NudgeDir::Left)
|
||||
)
|
||||
),
|
||||
vc_place(
|
||||
trx,
|
||||
try_,
|
||||
vc_btn(
|
||||
icons::arrow_left(11.0, tint),
|
||||
TRI,
|
||||
Message::ViewCubeNudge(NudgeDir::Right)
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
container(controls)
|
||||
.width(iced::Length::Fixed(r))
|
||||
.height(iced::Length::Fixed(r))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The WCS / named-UCS selector shown under the cube.
|
||||
pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> Element<'a, Message> {
|
||||
let light = Color {
|
||||
r: 0.85,
|
||||
g: 0.87,
|
||||
b: 0.93,
|
||||
a: 1.0,
|
||||
};
|
||||
let mut options = vec!["WCS".to_string()];
|
||||
options.extend(names);
|
||||
let selected = if current.is_empty() {
|
||||
"WCS".to_string()
|
||||
} else {
|
||||
current
|
||||
};
|
||||
pick_list(options, Some(selected), Message::SetViewcubeUcs)
|
||||
.text_size(11)
|
||||
.padding([2, 6])
|
||||
.style(move |_: &Theme, _| iced::widget::pick_list::Style {
|
||||
background: Background::Color(Color {
|
||||
r: 0.16,
|
||||
g: 0.17,
|
||||
b: 0.20,
|
||||
a: 0.92,
|
||||
}),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
text_color: light,
|
||||
placeholder_color: light,
|
||||
handle_color: light,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user