forked from 1b-agent/opencadstudio-upstream
Merge pull request #1472 from ramox81:codex/pdf-attach-parity
This commit is contained in:
commit
2e60c4da1b
63 files changed
+7470
-405
No files matched your search
+106
-70
@@ -25,6 +25,8 @@ mod draw_panel;
|
||||
pub(crate) use draw_panel::tools as panel_tools;
|
||||
mod modify_panel;
|
||||
mod color_dropdown;
|
||||
mod underlay_tab;
|
||||
pub use underlay_tab::{UnderlayContext, UnderlayTabMsg};
|
||||
use widgets::{StyleContext, *};
|
||||
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
|
||||
mod collapse;
|
||||
@@ -103,6 +105,10 @@ pub struct Ribbon {
|
||||
/// Set by `CollapsePanels` when the tool row is in its tight state; the mode
|
||||
/// selector hides itself then to give the cramped tab row its space back.
|
||||
collapse_tight: Arc<AtomicBool>,
|
||||
/// The selected PDF underlay's values while the contextual tab exists.
|
||||
underlay_ctx: Option<UnderlayContext>,
|
||||
/// The contextual underlay tab is the one shown.
|
||||
underlay_tab_active: bool,
|
||||
}
|
||||
|
||||
/// Per-layer display data shown in the ribbon layer dropdown.
|
||||
@@ -196,6 +202,8 @@ impl Ribbon {
|
||||
tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())),
|
||||
collapse_mode: CollapseMode::default(),
|
||||
collapse_tight: Arc::new(AtomicBool::new(false)),
|
||||
underlay_ctx: None,
|
||||
underlay_tab_active: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +281,7 @@ impl Ribbon {
|
||||
pub fn select(&mut self, index: usize) {
|
||||
if index < self.modules.len() {
|
||||
self.active = index;
|
||||
self.underlay_tab_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,6 +345,58 @@ impl Ribbon {
|
||||
show_block_palette,
|
||||
show_file_tabs: self.show_file_tabs,
|
||||
show_layout_tabs: self.show_layout_tabs,
|
||||
underlay_mono: self.underlay_ctx.as_ref().is_some_and(|c| c.monochrome),
|
||||
underlay_shown: self.underlay_ctx.as_ref().is_some_and(|c| c.shown),
|
||||
underlay_snap: self.underlay_ctx.as_ref().is_some_and(|c| c.snap),
|
||||
}
|
||||
}
|
||||
|
||||
/// One panel in its four densities; `lead` adds controls before the
|
||||
/// group's own tools.
|
||||
fn panel<'a>(
|
||||
&'a self,
|
||||
g: &'a RibbonGroup,
|
||||
ts: widgets::ToggleState,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
lead: &dyn Fn(bool) -> Vec<Element<'a, Message>>,
|
||||
) -> Panel<'a> {
|
||||
let group = |compact: bool| {
|
||||
render_group(
|
||||
compact,
|
||||
g,
|
||||
lead(compact),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
style_ctx,
|
||||
)
|
||||
};
|
||||
let button = |tight: bool| {
|
||||
collapse_button(
|
||||
g,
|
||||
self.last_panel_tool.get(g.title).copied(),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
style_ctx,
|
||||
tight,
|
||||
)
|
||||
};
|
||||
Panel {
|
||||
id: g.title.to_string(),
|
||||
elements: [group(false), group(true), button(false), button(true)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +515,7 @@ impl Ribbon {
|
||||
return acc;
|
||||
}
|
||||
|
||||
let is_active = i == self.active;
|
||||
let is_active = i == self.active && !self.underlay_tab_shown();
|
||||
let is_contextual = module.id() == "layout";
|
||||
let btn = container(
|
||||
button(text(crate::i18n::ribbon_module_title(module.id(), module.title())).size(12))
|
||||
@@ -524,6 +585,11 @@ impl Ribbon {
|
||||
},
|
||||
);
|
||||
|
||||
let mut tab_items = tab_items;
|
||||
if self.underlay_ctx.is_some() {
|
||||
tab_items.push(underlay_tab_button(self.underlay_tab_shown()));
|
||||
}
|
||||
|
||||
// Tabs may squeeze their gaps to fit before wrapping: from the normal 6px
|
||||
// down to -12px on a narrow (e.g. phone) tab row, tucking neighbours into
|
||||
// each other's 14px side padding without overlapping the labels. When the
|
||||
@@ -603,74 +669,15 @@ impl Ribbon {
|
||||
// fit they degrade from the right — a panel's large buttons first
|
||||
// shrink to compact icon columns, then it collapses to a ▾ flyout
|
||||
// button. See `CollapsePanels`.
|
||||
let panels: Vec<Panel<'_>> = groups
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let ts = self.toggle_state(show_block_palette);
|
||||
Panel {
|
||||
id: g.title.to_string(),
|
||||
elements: [render_group(
|
||||
false,
|
||||
g,
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
),
|
||||
render_group(
|
||||
true,
|
||||
g,
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
),
|
||||
collapse_button(
|
||||
g,
|
||||
self.last_panel_tool.get(g.title).copied(),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
false,
|
||||
),
|
||||
collapse_button(
|
||||
g,
|
||||
self.last_panel_tool.get(g.title).copied(),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
ts,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
true,
|
||||
),
|
||||
],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let ts = self.toggle_state(show_block_palette);
|
||||
let panels: Vec<Panel<'_>> = if self.underlay_tab_shown() {
|
||||
self.underlay_panels(ts, &style_ctx)
|
||||
} else {
|
||||
groups
|
||||
.iter()
|
||||
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
|
||||
.collect()
|
||||
};
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
|
||||
.report_height(self.tool_bar_h.clone())
|
||||
.report_tight(self.collapse_tight.clone())
|
||||
@@ -1322,6 +1329,7 @@ impl Ribbon {
|
||||
fn render_group<'a>(
|
||||
compact: bool,
|
||||
group: &RibbonGroup,
|
||||
lead: Vec<Element<'a, Message>>,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
@@ -1333,7 +1341,7 @@ fn render_group<'a>(
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
) -> Element<'a, Message> {
|
||||
let mut items_row: Vec<Element<Message>> = Vec::new();
|
||||
let mut items_row: Vec<Element<Message>> = lead;
|
||||
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
||||
|
||||
let ctx = widgets::RenderCtx {
|
||||
@@ -1615,3 +1623,31 @@ mod tests {
|
||||
assert_eq!(ribbon.open_dropdown, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// The contextual underlay tab's button: framed in the accent colour.
|
||||
fn underlay_tab_button<'a>(active: bool) -> Element<'a, Message> {
|
||||
let btn = button(text(t!("PDF Underlay")).size(12))
|
||||
.on_press(Message::RibbonSelectUnderlayTab)
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let pair = match (active, status) {
|
||||
(true, _) => palette.background.weakest,
|
||||
(false, button::Status::Hovered) => palette.background.weak,
|
||||
_ => palette.background.base,
|
||||
};
|
||||
button::Style {
|
||||
background: (active || matches!(status, button::Status::Hovered))
|
||||
.then_some(Background::Color(pair.color)),
|
||||
text_color: if active { pair.text } else { palette.primary.base.color },
|
||||
border: Border {
|
||||
color: palette.primary.base.color,
|
||||
width: if active { 2.0 } else { 1.0 },
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: false,
|
||||
}
|
||||
})
|
||||
.padding([5, 14]);
|
||||
container(btn).into()
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//! The contextual "PDF Underlay" tab: shown and brought forward while only
|
||||
//! PDF underlays are selected, gone when the selection changes.
|
||||
|
||||
use iced::widget::{column, row, slider, text, text_input};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use super::*;
|
||||
use crate::modules::{ModuleEvent, ToolDef};
|
||||
|
||||
/// What the tab shows for the selected underlay (the first one).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UnderlayContext {
|
||||
pub contrast: u8,
|
||||
pub fade: u8,
|
||||
pub monochrome: bool,
|
||||
pub shown: bool,
|
||||
pub snap: bool,
|
||||
pub contrast_text: String,
|
||||
pub fade_text: String,
|
||||
}
|
||||
|
||||
impl UnderlayContext {
|
||||
pub fn new(contrast: u8, fade: u8, monochrome: bool, shown: bool, snap: bool) -> Self {
|
||||
Self {
|
||||
contrast,
|
||||
fade,
|
||||
monochrome,
|
||||
shown,
|
||||
snap,
|
||||
contrast_text: contrast.to_string(),
|
||||
fade_text: fade.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The tab's edits; the host writes them to the selected underlays.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UnderlayTabMsg {
|
||||
Contrast(u8),
|
||||
Fade(u8),
|
||||
ContrastText(String),
|
||||
FadeText(String),
|
||||
/// Slider released or value submitted: write contrast and fade.
|
||||
Commit,
|
||||
}
|
||||
|
||||
pub(super) const ADJUST: &str = "Adjust";
|
||||
|
||||
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
|
||||
ToolDef {
|
||||
id,
|
||||
label,
|
||||
icon: IconKind::Svg(icon),
|
||||
event: ModuleEvent::Command(id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn groups() -> &'static [RibbonGroup] {
|
||||
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
|
||||
const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg");
|
||||
const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg");
|
||||
GROUPS.get_or_init(|| {
|
||||
vec![
|
||||
RibbonGroup {
|
||||
title: ADJUST,
|
||||
tools: vec![RibbonItem::LargeTool(tool(
|
||||
"_PDFULMONO",
|
||||
"Display in\nMonochrome",
|
||||
FRAMES,
|
||||
))],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Clipping",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP)),
|
||||
RibbonItem::LargeTool(tool("_PDFULUNCLIP", "Remove\nClipping", CLIP)),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Options",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("_PDFULSHOW", "Show\nUnderlay", FRAMES)),
|
||||
RibbonItem::LargeTool(tool(
|
||||
"_PDFULSNAP",
|
||||
"Enable\nSnap",
|
||||
include_bytes!("../../../assets/icons/snap_underlays.svg"),
|
||||
)),
|
||||
RibbonItem::LargeTool(tool(
|
||||
"EXTERNALREFERENCES",
|
||||
"External\nReferences",
|
||||
crate::ui::icons::FOLDER_OPEN,
|
||||
)),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "PDF Layers",
|
||||
tools: vec![RibbonItem::LargeTool(tool(
|
||||
"ULAYERS",
|
||||
"Edit\nLayers",
|
||||
include_bytes!("../../../assets/icons/underlay_layers.svg"),
|
||||
))],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "PDF Import",
|
||||
tools: vec![RibbonItem::LargeTool(tool(
|
||||
"_PDFULIMPORT",
|
||||
"Import As\nObjects",
|
||||
include_bytes!("../../../assets/icons/cui_import.svg"),
|
||||
))],
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/// Contrast / Fade: a label, a 0–100 slider and the value box.
|
||||
fn adjust_rows<'a>(ctx: &'a UnderlayContext) -> Element<'a, Message> {
|
||||
let line = |label: &'static str,
|
||||
value: u8,
|
||||
buffer: &'a str,
|
||||
on_slide: fn(u8) -> UnderlayTabMsg,
|
||||
on_text: fn(String) -> UnderlayTabMsg| {
|
||||
row![
|
||||
text(t!(label)).size(11).width(Length::Fixed(64.0)),
|
||||
slider(0..=100u8, value, move |v| Message::UnderlayTab(on_slide(v)))
|
||||
.on_release(Message::UnderlayTab(UnderlayTabMsg::Commit))
|
||||
.width(Length::Fixed(110.0)),
|
||||
text_input("", buffer)
|
||||
.on_input(move |s| Message::UnderlayTab(on_text(s)))
|
||||
.on_submit(Message::UnderlayTab(UnderlayTabMsg::Commit))
|
||||
.size(11)
|
||||
.padding([2, 4])
|
||||
.width(Length::Fixed(44.0)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
column![
|
||||
line(
|
||||
"Contrast",
|
||||
ctx.contrast,
|
||||
&ctx.contrast_text,
|
||||
UnderlayTabMsg::Contrast,
|
||||
UnderlayTabMsg::ContrastText,
|
||||
),
|
||||
line(
|
||||
"Fade",
|
||||
ctx.fade,
|
||||
&ctx.fade_text,
|
||||
UnderlayTabMsg::Fade,
|
||||
UnderlayTabMsg::FadeText,
|
||||
),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding([6, 4])
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Ribbon {
|
||||
/// Show the tab for the selected underlay, or drop it (`None`). A tab
|
||||
/// that appears comes to the front; one that goes returns the tab that
|
||||
/// was active before it.
|
||||
pub fn set_underlay_context(&mut self, context: Option<UnderlayContext>) {
|
||||
match (&self.underlay_ctx, &context) {
|
||||
(None, Some(_)) => self.underlay_tab_active = true,
|
||||
(Some(_), None) => self.underlay_tab_active = false,
|
||||
_ => {}
|
||||
}
|
||||
// A value being typed survives a refresh that leaves the value as is.
|
||||
let mut context = context;
|
||||
if let (Some(old), Some(new)) = (&self.underlay_ctx, &mut context) {
|
||||
if (old.contrast, old.fade) == (new.contrast, new.fade) {
|
||||
new.contrast_text = old.contrast_text.clone();
|
||||
new.fade_text = old.fade_text.clone();
|
||||
}
|
||||
}
|
||||
self.underlay_ctx = context;
|
||||
}
|
||||
|
||||
pub fn underlay_context_mut(&mut self) -> Option<&mut UnderlayContext> {
|
||||
self.underlay_ctx.as_mut()
|
||||
}
|
||||
|
||||
/// Select a tab by its module id; "pdf_underlay" is the contextual tab.
|
||||
pub fn select_by_id(&mut self, id: &str) -> bool {
|
||||
if id == "pdf_underlay" {
|
||||
self.select_underlay_tab();
|
||||
return self.underlay_tab_active;
|
||||
}
|
||||
match self.modules.iter().position(|m| m.id() == id) {
|
||||
Some(index) => {
|
||||
self.select(index);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_underlay_tab(&mut self) {
|
||||
if self.underlay_ctx.is_some() {
|
||||
self.underlay_tab_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn underlay_tab_shown(&self) -> bool {
|
||||
self.underlay_tab_active && self.underlay_ctx.is_some()
|
||||
}
|
||||
|
||||
pub(super) fn underlay_panels<'a>(
|
||||
&'a self,
|
||||
ts: widgets::ToggleState,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
) -> Vec<Panel<'a>> {
|
||||
let Some(ctx) = self.underlay_ctx.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
groups()
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let lead = |_: bool| -> Vec<Element<'a, Message>> {
|
||||
if g.title == ADJUST {
|
||||
vec![adjust_rows(ctx)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
self.panel(g, ts, style_ctx, &lead)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,9 @@ pub(super) struct ToggleState {
|
||||
pub show_block_palette: bool,
|
||||
pub show_file_tabs: bool,
|
||||
pub show_layout_tabs: bool,
|
||||
pub underlay_mono: bool,
|
||||
pub underlay_shown: bool,
|
||||
pub underlay_snap: bool,
|
||||
}
|
||||
|
||||
// ── Layout constants (single source of truth: ROW_H from ui::mod) ─────────
|
||||
@@ -375,6 +378,9 @@ pub(super) fn is_active_tool(
|
||||
"UCSICON" => state.show_ucs_icon,
|
||||
"PROPERTIES" => state.show_properties,
|
||||
"BLOCKPALETTE" => state.show_block_palette,
|
||||
"_PDFULMONO" => state.underlay_mono,
|
||||
"_PDFULSHOW" => state.underlay_shown,
|
||||
"_PDFULSNAP" => state.underlay_snap,
|
||||
"FILETAB" => state.show_file_tabs,
|
||||
"LAYOUTTAB" => state.show_layout_tabs,
|
||||
id => active_tool.as_deref() == Some(id),
|
||||
|
||||
@@ -24,6 +24,7 @@ pub mod annotation_data;
|
||||
pub mod alias_editor;
|
||||
pub mod find_replace;
|
||||
pub mod named_parameters;
|
||||
pub mod pdf_dialogs;
|
||||
pub mod xref_attach;
|
||||
pub mod xref_help;
|
||||
pub mod xref_manager;
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
//! PDF dialogs: Attach PDF Underlay (pages, path type, insertion, scale,
|
||||
//! rotation), Underlay Layers, PDF Import Settings and Import PDF.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, image, pick_list, radio, row, scrollable, text,
|
||||
text_input, Space,
|
||||
};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::io::xref_model::Pathtype;
|
||||
use crate::modules::insert::pdf_import::{ImportLayers, PdfImportSettings};
|
||||
use crate::t;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::form::{button_style, field_style};
|
||||
use crate::ui::window::block_definition::{group, labeled_checkbox};
|
||||
|
||||
/// One edit in any of the PDF dialogs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PdfDialogMsg {
|
||||
// Attach
|
||||
AttachName(String),
|
||||
AttachBrowse,
|
||||
AttachPage(usize),
|
||||
AttachPathType(PathTypeChoice),
|
||||
AttachInsertOnScreen(bool),
|
||||
AttachInsert(usize, String),
|
||||
AttachScaleOnScreen(bool),
|
||||
AttachScale(String),
|
||||
AttachRotationOnScreen(bool),
|
||||
AttachRotation(String),
|
||||
AttachDetails(bool),
|
||||
AttachOk,
|
||||
// Underlay Layers
|
||||
LayersUnderlay(String),
|
||||
LayersSearch(String),
|
||||
LayersToggle(String),
|
||||
LayersOk,
|
||||
// Import settings (the settings dialog and the Import PDF dialog)
|
||||
Vector(bool),
|
||||
Fills(bool),
|
||||
Text(bool),
|
||||
Raster(bool),
|
||||
Layers(ImportLayers),
|
||||
AsBlock(bool),
|
||||
Join(bool),
|
||||
Hatches(bool),
|
||||
Lineweights(bool),
|
||||
Linetypes(bool),
|
||||
SettingsOk,
|
||||
// Import PDF
|
||||
ImportBrowse,
|
||||
ImportPage(usize),
|
||||
ImportPageText(String),
|
||||
ImportInsertOnScreen(bool),
|
||||
ImportScale(String),
|
||||
ImportRotation(RotationChoice),
|
||||
ImportOk,
|
||||
Options,
|
||||
Help(&'static str),
|
||||
}
|
||||
|
||||
fn msg(m: PdfDialogMsg) -> Message {
|
||||
Message::PdfDialog(m)
|
||||
}
|
||||
|
||||
/// Path type as the Attach dialog lists it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PathTypeChoice(pub Pathtype);
|
||||
|
||||
impl PathTypeChoice {
|
||||
pub const ALL: [PathTypeChoice; 3] = [
|
||||
PathTypeChoice(Pathtype::Full),
|
||||
PathTypeChoice(Pathtype::Relative),
|
||||
PathTypeChoice(Pathtype::None),
|
||||
];
|
||||
}
|
||||
|
||||
impl fmt::Display for PathTypeChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self.0 {
|
||||
Pathtype::Full => t!("Full path"),
|
||||
Pathtype::Relative => t!("Relative path"),
|
||||
Pathtype::None => t!("No path"),
|
||||
};
|
||||
f.write_str(label.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotation of an imported page: the four right angles.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RotationChoice(pub u16);
|
||||
|
||||
impl RotationChoice {
|
||||
pub const ALL: [RotationChoice; 4] = [
|
||||
RotationChoice(0),
|
||||
RotationChoice(90),
|
||||
RotationChoice(180),
|
||||
RotationChoice(270),
|
||||
];
|
||||
}
|
||||
|
||||
impl fmt::Display for RotationChoice {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A page of the chosen file and its thumbnail.
|
||||
#[derive(Clone)]
|
||||
pub struct PageThumb {
|
||||
pub label: String,
|
||||
pub image: Option<image::Handle>,
|
||||
}
|
||||
|
||||
/// Thumbnails of every page, about 100 px wide.
|
||||
// ponytail: all pages are rasterised when the dialog opens; lazy per-row
|
||||
// thumbnails if files with hundreds of pages matter.
|
||||
pub fn page_thumbs(path: &str) -> Vec<PageThumb> {
|
||||
use crate::scene::model::pdf_raster::{page_count, page_size_inches, rasterize_page_at_dpi};
|
||||
let count = page_count(path).unwrap_or(0);
|
||||
(1..=count)
|
||||
.map(|n| {
|
||||
let label = n.to_string();
|
||||
let image = page_size_inches(path, &label).and_then(|(w, h)| {
|
||||
let dpi = (100.0 / w.max(h).max(0.1)) as f32;
|
||||
let page = rasterize_page_at_dpi(path, &label, dpi)?;
|
||||
Some(image::Handle::from_rgba(page.width, page.height, page.pixels.to_vec()))
|
||||
});
|
||||
PageThumb { label, image }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Ctrl adds or removes a page, Shift takes the range from the last click,
|
||||
/// a plain click selects that page alone.
|
||||
pub fn click_page(selected: &mut Vec<usize>, anchor: &mut usize, page: usize, ctrl: bool, shift: bool) {
|
||||
if shift {
|
||||
let (a, b) = if *anchor <= page { (*anchor, page) } else { (page, *anchor) };
|
||||
*selected = (a..=b).collect();
|
||||
} else if ctrl {
|
||||
match selected.iter().position(|p| *p == page) {
|
||||
Some(at) if selected.len() > 1 => {
|
||||
selected.remove(at);
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
selected.push(page);
|
||||
selected.sort_unstable();
|
||||
}
|
||||
}
|
||||
*anchor = page;
|
||||
} else {
|
||||
*selected = vec![page];
|
||||
*anchor = page;
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_style(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
color: theme.palette().background.strong.color,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn page_grid<'a>(
|
||||
pages: &'a [PageThumb],
|
||||
selected: &[usize],
|
||||
on_click: fn(usize) -> PdfDialogMsg,
|
||||
height: f32,
|
||||
) -> Element<'a, Message> {
|
||||
let tiles = pages.iter().enumerate().map(|(index, page)| {
|
||||
let chosen = selected.contains(&index);
|
||||
let picture: Element<'a, Message> = match &page.image {
|
||||
Some(handle) => image(handle.clone()).width(Fill).height(Fill).into(),
|
||||
None => Space::new().width(Fill).height(Fill).into(),
|
||||
};
|
||||
let tile = column![
|
||||
container(picture)
|
||||
.width(Length::Fixed(100.0))
|
||||
.height(Length::Fixed(60.0))
|
||||
.padding(2)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(iced::Color::WHITE)),
|
||||
border: Border {
|
||||
width: if chosen { 2.0 } else { 1.0 },
|
||||
radius: 1.0.into(),
|
||||
color: if chosen {
|
||||
theme.palette().primary.base.color
|
||||
} else {
|
||||
theme.palette().background.strong.color
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
container(text(page.label.clone()).size(11))
|
||||
.width(Length::Fixed(100.0))
|
||||
.align_x(iced::Center)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: chosen
|
||||
.then(|| Background::Color(theme.palette().primary.weak.color)),
|
||||
..Default::default()
|
||||
}),
|
||||
]
|
||||
.spacing(1);
|
||||
button(tile)
|
||||
.on_press(msg(on_click(index)))
|
||||
.padding(2)
|
||||
.style(|_: &Theme, _| button::Style::default())
|
||||
.into()
|
||||
});
|
||||
container(scrollable(
|
||||
iced::widget::Row::with_children(tiles.collect::<Vec<_>>())
|
||||
.spacing(8)
|
||||
.wrap()
|
||||
.vertical_spacing(8),
|
||||
))
|
||||
.padding(6)
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(height))
|
||||
.style(frame_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn value_field<'a>(
|
||||
label: String,
|
||||
value: &'a str,
|
||||
enabled: bool,
|
||||
label_width: f32,
|
||||
on_input: impl Fn(String) -> Message + 'a,
|
||||
) -> Element<'a, Message> {
|
||||
let mut field = text_input("", value)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style);
|
||||
if enabled {
|
||||
field = field.on_input(on_input);
|
||||
}
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(Length::Fixed(label_width)),
|
||||
field
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn read_only<'a>(label: String, value: &'a str, label_width: f32) -> Element<'a, Message> {
|
||||
value_field(label, value, false, label_width, |_| Message::Noop)
|
||||
}
|
||||
|
||||
fn footer<'a>(ok: PdfDialogMsg, help: &'static str) -> iced::widget::Row<'a, Message> {
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
button(text(t!("OK")).size(11))
|
||||
.on_press(msg(ok))
|
||||
.style(button_style(true))
|
||||
.padding([4, 16]),
|
||||
button(text(t!("Cancel")).size(11))
|
||||
.on_press(Message::CloseModal)
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
button(text(t!("Help")).size(11))
|
||||
.on_press(msg(PdfDialogMsg::Help(help)))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12]),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
}
|
||||
|
||||
// ── Attach PDF Underlay ────────────────────────────────────────────────────
|
||||
|
||||
/// Choices the Attach dialog keeps for its next opening.
|
||||
#[derive(Clone)]
|
||||
pub struct AttachMemory {
|
||||
pub path_type: PathTypeChoice,
|
||||
pub insert_on_screen: bool,
|
||||
pub scale_on_screen: bool,
|
||||
pub scale: String,
|
||||
pub rotation_on_screen: bool,
|
||||
pub rotation: String,
|
||||
pub details: bool,
|
||||
}
|
||||
|
||||
impl Default for AttachMemory {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path_type: PathTypeChoice(Pathtype::Relative),
|
||||
insert_on_screen: true,
|
||||
scale_on_screen: false,
|
||||
scale: "1.0000".into(),
|
||||
rotation_on_screen: false,
|
||||
rotation: "0".into(),
|
||||
details: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static ATTACH_MEMORY: std::sync::Mutex<Option<AttachMemory>> = std::sync::Mutex::new(None);
|
||||
|
||||
pub struct PdfAttachState {
|
||||
/// The file read (absolute).
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
/// PDF files already attached: name and read path.
|
||||
pub existing: Vec<(String, String)>,
|
||||
pub name_combo: combo_box::State<String>,
|
||||
pub pages: Vec<PageThumb>,
|
||||
pub selected: Vec<usize>,
|
||||
pub anchor: usize,
|
||||
pub path_type: PathTypeChoice,
|
||||
pub insert_on_screen: bool,
|
||||
pub insert: [String; 3],
|
||||
pub scale_on_screen: bool,
|
||||
pub scale: String,
|
||||
pub rotation_on_screen: bool,
|
||||
pub rotation: String,
|
||||
pub found_in: String,
|
||||
pub saved_path: String,
|
||||
pub page_size: String,
|
||||
pub details: bool,
|
||||
}
|
||||
|
||||
impl PdfAttachState {
|
||||
pub fn new(existing: Vec<(String, String)>) -> Self {
|
||||
let memory = ATTACH_MEMORY
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|m| m.clone())
|
||||
.unwrap_or_default();
|
||||
let names = existing.iter().map(|(name, _)| name.clone()).collect();
|
||||
Self {
|
||||
path: String::new(),
|
||||
name: String::new(),
|
||||
existing,
|
||||
name_combo: combo_box::State::new(names),
|
||||
pages: Vec::new(),
|
||||
selected: vec![0],
|
||||
anchor: 0,
|
||||
path_type: memory.path_type,
|
||||
insert_on_screen: memory.insert_on_screen,
|
||||
insert: ["0.0000".into(), "0.0000".into(), "0.0000".into()],
|
||||
scale_on_screen: memory.scale_on_screen,
|
||||
scale: memory.scale,
|
||||
rotation_on_screen: memory.rotation_on_screen,
|
||||
rotation: memory.rotation,
|
||||
found_in: String::new(),
|
||||
saved_path: String::new(),
|
||||
page_size: String::new(),
|
||||
details: memory.details,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remember(&self) {
|
||||
if let Ok(mut m) = ATTACH_MEMORY.lock() {
|
||||
*m = Some(AttachMemory {
|
||||
path_type: self.path_type,
|
||||
insert_on_screen: self.insert_on_screen,
|
||||
scale_on_screen: self.scale_on_screen,
|
||||
scale: self.scale.clone(),
|
||||
rotation_on_screen: self.rotation_on_screen,
|
||||
rotation: self.rotation.clone(),
|
||||
details: self.details,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_labels(&self) -> Vec<String> {
|
||||
self.selected
|
||||
.iter()
|
||||
.filter_map(|i| self.pages.get(*i).map(|p| p.label.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_attach<'a>(
|
||||
state: &'a PdfAttachState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let name_combo = combo_box(
|
||||
&state.name_combo,
|
||||
"",
|
||||
(!state.name.is_empty()).then_some(&state.name),
|
||||
|chosen: String| msg(PdfDialogMsg::AttachName(chosen)),
|
||||
)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill);
|
||||
let name_section = column![
|
||||
text(t!("Name:")).size(11).style(muted_style),
|
||||
row![
|
||||
name_combo,
|
||||
button(text(t!("Browse...")).size(11))
|
||||
.on_press(msg(PdfDialogMsg::AttachBrowse))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])
|
||||
.width(Length::Fixed(96.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(3);
|
||||
|
||||
let pages = group(
|
||||
t!("Select one or more pages from the PDF file:").into_owned(),
|
||||
page_grid(&state.pages, &state.selected, PdfDialogMsg::AttachPage, 250.0),
|
||||
Length::Shrink,
|
||||
);
|
||||
|
||||
let path_type = group(
|
||||
t!("Path type").into_owned(),
|
||||
pick_list(Some(state.path_type), PathTypeChoice::ALL, |c| c.to_string())
|
||||
.on_select(|v| msg(PdfDialogMsg::AttachPathType(v)))
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill),
|
||||
Length::Shrink,
|
||||
);
|
||||
let insert_enabled = !state.insert_on_screen;
|
||||
let insertion = group(
|
||||
t!("Insertion point").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.insert_on_screen, |v| msg(
|
||||
PdfDialogMsg::AttachInsertOnScreen(v)
|
||||
)),
|
||||
value_field("X:".into(), &state.insert[0], insert_enabled, 16.0, |v| msg(
|
||||
PdfDialogMsg::AttachInsert(0, v)
|
||||
)),
|
||||
value_field("Y:".into(), &state.insert[1], insert_enabled, 16.0, |v| msg(
|
||||
PdfDialogMsg::AttachInsert(1, v)
|
||||
)),
|
||||
value_field("Z:".into(), &state.insert[2], insert_enabled, 16.0, |v| msg(
|
||||
PdfDialogMsg::AttachInsert(2, v)
|
||||
)),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let middle = column![path_type, insertion].spacing(6).width(Length::FillPortion(1));
|
||||
|
||||
let mut scale_field = text_input("", &state.scale)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style);
|
||||
if !state.scale_on_screen {
|
||||
scale_field = scale_field.on_input(|v| msg(PdfDialogMsg::AttachScale(v)));
|
||||
}
|
||||
let scale = group(
|
||||
t!("Scale").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.scale_on_screen, |v| msg(
|
||||
PdfDialogMsg::AttachScaleOnScreen(v)
|
||||
)),
|
||||
scale_field,
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let rotation = group(
|
||||
t!("Rotation").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify On-screen"), state.rotation_on_screen, |v| msg(
|
||||
PdfDialogMsg::AttachRotationOnScreen(v)
|
||||
)),
|
||||
value_field(
|
||||
t!("Angle:").into_owned(),
|
||||
&state.rotation,
|
||||
!state.rotation_on_screen,
|
||||
44.0,
|
||||
|v| msg(PdfDialogMsg::AttachRotation(v))
|
||||
),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
);
|
||||
let right = column![scale, rotation].spacing(6).width(Length::FillPortion(1));
|
||||
|
||||
let body = row![
|
||||
container(pages).width(Length::FillPortion(1)),
|
||||
middle,
|
||||
right
|
||||
]
|
||||
.spacing(6)
|
||||
.width(Fill);
|
||||
|
||||
let details: Element<'a, Message> = if state.details {
|
||||
group(
|
||||
t!("Location").into_owned(),
|
||||
column![
|
||||
read_only(t!("Found in:").into_owned(), &state.found_in, 92.0),
|
||||
read_only(t!("Saved path:").into_owned(), &state.saved_path, 92.0),
|
||||
read_only(t!("Page size:").into_owned(), &state.page_size, 92.0),
|
||||
]
|
||||
.spacing(5),
|
||||
Length::Shrink,
|
||||
)
|
||||
} else {
|
||||
Space::new().height(0).into()
|
||||
};
|
||||
|
||||
let details_label = if state.details {
|
||||
t!("Hide Details")
|
||||
} else {
|
||||
t!("Show Details")
|
||||
};
|
||||
let footer = row![button(text(details_label).size(11))
|
||||
.on_press(msg(PdfDialogMsg::AttachDetails(!state.details)))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])]
|
||||
.push(footer(PdfDialogMsg::AttachOk, "attach"))
|
||||
.align_y(iced::Center);
|
||||
|
||||
column![name_section, body, details, footer]
|
||||
.spacing(6)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Underlay Layers ────────────────────────────────────────────────────────
|
||||
|
||||
/// One PDF underlay the dialog can show: its name, its file's layers, and
|
||||
/// which of them it turns off.
|
||||
#[derive(Clone)]
|
||||
pub struct LayerTarget {
|
||||
pub name: String,
|
||||
pub handle: acadrust::Handle,
|
||||
pub layers: Vec<String>,
|
||||
pub hidden: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct UnderlayLayersState {
|
||||
pub targets: Vec<LayerTarget>,
|
||||
pub current: usize,
|
||||
pub search: String,
|
||||
}
|
||||
|
||||
pub fn view_layers<'a>(
|
||||
state: &'a UnderlayLayersState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let names: Vec<String> = state.targets.iter().map(|t| t.name.clone()).collect();
|
||||
let target = state.targets.get(state.current);
|
||||
let reference = row![
|
||||
text(t!("Reference name:")).size(11).width(Length::Fixed(110.0)),
|
||||
pick_list(target.map(|t| t.name.clone()), names, |n: &String| n.clone())
|
||||
.on_select(|n| msg(PdfDialogMsg::LayersUnderlay(n)))
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
let search = text_input(t!("Search for layer").as_ref(), &state.search)
|
||||
.on_input(|s| msg(PdfDialogMsg::LayersSearch(s)))
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(220.0))
|
||||
.style(field_style);
|
||||
|
||||
let header = row![
|
||||
text(t!("On")).size(11).style(muted_style).width(Length::Fixed(44.0)),
|
||||
text(t!("Name")).size(11).style(muted_style),
|
||||
]
|
||||
.padding([4, 8]);
|
||||
let body: Element<'a, Message> = match target {
|
||||
Some(t) if !t.layers.is_empty() => {
|
||||
let needle = state.search.to_lowercase();
|
||||
let rows = t
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|name| needle.is_empty() || name.to_lowercase().contains(&needle))
|
||||
.map(|name| {
|
||||
let on = !t.hidden.contains(name);
|
||||
let bulb = button(text("●").size(12).style(move |theme: &Theme| text::Style {
|
||||
color: Some(if on {
|
||||
theme.palette().warning.base.color
|
||||
} else {
|
||||
theme.palette().background.strong.color
|
||||
}),
|
||||
}))
|
||||
.on_press(msg(PdfDialogMsg::LayersToggle(name.clone())))
|
||||
.padding([0, 4])
|
||||
.style(|_: &Theme, _| button::Style::default());
|
||||
row![
|
||||
container(bulb).width(Length::Fixed(44.0)),
|
||||
text(name.clone()).size(11)
|
||||
]
|
||||
.padding([2, 8])
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
});
|
||||
scrollable(iced::widget::Column::with_children(rows.collect::<Vec<_>>()))
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
_ => container(text(t!("This file does not contain any layers.")).size(11))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.align_x(iced::Center)
|
||||
.align_y(iced::Center)
|
||||
.into(),
|
||||
};
|
||||
let list = container(column![header, body])
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(300.0))
|
||||
.style(frame_style);
|
||||
|
||||
column![
|
||||
text(t!("Select an underlay to view its layers.")).size(11),
|
||||
reference,
|
||||
search,
|
||||
list,
|
||||
footer(PdfDialogMsg::LayersOk, "layers"),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── PDF Import Settings ────────────────────────────────────────────────────
|
||||
|
||||
fn settings_groups<'a>(s: &'a PdfImportSettings) -> (Element<'a, Message>, Element<'a, Message>, Element<'a, Message>) {
|
||||
let fills: Element<'a, Message> = if s.vector {
|
||||
labeled_checkbox(t!("Solid fills"), s.fills, |v| msg(PdfDialogMsg::Fills(v)))
|
||||
} else {
|
||||
labeled_checkbox(t!("Solid fills"), s.fills, move |_| Message::Noop)
|
||||
};
|
||||
let data = group(
|
||||
t!("PDF data to import").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Vector geometry"), s.vector, |v| msg(PdfDialogMsg::Vector(v))),
|
||||
container(fills).padding(iced::Padding { left: 20.0, ..Default::default() }),
|
||||
labeled_checkbox(t!("TrueType text"), s.text, |v| msg(PdfDialogMsg::Text(v))),
|
||||
labeled_checkbox(t!("Raster images"), s.raster, |v| msg(PdfDialogMsg::Raster(v))),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Fill,
|
||||
);
|
||||
let layer_radio = |label: std::borrow::Cow<'static, str>, value: ImportLayers| {
|
||||
radio(label, value, Some(s.layers), |v| msg(PdfDialogMsg::Layers(v)))
|
||||
.size(14)
|
||||
.text_size(11)
|
||||
};
|
||||
let layers = group(
|
||||
t!("Layers").into_owned(),
|
||||
column![
|
||||
layer_radio(t!("Use PDF layers"), ImportLayers::Pdf),
|
||||
layer_radio(t!("Create object layers"), ImportLayers::Object),
|
||||
layer_radio(t!("Current layer"), ImportLayers::Current),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Fill,
|
||||
);
|
||||
let options = group(
|
||||
t!("Import options").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Import as block"), s.as_block, |v| msg(PdfDialogMsg::AsBlock(v))),
|
||||
labeled_checkbox(t!("Join line and arc segments"), s.join, |v| msg(
|
||||
PdfDialogMsg::Join(v)
|
||||
)),
|
||||
labeled_checkbox(t!("Convert solid fills to hatches"), s.hatches, |v| msg(
|
||||
PdfDialogMsg::Hatches(v)
|
||||
)),
|
||||
labeled_checkbox(t!("Apply lineweight properties"), s.lineweights, |v| msg(
|
||||
PdfDialogMsg::Lineweights(v)
|
||||
)),
|
||||
labeled_checkbox(t!("Infer linetypes from collinear dashes"), s.linetypes, |v| msg(
|
||||
PdfDialogMsg::Linetypes(v)
|
||||
)),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Shrink,
|
||||
);
|
||||
(data, layers, options)
|
||||
}
|
||||
|
||||
pub fn view_import_settings<'a>(
|
||||
settings: &'a PdfImportSettings,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let (data, layers, options) = settings_groups(settings);
|
||||
let side_button = |label: std::borrow::Cow<'static, str>, on: Message, accent: bool| {
|
||||
button(text(label).size(11).width(Fill).align_x(iced::Center))
|
||||
.on_press(on)
|
||||
.style(button_style(accent))
|
||||
.padding([4, 12])
|
||||
.width(Length::Fixed(110.0))
|
||||
};
|
||||
let left = column![
|
||||
row![data, layers].spacing(8).height(Length::Fixed(140.0)),
|
||||
options
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Fill);
|
||||
let right = column![
|
||||
side_button(t!("OK"), msg(PdfDialogMsg::SettingsOk), true),
|
||||
side_button(t!("Cancel"), Message::CloseModal, false),
|
||||
side_button(t!("Options..."), msg(PdfDialogMsg::Options), false),
|
||||
side_button(t!("Help"), msg(PdfDialogMsg::Help("settings")), false),
|
||||
]
|
||||
.spacing(8);
|
||||
row![left, right]
|
||||
.spacing(10)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
// ── Import PDF (File) ──────────────────────────────────────────────────────
|
||||
|
||||
pub struct PdfImportFileState {
|
||||
pub path: String,
|
||||
pub pages: Vec<PageThumb>,
|
||||
pub selected: usize,
|
||||
pub page_text: String,
|
||||
pub insert_on_screen: bool,
|
||||
pub scale: String,
|
||||
pub rotation: RotationChoice,
|
||||
pub page_size: String,
|
||||
pub settings: PdfImportSettings,
|
||||
}
|
||||
|
||||
pub fn view_import_file<'a>(
|
||||
state: &'a PdfImportFileState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let file_name = std::path::Path::new(&state.path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let file_row = row![
|
||||
text(t!("File name:")).size(11).width(Length::Fixed(70.0)),
|
||||
text_input("", &file_name)
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill)
|
||||
.style(field_style),
|
||||
button(text(t!("Browse...")).size(11))
|
||||
.on_press(msg(PdfDialogMsg::ImportBrowse))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])
|
||||
.width(Length::Fixed(96.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
|
||||
let selected = [state.selected];
|
||||
let page_group = group(
|
||||
t!("Page to import").into_owned(),
|
||||
column![
|
||||
row![
|
||||
text(t!("Page:")).size(11),
|
||||
text_input("", &state.page_text)
|
||||
.on_input(|s| msg(PdfDialogMsg::ImportPageText(s)))
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(56.0))
|
||||
.style(field_style),
|
||||
text(crate::tf!("Total: {count}", count = state.pages.len()))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
page_grid(&state.pages, &selected, PdfDialogMsg::ImportPage, 300.0),
|
||||
row![
|
||||
text(crate::tf!("Page size: {size}", size = state.page_size.clone()))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
Space::new().width(Fill),
|
||||
text(crate::tf!("PDF scale: {scale}:1", scale = state.scale.trim()))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
],
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Shrink,
|
||||
);
|
||||
|
||||
let location = group(
|
||||
t!("Location").into_owned(),
|
||||
column![
|
||||
labeled_checkbox(t!("Specify insertion point on-screen"), state.insert_on_screen, |v| {
|
||||
msg(PdfDialogMsg::ImportInsertOnScreen(v))
|
||||
}),
|
||||
row![
|
||||
text(t!("Scale:")).size(11).style(muted_style),
|
||||
text_input("", &state.scale)
|
||||
.on_input(|s| msg(PdfDialogMsg::ImportScale(s)))
|
||||
.size(11)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(80.0))
|
||||
.style(field_style),
|
||||
Space::new().width(Length::Fixed(20.0)),
|
||||
text(t!("Rotation:")).size(11).style(muted_style),
|
||||
pick_list(Some(state.rotation), RotationChoice::ALL, |c| c.to_string())
|
||||
.on_select(|v| msg(PdfDialogMsg::ImportRotation(v)))
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fixed(76.0)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(6),
|
||||
Length::Shrink,
|
||||
);
|
||||
let (data, layers, options) = settings_groups(&state.settings);
|
||||
let right = column![
|
||||
location,
|
||||
row![data, layers].spacing(8).height(Length::Fixed(140.0)),
|
||||
options
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Length::FillPortion(1));
|
||||
let body = row![container(page_group).width(Length::FillPortion(1)), right].spacing(8);
|
||||
|
||||
let footer = row![button(text(t!("Options...")).size(11))
|
||||
.on_press(msg(PdfDialogMsg::Options))
|
||||
.style(button_style(false))
|
||||
.padding([4, 12])]
|
||||
.push(footer(PdfDialogMsg::ImportOk, "import"))
|
||||
.align_y(iced::Center);
|
||||
|
||||
column![file_row, body, footer]
|
||||
.spacing(8)
|
||||
.padding([8, 10])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user