style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+7 -10
View File
@@ -13,9 +13,7 @@ fn primary_style(theme: &Theme) -> iced::widget::text::Style {
fn surface_style(theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color,
)),
background: Some(Background::Color(theme.palette().background.weakest.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
@@ -53,7 +51,10 @@ fn info_row<'a>(
) -> Element<'a, Message> {
container(
row![
text(label).size(11).style(muted_style).width(Length::Fixed(110.0)),
text(label)
.size(11)
.style(muted_style)
.width(Length::Fixed(110.0)),
text(value).size(13).width(Fill),
]
.spacing(8)
@@ -169,9 +170,7 @@ fn build_label() -> std::borrow::Cow<'static, str> {
}
}
pub fn view_window(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
pub fn view_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Message> {
// The hero carries the full build identity (`2026.36+194.gef189d77`);
// the cards split it into the release version and the build details.
let version = format!("v{}", env!("OCS_FULL_VERSION"));
@@ -260,9 +259,7 @@ pub fn view_window(
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.into()
+40 -33
View File
@@ -7,10 +7,10 @@
//! unsaved-changes guard on close.
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Length, Theme};
use crate::t;
/// Which column of an alias row a text edit targets.
#[derive(Clone, Copy, Debug)]
@@ -37,18 +37,16 @@ fn danger_input_style(
let danger = theme.palette().danger.base;
iced::widget::text_input::Style {
background: Background::Color(theme.palette().danger.weak.color),
border: iced::border::rounded(4)
.color(danger.color)
.width(
if matches!(
status,
iced::widget::text_input::Status::Focused { is_hovered: _ }
) {
1.5
} else {
1.0
},
),
border: iced::border::rounded(4).color(danger.color).width(
if matches!(
status,
iced::widget::text_input::Status::Focused { is_hovered: _ }
) {
1.5
} else {
1.0
},
),
icon: danger.color,
placeholder: danger.color.scale_alpha(0.7),
value: danger.text,
@@ -77,7 +75,12 @@ pub fn view_window<'a>(
// Right gutter reserved so the scrollbar has its own lane and never sits on
// top of the row delete (✕) buttons. Applied to both the header and the
// scrollable rows so the columns stay aligned.
let gutter = iced::Padding { top: 0.0, right: GUTTER, bottom: 0.0, left: 0.0 };
let gutter = iced::Padding {
top: 0.0,
right: GUTTER,
bottom: 0.0,
left: 0.0,
};
let head = container(
row![
@@ -94,7 +97,11 @@ pub fn view_window<'a>(
let alias_key = alias.trim().to_uppercase();
let duplicate = !alias_key.is_empty() && duplicate_aliases.contains(alias_key.as_str());
let alias_box = text_input(t!("alias").as_ref(), alias)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Alias, value: v })
.on_input(move |v| Message::AliasEditorInput {
idx,
field: AliasField::Alias,
value: v,
})
.size(13)
.padding([3, 6])
.width(Length::Fixed(120.0));
@@ -103,10 +110,13 @@ pub fn view_window<'a>(
} else {
alias_box
};
let unknown_command =
!cmd.trim().is_empty() && unknown_commands.contains(cmd.trim());
let unknown_command = !cmd.trim().is_empty() && unknown_commands.contains(cmd.trim());
let cmd_box = text_input(t!("command").as_ref(), cmd)
.on_input(move |v| Message::AliasEditorInput { idx, field: AliasField::Command, value: v })
.on_input(move |v| Message::AliasEditorInput {
idx,
field: AliasField::Command,
value: v,
})
.size(13)
.padding([3, 6])
.width(sizing.width);
@@ -261,9 +271,15 @@ pub fn view_window<'a>(
Space::new().height(6),
conflict_banner,
Space::new().height(4),
row![stats, add_area, reset_area, Space::new().width(sizing.width), apply_area]
.spacing(8)
.align_y(iced::Center),
row![
stats,
add_area,
reset_area,
Space::new().width(sizing.width),
apply_area
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(6)
.width(sizing.width)
@@ -273,9 +289,7 @@ pub fn view_window<'a>(
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
});
@@ -290,12 +304,7 @@ pub fn view_window<'a>(
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
theme.palette().background.strongest.color.scale_alpha(0.55),
)),
..Default::default()
}),
@@ -323,9 +332,7 @@ pub fn view_window<'a>(
.padding(16)
.width(Length::Fixed(320.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
border: iced::Border {
color: theme.palette().background.neutral.color,
width: 1.0,
+648 -123
View File
@@ -8,7 +8,9 @@ use std::collections::BTreeSet;
use std::fmt;
use codec::types::Handle;
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Border, Element, Fill, Length, Theme};
use crate::app::Message;
@@ -348,14 +350,46 @@ impl Default for DataExtractionState {
selection_handles: Vec::new(),
objects: Vec::new(),
properties: vec![
ExtractionProperty { key: "type".into(), name: "Type".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "handle".into(), name: "Handle".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "layer".into(), name: "Layer".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "color".into(), name: "Color".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "linetype".into(), name: "Linetype".into(), category: "General".into(), checked: true },
ExtractionProperty { key: "details".into(), name: "Details".into(), category: "Geometry".into(), checked: true },
ExtractionProperty {
key: "type".into(),
name: "Type".into(),
category: "General".into(),
checked: true,
},
ExtractionProperty {
key: "handle".into(),
name: "Handle".into(),
category: "General".into(),
checked: true,
},
ExtractionProperty {
key: "layer".into(),
name: "Layer".into(),
category: "General".into(),
checked: true,
},
ExtractionProperty {
key: "color".into(),
name: "Color".into(),
category: "General".into(),
checked: true,
},
ExtractionProperty {
key: "linetype".into(),
name: "Linetype".into(),
category: "General".into(),
checked: true,
},
ExtractionProperty {
key: "details".into(),
name: "Details".into(),
category: "Geometry".into(),
checked: true,
},
],
categories: ["General".to_string(), "Geometry".to_string()].into_iter().collect(),
categories: ["General".to_string(), "Geometry".to_string()]
.into_iter()
.collect(),
combine_identical: false,
show_count: true,
show_name: true,
@@ -402,15 +436,32 @@ fn group<'a>(title: impl Into<String>, body: Element<'a, Message>) -> Element<'a
.padding(9)
.width(Fill)
.style(|theme: &Theme| container::Style {
border: Border { width: 1.0, radius: 4.0.into(), color: theme.palette().background.strong.color },
border: Border {
width: 1.0,
radius: 4.0.into(),
color: theme.palette().background.strong.color,
},
..Default::default()
})
.into()
}
fn labeled_input<'a>(label: impl Into<String>, value: &'a str, msg: fn(String) -> Message) -> Element<'a, Message> {
row![text(label.into()).size(11).style(muted).width(118), text_input("", value).on_input(msg).padding([4, 7]).size(12).width(Fill)]
.spacing(8).align_y(iced::Center).into()
fn labeled_input<'a>(
label: impl Into<String>,
value: &'a str,
msg: fn(String) -> Message,
) -> Element<'a, Message> {
row![
text(label.into()).size(11).style(muted).width(118),
text_input("", value)
.on_input(msg)
.padding([4, 7])
.size(12)
.width(Fill)
]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn cell_style_picker<'a>(
@@ -422,7 +473,11 @@ fn cell_style_picker<'a>(
text(label.into()).size(11).style(muted).width(118),
pick_list(
Some(selected.to_string()),
vec!["Title".to_string(), "Header".to_string(), "Data".to_string()],
vec![
"Title".to_string(),
"Header".to_string(),
"Data".to_string()
],
|value| value.clone(),
)
.on_select(msg)
@@ -435,114 +490,340 @@ fn cell_style_picker<'a>(
.into()
}
fn radio_button<'a>(selected: bool, label: impl Into<String>, msg: Message) -> Element<'a, Message> {
fn radio_button<'a>(
selected: bool,
label: impl Into<String>,
msg: Message,
) -> Element<'a, Message> {
form_radio(label, true, selected.then_some(true), move |_| msg.clone()).into()
}
fn preview_grid<'a>(rows: &'a [Vec<String>]) -> Element<'a, Message> {
if rows.is_empty() {
return text(t!("No preview available.")).size(11).style(muted).into();
return text(t!("No preview available."))
.size(11)
.style(muted)
.into();
}
let lines = rows.iter().take(8).map(|row| row.join(" | ")).collect::<Vec<_>>().join("\n");
container(text(lines).size(11)).padding(6).width(Fill).into()
let lines = rows
.iter()
.take(8)
.map(|row| row.join(" | "))
.collect::<Vec<_>>()
.join("\n");
container(text(lines).size(11))
.padding(6)
.width(Fill)
.into()
}
pub fn table_insert_view(state: &TableInsertState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let style = pick_list(Some(state.style.clone()), state.styles.clone(), |value| value.clone()).on_select(Message::TableInsertStyle).padding([3, 6]).text_size(12).width(Fill);
pub fn table_insert_view(
state: &TableInsertState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
let style = pick_list(Some(state.style.clone()), state.styles.clone(), |value| {
value.clone()
})
.on_select(Message::TableInsertStyle)
.padding([3, 6])
.text_size(12)
.width(Fill);
let source = column![
radio_button(state.source == TableSource::Empty, t!("Start from empty table"), Message::TableInsertField(TableInsertField::Source(TableSource::Empty))),
radio_button(state.source == TableSource::DataLink, t!("Start from data link"), Message::TableInsertField(TableInsertField::Source(TableSource::DataLink))),
radio_button(state.source == TableSource::DataExtraction, t!("Start from data extraction"), Message::TableInsertField(TableInsertField::Source(TableSource::DataExtraction))),
].spacing(1);
radio_button(
state.source == TableSource::Empty,
t!("Start from empty table"),
Message::TableInsertField(TableInsertField::Source(TableSource::Empty))
),
radio_button(
state.source == TableSource::DataLink,
t!("Start from data link"),
Message::TableInsertField(TableInsertField::Source(TableSource::DataLink))
),
radio_button(
state.source == TableSource::DataExtraction,
t!("Start from data extraction"),
Message::TableInsertField(TableInsertField::Source(TableSource::DataExtraction))
),
]
.spacing(1);
let link_picker: Element<'_, Message> = if state.source == TableSource::DataLink {
row![
text(t!("Data link")).size(11).style(muted).width(118),
pick_list(state.link.clone(), state.links.clone(), |value| value.to_string()).on_select(|value| Message::TableInsertField(TableInsertField::Link(value))).padding([3, 6]).text_size(12).width(Fill),
button(text(t!("Manage…")).size(11)).on_press(Message::DataLinkManagerOpen).padding([4, 8]),
].spacing(8).align_y(iced::Center).into()
} else { Space::new().height(0).into() };
pick_list(state.link.clone(), state.links.clone(), |value| value
.to_string())
.on_select(|value| Message::TableInsertField(TableInsertField::Link(value)))
.padding([3, 6])
.text_size(12)
.width(Fill),
button(text(t!("Manage…")).size(11))
.on_press(Message::DataLinkManagerOpen)
.padding([4, 8]),
]
.spacing(8)
.align_y(iced::Center)
.into()
} else {
Space::new().height(0).into()
};
let insertion = column![
radio_button(state.insertion == TableInsertion::Point, t!("Specify insertion point"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Point))),
radio_button(state.insertion == TableInsertion::Window, t!("Specify window"), Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Window))),
radio_button(
state.insertion == TableInsertion::Point,
t!("Specify insertion point"),
Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Point))
),
radio_button(
state.insertion == TableInsertion::Window,
t!("Specify window"),
Message::TableInsertField(TableInsertField::Insertion(TableInsertion::Window))
),
];
let settings = column![
labeled_input(t!("Columns"), &state.columns, |v| Message::TableInsertField(TableInsertField::Columns(v))),
labeled_input(t!("Column width"), &state.column_width, |v| Message::TableInsertField(TableInsertField::ColumnWidth(v))),
labeled_input(t!("Data rows"), &state.data_rows, |v| Message::TableInsertField(TableInsertField::DataRows(v))),
labeled_input(t!("Row height"), &state.row_height, |v| Message::TableInsertField(TableInsertField::RowHeight(v))),
].spacing(5);
labeled_input(
t!("Columns"),
&state.columns,
|v| Message::TableInsertField(TableInsertField::Columns(v))
),
labeled_input(t!("Column width"), &state.column_width, |v| {
Message::TableInsertField(TableInsertField::ColumnWidth(v))
}),
labeled_input(t!("Data rows"), &state.data_rows, |v| {
Message::TableInsertField(TableInsertField::DataRows(v))
}),
labeled_input(t!("Row height"), &state.row_height, |v| {
Message::TableInsertField(TableInsertField::RowHeight(v))
}),
]
.spacing(5);
let cell_styles = column![
cell_style_picker(t!("First row"), &state.first_row_style, |v| Message::TableInsertField(TableInsertField::FirstRowStyle(v))),
cell_style_picker(t!("Second row"), &state.second_row_style, |v| Message::TableInsertField(TableInsertField::SecondRowStyle(v))),
cell_style_picker(t!("All other rows"), &state.other_row_style, |v| Message::TableInsertField(TableInsertField::OtherRowStyle(v))),
].spacing(5);
cell_style_picker(t!("First row"), &state.first_row_style, |v| {
Message::TableInsertField(TableInsertField::FirstRowStyle(v))
}),
cell_style_picker(t!("Second row"), &state.second_row_style, |v| {
Message::TableInsertField(TableInsertField::SecondRowStyle(v))
}),
cell_style_picker(t!("All other rows"), &state.other_row_style, |v| {
Message::TableInsertField(TableInsertField::OtherRowStyle(v))
}),
]
.spacing(5);
column![
group(t!("Table style"), row![style].into()),
group(t!("Insert options"), column![source, link_picker].spacing(4).into()),
row![group(t!("Insertion behavior"), insertion.into()), group(t!("Column & row settings"), settings.into())].spacing(8),
group(
t!("Insert options"),
column![source, link_picker].spacing(4).into()
),
row![
group(t!("Insertion behavior"), insertion.into()),
group(t!("Column & row settings"), settings.into())
]
.spacing(8),
group(t!("Set cell styles"), cell_styles.into()),
checkbox(state.preview).label(t!("Preview")).on_toggle(|v| Message::TableInsertField(TableInsertField::Preview(v))).size(14),
checkbox(state.preview)
.label(t!("Preview"))
.on_toggle(|v| Message::TableInsertField(TableInsertField::Preview(v)))
.size(14),
text(&state.error).size(11),
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("OK"), Message::TableInsertApply, true),
].spacing(8),
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
]
.spacing(8),
]
.spacing(8)
.padding(10)
.width(sizing.width)
.height(sizing.height)
.into()
}
pub fn data_link_view(state: &DataLinkManagerState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
pub fn data_link_view(
state: &DataLinkManagerState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
let mut links = column![text(t!("Spreadsheet Links")).size(11).style(muted)].spacing(2);
links = links.push(button(text(t!("Create a New Data Link")).size(12)).on_press(Message::DataLinkNew).padding([5, 7]).style(button::text).width(Fill));
links = links.push(
button(text(t!("Create a New Data Link")).size(12))
.on_press(Message::DataLinkNew)
.padding([5, 7])
.style(button::text)
.width(Fill),
);
for link in &state.links {
let selected = state.selected.as_ref().is_some_and(|v| v.handle == link.handle);
let label = if selected { format!("● {link}") } else { format!(" {link}") };
links = links.push(button(text(label).size(12)).on_press(Message::DataLinkSelect(link.handle)).padding([5, 7]).style(button::text).width(Fill));
let selected = state
.selected
.as_ref()
.is_some_and(|v| v.handle == link.handle);
let label = if selected {
format!("● {link}")
} else {
format!(" {link}")
};
links = links.push(
button(text(label).size(12))
.on_press(Message::DataLinkSelect(link.handle))
.padding([5, 7])
.style(button::text)
.width(Fill),
);
}
let tree = container(scrollable(links).height(Fill)).padding(6).width(Length::Fixed(220.0)).height(Fill);
let tree = container(scrollable(links).height(Fill))
.padding(6)
.width(Length::Fixed(220.0))
.height(Fill);
let right: Element<'_, Message> = if state.editing {
let mut editor = column![
labeled_input(t!("Data link name"), &state.name, |v| Message::DataLinkField(DataLinkField::Name(v))),
row![text(t!("File")).size(11).style(muted).width(118), text_input("", &state.path).on_input(|v| Message::DataLinkField(DataLinkField::Path(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataLinkBrowse).padding([4, 9])].spacing(6).align_y(iced::Center),
row![text(t!("Path type")).size(11).style(muted).width(118), pick_list(Some(state.path_type), vec![LinkPathType::Full, LinkPathType::Relative, LinkPathType::FileName], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::PathType(value))).width(Fill).padding([3, 6])].spacing(8),
row![text(t!("Link option")).size(11).style(muted).width(118), pick_list(Some(state.range_kind), vec![LinkRange::EntireSheet, LinkRange::NamedRange, LinkRange::CellRange], |value| value.to_string()).on_select(|value| Message::DataLinkField(DataLinkField::RangeKind(value))).width(Fill).padding([3, 6])].spacing(8),
].spacing(6);
labeled_input(t!("Data link name"), &state.name, |v| {
Message::DataLinkField(DataLinkField::Name(v))
}),
row![
text(t!("File")).size(11).style(muted).width(118),
text_input("", &state.path)
.on_input(|v| Message::DataLinkField(DataLinkField::Path(v)))
.padding([4, 7])
.size(12)
.width(Fill),
button(text("…"))
.on_press(Message::DataLinkBrowse)
.padding([4, 9])
]
.spacing(6)
.align_y(iced::Center),
row![
text(t!("Path type")).size(11).style(muted).width(118),
pick_list(
Some(state.path_type),
vec![
LinkPathType::Full,
LinkPathType::Relative,
LinkPathType::FileName
],
|value| value.to_string()
)
.on_select(|value| Message::DataLinkField(DataLinkField::PathType(value)))
.width(Fill)
.padding([3, 6])
]
.spacing(8),
row![
text(t!("Link option")).size(11).style(muted).width(118),
pick_list(
Some(state.range_kind),
vec![
LinkRange::EntireSheet,
LinkRange::NamedRange,
LinkRange::CellRange
],
|value| value.to_string()
)
.on_select(|value| Message::DataLinkField(DataLinkField::RangeKind(value)))
.width(Fill)
.padding([3, 6])
]
.spacing(8),
]
.spacing(6);
if !state.sheets.is_empty() && state.range_kind != LinkRange::NamedRange {
editor = editor.push(row![
text(t!("Sheet")).size(11).style(muted).width(118),
pick_list(Some(state.sheet.clone()), state.sheets.clone(), |value| value.clone())
editor = editor.push(
row![
text(t!("Sheet")).size(11).style(muted).width(118),
pick_list(Some(state.sheet.clone()), state.sheets.clone(), |value| {
value.clone()
})
.on_select(|value| Message::DataLinkField(DataLinkField::Sheet(value)))
.width(Fill)
.padding([3, 6]),
].spacing(8));
]
.spacing(8),
);
}
if state.range_kind == LinkRange::NamedRange && !state.named_ranges.is_empty() {
editor = editor.push(row![
text(t!("Named range")).size(11).style(muted).width(118),
pick_list(Some(state.range.clone()), state.named_ranges.clone(), |value| value.clone())
editor = editor.push(
row![
text(t!("Named range")).size(11).style(muted).width(118),
pick_list(
Some(state.range.clone()),
state.named_ranges.clone(),
|value| value.clone()
)
.on_select(|value| Message::DataLinkField(DataLinkField::Range(value)))
.width(Fill)
.padding([3, 6]),
].spacing(8));
]
.spacing(8),
);
} else if state.range_kind != LinkRange::EntireSheet {
editor = editor.push(labeled_input(t!("Range"), &state.range, |v| Message::DataLinkField(DataLinkField::Range(v))));
editor = editor.push(labeled_input(t!("Range"), &state.range, |v| {
Message::DataLinkField(DataLinkField::Range(v))
}));
}
editor = editor.push(checkbox(state.more_options).label(t!("More options")).on_toggle(|v| Message::DataLinkField(DataLinkField::MoreOptions(v))).size(14));
editor = editor.push(
checkbox(state.more_options)
.label(t!("More options"))
.on_toggle(|v| Message::DataLinkField(DataLinkField::MoreOptions(v)))
.size(14),
);
if state.more_options {
editor = editor
.push(checkbox(state.allow_write).label(t!("Allow writing to source file")).on_toggle(|v| Message::DataLinkField(DataLinkField::AllowWrite(v))).size(14))
.push(checkbox(state.use_source_formatting).label(t!("Use source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UseSourceFormatting(v))).size(14))
.push(checkbox(state.update_source_formatting).label(t!("Keep table updated to source formatting")).on_toggle(|v| Message::DataLinkField(DataLinkField::UpdateSourceFormatting(v))).size(14));
.push(
checkbox(state.allow_write)
.label(t!("Allow writing to source file"))
.on_toggle(|v| Message::DataLinkField(DataLinkField::AllowWrite(v)))
.size(14),
)
.push(
checkbox(state.use_source_formatting)
.label(t!("Use source formatting"))
.on_toggle(|v| {
Message::DataLinkField(DataLinkField::UseSourceFormatting(v))
})
.size(14),
)
.push(
checkbox(state.update_source_formatting)
.label(t!("Keep table updated to source formatting"))
.on_toggle(|v| {
Message::DataLinkField(DataLinkField::UpdateSourceFormatting(v))
})
.size(14),
);
}
editor = editor.push(checkbox(state.insert_table).label(t!("Insert table after saving link")).on_toggle(|v| Message::DataLinkField(DataLinkField::InsertTable(v))).size(14));
editor = editor.push(
checkbox(state.insert_table)
.label(t!("Insert table after saving link"))
.on_toggle(|v| Message::DataLinkField(DataLinkField::InsertTable(v)))
.size(14),
);
group(t!("Link settings"), editor.into())
} else if let Some(link) = &state.selected {
column![
group(t!("Details"), column![text(format!("{}: {}", t!("Name"), link.name)).size(11), text(format!("{}: {}", t!("File"), link.path)).size(11), text(if link.valid { t!("Status: Linked") } else { t!("Status: Source not found") }).size(11)].spacing(5).into()),
group(
t!("Details"),
column![
text(format!("{}: {}", t!("Name"), link.name)).size(11),
text(format!("{}: {}", t!("File"), link.path)).size(11),
text(if link.valid {
t!("Status: Linked")
} else {
t!("Status: Source not found")
})
.size(11)
]
.spacing(5)
.into()
),
group(t!("Preview"), preview_grid(&state.preview)),
].spacing(8).into()
]
.spacing(8)
.into()
} else {
group(t!("Details"), text(t!("Select a data link or create a new one.")).size(11).style(muted).into())
group(
t!("Details"),
text(t!("Select a data link or create a new one."))
.size(11)
.style(muted)
.into(),
)
};
let actions = if state.editing {
row![
@@ -553,14 +834,36 @@ pub fn data_link_view(state: &DataLinkManagerState, sizing: crate::ui::modal::Mo
.spacing(8)
} else {
let enabled = state.selected.is_some();
let edit = dialog_button_styled_opt(t!("Edit"), enabled.then_some(Message::DataLinkEdit), button::secondary);
let delete = dialog_button_styled_opt(t!("Delete"), enabled.then_some(Message::DataLinkDelete), button::secondary);
let insert = dialog_button_styled_opt(t!("Insert Table"), enabled.then_some(Message::DataLinkInsert), button::primary);
let edit = dialog_button_styled_opt(
t!("Edit"),
enabled.then_some(Message::DataLinkEdit),
button::secondary,
);
let delete = dialog_button_styled_opt(
t!("Delete"),
enabled.then_some(Message::DataLinkDelete),
button::secondary,
);
let insert = dialog_button_styled_opt(
t!("Insert Table"),
enabled.then_some(Message::DataLinkInsert),
button::primary,
);
let close = dialog_button(t!("Close"), Message::DataLinkClose, false);
row![edit, delete, Space::new().width(Fill), insert, close].spacing(8)
};
column![row![tree, container(right).width(Fill).height(Fill)].spacing(8).height(Fill), text(&state.status).size(11), actions]
.spacing(7).padding(10).width(sizing.width).height(sizing.height).into()
column![
row![tree, container(right).width(Fill).height(Fill)]
.spacing(8)
.height(Fill),
text(&state.status).size(11),
actions
]
.spacing(7)
.padding(10)
.width(sizing.width)
.height(sizing.height)
.into()
}
fn wizard_nav(state: &DataExtractionState) -> iced::widget::Row<'_, Message> {
@@ -576,45 +879,132 @@ fn wizard_nav(state: &DataExtractionState) -> iced::widget::Row<'_, Message> {
} else {
t!("Next")
},
state.error.is_empty().then_some(if state.page == ExtractionPage::Finish {
Message::DataExtractionFinish
} else {
Message::DataExtractionNext
}),
state
.error
.is_empty()
.then_some(if state.page == ExtractionPage::Finish {
Message::DataExtractionFinish
} else {
Message::DataExtractionNext
}),
button::primary,
);
row![Space::new().width(Fill), back, cancel, next].spacing(8)
}
pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
pub fn data_extraction_view(
state: &DataExtractionState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'_, Message> {
let add_folder = button(text(t!("Add folder…")).size(12)).padding([5, 10]);
#[cfg(not(target_arch = "wasm32"))]
let add_folder = add_folder.on_press(Message::DataExtractionAddFolder);
let body: Element<'_, Message> = match state.page {
ExtractionPage::Begin => group(t!("Begin"), column![
radio_button(state.begin == ExtractionBegin::New, t!("Create a new data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::New))),
radio_button(state.begin == ExtractionBegin::Template, t!("Use a previous extraction as a template"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Template))),
radio_button(state.begin == ExtractionBegin::Edit, t!("Edit an existing data extraction"), Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Edit))),
row![text(t!("Extraction settings")).size(11).style(muted).width(118), text_input("", &state.settings_path).on_input(|v| Message::DataExtractionField(DataExtractionField::SettingsPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseSettings).padding([4, 9])].spacing(6).align_y(iced::Center),
].spacing(5).into()),
ExtractionPage::Source => group(t!("Define data source"), column![
radio_button(state.source == ExtractionSource::CurrentDrawing, t!("Current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentDrawing))),
radio_button(state.source == ExtractionSource::CurrentSelection, t!("Select objects in the current drawing"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::CurrentSelection))),
radio_button(state.source == ExtractionSource::DrawingsAndFolders, t!("Drawings / folders"), Message::DataExtractionField(DataExtractionField::Source(ExtractionSource::DrawingsAndFolders))),
checkbox(state.include_current).label(t!("Include current drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeCurrent(v))).size(14),
checkbox(state.include_subfolders).label(t!("Include subfolders")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::IncludeSubfolders(v))).size(14),
row![
button(text(t!("Add drawings…")).size(12)).on_press(Message::DataExtractionAddDrawings).padding([5, 10]),
add_folder,
button(text(t!("Clear")).size(12)).on_press(Message::DataExtractionClearSources).padding([5, 10]),
].spacing(6),
text(state.source_files.join("\n")).size(10),
].spacing(6).into()),
ExtractionPage::Begin => group(
t!("Begin"),
column![
radio_button(
state.begin == ExtractionBegin::New,
t!("Create a new data extraction"),
Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::New))
),
radio_button(
state.begin == ExtractionBegin::Template,
t!("Use a previous extraction as a template"),
Message::DataExtractionField(DataExtractionField::Begin(
ExtractionBegin::Template
))
),
radio_button(
state.begin == ExtractionBegin::Edit,
t!("Edit an existing data extraction"),
Message::DataExtractionField(DataExtractionField::Begin(ExtractionBegin::Edit))
),
row![
text(t!("Extraction settings"))
.size(11)
.style(muted)
.width(118),
text_input("", &state.settings_path)
.on_input(|v| Message::DataExtractionField(
DataExtractionField::SettingsPath(v)
))
.padding([4, 7])
.size(12)
.width(Fill),
button(text("…"))
.on_press(Message::DataExtractionBrowseSettings)
.padding([4, 9])
]
.spacing(6)
.align_y(iced::Center),
]
.spacing(5)
.into(),
),
ExtractionPage::Source => group(
t!("Define data source"),
column![
radio_button(
state.source == ExtractionSource::CurrentDrawing,
t!("Current drawing"),
Message::DataExtractionField(DataExtractionField::Source(
ExtractionSource::CurrentDrawing
))
),
radio_button(
state.source == ExtractionSource::CurrentSelection,
t!("Select objects in the current drawing"),
Message::DataExtractionField(DataExtractionField::Source(
ExtractionSource::CurrentSelection
))
),
radio_button(
state.source == ExtractionSource::DrawingsAndFolders,
t!("Drawings / folders"),
Message::DataExtractionField(DataExtractionField::Source(
ExtractionSource::DrawingsAndFolders
))
),
checkbox(state.include_current)
.label(t!("Include current drawing"))
.on_toggle(|v| Message::DataExtractionField(
DataExtractionField::IncludeCurrent(v)
))
.size(14),
checkbox(state.include_subfolders)
.label(t!("Include subfolders"))
.on_toggle(|v| Message::DataExtractionField(
DataExtractionField::IncludeSubfolders(v)
))
.size(14),
row![
button(text(t!("Add drawings…")).size(12))
.on_press(Message::DataExtractionAddDrawings)
.padding([5, 10]),
add_folder,
button(text(t!("Clear")).size(12))
.on_press(Message::DataExtractionClearSources)
.padding([5, 10]),
]
.spacing(6),
text(state.source_files.join("\n")).size(10),
]
.spacing(6)
.into(),
),
ExtractionPage::Objects => {
let mut list = column![].spacing(3);
for (i, item) in state.objects.iter().enumerate() {
list = list.push(checkbox(item.checked).label(format!("{} ({})", item.name, item.count)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Object(i, v))).size(14));
list = list.push(
checkbox(item.checked)
.label(format!("{} ({})", item.name, item.count))
.on_toggle(move |v| {
Message::DataExtractionField(DataExtractionField::Object(i, v))
})
.size(14),
);
}
group(t!("Select objects"), scrollable(list).height(Fill).into())
}
@@ -623,34 +1013,169 @@ pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::moda
for category in ["General", "Geometry"] {
let checked = state.categories.contains(category);
let key = category.to_string();
categories = categories.push(checkbox(checked).label(category).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Category(key.clone(), v))).size(14));
categories = categories.push(
checkbox(checked)
.label(category)
.on_toggle(move |v| {
Message::DataExtractionField(DataExtractionField::Category(
key.clone(),
v,
))
})
.size(14),
);
}
let mut list = column![categories].spacing(4);
for (i, item) in state.properties.iter().enumerate().filter(|(_, p)| state.categories.contains(&p.category)) {
list = list.push(checkbox(item.checked).label(format!("{} · {}", item.name, item.category)).on_toggle(move |v| Message::DataExtractionField(DataExtractionField::Property(i, v))).size(14));
for (i, item) in state
.properties
.iter()
.enumerate()
.filter(|(_, p)| state.categories.contains(&p.category))
{
list = list.push(
checkbox(item.checked)
.label(format!("{} · {}", item.name, item.category))
.on_toggle(move |v| {
Message::DataExtractionField(DataExtractionField::Property(i, v))
})
.size(14),
);
}
group(t!("Select properties"), scrollable(list).height(Fill).into())
group(
t!("Select properties"),
scrollable(list).height(Fill).into(),
)
}
ExtractionPage::Refine => group(t!("Refine data"), column![
row![checkbox(state.combine_identical).label(t!("Combine identical rows")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::CombineIdentical(v))).size(14), checkbox(state.show_count).label(t!("Show count column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowCount(v))).size(14), checkbox(state.show_name).label(t!("Show name column")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::ShowName(v))).size(14)].spacing(12),
scrollable(preview_grid(&state.preview)).height(Fill),
].spacing(8).into()),
ExtractionPage::Output => group(t!("Choose output"), column![
checkbox(state.output_table).label(t!("Insert data extraction table into drawing")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputTable(v))).size(14),
checkbox(state.output_file).label(t!("Output data to external file")).on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputFile(v))).size(14),
row![text(t!("Output file")).size(11).style(muted).width(118), text_input("", &state.output_path).on_input(|v| Message::DataExtractionField(DataExtractionField::OutputPath(v))).padding([4, 7]).size(12).width(Fill), button(text("…")).on_press(Message::DataExtractionBrowseOutput).padding([4, 9])].spacing(6).align_y(iced::Center),
].spacing(7).into()),
ExtractionPage::TableStyle => group(t!("Table style"), column![
row![text(t!("Style")).size(11).style(muted).width(118), pick_list(Some(state.table_style.clone()), state.table_styles.clone(), |value| value.clone()).on_select(|value| Message::DataExtractionField(DataExtractionField::TableStyle(value))).padding([3, 6]).width(Fill)].spacing(8),
labeled_input(t!("Title"), &state.table_title, |v| Message::DataExtractionField(DataExtractionField::TableTitle(v))),
preview_grid(&state.preview),
].spacing(7).into()),
ExtractionPage::Finish => group(t!("Finish"), column![text(t!("The selected drawing data is ready to be extracted.")).size(12), text(format!("{}: {}", t!("Rows"), state.preview.len().saturating_sub(1))).size(11), text(format!("{}: {}{}", t!("Output"), if state.output_table { t!("Drawing table") } else { "".into() }, if state.output_file { format!(" {}", t!("External file")) } else { String::new() })).size(11)].spacing(7).into()),
ExtractionPage::Refine => group(
t!("Refine data"),
column![
row![
checkbox(state.combine_identical)
.label(t!("Combine identical rows"))
.on_toggle(|v| Message::DataExtractionField(
DataExtractionField::CombineIdentical(v)
))
.size(14),
checkbox(state.show_count)
.label(t!("Show count column"))
.on_toggle(|v| Message::DataExtractionField(
DataExtractionField::ShowCount(v)
))
.size(14),
checkbox(state.show_name)
.label(t!("Show name column"))
.on_toggle(
|v| Message::DataExtractionField(DataExtractionField::ShowName(v))
)
.size(14)
]
.spacing(12),
scrollable(preview_grid(&state.preview)).height(Fill),
]
.spacing(8)
.into(),
),
ExtractionPage::Output => group(
t!("Choose output"),
column![
checkbox(state.output_table)
.label(t!("Insert data extraction table into drawing"))
.on_toggle(
|v| Message::DataExtractionField(DataExtractionField::OutputTable(v))
)
.size(14),
checkbox(state.output_file)
.label(t!("Output data to external file"))
.on_toggle(|v| Message::DataExtractionField(DataExtractionField::OutputFile(v)))
.size(14),
row![
text(t!("Output file")).size(11).style(muted).width(118),
text_input("", &state.output_path)
.on_input(|v| Message::DataExtractionField(
DataExtractionField::OutputPath(v)
))
.padding([4, 7])
.size(12)
.width(Fill),
button(text("…"))
.on_press(Message::DataExtractionBrowseOutput)
.padding([4, 9])
]
.spacing(6)
.align_y(iced::Center),
]
.spacing(7)
.into(),
),
ExtractionPage::TableStyle => group(
t!("Table style"),
column![
row![
text(t!("Style")).size(11).style(muted).width(118),
pick_list(
Some(state.table_style.clone()),
state.table_styles.clone(),
|value| value.clone()
)
.on_select(|value| Message::DataExtractionField(
DataExtractionField::TableStyle(value)
))
.padding([3, 6])
.width(Fill)
]
.spacing(8),
labeled_input(t!("Title"), &state.table_title, |v| {
Message::DataExtractionField(DataExtractionField::TableTitle(v))
}),
preview_grid(&state.preview),
]
.spacing(7)
.into(),
),
ExtractionPage::Finish => group(
t!("Finish"),
column![
text(t!("The selected drawing data is ready to be extracted.")).size(12),
text(format!(
"{}: {}",
t!("Rows"),
state.preview.len().saturating_sub(1)
))
.size(11),
text(format!(
"{}: {}{}",
t!("Output"),
if state.output_table {
t!("Drawing table")
} else {
"".into()
},
if state.output_file {
format!(" {}", t!("External file"))
} else {
String::new()
}
))
.size(11)
]
.spacing(7)
.into(),
),
};
column![
text(format!("{} {}/8", t!("Data Extraction"), state.page.number())).size(13),
text(format!(
"{} {}/8",
t!("Data Extraction"),
state.page.number()
))
.size(13),
container(body).height(Fill).width(Fill),
text(&state.error).size(11),
wizard_nav(state),
].spacing(8).padding(10).width(sizing.width).height(sizing.height).into()
]
.spacing(8)
.padding(10)
.width(sizing.width)
.height(sizing.height)
.into()
}
+177 -50
View File
@@ -103,21 +103,39 @@ fn msg(m: AttdefDialogMsg) -> Message {
}
fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(column![text(title.to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
container(
column![
text(title.to_uppercase()).size(10).style(accent_text),
content.into()
]
.spacing(8),
)
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
}
fn labelled<'a>(label: String, field: Element<'a, Message>, enabled: bool) -> Element<'a, Message> {
let label = text(label).size(12).width(Length::Fixed(118.0));
let label = if enabled { label } else { label.style(muted_style) };
let label = if enabled {
label
} else {
label.style(muted_style)
};
row![label, field].spacing(8).align_y(iced::Center).into()
}
fn input<'a>(value: &str, enabled: bool, on: fn(String) -> AttdefDialogMsg) -> Element<'a, Message> {
let mut field = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
fn input<'a>(
value: &str,
enabled: bool,
on: fn(String) -> AttdefDialogMsg,
) -> Element<'a, Message> {
let mut field = text_input("", value)
.size(12)
.padding([5, 8])
.width(Fill)
.style(field_style);
if enabled {
field = field.on_input(move |v| msg(on(v)));
}
@@ -138,7 +156,12 @@ fn pick<'a>(
Some(m) => b.on_press(msg(m)),
None => b,
};
iced::widget::tooltip(b, text(tip).size(11), iced::widget::tooltip::Position::Bottom).into()
iced::widget::tooltip(
b,
text(tip).size(11),
iced::widget::tooltip::Position::Bottom,
)
.into()
}
fn mode<'a>(label: String, on: bool, bit: u8) -> Element<'a, Message> {
@@ -170,8 +193,13 @@ fn error_band<'a>(error: &Option<String>) -> Option<Element<'a, Message>> {
.padding([6, 10])
.width(Fill)
.style(|theme: &iced::Theme| container::Style {
background: Some(iced::Background::Color(theme.palette().danger.base.color.scale_alpha(0.18))),
border: iced::Border { radius: 4.0.into(), ..Default::default() },
background: Some(iced::Background::Color(
theme.palette().danger.base.color.scale_alpha(0.18),
)),
border: iced::Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
})
.into(),
@@ -200,30 +228,61 @@ fn footer<'a>(action: std::borrow::Cow<'static, str>, ok: AttdefDialogMsg) -> El
.into()
}
pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
pub fn view<'a>(
state: &'a AttdefDialogState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let f = state.aflags;
let constant = f & AFLAG_CONSTANT != 0;
let multiline = f & AFLAG_MULTILINE != 0;
let modes = card(
t!("Mode").into_owned(),
column![
mode(t!("Invisible").into_owned(), f & AFLAG_INVISIBLE != 0, AFLAG_INVISIBLE),
mode(
t!("Invisible").into_owned(),
f & AFLAG_INVISIBLE != 0,
AFLAG_INVISIBLE
),
mode(t!("Constant").into_owned(), constant, AFLAG_CONSTANT),
mode(t!("Verify").into_owned(), f & AFLAG_VERIFY != 0, AFLAG_VERIFY),
mode(t!("Preset").into_owned(), f & AFLAG_PRESET != 0, AFLAG_PRESET),
mode(t!("Lock position").into_owned(), f & AFLAG_LOCK != 0, AFLAG_LOCK),
mode(t!("Multiple lines").into_owned(), multiline, AFLAG_MULTILINE),
mode(
t!("Verify").into_owned(),
f & AFLAG_VERIFY != 0,
AFLAG_VERIFY
),
mode(
t!("Preset").into_owned(),
f & AFLAG_PRESET != 0,
AFLAG_PRESET
),
mode(
t!("Lock position").into_owned(),
f & AFLAG_LOCK != 0,
AFLAG_LOCK
),
mode(
t!("Multiple lines").into_owned(),
multiline,
AFLAG_MULTILINE
),
]
.spacing(7),
);
let coords_on = !state.on_screen && !state.align_below;
let coord = |i: usize, axis: &'static str| -> Element<'a, Message> {
let mut field = text_input("", &state.coords[i]).size(12).padding([5, 8]).width(Fill).style(field_style);
let mut field = text_input("", &state.coords[i])
.size(12)
.padding([5, 8])
.width(Fill)
.style(field_style);
if coords_on {
field = field.on_input(move |v| msg(AttdefDialogMsg::Coord(i, v)));
}
let label = text(axis).size(12).width(Length::Fixed(14.0));
let label = if coords_on { label } else { label.style(muted_style) };
let label = if coords_on {
label
} else {
label.style(muted_style)
};
row![label, field].spacing(8).align_y(iced::Center).into()
};
let mut on_screen = checkbox(state.on_screen)
@@ -240,7 +299,11 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
let attribute = card(
t!("Attribute").into_owned(),
column![
labelled(t!("Tag").into_owned(), input(&state.tag, true, AttdefDialogMsg::Tag), true),
labelled(
t!("Tag").into_owned(),
input(&state.tag, true, AttdefDialogMsg::Tag),
true
),
labelled(
t!("Prompt").into_owned(),
input(&state.prompt, !constant, AttdefDialogMsg::Prompt),
@@ -249,7 +312,8 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
labelled(
if constant { t!("Value") } else { t!("Default") }.into_owned(),
{
let mut value = row![input(&state.default, true, AttdefDialogMsg::Default)].spacing(6);
let mut value =
row![input(&state.default, true, AttdefDialogMsg::Default)].spacing(6);
if multiline {
value = value.push(pick(
text("...").size(13).into(),
@@ -257,7 +321,11 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
Some(AttdefDialogMsg::EditValue),
));
}
value.push(pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::InsertField)))
value.push(pick(
text("fx").size(12).into(),
t!("Insert field").into_owned(),
Some(AttdefDialogMsg::InsertField),
))
}
.spacing(6)
.into(),
@@ -279,22 +347,36 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
.width(Fill)
.into()
} else {
container(text(JustifyChoice(state.justify).to_string()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill)
.into()
container(
text(JustifyChoice(state.justify).to_string())
.size(12)
.style(muted_style),
)
.padding([5, 8])
.width(Fill)
.into()
};
let style: Element<'a, Message> = if text_on {
pick_list(Some(state.style.clone()), state.styles.clone(), |s: &String| s.clone())
.on_select(|s| msg(AttdefDialogMsg::Style(s)))
.text_size(12)
pick_list(
Some(state.style.clone()),
state.styles.clone(),
|s: &String| s.clone(),
)
.on_select(|s| msg(AttdefDialogMsg::Style(s)))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
} else {
container(text(state.style.clone()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill)
.into()
} else {
container(text(state.style.clone()).size(12).style(muted_style)).padding([5, 8]).width(Fill).into()
};
let mut annotative = checkbox(state.annotative).label(t!("Annotative").into_owned()).text_size(12).size(14);
let mut annotative = checkbox(state.annotative)
.label(t!("Annotative").into_owned())
.text_size(12)
.size(14);
if text_on {
annotative = annotative.on_toggle(|v| msg(AttdefDialogMsg::Annotative(v)));
}
@@ -303,14 +385,22 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
let settings = card(
t!("Text Settings").into_owned(),
column![
labelled(t!("Justification").into_owned(), justify, text_on && !multiline),
labelled(
t!("Justification").into_owned(),
justify,
text_on && !multiline
),
labelled(t!("Text style").into_owned(), style, text_on),
row![Space::new().width(Length::Fixed(126.0)), annotative],
labelled(
t!("Text height").into_owned(),
row![
input(&state.height, size_on, AttdefDialogMsg::Height),
pick(screen_pick(), t!("Specify on-screen").into_owned(), size_on.then_some(AttdefDialogMsg::PickHeight)),
pick(
screen_pick(),
t!("Specify on-screen").into_owned(),
size_on.then_some(AttdefDialogMsg::PickHeight)
),
]
.spacing(6)
.into(),
@@ -320,7 +410,11 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
t!("Rotation").into_owned(),
row![
input(&state.rotation, rot_on, AttdefDialogMsg::Rotation),
pick(screen_pick(), t!("Specify on-screen").into_owned(), rot_on.then_some(AttdefDialogMsg::PickRotation)),
pick(
screen_pick(),
t!("Specify on-screen").into_owned(),
rot_on.then_some(AttdefDialogMsg::PickRotation)
),
]
.spacing(6)
.into(),
@@ -351,7 +445,9 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
align = align.on_toggle(|v| msg(AttdefDialogMsg::AlignBelow(v)));
}
let body = row![
column![modes, insertion].spacing(10).width(Length::Fixed(200.0)),
column![modes, insertion]
.spacing(10)
.width(Length::Fixed(200.0)),
column![attribute, settings].spacing(10).width(Fill),
]
.spacing(10);
@@ -367,29 +463,60 @@ pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSiz
.into()
}
pub fn view_edit<'a>(state: &'a AttdefEditState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let field = |label: String, value: &'a str, enabled: bool, on: fn(String) -> AttdefDialogMsg| {
let mut input = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
if enabled {
input = input.on_input(move |v| msg(on(v)));
}
let label = text(label).size(12).width(Length::Fixed(80.0));
let label = if enabled { label } else { label.style(muted_style) };
row![label, input].spacing(8).align_y(iced::Center)
};
pub fn view_edit<'a>(
state: &'a AttdefEditState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let field =
|label: String, value: &'a str, enabled: bool, on: fn(String) -> AttdefDialogMsg| {
let mut input = text_input("", value)
.size(12)
.padding([5, 8])
.width(Fill)
.style(field_style);
if enabled {
input = input.on_input(move |v| msg(on(v)));
}
let label = text(label).size(12).width(Length::Fixed(80.0));
let label = if enabled {
label
} else {
label.style(muted_style)
};
row![label, input].spacing(8).align_y(iced::Center)
};
let content = card(
t!("Attribute Definition").into_owned(),
column![
field(t!("Tag").into_owned(), &state.tag, true, AttdefDialogMsg::EditTag),
field(t!("Prompt").into_owned(), &state.prompt, !state.constant, AttdefDialogMsg::EditPrompt),
field(
t!("Tag").into_owned(),
&state.tag,
true,
AttdefDialogMsg::EditTag
),
field(
t!("Prompt").into_owned(),
&state.prompt,
!state.constant,
AttdefDialogMsg::EditPrompt
),
row![
field(
if state.constant { t!("Value") } else { t!("Default") }.into_owned(),
if state.constant {
t!("Value")
} else {
t!("Default")
}
.into_owned(),
&state.default,
true,
AttdefDialogMsg::EditDefault
),
pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::EditInsertField)),
pick(
text("fx").size(12).into(),
t!("Insert field").into_owned(),
Some(AttdefDialogMsg::EditInsertField)
),
]
.spacing(6)
.align_y(iced::Center),
+167 -64
View File
@@ -14,13 +14,11 @@
//! edits, undo and repaint live in the update handler (`Message::AttrEditorApply`).
use crate::app::Message;
use crate::t;
use codec::entities::{HorizontalAlignment, VerticalAlignment};
use codec::types::{Color as AcadColor, LineWeight};
use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
@@ -67,21 +65,77 @@ pub struct AttrRow {
/// standard text-justification list. The stored labels are the English source
/// texts; `justify_label` / `justify_from_label` translate both directions.
pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
("Left", HorizontalAlignment::Left, VerticalAlignment::Baseline),
("Center", HorizontalAlignment::Center, VerticalAlignment::Baseline),
("Right", HorizontalAlignment::Right, VerticalAlignment::Baseline),
("Aligned", HorizontalAlignment::Aligned, VerticalAlignment::Baseline),
("Middle", HorizontalAlignment::Middle, VerticalAlignment::Baseline),
(
"Left",
HorizontalAlignment::Left,
VerticalAlignment::Baseline,
),
(
"Center",
HorizontalAlignment::Center,
VerticalAlignment::Baseline,
),
(
"Right",
HorizontalAlignment::Right,
VerticalAlignment::Baseline,
),
(
"Aligned",
HorizontalAlignment::Aligned,
VerticalAlignment::Baseline,
),
(
"Middle",
HorizontalAlignment::Middle,
VerticalAlignment::Baseline,
),
("Fit", HorizontalAlignment::Fit, VerticalAlignment::Baseline),
("Top Left", HorizontalAlignment::Left, VerticalAlignment::Top),
("Top Center", HorizontalAlignment::Center, VerticalAlignment::Top),
("Top Right", HorizontalAlignment::Right, VerticalAlignment::Top),
("Middle Left", HorizontalAlignment::Left, VerticalAlignment::Middle),
("Middle Center", HorizontalAlignment::Center, VerticalAlignment::Middle),
("Middle Right", HorizontalAlignment::Right, VerticalAlignment::Middle),
("Bottom Left", HorizontalAlignment::Left, VerticalAlignment::Bottom),
("Bottom Center", HorizontalAlignment::Center, VerticalAlignment::Bottom),
("Bottom Right", HorizontalAlignment::Right, VerticalAlignment::Bottom),
(
"Top Left",
HorizontalAlignment::Left,
VerticalAlignment::Top,
),
(
"Top Center",
HorizontalAlignment::Center,
VerticalAlignment::Top,
),
(
"Top Right",
HorizontalAlignment::Right,
VerticalAlignment::Top,
),
(
"Middle Left",
HorizontalAlignment::Left,
VerticalAlignment::Middle,
),
(
"Middle Center",
HorizontalAlignment::Center,
VerticalAlignment::Middle,
),
(
"Middle Right",
HorizontalAlignment::Right,
VerticalAlignment::Middle,
),
(
"Bottom Left",
HorizontalAlignment::Left,
VerticalAlignment::Bottom,
),
(
"Bottom Center",
HorizontalAlignment::Center,
VerticalAlignment::Bottom,
),
(
"Bottom Right",
HorizontalAlignment::Right,
VerticalAlignment::Bottom,
),
];
/// The justification label for an (h, v) pair (defaults to "Left"). Kept in
@@ -149,7 +203,11 @@ fn field_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
};
text_input::Style {
background: Background::Color(palette.background.base.color),
border: Border { color: border, width: 1.0, radius: 3.0.into() },
border: Border {
color: border,
width: 1.0,
radius: 3.0.into(),
},
icon: palette.background.base.text,
placeholder: palette.background.base.text.scale_alpha(0.48),
value: palette.background.base.text,
@@ -236,7 +294,10 @@ fn localized_pick_field<'a>(
on_select: impl Fn(String) -> Message + 'a,
width: Length,
) -> Element<'a, Message> {
let options = options.into_iter().map(LocalizedChoice::new).collect::<Vec<_>>();
let options = options
.into_iter()
.map(LocalizedChoice::new)
.collect::<Vec<_>>();
let selected = selected.map(LocalizedChoice::new);
let pl = iced::widget::pick_list(selected, options, |value| value.to_string())
.on_select(move |choice: LocalizedChoice| on_select(choice.raw))
@@ -246,7 +307,11 @@ fn localized_pick_field<'a>(
field_row(label, pl.into(), width)
}
fn tab_button<'a>(label: Cow<'static, str>, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
fn tab_button<'a>(
label: Cow<'static, str>,
this: AttrTab,
active: AttrTab,
) -> Element<'a, Message> {
let is_active = this == active;
button(text(label).size(11))
.padding([4, 12])
@@ -261,14 +326,14 @@ fn tab_button<'a>(label: Cow<'static, str>, this: AttrTab, active: AttrTab) -> E
_ => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
background: Some(Background::Color(pair.color)),
text_color: pair.text,
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}
})
.into()
@@ -298,16 +363,16 @@ pub fn view_window<'a>(
.style(button::primary);
let toolbar = container(
row![
text(t!("Block: %{block}", block = block)).size(12).style(muted_style),
text(t!("Block: %{block}", block = block))
.size(12)
.style(muted_style),
Space::new().width(width),
apply,
]
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weak.color
)),
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(width)
@@ -321,22 +386,23 @@ pub fn view_window<'a>(
.spacing(2);
let body: Element<'_, Message> = if rows.is_empty() {
text(t!("This block has no attributes.")).size(13).style(muted_style).into()
text(t!("This block has no attributes."))
.size(13)
.style(muted_style)
.into()
} else {
match tab {
AttrTab::Attribute => attribute_tab(rows, selected, width, height),
AttrTab::TextOptions => {
text_options_tab(&rows[selected.min(rows.len() - 1)], styles, width, height)
}
AttrTab::Properties => {
properties_tab(
&rows[selected.min(rows.len() - 1)],
layers,
linetypes,
width,
height,
)
}
AttrTab::Properties => properties_tab(
&rows[selected.min(rows.len() - 1)],
layers,
linetypes,
width,
height,
),
}
};
@@ -347,9 +413,7 @@ pub fn view_window<'a>(
container(column![toolbar, hdivider(width), content])
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(width)
@@ -441,16 +505,39 @@ fn text_options_tab<'a>(
let justify_sel = Some(justify_label(r.h_align, r.v_align).to_string());
column![
pick_field(t!("Text Style"), styles, style_sel, |s| {
Message::AttrEditorTextStyle(s)
}, width),
localized_pick_field(t!("Justification"), justify_opts, justify_sel, |s| {
Message::AttrEditorJustify(s)
}, width),
pick_field(
t!("Text Style"),
styles,
style_sel,
|s| { Message::AttrEditorTextStyle(s) },
width
),
localized_pick_field(
t!("Justification"),
justify_opts,
justify_sel,
|s| { Message::AttrEditorJustify(s) },
width
),
edit_field(t!("Height"), &r.height, Message::AttrEditorHeight, width),
edit_field(t!("Rotation"), &r.rotation, Message::AttrEditorRotation, width),
edit_field(t!("Width Factor"), &r.width_factor, Message::AttrEditorWidth, width),
edit_field(t!("Oblique Angle"), &r.oblique, Message::AttrEditorOblique, width),
edit_field(
t!("Rotation"),
&r.rotation,
Message::AttrEditorRotation,
width
),
edit_field(
t!("Width Factor"),
&r.width_factor,
Message::AttrEditorWidth,
width
),
edit_field(
t!("Oblique Angle"),
&r.oblique,
Message::AttrEditorOblique,
width
),
field_row(
Cow::Borrowed(""),
checkbox(r.backwards)
@@ -505,17 +592,33 @@ fn properties_tab<'a>(
let lw_opts = lw_options();
let lw_sel = LwItem(r.line_weight);
let lw = iced::widget::pick_list(Some(lw_sel), lw_opts, |value| value.to_string())
.on_select(|it: LwItem| Message::AttrEditorLineweight(it.0))
.text_size(13)
.padding([3, 6])
.width(width);
.on_select(|it: LwItem| Message::AttrEditorLineweight(it.0))
.text_size(13)
.padding([3, 6])
.width(width);
column![
pick_field(t!("Layer"), layers, layer_sel, Message::AttrEditorLayer, width),
localized_pick_field(t!("Linetype"), linetypes, lt_sel, Message::AttrEditorLinetype, width),
localized_pick_field(t!("Color"), color_opts, color_sel, |s| {
Message::AttrEditorColor(s)
}, width),
pick_field(
t!("Layer"),
layers,
layer_sel,
Message::AttrEditorLayer,
width
),
localized_pick_field(
t!("Linetype"),
linetypes,
lt_sel,
Message::AttrEditorLinetype,
width
),
localized_pick_field(
t!("Color"),
color_opts,
color_sel,
|s| { Message::AttrEditorColor(s) },
width
),
field_row(t!("Lineweight"), lw.into(), width),
]
.spacing(8)
+6 -2
View File
@@ -77,10 +77,14 @@ pub fn view_window<'a>(
let conditions = column![
checkbox(settings.tangent_must_share_point)
.label(crate::t!("Tangent objects must share an intersection point"))
.label(crate::t!(
"Tangent objects must share an intersection point"
))
.on_toggle(|_| Message::AutoConstrainToggleTangentPoint),
checkbox(settings.perpendicular_must_intersect)
.label(crate::t!("Perpendicular objects must share an intersection point"))
.label(crate::t!(
"Perpendicular objects must share an intersection point"
))
.on_toggle(|_| Message::AutoConstrainTogglePerpendicularIntersection),
]
.spacing(8);
+315 -98
View File
@@ -245,9 +245,8 @@ impl BlockPalette {
/// The items of the current tab that pass the filter.
pub fn items(&self) -> Vec<Item> {
let pattern = self.search.trim();
let pass = |name: &str| {
pattern.is_empty() || crate::io::xref_model::wildcard_match(name, pattern)
};
let pass =
|name: &str| pattern.is_empty() || crate::io::xref_model::wildcard_match(name, pattern);
match self.tab {
Tab::Current => self
.blocks
@@ -271,13 +270,21 @@ impl BlockPalette {
.library_entries
.iter()
.filter(|e| e.folder || pass(&display_name(&e.path)))
.map(|e| if e.folder { Item::Folder(e.path.clone()) } else { Item::File(e.path.clone()) })
.map(|e| {
if e.folder {
Item::Folder(e.path.clone())
} else {
Item::File(e.path.clone())
}
})
.collect(),
}
}
fn current_entry(&self, name: &str) -> Option<&BlockEntry> {
self.blocks.iter().find(|b| b.name.eq_ignore_ascii_case(name))
self.blocks
.iter()
.find(|b| b.name.eq_ignore_ascii_case(name))
}
}
@@ -287,7 +294,9 @@ pub fn short(v: f64) -> String {
}
fn display_name(path: &std::path::Path) -> String {
path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default()
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
}
/// Canvas program that draws a block's wires fit-to-box inside its bounds.
@@ -310,8 +319,12 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
let pad = 4.0;
let inner_w = (bounds.width - 2.0 * pad).max(1.0);
let inner_h = (bounds.height - 2.0 * pad).max(1.0);
let (mut minx, mut miny, mut maxx, mut maxy) =
(f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
let (mut minx, mut miny, mut maxx, mut maxy) = (
f32::INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NEG_INFINITY,
);
let mut any = false;
for w in self.wires {
for p in &w.points {
@@ -325,8 +338,19 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
}
}
if !any {
let rect = Path::rectangle(iced::Point::new(pad, pad), iced::Size::new(inner_w, inner_h));
frame.fill(&rect, Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 });
let rect = Path::rectangle(
iced::Point::new(pad, pad),
iced::Size::new(inner_w, inner_h),
);
frame.fill(
&rect,
Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
},
);
return vec![frame.into_geometry()];
}
let span_x = (maxx - minx).max(1e-6);
@@ -334,9 +358,15 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
let scale = (inner_w / span_x).min(inner_h / span_y);
let ox = (bounds.width - span_x * scale) * 0.5;
let oy = (bounds.height - span_y * scale) * 0.5;
let map = |x: f32, y: f32| iced::Point::new(ox + (x - minx) * scale, oy + (maxy - y) * scale);
let map =
|x: f32, y: f32| iced::Point::new(ox + (x - minx) * scale, oy + (maxy - y) * scale);
for w in self.wires {
let col = Color { r: w.color[0], g: w.color[1], b: w.color[2], a: 0.55 };
let col = Color {
r: w.color[0],
g: w.color[1],
b: w.color[2],
a: 0.55,
};
for tri in w.fill_tris.chunks(3) {
if tri.len() == 3 {
let path = Path::new(|p| {
@@ -350,7 +380,12 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
}
}
for w in self.wires {
let col = Color { r: w.color[0], g: w.color[1], b: w.color[2], a: 1.0 };
let col = Color {
r: w.color[0],
g: w.color[1],
b: w.color[2],
a: 1.0,
};
let width = if w.line_weight_px > 1.5 { 2.0 } else { 1.0 };
let mut run: Vec<iced::Point> = Vec::new();
let flush = |frame: &mut Frame, run: &mut Vec<iced::Point>| {
@@ -380,7 +415,10 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
/// A block preview (the palette's canvas) for other dialogs.
pub(crate) fn preview_canvas<'a>(wires: &'a [WireModel], height: f32) -> Element<'a, Message> {
canvas(BlockPreviewCanvas { wires }).width(Fill).height(Length::Fixed(height)).into()
canvas(BlockPreviewCanvas { wires })
.width(Fill)
.height(Length::Fixed(height))
.into()
}
fn pm(m: BlockPaletteMsg) -> Message {
@@ -403,14 +441,23 @@ fn thumb<'a>(palette: &'a BlockPalette, item: &Item, size: f32) -> Element<'a, M
let file_thumb = |path: &str| palette.file_thumbs.get(path).cloned().flatten();
match item {
Item::Current(name) => match wires(name) {
Some(w) => canvas(BlockPreviewCanvas { wires: w }).width(Fill).height(Length::Fixed(size)).into(),
Some(w) => canvas(BlockPreviewCanvas { wires: w })
.width(Fill)
.height(Length::Fixed(size))
.into(),
None => blank(),
},
Item::Ref(r) => {
if !r.drawing {
let seen = palette.ref_wires.get(&format!("{}|{}", r.source, r.name.to_ascii_uppercase()));
let seen =
palette
.ref_wires
.get(&format!("{}|{}", r.source, r.name.to_ascii_uppercase()));
if let Some(w) = seen.map(|w| &w[..]).or_else(|| wires(&r.name)) {
return canvas(BlockPreviewCanvas { wires: w }).width(Fill).height(Length::Fixed(size)).into();
return canvas(BlockPreviewCanvas { wires: w })
.width(Fill)
.height(Length::Fixed(size))
.into();
}
}
match file_thumb(&r.source) {
@@ -419,15 +466,23 @@ fn thumb<'a>(palette: &'a BlockPalette, item: &Item, size: f32) -> Element<'a, M
}
}
Item::File(path) => {
match palette.library_entries.iter().find(|e| &e.path == path).and_then(|e| e.thumb.clone()) {
match palette
.library_entries
.iter()
.find(|e| &e.path == path)
.and_then(|e| e.thumb.clone())
{
Some(h) => image(h).width(Fill).height(Length::Fixed(size)).into(),
None => blank(),
}
}
Item::Folder(_) => container(crate::ui::icons::themed_secondary(crate::ui::icons::FOLDER_OPEN, size * 0.6))
.center_x(Fill)
.center_y(Length::Fixed(size))
.into(),
Item::Folder(_) => container(crate::ui::icons::themed_secondary(
crate::ui::icons::FOLDER_OPEN,
size * 0.6,
))
.center_x(Fill)
.center_y(Length::Fixed(size))
.into(),
}
}
@@ -446,15 +501,24 @@ fn item_menu(tab: Tab, item: Item, in_drawing: bool, external: bool) -> Element<
s
});
if enabled {
b.on_press(pm(BlockPaletteMsg::Menu(item.clone(), action))).into()
b.on_press(pm(BlockPaletteMsg::Menu(item.clone(), action)))
.into()
} else {
b.into()
}
};
let favorite = if tab == Tab::Favorites {
entry(crate::t!("Remove from Favorites").into_owned(), MenuAction::Unfavorite, true)
entry(
crate::t!("Remove from Favorites").into_owned(),
MenuAction::Unfavorite,
true,
)
} else {
entry(crate::t!("Add to Favorites").into_owned(), MenuAction::Favorite, !matches!(item, Item::Folder(_)))
entry(
crate::t!("Add to Favorites").into_owned(),
MenuAction::Favorite,
!matches!(item, Item::Folder(_)),
)
};
let remove_label = match tab {
Tab::Recent | Tab::Favorites => crate::t!("Remove from List"),
@@ -467,15 +531,34 @@ fn item_menu(tab: Tab, item: Item, in_drawing: bool, external: bool) -> Element<
};
container(
column![
entry(crate::t!("Insert").into_owned(), MenuAction::Insert, !matches!(item, Item::Folder(_))),
entry(crate::t!("Redefine").into_owned(), MenuAction::Redefine, external),
entry(
crate::t!("Insert").into_owned(),
MenuAction::Insert,
!matches!(item, Item::Folder(_))
),
entry(
crate::t!("Redefine").into_owned(),
MenuAction::Redefine,
external
),
favorite,
container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}),
entry(crate::t!("Edit Block").into_owned(), MenuAction::Edit, in_drawing),
entry(remove_label.into_owned(), MenuAction::Remove, remove_enabled),
container(Space::new())
.height(1)
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}),
entry(
crate::t!("Edit Block").into_owned(),
MenuAction::Edit,
in_drawing
),
entry(
remove_label.into_owned(),
MenuAction::Remove,
remove_enabled
),
]
.spacing(1),
)
@@ -496,11 +579,13 @@ fn item_menu(tab: Tab, item: Item, in_drawing: bool, external: bool) -> Element<
/// One item as a card / row, with press, release and the context menu.
fn item_view<'a>(palette: &'a BlockPalette, item: Item) -> Element<'a, Message> {
let name = item.name();
let placing = matches!(&item, Item::Current(n) if palette.placing.as_deref() == Some(n.as_str()));
let placing =
matches!(&item, Item::Current(n) if palette.placing.as_deref() == Some(n.as_str()));
let in_drawing = palette.current_entry(&name).is_some()
&& !matches!(&item, Item::Ref(r) if r.drawing)
&& !matches!(item, Item::File(_) | Item::Folder(_));
let external = matches!(&item, Item::Ref(r) if !r.source.is_empty()) || matches!(item, Item::File(_));
let external =
matches!(&item, Item::Ref(r) if !r.source.is_empty()) || matches!(item, Item::File(_));
let body: Element<'a, Message> = match palette.view.grid() {
Some((_, size)) => column![
thumb(palette, &item, size),
@@ -524,7 +609,8 @@ fn item_view<'a>(palette: &'a BlockPalette, item: Item) -> Element<'a, Message>
let (ann, dynamic) = palette
.current_entry(&name)
.map_or((false, false), |e| (e.annotative, e.dynamic));
let yes_no = |v: bool| if v { crate::t!("Yes") } else { crate::t!("No") }.into_owned();
let yes_no =
|v: bool| if v { crate::t!("Yes") } else { crate::t!("No") }.into_owned();
row![
container(thumb(palette, &item, 16.0)).width(Length::Fixed(40.0)),
text(name.clone()).size(11).width(Fill),
@@ -560,7 +646,10 @@ fn item_view<'a>(palette: &'a BlockPalette, item: Item) -> Element<'a, Message>
.on_release(pm(BlockPaletteMsg::Release(item.clone())))
.interaction(iced::mouse::Interaction::Pointer);
let tab = palette.tab;
iced_aw::ContextMenu::new(area, move || item_menu(tab, item.clone(), in_drawing, external)).into()
iced_aw::ContextMenu::new(area, move || {
item_menu(tab, item.clone(), in_drawing, external)
})
.into()
}
fn icon_button<'a>(icon: IconKind, tip: &str, msg: BlockPaletteMsg) -> Element<'a, Message> {
@@ -582,7 +671,11 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
.into()
};
let num = |value: &'a str, f: fn(String) -> OptionMsg, enabled: bool| -> Element<'a, Message> {
let mut input = text_input("", value).size(11).padding([3, 6]).width(Length::Fixed(48.0)).style(field_style);
let mut input = text_input("", value)
.size(11)
.padding([3, 6])
.width(Length::Fixed(48.0))
.style(field_style);
if enabled {
input = input.on_input(move |v| pm(BlockPaletteMsg::Option(f(v))));
}
@@ -590,7 +683,9 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
};
let header = mouse_area(
row![
text(crate::t!("Options").to_uppercase()).size(10).style(accent_text),
text(crate::t!("Options").to_uppercase())
.size(10)
.style(accent_text),
Space::new().width(Fill),
if palette.options_collapsed {
crate::ui::icons::themed_arrow_right(12.0)
@@ -604,24 +699,39 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
.interaction(iced::mouse::Interaction::Pointer);
let mut content = column![header].spacing(6);
if !palette.options_collapsed {
let scale_kinds = vec![crate::t!("Scale").into_owned(), crate::t!("Uniform Scale").into_owned()];
let scale_kinds = vec![
crate::t!("Scale").into_owned(),
crate::t!("Uniform Scale").into_owned(),
];
let current_kind = scale_kinds[o.uniform as usize].clone();
let uniform_label = scale_kinds[1].clone();
let kind = pick_list(Some(current_kind), scale_kinds, |v: &String| v.clone())
.on_select(move |v: String| pm(BlockPaletteMsg::Option(OptionMsg::Uniform(v == uniform_label))))
.text_size(11)
.padding([3, 6]);
.on_select(move |v: String| {
pm(BlockPaletteMsg::Option(OptionMsg::Uniform(
v == uniform_label,
)))
})
.text_size(11)
.padding([3, 6]);
let fields_on = !o.scale;
let mut scale_row = row![
checkbox(o.scale).size(14).on_toggle(|v| pm(BlockPaletteMsg::Option(OptionMsg::Scale(v)))),
checkbox(o.scale)
.size(14)
.on_toggle(|v| pm(BlockPaletteMsg::Option(OptionMsg::Scale(v)))),
kind,
]
.spacing(6)
.align_y(iced::Center);
// The scale values sit on their own line under the switch.
let mut xyz_row = row![Space::new().width(Length::Fixed(20.0))].spacing(6).align_y(iced::Center);
let mut xyz_row = row![Space::new().width(Length::Fixed(20.0))]
.spacing(6)
.align_y(iced::Center);
if o.scale {
scale_row = scale_row.push(text(crate::t!("specified on screen")).size(11).style(muted_style));
scale_row = scale_row.push(
text(crate::t!("specified on screen"))
.size(11)
.style(muted_style),
);
} else if o.uniform {
xyz_row = xyz_row.push(num(&palette.option_text[0], OptionMsg::X, fields_on));
} else {
@@ -633,34 +743,69 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
.push(text("Z").size(11))
.push(num(&palette.option_text[2], OptionMsg::Z, fields_on));
}
let mut rot_row = row![check(crate::t!("Rotation").into_owned(), o.rotation, OptionMsg::Rotation), Space::new().width(Fill)]
.spacing(6)
.align_y(iced::Center);
let mut rot_row = row![
check(
crate::t!("Rotation").into_owned(),
o.rotation,
OptionMsg::Rotation
),
Space::new().width(Fill)
]
.spacing(6)
.align_y(iced::Center);
if o.rotation {
rot_row = rot_row.push(text(crate::t!("specified on screen")).size(11).style(muted_style));
rot_row = rot_row.push(
text(crate::t!("specified on screen"))
.size(11)
.style(muted_style),
);
} else {
rot_row = rot_row
.push(num(&palette.option_text[3], OptionMsg::Angle, true))
.push(text(crate::t!("Angle")).size(11));
}
content = content
.push(check(crate::t!("Insertion Point").into_owned(), o.insertion_point, OptionMsg::InsertionPoint))
.push(check(
crate::t!("Insertion Point").into_owned(),
o.insertion_point,
OptionMsg::InsertionPoint,
))
.push(scale_row);
if !o.scale {
content = content.push(xyz_row);
}
content = content
.push(rot_row)
.push(check(crate::t!("Auto-Placement").into_owned(), o.auto_placement, OptionMsg::AutoPlacement))
.push(check(crate::t!("Repeat Placement").into_owned(), o.repeat, OptionMsg::Repeat))
.push(check(crate::t!("Explode").into_owned(), o.explode, OptionMsg::Explode));
.push(check(
crate::t!("Auto-Placement").into_owned(),
o.auto_placement,
OptionMsg::AutoPlacement,
))
.push(check(
crate::t!("Repeat Placement").into_owned(),
o.repeat,
OptionMsg::Repeat,
))
.push(check(
crate::t!("Explode").into_owned(),
o.explode,
OptionMsg::Explode,
));
}
container(content).padding([8, 10]).width(Fill).style(card_style).into()
container(content)
.padding([8, 10])
.width(Fill)
.style(card_style)
.into()
}
/// Build the docked panel element from the palette state.
pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<'_, Message> {
let title_bar = crate::ui::dock::title_bar(PanelId::BlockPalette, crate::t!("Blocks").into_owned(), auto_collapse);
let title_bar = crate::ui::dock::title_bar(
PanelId::BlockPalette,
crate::t!("Blocks").into_owned(),
auto_collapse,
);
let tabs = {
let buttons = [Tab::Current, Tab::Recent, Tab::Favorites, Tab::Libraries].map(|t| {
@@ -682,12 +827,19 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
.padding([4, 8])
.size(12)
.style(field_style);
let view_pick = pick_list(Some(palette.view.label()), ViewMode::ALL.map(|v| v.label()).to_vec(), |v: &String| v.clone())
.on_select(|label: String| {
pm(BlockPaletteMsg::View(
ViewMode::ALL.into_iter().find(|v| v.label() == label).unwrap_or_default(),
))
})
let view_pick = pick_list(
Some(palette.view.label()),
ViewMode::ALL.map(|v| v.label()).to_vec(),
|v: &String| v.clone(),
)
.on_select(|label: String| {
pm(BlockPaletteMsg::View(
ViewMode::ALL
.into_iter()
.find(|v| v.label() == label)
.unwrap_or_default(),
))
})
.text_size(11)
.width(Length::Fixed(118.0))
.padding([3, 6]);
@@ -714,15 +866,21 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
.text_size(11)
.padding([3, 6])
.width(Fill),
button(crate::ui::icons::themed(crate::ui::icons::FOLDER_OPEN, 14.0))
.on_press(pm(BlockPaletteMsg::LibraryBrowse))
.style(button_style(false))
.padding([3, 8]),
button(crate::ui::icons::themed(
crate::ui::icons::FOLDER_OPEN,
14.0
))
.on_press(pm(BlockPaletteMsg::LibraryBrowse))
.style(button_style(false))
.padding([3, 8]),
]
.spacing(4)
.align_y(iced::Center);
let in_sub = palette.library_dir.as_ref().is_some_and(|d| {
palette.libraries.first().is_some_and(|root| d != &PathBuf::from(root))
palette
.libraries
.first()
.is_some_and(|root| d != &PathBuf::from(root))
});
if in_sub {
lib_row = lib_row.push(
@@ -739,19 +897,36 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
Ok(rel) => rel.display().to_string(),
Err(_) => dir.display().to_string(),
};
top = top.push(text(format!("{} {shown}", crate::t!("Path:"))).size(10).style(accent_text));
top = top.push(
text(format!("{} {shown}", crate::t!("Path:")))
.size(10)
.style(accent_text),
);
}
}
top = top.push(text(palette.tab.heading().to_uppercase()).size(10).style(accent_text));
top = top.push(
text(palette.tab.heading().to_uppercase())
.size(10)
.style(accent_text),
);
let items = palette.items();
let body: Element<'_, Message> = if items.is_empty() {
let msg = match palette.tab {
Tab::Current if palette.cached_names.is_empty() => crate::t!("No blocks in this drawing"),
Tab::Libraries if palette.libraries.is_empty() => crate::t!("Choose a library folder with …"),
Tab::Current if palette.cached_names.is_empty() => {
crate::t!("No blocks in this drawing")
}
Tab::Libraries if palette.libraries.is_empty() => {
crate::t!("Choose a library folder with …")
}
_ => crate::t!("No matches"),
};
container(text(msg).size(12).style(muted_style)).center_x(Fill).center_y(Fill).width(Fill).height(Fill).into()
container(text(msg).size(12).style(muted_style))
.center_x(Fill)
.center_y(Fill)
.width(Fill)
.height(Fill)
.into()
} else {
let mut col = column![].spacing(4);
match palette.view.grid() {
@@ -783,9 +958,18 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
if palette.view == ViewMode::Details {
col = col.push(
row![
text(crate::t!("Icon")).size(11).style(muted_style).width(Length::Fixed(40.0)),
text(crate::t!("Name")).size(11).style(muted_style).width(Fill),
text(crate::t!("Annotative / Dynamic")).size(11).style(muted_style).width(Length::Fixed(140.0)),
text(crate::t!("Icon"))
.size(11)
.style(muted_style)
.width(Length::Fixed(40.0)),
text(crate::t!("Name"))
.size(11)
.style(muted_style)
.width(Fill),
text(crate::t!("Annotative / Dynamic"))
.size(11)
.style(muted_style)
.width(Length::Fixed(140.0)),
]
.spacing(4),
);
@@ -795,13 +979,21 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
}
}
}
scrollable(container(col).padding(iced::Padding { top: 0.0, right: 8.0, bottom: 6.0, left: 0.0 }))
.width(Fill)
.height(Fill)
.into()
scrollable(container(col).padding(iced::Padding {
top: 0.0,
right: 8.0,
bottom: 6.0,
left: 0.0,
}))
.width(Fill)
.height(Fill)
.into()
};
crate::ui::dock::frame(column![title_bar, top, body, options_card(palette)].spacing(6), width)
crate::ui::dock::frame(
column![title_bar, top, body, options_card(palette)].spacing(6),
width,
)
}
/// Width of the ribbon's block gallery.
@@ -819,7 +1011,11 @@ pub fn gallery(palette: &BlockPalette) -> Element<'_, Message> {
let mut r = row![].spacing(4).width(Fill);
for block in chunk {
let card = column![
canvas(BlockPreviewCanvas { wires: &block.wires }).width(Fill).height(Length::Fixed(48.0)),
canvas(BlockPreviewCanvas {
wires: &block.wires
})
.width(Fill)
.height(Length::Fixed(48.0)),
text(crate::ui::text_util::elide(&block.name, 10))
.size(10)
.width(Fill)
@@ -841,14 +1037,23 @@ pub fn gallery(palette: &BlockPalette) -> Element<'_, Message> {
grid = grid.push(r);
}
let blocks: Element<'_, Message> = if palette.blocks.is_empty() {
container(text(crate::t!("No blocks in this drawing")).size(11).style(muted_style))
.padding(10)
.center_x(Fill)
.into()
container(
text(crate::t!("No blocks in this drawing"))
.size(11)
.style(muted_style),
)
.padding(10)
.center_x(Fill)
.into()
} else {
scrollable(container(grid).padding(iced::Padding { top: 0.0, right: 8.0, bottom: 0.0, left: 0.0 }))
.height(Length::Shrink)
.into()
scrollable(container(grid).padding(iced::Padding {
top: 0.0,
right: 8.0,
bottom: 0.0,
left: 0.0,
}))
.height(Length::Shrink)
.into()
};
let link = |label: std::borrow::Cow<'static, str>, tab: &str| -> Element<'_, Message> {
button(text(label.into_owned()).size(11))
@@ -860,12 +1065,19 @@ pub fn gallery(palette: &BlockPalette) -> Element<'_, Message> {
};
container(
column![
text(crate::t!("Current Drawing Blocks").to_uppercase()).size(10).style(accent_text),
container(blocks).height(Length::Fixed(((palette.blocks.len().div_ceil(4)) as f32 * 79.0).clamp(36.0, 260.0))),
container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}),
text(crate::t!("Current Drawing Blocks").to_uppercase())
.size(10)
.style(accent_text),
container(blocks).height(Length::Fixed(
((palette.blocks.len().div_ceil(4)) as f32 * 79.0).clamp(36.0, 260.0)
)),
container(Space::new())
.height(1)
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}),
link(crate::t!("Recent Blocks..."), "RECENT"),
link(crate::t!("Favorite Blocks..."), "FAVORITES"),
link(crate::t!("Blocks from Libraries..."), "LIBRARIES"),
@@ -880,15 +1092,20 @@ pub fn gallery(palette: &BlockPalette) -> Element<'_, Message> {
/// Theme-aware foreground/background pair for a block card (normal,
/// hovered/pressed, or the block being placed).
pub(crate) fn block_card_colors(theme: &Theme, is_placing: bool, status: button::Status) -> (Color, Color) {
pub(crate) fn block_card_colors(
theme: &Theme,
is_placing: bool,
status: button::Status,
) -> (Color, Color) {
let palette = theme.palette();
if is_placing {
(palette.primary.base.color, palette.primary.base.text)
} else {
match status {
button::Status::Hovered | button::Status::Pressed => {
(palette.background.strong.color, palette.background.strong.text)
}
button::Status::Hovered | button::Status::Pressed => (
palette.background.strong.color,
palette.background.strong.text,
),
_ => (palette.background.weak.color, palette.background.weak.text),
}
}
+20 -12
View File
@@ -54,11 +54,10 @@ fn bodies(document: &CadDocument) -> Vec<(Handle, String)> {
.filter(|entity| matches!(entity, EntityType::Solid3D(_)))
.map(|entity| {
let handle = entity.common().handle;
let label = crate::scene::model::solid_history::primitive_property_operation(
document, handle,
)
.as_ref()
.map_or("Solid", operation_label);
let label =
crate::scene::model::solid_history::primitive_property_operation(document, handle)
.as_ref()
.map_or("Solid", operation_label);
(handle, format!("{label} {:X}", handle.value()))
})
.collect()
@@ -87,10 +86,14 @@ fn entry_row(label: String, command: String) -> Element<'static, Message> {
/// A row that states something rather than doing something.
fn note_row(label: String) -> Element<'static, Message> {
container(text(label).size(11).style(crate::ui::style::common::muted_style))
.width(Fill)
.padding([3, 10])
.into()
container(
text(label)
.size(11)
.style(crate::ui::style::common::muted_style),
)
.width(Fill)
.padding([3, 10])
.into()
}
pub fn view<'a>(
@@ -99,8 +102,11 @@ pub fn view<'a>(
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), auto_collapse);
let title_bar = crate::ui::dock::title_bar(
PanelId::Browser,
crate::t!("Browser").into_owned(),
auto_collapse,
);
let mut tree = column![].spacing(1);
@@ -123,7 +129,9 @@ pub fn view<'a>(
}
let bodies = bodies(document);
tree = tree.push(section_header(crate::tf!("Bodies ({})", bodies.len()).into_owned()));
tree = tree.push(section_header(
crate::tf!("Bodies ({})", bodies.len()).into_owned(),
));
if bodies.is_empty() {
tree = tree.push(note_row(crate::t!("No solids yet").into_owned()));
} else {
+257 -68
View File
@@ -9,7 +9,10 @@ use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, field_style};
use codec::count::{BlockInstance, CountKey};
use codec::{CadDocument, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::widget::{
button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip,
Space,
};
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
const BLOCK_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/block.svg");
@@ -73,7 +76,11 @@ impl CountMode {
/// The key the match options build from the reference instance.
pub fn key(&self, instances: &[BlockInstance]) -> CountKey {
match &self.target {
Some(CountTarget::Block { reference: Some(r), matching, .. }) => instances
Some(CountTarget::Block {
reference: Some(r),
matching,
..
}) => instances
.iter()
.find(|i| i.handle == *r)
.map(|i| CountKey::of(i, matching[0], matching[1], matching[2]))
@@ -102,7 +109,11 @@ impl CountMode {
pub fn compute(&self, doc: &CadDocument) -> (CountResult, usize) {
if let Some(CountTarget::Group(handles)) = &self.target {
let groups = codec::count::group_matches(doc, handles, self.area.as_deref());
let result = CountResult { counted: groups.concat(), errors: Vec::new(), overlapped: Vec::new() };
let result = CountResult {
counted: groups.concat(),
errors: Vec::new(),
overlapped: Vec::new(),
};
return (result, groups.len());
}
let result = self.block_result(doc);
@@ -118,9 +129,23 @@ impl CountMode {
let key = self.key(&instances);
let mine = |i: &&BlockInstance| i.name.eq_ignore_ascii_case(name) && key.matches(i);
CountResult {
counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(),
errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(),
overlapped: instances.iter().filter(mine).filter_map(|i| i.duplicate_of).collect(),
counted: instances
.iter()
.filter(mine)
.filter(|i| i.duplicate_of.is_none())
.map(|i| i.handle)
.collect(),
errors: instances
.iter()
.filter(mine)
.filter(|i| i.duplicate_of.is_some())
.map(|i| i.handle)
.collect(),
overlapped: instances
.iter()
.filter(mine)
.filter_map(|i| i.duplicate_of)
.collect(),
}
}
}
@@ -160,7 +185,14 @@ pub struct CountPalette {
/// on with all of model space (kept with the user settings).
pub invalid_choice: u8,
/// The list's references for (document tab id, geometry epoch, area).
pub list_cache: std::cell::RefCell<Option<(u64, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
pub list_cache: std::cell::RefCell<
Option<(
u64,
u64,
Option<Vec<[f64; 2]>>,
std::rc::Rc<Vec<BlockInstance>>,
)>,
>,
}
impl Default for CountPalette {
@@ -187,7 +219,10 @@ impl Default for CountPalette {
impl CountPalette {
pub fn expansion(&self, name: &str) -> Option<[bool; 3]> {
let key = name.to_ascii_uppercase();
self.expanded.iter().find(|(n, _)| *n == key).map(|(_, e)| *e)
self.expanded
.iter()
.find(|(n, _)| *n == key)
.map(|(_, e)| *e)
}
}
@@ -251,10 +286,18 @@ pub fn rows(instances: &[BlockInstance], palette: &CountPalette) -> Vec<Row> {
let mut rows: Vec<Row> = codec::count::block_counts(instances)
.into_iter()
.filter(|c| query.is_empty() || c.name.to_lowercase().contains(&query))
.map(|c| Row { count: c.counted.len(), errors: c.duplicates.len(), name: c.name })
.map(|c| Row {
count: c.counted.len(),
errors: c.duplicates.len(),
name: c.name,
})
.collect();
if palette.by_count {
rows.sort_by(|a, b| a.count.cmp(&b.count).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
rows.sort_by(|a, b| {
a.count
.cmp(&b.count)
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
}
if palette.descending {
rows.reverse();
@@ -283,27 +326,50 @@ fn row_menu(name: String, key: CountKey, expandable: bool) -> Element<'static, M
.into()
};
let mut items = column![
entry(crate::t!("Review Count Details").into_owned(), RowAction::Review),
entry(crate::t!("Insert Count Field").into_owned(), RowAction::Field),
entry(
crate::t!("Review Count Details").into_owned(),
RowAction::Review
),
entry(
crate::t!("Insert Count Field").into_owned(),
RowAction::Field
),
]
.spacing(1);
if expandable {
items = items
.push(container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}))
.push(container(text(crate::t!("EXPAND BY")).size(10).style(muted_style)).padding([3, 12]))
.push(
container(Space::new())
.height(1)
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
..Default::default()
}),
)
.push(
container(text(crate::t!("EXPAND BY")).size(10).style(muted_style))
.padding([3, 12]),
)
.push(entry(crate::t!("Layer").into_owned(), RowAction::Expand(0)))
.push(entry(crate::t!("Scale").into_owned(), RowAction::Expand(1)))
.push(entry(crate::t!("Mirror State").into_owned(), RowAction::Expand(2)));
.push(entry(
crate::t!("Mirror State").into_owned(),
RowAction::Expand(2),
));
}
container(items)
.padding(4)
.width(Length::Fixed(200.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
})
.into()
@@ -319,16 +385,25 @@ fn list_row<'a>(
depth: u16,
expanded: Option<bool>,
) -> Element<'a, Message> {
let lead: Element<'a, Message> = if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) {
let on = checked.contains(&name.to_ascii_uppercase());
let n = name.to_string();
checkbox(on).size(14).on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v))).into()
} else if expanded == Some(true) {
container(crate::ui::icons::themed(crate::ui::icons::MINUS, 12.0)).width(Length::Fixed(14.0)).into()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
let mut cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 18.0)), lead];
let lead: Element<'a, Message> =
if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) {
let on = checked.contains(&name.to_ascii_uppercase());
let n = name.to_string();
checkbox(on)
.size(14)
.on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v)))
.into()
} else if expanded == Some(true) {
container(crate::ui::icons::themed(crate::ui::icons::MINUS, 12.0))
.width(Length::Fixed(14.0))
.into()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
let mut cells = row![
Space::new().width(Length::Fixed(f32::from(depth) * 18.0)),
lead
];
if depth == 0 {
cells = cells.push(small_icon(BLOCK_ICON, 14.0));
}
@@ -341,7 +416,9 @@ fn list_row<'a>(
Some(n) => Element::from(text(n.to_string()).size(12)),
None => crate::ui::icons::themed(crate::ui::icons::MORE, 12.0),
});
let body = container(cells.spacing(6).align_y(iced::Center)).width(Fill).padding([4, 6]);
let body = container(cells.spacing(6).align_y(iced::Center))
.width(Fill)
.padding([4, 6]);
let area = mouse_area(body)
.on_press(msg(CountMsg::Open(name.to_string(), key.clone())))
.interaction(iced::mouse::Interaction::Pointer);
@@ -349,7 +426,12 @@ fn list_row<'a>(
iced_aw::ContextMenu::new(area, move || row_menu(n.clone(), key.clone(), expandable)).into()
}
fn header_button<'a>(label: String, active: bool, descending: bool, m: CountMsg) -> Element<'a, Message> {
fn header_button<'a>(
label: String,
active: bool,
descending: bool,
m: CountMsg,
) -> Element<'a, Message> {
let mut content = row![text(label).size(11)].spacing(3).align_y(iced::Center);
if active {
content = content.push(crate::ui::icons::themed_arrow_toggle(!descending, 8.0));
@@ -379,9 +461,19 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
.spacing(4)
.align_y(iced::Center);
let header = row![
header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)),
header_button(
crate::t!("Name").into_owned(),
!palette.by_count,
palette.descending,
CountMsg::Sort(false)
),
Space::new().width(Fill),
header_button(crate::t!("Count").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)),
header_button(
crate::t!("Count").into_owned(),
palette.by_count,
palette.descending,
CountMsg::Sort(true)
),
]
.align_y(iced::Center);
let mut list = column![].spacing(1);
@@ -401,7 +493,16 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
if let Some([l, s, m]) = expansion {
for (key, n) in codec::count::expanded_counts(instances, &r.name, l, s, m) {
let label = key.label();
list = list.push(list_row(palette, &r.name, key, label, Some(n), false, 1, None));
list = list.push(list_row(
palette,
&r.name,
key,
label,
Some(n),
false,
1,
None,
));
}
}
}
@@ -416,13 +517,20 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
.into(),
Some(checked) => {
let all = rows(instances, palette);
let every = !all.is_empty() && all.iter().all(|r| checked.contains(&r.name.to_ascii_uppercase()));
let mut insert = button(text(crate::t!("Insert")).size(12)).style(button_style(true)).padding([5, 12]);
let every = !all.is_empty()
&& all
.iter()
.all(|r| checked.contains(&r.name.to_ascii_uppercase()));
let mut insert = button(text(crate::t!("Insert")).size(12))
.style(button_style(true))
.padding([5, 12]);
if !checked.is_empty() {
insert = insert.on_press(msg(CountMsg::TableInsert));
}
row![
checkbox(every).size(14).on_toggle(|v| msg(CountMsg::TableAll(v))),
checkbox(every)
.size(14)
.on_toggle(|v| msg(CountMsg::TableAll(v))),
Space::new().width(Fill),
button(text(crate::t!("Cancel")).size(12))
.on_press(msg(CountMsg::TableCancel))
@@ -450,18 +558,32 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
fn card(theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
}
}
/// Count mode in the palette: back to the list, the counted block with the
/// match options, and the error report.
fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocument) -> Element<'a, Message> {
let back = button(row![crate::ui::icons::themed_arrow_left(10.0), text(crate::t!("Back to List")).size(12)].spacing(4).align_y(iced::Center))
.on_press(msg(CountMsg::Back))
.style(button::text)
.padding([2, 2]);
fn mode_view<'a>(
palette: &'a CountPalette,
mode: &'a CountMode,
doc: &CadDocument,
) -> Element<'a, Message> {
let back = button(
row![
crate::ui::icons::themed_arrow_left(10.0),
text(crate::t!("Back to List")).size(12)
]
.spacing(4)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::Back))
.style(button::text)
.padding([2, 2]);
let result = mode.result(doc);
let mut body = column![back].spacing(8);
if let Some(name) = mode.target_name() {
@@ -469,7 +591,8 @@ fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocume
row![
crate::ui::icons::themed_success(CHECK_ICON, 12.0),
text(match mode.target {
Some(CountTarget::Group(_)) => crate::tf!("Geometries: {}", mode.count(doc)).into_owned(),
Some(CountTarget::Group(_)) =>
crate::tf!("Geometries: {}", mode.count(doc)).into_owned(),
_ => format!("{name}: {}", mode.count(doc)),
})
.size(12)
@@ -482,8 +605,13 @@ fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocume
.style(button::text)
.padding(0)]
.spacing(6);
if let (false, Some(CountTarget::Block { matching, .. })) = (palette.details_closed, &mode.target) {
for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"].iter().enumerate() {
if let (false, Some(CountTarget::Block { matching, .. })) =
(palette.details_closed, &mode.target)
{
for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"]
.iter()
.enumerate()
{
details = details.push(
checkbox(matching[k])
.label(crate::t!(*label).into_owned())
@@ -499,7 +627,9 @@ fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocume
let mut report = column![button(
row![
crate::ui::icons::themed_warning(WARNING_ICON, 12.0),
text(crate::tf!("Count Error Report: {}", result.errors.len())).size(12).width(Fill),
text(crate::tf!("Count Error Report: {}", result.errors.len()))
.size(12)
.width(Fill),
]
.spacing(6)
.align_y(iced::Center),
@@ -532,7 +662,11 @@ pub fn view<'a>(
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse);
let title_bar = crate::ui::dock::title_bar(
PanelId::Count,
crate::t!("Count").into_owned(),
auto_collapse,
);
let body = match mode {
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
_ => {
@@ -542,7 +676,8 @@ pub fn view<'a>(
let instances = match cache.as_ref() {
Some((d, e, a, list)) if (*d, *e) == key && *a == area => list.clone(),
_ => {
let list = std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref()));
let list =
std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref()));
*cache = Some((key.0, key.1, area, list.clone()));
list
}
@@ -553,12 +688,18 @@ pub fn view<'a>(
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
}
fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option<CountMsg>) -> Element<'a, Message> {
fn bar_button<'a>(
icon: Element<'a, Message>,
tip: String,
m: Option<CountMsg>,
) -> Element<'a, Message> {
let mut b = button(icon).style(button::text).padding([3, 5]);
if let Some(m) = m {
b = b.on_press(msg(m));
}
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
tooltip(b, text(tip).size(10), tooltip::Position::Bottom)
.gap(4)
.into()
}
/// The floating toolbar over the drawing in count mode: the count, the
@@ -566,7 +707,11 @@ fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option<CountMsg>)
pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> {
let has_target = mode.target.is_some();
let result = mode.result(doc);
let count = if has_target { mode.count(doc).to_string() } else { "-".to_string() };
let count = if has_target {
mode.count(doc).to_string()
} else {
"-".to_string()
};
let status: Element<'a, Message> = if !result.errors.is_empty() {
crate::ui::icons::themed_warning(WARNING_ICON, 14.0)
} else if has_target {
@@ -585,21 +730,55 @@ pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message>
let bar = row![
text(crate::tf!("Count: {}", count)).size(13),
status,
bar_button(crate::ui::icons::themed_arrow_left(12.0), crate::t!("Previous").into_owned(), on(CountMsg::Prev)),
bar_button(crate::ui::icons::themed_arrow_right(12.0), crate::t!("Next").into_owned(), on(CountMsg::Next)),
bar_button(crate::ui::icons::themed_success(AREA_ICON, 16.0), crate::t!("Count in a specified area").into_owned(), Some(CountMsg::Area)),
bar_button(crate::ui::icons::themed_success(SELECT_ICON, 16.0), crate::t!("Select objects").into_owned(), Some(CountMsg::Select)),
bar_button(icon(TABLE_ICON), crate::t!("Insert Count Field").into_owned(), on(CountMsg::Field)),
bar_button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0), crate::t!("Close").into_owned(), Some(CountMsg::Close)),
bar_button(
crate::ui::icons::themed_arrow_left(12.0),
crate::t!("Previous").into_owned(),
on(CountMsg::Prev)
),
bar_button(
crate::ui::icons::themed_arrow_right(12.0),
crate::t!("Next").into_owned(),
on(CountMsg::Next)
),
bar_button(
crate::ui::icons::themed_success(AREA_ICON, 16.0),
crate::t!("Count in a specified area").into_owned(),
Some(CountMsg::Area)
),
bar_button(
crate::ui::icons::themed_success(SELECT_ICON, 16.0),
crate::t!("Select objects").into_owned(),
Some(CountMsg::Select)
),
bar_button(
icon(TABLE_ICON),
crate::t!("Insert Count Field").into_owned(),
on(CountMsg::Field)
),
bar_button(
crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0),
crate::t!("Close").into_owned(),
Some(CountMsg::Close)
),
]
.spacing(6)
.align_y(iced::Center);
let bar = container(bar).padding([4, 10]).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
});
container(iced::widget::opaque(bar)).width(Fill).align_x(iced::Center).padding([10, 0]).into()
let bar = container(bar)
.padding([4, 10])
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
});
container(iced::widget::opaque(bar))
.width(Fill)
.align_x(iced::Center)
.padding([10, 0])
.into()
}
/// The blue frame around the drawing area in count mode.
@@ -608,7 +787,11 @@ pub fn border<'a>() -> Element<'a, Message> {
.width(Fill)
.height(Fill)
.style(|_: &Theme| container::Style {
border: Border { color: Color::from_rgb(0.18, 0.52, 0.95), width: 2.0, radius: 0.0.into() },
border: Border {
color: Color::from_rgb(0.18, 0.52, 0.95),
width: 2.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
@@ -619,14 +802,20 @@ pub fn aci_rgba(index: i16) -> [f32; 4] {
crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index))
}
/// The Invalid Area dialog, shown when the count area's boundary is gone.
pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
pub fn invalid_area_view<'a>(
palette: &CountPalette,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let action = |title: String, detail: String, m: CountMsg| -> Element<'a, Message> {
button(
row![
crate::ui::icons::themed_primary(crate::ui::icons::ARROW_LONG_RIGHT, 16.0),
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
column![
text(title).size(13),
text(detail).size(11).style(muted_style)
]
.spacing(2),
]
.spacing(10)
.align_y(iced::Center),
+54 -45
View File
@@ -7,9 +7,7 @@ use iced::{Background, Border, Element, Fill, Theme};
use std::borrow::Cow;
/// Active tab in the Drafting Settings dialog.
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize,
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DraftingSettingsTab {
#[default]
@@ -116,12 +114,17 @@ pub fn parse_grid_major(s: &str) -> Option<u32> {
}
/// Helper for grouped sub-panels with a light border and header title.
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
fn group<'a>(
title: impl Into<String>,
body: impl Into<Element<'a, Message>>,
) -> Element<'a, Message> {
container(
column![
text(title.into()).size(11).style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.75)),
}),
text(title.into())
.size(11)
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().background.strongest.text.scale_alpha(0.75)),
}),
body.into(),
]
.spacing(6),
@@ -167,10 +170,9 @@ pub fn view_window<'a>(
let palette = theme.palette();
let pair = match (is_active, status) {
(true, _) => palette.primary.strong,
(
false,
button::Status::Hovered | button::Status::Pressed,
) => palette.background.strong,
(false, button::Status::Hovered | button::Status::Pressed) => {
palette.background.strong
}
_ => palette.background.weak,
};
button::Style {
@@ -194,12 +196,27 @@ pub fn view_window<'a>(
let tab_row = row![
tab_button(DraftingSettingsTab::SnapAndGrid, crate::t!("Snap and Grid")),
tab_button(DraftingSettingsTab::PolarTracking, crate::t!("Polar Tracking")),
tab_button(
DraftingSettingsTab::PolarTracking,
crate::t!("Polar Tracking")
),
tab_button(DraftingSettingsTab::ObjectSnap, crate::t!("Object Snap")),
tab_button(DraftingSettingsTab::ObjectSnap3D, crate::t!("3D Object Snap")),
tab_button(DraftingSettingsTab::DynamicInput, crate::t!("Dynamic Input")),
tab_button(DraftingSettingsTab::QuickProperties, crate::t!("Quick Properties")),
tab_button(DraftingSettingsTab::SelectionCycling, crate::t!("Selection Cycling")),
tab_button(
DraftingSettingsTab::ObjectSnap3D,
crate::t!("3D Object Snap")
),
tab_button(
DraftingSettingsTab::DynamicInput,
crate::t!("Dynamic Input")
),
tab_button(
DraftingSettingsTab::QuickProperties,
crate::t!("Quick Properties")
),
tab_button(
DraftingSettingsTab::SelectionCycling,
crate::t!("Selection Cycling")
),
]
.spacing(2)
.width(Fill)
@@ -209,9 +226,7 @@ pub fn view_window<'a>(
.width(Fill)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
});
@@ -287,7 +302,11 @@ pub fn view_window<'a>(
text(crate::t!("F5 cycles Left, Top, and Right.")).size(10.5),
Space::new().height(4),
row![
text(crate::t!("Rotation: %{angle}°", angle = state.snap_angle_deg)).size(11),
text(crate::t!(
"Rotation: %{angle}°",
angle = state.snap_angle_deg
))
.size(11),
button(text(crate::t!("Reset rotation")).size(10))
.on_press(Message::DraftingSettingsResetRotation)
.style(button::secondary)
@@ -488,8 +507,7 @@ pub fn view_window<'a>(
);
column![
row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle]
.align_y(iced::Center),
row![osnap_on_toggle, Space::new().width(24), otrack_on_toggle].align_y(iced::Center),
action_buttons,
Space::new().height(4),
snap_modes_group,
@@ -559,15 +577,15 @@ pub fn view_window<'a>(
let prompt_group = group(
crate::t!("Dynamic Prompts"),
column![
row![
checkbox(true).size(14),
text(crate::t!("Show command prompting and command input near crosshairs"))
.size(11),
]
.spacing(7)
.align_y(iced::Center),
column![row![
checkbox(true).size(14),
text(crate::t!(
"Show command prompting and command input near crosshairs"
))
.size(11),
]
.spacing(7)
.align_y(iced::Center),]
.spacing(6),
);
@@ -608,13 +626,9 @@ pub fn view_window<'a>(
.spacing(6),
);
column![
quick_props_toggle,
Space::new().height(4),
palette_group,
]
.spacing(10)
.width(Fill)
column![quick_props_toggle, Space::new().height(4), palette_group,]
.spacing(10)
.width(Fill)
};
// ── Tab 7: Selection Cycling ─────────────────────────────────────────
@@ -644,13 +658,9 @@ pub fn view_window<'a>(
.spacing(6),
);
column![
sel_cycling_toggle,
Space::new().height(4),
cycling_group,
]
.spacing(10)
.width(Fill)
column![sel_cycling_toggle, Space::new().height(4), cycling_group,]
.spacing(10)
.width(Fill)
};
// ── Active Tab Content ───────────────────────────────────────────────
@@ -719,4 +729,3 @@ pub fn view_window<'a>(
Message::DraftingSettingsCloseKeep,
)
}
+481 -120
View File
@@ -2,7 +2,9 @@
//! dialogs): a category and field list on the left, the chosen field's
//! format on the right, a live preview and the field expression below.
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Element, Fill, Length};
use crate::app::Message;
@@ -26,17 +28,49 @@ pub enum FieldTarget {
/// application evaluates).
pub const CATEGORIES: &[(&str, &[&str])] = &[
("All", &[]),
("Date & Time", &["CreateDate", "Date", "PlotDate", "SaveDate"]),
(
"Date & Time",
&["CreateDate", "Date", "PlotDate", "SaveDate"],
),
(
"Document",
&["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"],
&[
"Author",
"Comments",
"Filename",
"Filesize",
"HyperlinkBase",
"Keywords",
"LastSavedBy",
"Subject",
"Title",
],
),
("Linked", &["Hyperlink"]),
("Objects", &["BlockPlaceholder", "Count", "CountInArea", "Formula", "NamedObject", "Object"]),
(
"Objects",
&[
"BlockPlaceholder",
"Count",
"CountInArea",
"Formula",
"NamedObject",
"Object",
],
),
("Other", &["DieselExpression", "SystemVariable"]),
(
"Plot",
&["DeviceName", "Login", "PageSetupName", "PaperSize", "PlotDate", "PlotOrientation", "PlotScale", "PlotStyleTable"],
&[
"DeviceName",
"Login",
"PageSetupName",
"PaperSize",
"PlotDate",
"PlotOrientation",
"PlotScale",
"PlotStyleTable",
],
),
("SheetSet", SHEET_SET_FIELDS),
];
@@ -92,24 +126,40 @@ fn sheet_set_code(name: &str, custom: &str) -> String {
/// The navigation tree of a SheetSet (set, subsets, sheets) or SheetView
/// (set, view categories) field: depth, component id, label, is a sheet.
pub fn nav_nodes(db: &codec::sheet_set::SheetSetDatabase, views: bool) -> Vec<(u16, String, String, bool)> {
pub fn nav_nodes(
db: &codec::sheet_set::SheetSetDatabase,
views: bool,
) -> Vec<(u16, String, String, bool)> {
use codec::sheet_set::{number_and_title, ComponentKind, Element as SsElement};
let set = db.sheet_set();
let mut out = vec![(0, set.id().to_string(), db.name().to_string(), false)];
if views {
// Views of the unnamed (default) category sit under the set, then each
// named category with its views.
let all: Vec<&SsElement> = db.sheets().into_iter().flat_map(|s| db.sheet_views(s)).collect();
let all: Vec<&SsElement> = db
.sheets()
.into_iter()
.flat_map(|s| db.sheet_views(s))
.collect();
let named: Vec<&SsElement> = db.view_categories();
let in_named = |v: &SsElement| {
codec::sheet_set::SheetSetDatabase::view_category_of(v).is_some_and(|c| named.iter().any(|n| n.id() == c))
codec::sheet_set::SheetSetDatabase::view_category_of(v)
.is_some_and(|c| named.iter().any(|n| n.id() == c))
};
for v in all.iter().filter(|v| !in_named(v)) {
out.push((1, v.id().to_string(), number_and_title(v), true));
}
for c in &named {
out.push((1, c.id().to_string(), c.prop("Name").unwrap_or("").to_string(), false));
for v in all.iter().filter(|v| codec::sheet_set::SheetSetDatabase::view_category_of(v) == Some(c.id())) {
out.push((
1,
c.id().to_string(),
c.prop("Name").unwrap_or("").to_string(),
false,
));
for v in all
.iter()
.filter(|v| codec::sheet_set::SheetSetDatabase::view_category_of(v) == Some(c.id()))
{
out.push((2, v.id().to_string(), number_and_title(v), true));
}
}
@@ -119,10 +169,17 @@ pub fn nav_nodes(db: &codec::sheet_set::SheetSetDatabase, views: bool) -> Vec<(u
for c in &el.children {
match ComponentKind::of(c) {
Some(ComponentKind::Subset) => {
out.push((depth, c.id().to_string(), c.prop("Name").unwrap_or("").to_string(), false));
out.push((
depth,
c.id().to_string(),
c.prop("Name").unwrap_or("").to_string(),
false,
));
go(c, depth + 1, out);
}
Some(ComponentKind::Sheet) => out.push((depth, c.id().to_string(), number_and_title(c), true)),
Some(ComponentKind::Sheet) => {
out.push((depth, c.id().to_string(), number_and_title(c), true))
}
_ => {}
}
}
@@ -132,14 +189,27 @@ pub fn nav_nodes(db: &codec::sheet_set::SheetSetDatabase, views: bool) -> Vec<(u
}
/// The properties a navigation node offers: list label, code property, version.
pub fn nav_props(db: &codec::sheet_set::SheetSetDatabase, id: &str) -> Vec<(String, String, &'static str)> {
use codec::sheet_set::{custom_properties, ComponentKind, CUSTOM_SHEET_PROP, CUSTOM_SHEET_SET_PROP};
let Some(el) = db.find(id) else { return Vec::new() };
pub fn nav_props(
db: &codec::sheet_set::SheetSetDatabase,
id: &str,
) -> Vec<(String, String, &'static str)> {
use codec::sheet_set::{
custom_properties, ComponentKind, CUSTOM_SHEET_PROP, CUSTOM_SHEET_SET_PROP,
};
let Some(el) = db.find(id) else {
return Vec::new();
};
let fixed = |list: &[(&str, &str, &'static str)]| -> Vec<(String, String, &'static str)> {
list.iter().map(|(l, p, v)| (l.to_string(), p.to_string(), *v)).collect()
list.iter()
.map(|(l, p, v)| (l.to_string(), p.to_string(), *v))
.collect()
};
let custom = |flag: i32| -> Vec<(String, String, &'static str)> {
custom_properties(db.sheet_set()).into_iter().filter(|(_, _, f)| f & flag != 0).map(|(n, _, _)| (n.clone(), n, "")).collect()
custom_properties(db.sheet_set())
.into_iter()
.filter(|(_, _, f)| f & flag != 0)
.map(|(n, _, _)| (n.clone(), n, ""))
.collect()
};
match ComponentKind::of(el) {
Some(ComponentKind::SheetSet) => {
@@ -180,13 +250,24 @@ pub fn nav_props(db: &codec::sheet_set::SheetSetDatabase, id: &str) -> Vec<(Stri
}
/// The sheet drawing, named view and layout a sheet or sheet view node links to.
pub fn nav_link(db: &codec::sheet_set::SheetSetDatabase, id: &str) -> Option<(String, String, String)> {
pub fn nav_link(
db: &codec::sheet_set::SheetSetDatabase,
id: &str,
) -> Option<(String, String, String)> {
let el = db.find(id)?;
let (sheet, view) = if el.name == "AcSmSheetView" {
let views = db.parent_of(id)?;
let sheet = db.parent_of(views.id())?;
(sheet.id().to_string(), el.named("NamedView").and_then(|v| v.prop("Name")).unwrap_or("").to_string())
} else if codec::sheet_set::ComponentKind::of(el) == Some(codec::sheet_set::ComponentKind::Sheet) {
(
sheet.id().to_string(),
el.named("NamedView")
.and_then(|v| v.prop("Name"))
.unwrap_or("")
.to_string(),
)
} else if codec::sheet_set::ComponentKind::of(el)
== Some(codec::sheet_set::ComponentKind::Sheet)
{
(id.to_string(), String::new())
} else {
return None;
@@ -199,8 +280,13 @@ impl FieldDialogState {
/// `\AcSm Database("…").SheetSet("…")[.Component("…")].Property` of the
/// picked node, with the text case and, on a sheet, the hyperlink to it.
fn navigation_code(&self) -> String {
let Some(db) = self.ss_sets.get(self.ss_set) else { return "\\AcSm".into() };
let Some((_, prop, version)) = nav_props(db, &self.ss_node).into_iter().find(|(l, _, _)| *l == self.ss_prop) else {
let Some(db) = self.ss_sets.get(self.ss_set) else {
return "\\AcSm".into();
};
let Some((_, prop, version)) = nav_props(db, &self.ss_node)
.into_iter()
.find(|(l, _, _)| *l == self.ss_prop)
else {
return "\\AcSm".into();
};
let set = db.sheet_set().id().to_string();
@@ -210,7 +296,13 @@ impl FieldDialogState {
let version = if scale {
codec::fields::PLOT_SCALE_FORMATS
.get(self.ss_scale_format)
.map_or("", |(_, c)| if c.starts_with("\\AcVar.16.2") { ".16.2" } else { "" })
.map_or("", |(_, c)| {
if c.starts_with("\\AcVar.16.2") {
".16.2"
} else {
""
}
})
} else {
version
};
@@ -269,25 +361,67 @@ pub fn viewport_scale_picture(k: usize) -> String {
/// The fields of a category; "All" lists every field once, sorted.
pub fn fields_of(category: usize) -> Vec<&'static str> {
if category == 0 {
let mut all: Vec<&'static str> = CATEGORIES.iter().flat_map(|(_, f)| f.iter().copied()).collect();
let mut all: Vec<&'static str> = CATEGORIES
.iter()
.flat_map(|(_, f)| f.iter().copied())
.collect();
all.sort_unstable_by_key(|s| s.to_ascii_lowercase());
all.dedup();
return all;
}
CATEGORIES.get(category).map(|(_, f)| f.to_vec()).unwrap_or_default()
CATEGORIES
.get(category)
.map(|(_, f)| f.to_vec())
.unwrap_or_default()
}
/// The date formats the dialog lists, in the reference order.
pub const DATE_FORMATS: &[&str] = &[
"M/d/yyyy", "dddd, MMMM dd, yyyy", "MMMM d, yyyy", "M/d/yy", "yyyy-MM-dd", "d-MMM-yy",
"M.d.yyyy", "MMM. d, yy", "d MMMM yyyy", "dd.MM.yyyy", "dd/MM/yyyy", "yyyy/MM/dd",
"yyyy-M-d", "MMMM yy", "MMM-yy", "M/d/yyyy h:mm tt", "M/d/yyyy h:mm:ss tt", "h:mm tt",
"h:mm:ss tt", "HH:mm", "HH:mm:ss", "%#x", "%#c", "%x", "%c", "%X",
"M/d/yyyy",
"dddd, MMMM dd, yyyy",
"MMMM d, yyyy",
"M/d/yy",
"yyyy-MM-dd",
"d-MMM-yy",
"M.d.yyyy",
"MMM. d, yy",
"d MMMM yyyy",
"dd.MM.yyyy",
"dd/MM/yyyy",
"yyyy/MM/dd",
"yyyy-M-d",
"MMMM yy",
"MMM-yy",
"M/d/yyyy h:mm tt",
"M/d/yyyy h:mm:ss tt",
"h:mm tt",
"h:mm:ss tt",
"HH:mm",
"HH:mm:ss",
"%#x",
"%#c",
"%x",
"%c",
"%X",
];
pub const TEXT_CASES: &[&str] = &["(none)", "Uppercase", "Lowercase", "First capital", "Title case"];
pub const TEXT_CASES: &[&str] = &[
"(none)",
"Uppercase",
"Lowercase",
"First capital",
"Title case",
];
pub const SIZE_UNITS: &[&str] = &["Bytes", "Kilobytes", "Megabytes"];
pub const NAMED_TYPES: &[&str] = &["Block", "Dimstyle", "Layer", "Linetype", "Tablestyle", "Textstyle", "View"];
pub const NAMED_TYPES: &[&str] = &[
"Block",
"Dimstyle",
"Layer",
"Linetype",
"Tablestyle",
"Textstyle",
"View",
];
/// How a field is formatted, by its name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -316,8 +450,18 @@ pub enum FieldKind {
}
/// Formula precisions: the current one, then 0 to 8 decimals.
pub const FORMULA_PRECISIONS: &[&str] =
&["Current precision", "0", "0.0", "0.00", "0.000", "0.0000", "0.00000", "0.000000", "0.0000000", "0.00000000"];
pub const FORMULA_PRECISIONS: &[&str] = &[
"Current precision",
"0",
"0.0",
"0.00",
"0.000",
"0.0000",
"0.00000",
"0.000000",
"0.0000000",
"0.00000000",
];
pub fn kind_of(name: &str) -> FieldKind {
match name {
@@ -450,10 +594,18 @@ impl FieldDialogState {
n => format!("{base} \\f \"%tc{n}\""),
};
match kind_of(self.name) {
FieldKind::Date => (format!("\\AcVar {} \\f \"{}\"", self.name, self.date_format), vec![]),
FieldKind::Date => (
format!("\\AcVar {} \\f \"{}\"", self.name, self.date_format),
vec![],
),
FieldKind::Text => (case(format!("\\AcVar {}", self.name)), vec![]),
FieldKind::Filename => {
let bits = self.file_parts + if self.file_extension && self.file_parts & 2 != 0 { 4 } else { 0 };
let bits = self.file_parts
+ if self.file_extension && self.file_parts & 2 != 0 {
4
} else {
0
};
// The text case goes first in the same format: "%tc1%fn6".
let case = match self.text_case {
0 => String::new(),
@@ -470,7 +622,9 @@ impl FieldDialogState {
vec![],
),
FieldKind::Formula => {
let format = codec::fields::FORMULA_FORMATS.get(self.formula_format).map_or("", |f| f.1);
let format = codec::fields::FORMULA_FORMATS
.get(self.formula_format)
.map_or("", |f| f.1);
let precision = match self.formula_precision {
0 => String::new(),
n => format!("%pr{}", n - 1),
@@ -483,7 +637,11 @@ impl FieldDialogState {
}
}
FieldKind::Hyperlink => (
format!("\\AcVar \\href \"{}##{}#0\"", self.hyperlink_url.trim(), self.hyperlink_text),
format!(
"\\AcVar \\href \"{}##{}#0\"",
self.hyperlink_url.trim(),
self.hyperlink_text
),
vec![],
),
FieldKind::PlotScale => (
@@ -493,22 +651,39 @@ impl FieldDialogState {
vec![],
),
FieldKind::Count => {
let evaluator = if self.name == "CountInArea" { "AcCount2" } else { "AcCount" };
let evaluator = if self.name == "CountInArea" {
"AcCount2"
} else {
"AcCount"
};
match self.count_expression.trim() {
"" => (format!("\\{evaluator}"), vec![]),
json => (format!("\\{evaluator} {json}"), vec![]),
}
}
FieldKind::SheetSet | FieldKind::SheetSetCustom => (case(sheet_set_code(self.name, &self.ss_custom)), vec![]),
FieldKind::SheetSet | FieldKind::SheetSetCustom => {
(case(sheet_set_code(self.name, &self.ss_custom)), vec![])
}
FieldKind::SheetSetNavigation => (self.navigation_code(), vec![]),
FieldKind::SheetSetPlaceholder => {
let (label, target, version) = PLACEHOLDER_TYPES[self.ss_placeholder.min(PLACEHOLDER_TYPES.len() - 1)];
let target = if label == "Custom" { format!("{target}{}", self.ss_custom) } else { target.to_string() };
let (label, target, version) =
PLACEHOLDER_TYPES[self.ss_placeholder.min(PLACEHOLDER_TYPES.len() - 1)];
let target = if label == "Custom" {
format!("{target}{}", self.ss_custom)
} else {
target.to_string()
};
if label == "ViewportScale" {
// The version follows the format, as for PlotScale.
let version = codec::fields::PLOT_SCALE_FORMATS
.get(self.ss_scale_format)
.map_or("", |(_, code)| if code.starts_with("\\AcVar.16.2") { ".16.2" } else { "" });
.map_or("", |(_, code)| {
if code.starts_with("\\AcVar.16.2") {
".16.2"
} else {
""
}
});
let base = format!("\\AcSm{version} {target}");
match viewport_scale_picture(self.ss_scale_format) {
f if f.is_empty() => (base, vec![]),
@@ -519,7 +694,8 @@ impl FieldDialogState {
}
}
FieldKind::BlockPlaceholder => {
let (_, property, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property];
let (_, property, format) =
codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property];
// Text properties take the chosen case; the others keep their own format.
let format = match (format, self.text_case) {
("%tc4", 0) => String::new(),
@@ -536,13 +712,17 @@ impl FieldDialogState {
FieldKind::SystemVariable => (format!("\\AcVar {}", self.sysvar), vec![]),
FieldKind::Diesel => (format!("\\AcDiesel {}", self.diesel), vec![]),
FieldKind::NamedObject => match self.named.and_then(|i| self.named_names.get(i)) {
Some((_, handle)) => (case("\\AcObjProp Object(%<\\_ObjIdx 0>%).Name".into()), vec![*handle]),
Some((_, handle)) => (
case("\\AcObjProp Object(%<\\_ObjIdx 0>%).Name".into()),
vec![*handle],
),
None => ("\\AcObjProp".into(), vec![]),
},
FieldKind::Object => match (&self.object, self.object_prop) {
(Some((handle, _)), Some(prop)) => {
(format!("\\AcObjProp Object(%<\\_ObjIdx 0>%).{prop}"), vec![*handle])
}
(Some((handle, _)), Some(prop)) => (
format!("\\AcObjProp Object(%<\\_ObjIdx 0>%).{prop}"),
vec![*handle],
),
_ => ("\\AcObjProp".into(), vec![]),
},
}
@@ -572,12 +752,17 @@ fn indexed_object_ids(formula: &str) -> (String, Vec<codec::Handle>) {
while let Some(start) = rest.find("%<\\_ObjId ") {
let after = &rest[start + "%<\\_ObjId ".len()..];
let Some(end) = after.find(">%") else { break };
let Ok(value) = u64::from_str_radix(after[..end].trim(), 16) else { break };
let Ok(value) = u64::from_str_radix(after[..end].trim(), 16) else {
break;
};
let handle = codec::Handle::new(value);
let index = objects.iter().position(|h| *h == handle).unwrap_or_else(|| {
objects.push(handle);
objects.len() - 1
});
let index = objects
.iter()
.position(|h| *h == handle)
.unwrap_or_else(|| {
objects.push(handle);
objects.len() - 1
});
out.push_str(&rest[..start]);
out.push_str(&format!("%<\\_ObjIdx {index}>%"));
rest = &after[end + 2..];
@@ -656,11 +841,17 @@ fn msg(m: FieldDialogMsg) -> Message {
}
fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(column![text(title.to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
container(
column![
text(title.to_uppercase()).size(10).style(accent_text),
content.into()
]
.spacing(8),
)
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
}
/// A selectable list: one row per item, the chosen one highlighted.
@@ -673,11 +864,13 @@ fn list<'a>(rows: Vec<(String, bool, FieldDialogMsg)>, height: f32) -> Element<'
.width(Fill)
.into()
});
container(scrollable(iced::widget::Column::with_children(rows.collect::<Vec<Element<'a, Message>>>()).spacing(1)))
.height(Length::Fixed(height))
.padding(2)
.style(|theme: &iced::Theme| field_style(theme, text_input::Status::Active).into_container())
.into()
container(scrollable(
iced::widget::Column::with_children(rows.collect::<Vec<Element<'a, Message>>>()).spacing(1),
))
.height(Length::Fixed(height))
.padding(2)
.style(|theme: &iced::Theme| field_style(theme, text_input::Status::Active).into_container())
.into()
}
trait IntoContainer {
@@ -707,7 +900,13 @@ fn case_list<'a>(state: &FieldDialogState) -> Element<'a, Message> {
TEXT_CASES
.iter()
.enumerate()
.map(|(i, c)| (t!(*c).into_owned(), state.text_case == i, FieldDialogMsg::TextCase(i)))
.map(|(i, c)| {
(
t!(*c).into_owned(),
state.text_case == i,
FieldDialogMsg::TextCase(i),
)
})
.collect(),
130.0,
)
@@ -732,14 +931,20 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
examples
.iter()
.enumerate()
.map(|(i, e)| (e.clone(), DATE_FORMATS.get(i) == Some(&state.date_format.as_str()), FieldDialogMsg::DateExample(i)))
.map(|(i, e)| (
e.clone(),
DATE_FORMATS.get(i) == Some(&state.date_format.as_str()),
FieldDialogMsg::DateExample(i)
))
.collect(),
220.0,
),
]
.spacing(8)
.into(),
FieldKind::Text => column![text(t!("Format")).size(12), case_list(state)].spacing(8).into(),
FieldKind::Text => column![text(t!("Format")).size(12), case_list(state)]
.spacing(8)
.into(),
FieldKind::Filename => {
let part = |label: &str, bits: u8| {
checkbox(state.file_parts == bits)
@@ -772,7 +977,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
SIZE_UNITS
.iter()
.enumerate()
.map(|(i, u)| (t!(*u).into_owned(), state.size_unit == i, FieldDialogMsg::SizeUnit(i)))
.map(|(i, u)| (
t!(*u).into_owned(),
state.size_unit == i,
FieldDialogMsg::SizeUnit(i)
))
.collect(),
90.0,
),
@@ -784,7 +993,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
list(
crate::entities::field::SYSVARS
.iter()
.map(|v| (v.to_string(), state.sysvar == *v, FieldDialogMsg::SysVar(v.to_string())))
.map(|v| (
v.to_string(),
state.sysvar == *v,
FieldDialogMsg::SysVar(v.to_string())
))
.collect(),
250.0,
),
@@ -803,10 +1016,19 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.into(),
FieldKind::NamedObject => column![
row![
text(t!("Named object type")).size(12).width(Length::Fixed(120.0)),
text(t!("Named object type"))
.size(12)
.width(Length::Fixed(120.0)),
pick_list(
Some(Pick(state.named_type, t!(NAMED_TYPES[state.named_type]).into_owned())),
NAMED_TYPES.iter().enumerate().map(|(i, n)| Pick(i, t!(*n).into_owned())).collect::<Vec<_>>(),
Some(Pick(
state.named_type,
t!(NAMED_TYPES[state.named_type]).into_owned()
)),
NAMED_TYPES
.iter()
.enumerate()
.map(|(i, n)| Pick(i, t!(*n).into_owned()))
.collect::<Vec<_>>(),
|p: &Pick| p.1.clone(),
)
.on_select(|p| msg(FieldDialogMsg::NamedType(p.0)))
@@ -822,7 +1044,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.named_names
.iter()
.enumerate()
.map(|(i, (n, _))| (n.clone(), state.named == Some(i), FieldDialogMsg::Named(i)))
.map(|(i, (n, _))| (
n.clone(),
state.named == Some(i),
FieldDialogMsg::Named(i)
))
.collect(),
150.0,
),
@@ -837,16 +1063,23 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.enumerate()
.map(|(i, (n, _))| Pick(i, t!(*n).into_owned()))
.collect();
let precisions: Vec<Pick> =
FORMULA_PRECISIONS.iter().enumerate().map(|(i, n)| Pick(i, t!(*n).into_owned())).collect();
let precisions: Vec<Pick> = FORMULA_PRECISIONS
.iter()
.enumerate()
.map(|(i, n)| Pick(i, t!(*n).into_owned()))
.collect();
column![
row![
text(t!("Format")).size(12).width(Length::Fixed(80.0)),
pick_list(Some(formats[state.formula_format].clone()), formats.clone(), |p: &Pick| p.1.clone())
.on_select(|p| msg(FieldDialogMsg::FormulaFormat(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill),
pick_list(
Some(formats[state.formula_format].clone()),
formats.clone(),
|p: &Pick| p.1.clone()
)
.on_select(|p| msg(FieldDialogMsg::FormulaFormat(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill),
]
.spacing(8)
.align_y(iced::Center),
@@ -870,7 +1103,9 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
}
FieldKind::Hyperlink => column![
row![
text(t!("Text to display")).size(12).width(Length::Fixed(120.0)),
text(t!("Text to display"))
.size(12)
.width(Length::Fixed(120.0)),
text_input("", &state.hyperlink_text)
.size(12)
.padding([5, 8])
@@ -898,13 +1133,19 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.spacing(8)
.into(),
FieldKind::BlockPlaceholder => match &state.placeholder_block {
None => column![text(t!("Only accessible in the block editor")).size(12).style(muted_style)].into(),
None => column![text(t!("Only accessible in the block editor"))
.size(12)
.style(muted_style)]
.into(),
Some(block) => {
let (_, _, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property];
let (_, _, format) =
codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property];
let mut panel = column![
row![
text(t!("Block name")).size(12).width(Length::Fixed(110.0)),
container(text(block.clone()).size(12).style(muted_style)).padding([5, 8]).width(Fill),
container(text(block.clone()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill),
]
.spacing(8)
.align_y(iced::Center),
@@ -914,7 +1155,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.iter()
.enumerate()
.map(|(i, (label, _, _))| {
(t!(*label).into_owned(), state.placeholder_property == i, FieldDialogMsg::PlaceholderProperty(i))
(
t!(*label).into_owned(),
state.placeholder_property == i,
FieldDialogMsg::PlaceholderProperty(i),
)
})
.collect(),
150.0,
@@ -922,7 +1167,9 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
]
.spacing(8);
if format == "%tc4" {
panel = panel.push(text(t!("Format")).size(12)).push(case_list(state));
panel = panel
.push(text(t!("Format")).size(12))
.push(case_list(state));
}
panel.into()
}
@@ -933,7 +1180,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
codec::fields::PLOT_SCALE_FORMATS
.iter()
.enumerate()
.map(|(i, (n, _))| (t!(*n).into_owned(), state.plot_scale == i, FieldDialogMsg::PlotScale(i)))
.map(|(i, (n, _))| (
t!(*n).into_owned(),
state.plot_scale == i,
FieldDialogMsg::PlotScale(i)
))
.collect(),
170.0,
),
@@ -941,9 +1192,15 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.spacing(8)
.into(),
FieldKind::Count => column![].into(),
FieldKind::SheetSet => column![text(t!("Format")).size(12), case_list(state)].spacing(8).into(),
FieldKind::SheetSet => column![text(t!("Format")).size(12), case_list(state)]
.spacing(8)
.into(),
FieldKind::SheetSetCustom => {
let names = if state.name == "CurrentSheetCustom" { &state.ss_sheet_custom } else { &state.ss_set_custom };
let names = if state.name == "CurrentSheetCustom" {
&state.ss_sheet_custom
} else {
&state.ss_set_custom
};
column![
text(t!("Format")).size(12),
case_list(state),
@@ -967,7 +1224,13 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
PLACEHOLDER_TYPES
.iter()
.enumerate()
.map(|(i, (n, _, _))| (n.to_string(), state.ss_placeholder == i, FieldDialogMsg::SsPlaceholder(i)))
.map(|(i, (n, _, _))| {
(
n.to_string(),
state.ss_placeholder == i,
FieldDialogMsg::SsPlaceholder(i),
)
})
.collect(),
200.0,
);
@@ -976,14 +1239,26 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
codec::fields::PLOT_SCALE_FORMATS
.iter()
.enumerate()
.map(|(i, (n, _))| (t!(*n).into_owned(), state.ss_scale_format == i, FieldDialogMsg::SsScaleFormat(i)))
.map(|(i, (n, _))| {
(
t!(*n).into_owned(),
state.ss_scale_format == i,
FieldDialogMsg::SsScaleFormat(i),
)
})
.collect(),
170.0,
)
} else {
case_list(state)
};
let mut col = column![text(t!("Placeholder type")).size(12), types, text(t!("Format")).size(12), format].spacing(8);
let mut col = column![
text(t!("Placeholder type")).size(12),
types,
text(t!("Format")).size(12),
format
]
.spacing(8);
if kind == "Custom" {
col = col.push(text(t!("Custom property name")).size(12)).push(
pick_list(
@@ -1000,7 +1275,12 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
col.into()
}
FieldKind::SheetSetNavigation => {
let picks: Vec<Pick> = state.ss_sets.iter().enumerate().map(|(i, d)| Pick(i, d.name().to_string())).collect();
let picks: Vec<Pick> = state
.ss_sets
.iter()
.enumerate()
.map(|(i, d)| Pick(i, d.name().to_string()))
.collect();
let current = picks.get(state.ss_set).cloned();
let set_row = row![
pick_list(current, picks, |p: &Pick| p.1.clone())
@@ -1008,27 +1288,49 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
.text_size(12)
.padding([5, 8])
.width(Fill),
button(text("...").size(12)).on_press(msg(FieldDialogMsg::SsBrowse)).style(button_style(false)).padding([5, 10]),
button(text("...").size(12))
.on_press(msg(FieldDialogMsg::SsBrowse))
.style(button_style(false))
.padding([5, 10]),
]
.spacing(6);
let db = state.ss_sets.get(state.ss_set);
let nodes = db.map(|d| nav_nodes(d, state.name == "SheetView")).unwrap_or_default();
let nodes = db
.map(|d| nav_nodes(d, state.name == "SheetView"))
.unwrap_or_default();
let tree = list(
nodes
.iter()
.map(|(depth, id, label, _)| {
(format!("{}{label}", " ".repeat(usize::from(*depth))), *id == state.ss_node, FieldDialogMsg::SsNode(id.clone()))
(
format!("{}{label}", " ".repeat(usize::from(*depth))),
*id == state.ss_node,
FieldDialogMsg::SsNode(id.clone()),
)
})
.collect(),
118.0,
);
let props = db.map(|d| nav_props(d, &state.ss_node)).unwrap_or_default();
let prop_list = list(
props.iter().map(|(l, _, _)| (l.clone(), *l == state.ss_prop, FieldDialogMsg::SsProp(l.clone()))).collect(),
props
.iter()
.map(|(l, _, _)| {
(
l.clone(),
*l == state.ss_prop,
FieldDialogMsg::SsProp(l.clone()),
)
})
.collect(),
92.0,
);
let linkable = db.is_some_and(|d| nav_link(d, &state.ss_node).is_some()) && state.ss_prop != "ViewportScale";
let mut href = checkbox(state.ss_href && linkable).label(t!("Associate hyperlink").into_owned()).text_size(12).size(14);
let linkable = db.is_some_and(|d| nav_link(d, &state.ss_node).is_some())
&& state.ss_prop != "ViewportScale";
let mut href = checkbox(state.ss_href && linkable)
.label(t!("Associate hyperlink").into_owned())
.text_size(12)
.size(14);
if linkable {
href = href.on_toggle(|v| msg(FieldDialogMsg::SsHref(v)));
}
@@ -1037,7 +1339,13 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
codec::fields::PLOT_SCALE_FORMATS
.iter()
.enumerate()
.map(|(i, (n, _))| (t!(*n).into_owned(), state.ss_scale_format == i, FieldDialogMsg::SsScaleFormat(i)))
.map(|(i, (n, _))| {
(
t!(*n).into_owned(),
state.ss_scale_format == i,
FieldDialogMsg::SsScaleFormat(i),
)
})
.collect(),
120.0,
)
@@ -1062,7 +1370,15 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
row![
text(t!("Object type")).size(12).width(Length::Fixed(96.0)),
container(
text(state.object.as_ref().map(|(_, t)| t.clone()).unwrap_or_default()).size(12).style(muted_style)
text(
state
.object
.as_ref()
.map(|(_, t)| t.clone())
.unwrap_or_default()
)
.size(12)
.style(muted_style)
)
.padding([5, 8])
.width(Fill),
@@ -1078,7 +1394,11 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
state
.object_props
.iter()
.map(|p| (t!(*p).into_owned(), state.object_prop == Some(*p), FieldDialogMsg::ObjectProp(p)))
.map(|p| (
t!(*p).into_owned(),
state.object_prop == Some(*p),
FieldDialogMsg::ObjectProp(p)
))
.collect(),
180.0,
),
@@ -1092,17 +1412,25 @@ pub fn view<'a>(
state: &'a FieldDialogState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let categories: Vec<Pick> = CATEGORIES.iter().enumerate().map(|(i, (c, _))| Pick(i, t!(*c).into_owned())).collect();
let categories: Vec<Pick> = CATEGORIES
.iter()
.enumerate()
.map(|(i, (c, _))| Pick(i, t!(*c).into_owned()))
.collect();
let left = card(
t!("Field").into_owned(),
column![
row![
text(t!("Category")).size(12).width(Length::Fixed(70.0)),
pick_list(Some(categories[state.category].clone()), categories.clone(), |p: &Pick| p.1.clone())
.on_select(|p| msg(FieldDialogMsg::Category(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill),
pick_list(
Some(categories[state.category].clone()),
categories.clone(),
|p: &Pick| p.1.clone()
)
.on_select(|p| msg(FieldDialogMsg::Category(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill),
]
.spacing(8)
.align_y(iced::Center),
@@ -1118,9 +1446,16 @@ pub fn view<'a>(
);
let preview = card(
t!("Preview").into_owned(),
container(text(if state.preview.is_empty() { "----".to_string() } else { state.preview.clone() }).size(15))
.padding([6, 8])
.width(Fill),
container(
text(if state.preview.is_empty() {
"----".to_string()
} else {
state.preview.clone()
})
.size(15),
)
.padding([6, 8])
.width(Fill),
);
let right = match kind_of(state.name) {
FieldKind::Formula => column![
@@ -1152,7 +1487,11 @@ pub fn view<'a>(
preview,
]
.spacing(10),
FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10),
FieldKind::Hyperlink => column![
card(t!("Hyperlink").into_owned(), format_panel(state)),
preview
]
.spacing(10),
FieldKind::Count => column![
card(
t!("Expression").into_owned(),
@@ -1180,27 +1519,49 @@ pub fn view<'a>(
preview,
]
.spacing(10),
FieldKind::BlockPlaceholder => column![card(t!("Block placeholder").into_owned(), format_panel(state)), preview].spacing(10),
FieldKind::SheetSetNavigation => column![card(t!("Sheet set").into_owned(), format_panel(state)), preview].spacing(10),
_ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10),
FieldKind::BlockPlaceholder => column![
card(t!("Block placeholder").into_owned(), format_panel(state)),
preview
]
.spacing(10),
FieldKind::SheetSetNavigation => column![
card(t!("Sheet set").into_owned(), format_panel(state)),
preview
]
.spacing(10),
_ => column![
card(t!("Format").into_owned(), format_panel(state)),
preview
]
.spacing(10),
};
let (code, _) = state.code();
let expression = column![
text(t!("Field expression")).size(12),
container(text(format!("%<{code}>%")).size(12).font(iced::Font::MONOSPACE).style(muted_style))
.padding([5, 8])
.width(Fill)
.style(|theme: &iced::Theme| field_style(theme, text_input::Status::Disabled).into_container()),
container(
text(format!("%<{code}>%"))
.size(12)
.font(iced::Font::MONOSPACE)
.style(muted_style)
)
.padding([5, 8])
.width(Fill)
.style(
|theme: &iced::Theme| field_style(theme, text_input::Status::Disabled).into_container()
),
]
.spacing(4);
let mut insert = button(text(t!("Insert")).size(12))
.style(button_style(true))
.padding([6, 16]);
.style(button_style(true))
.padding([6, 16]);
if state.complete() {
insert = insert.on_press(msg(FieldDialogMsg::Ok));
}
let footer = row![
button(text("?").size(12)).on_press(msg(FieldDialogMsg::Help)).style(button_style(false)).padding([5, 11]),
button(text("?").size(12))
.on_press(msg(FieldDialogMsg::Help))
.style(button_style(false))
.padding([5, 11]),
Space::new().width(Fill),
dialog_button(t!("Cancel"), Message::CloseModal, false),
insert,
+4 -2
View File
@@ -43,8 +43,10 @@ pub fn view_window<'a>(
]
.spacing(8)
.align_y(iced::Center),
text(t!("Searches Text, MText, Attribute Definitions, and block attribute values."))
.size(11),
text(t!(
"Searches Text, MText, Attribute Definitions, and block attribute values."
))
.size(11),
text(status).size(11),
row![
Space::new().width(field_width),
+71 -65
View File
@@ -152,9 +152,7 @@ fn remove_escape(input: &str, code: &str) -> Option<String> {
break;
};
let end = start + 1 + relative_end;
if crate::entities::tolerance::symbol_font_switch(&input[start + 1..end])
== Some(target)
{
if crate::entities::tolerance::symbol_font_switch(&input[start + 1..end]) == Some(target) {
let mut remaining = String::with_capacity(input.len() - (end + 1 - start));
remaining.push_str(&input[..start]);
remaining.push_str(&input[end + 1..]);
@@ -314,8 +312,14 @@ impl State {
pub fn is_valid(&self) -> bool {
!self.symbol.is_empty()
|| !self.symbol_tail.is_empty()
|| self.tolerances.iter().any(|entry| !entry.value.trim().is_empty())
|| self.datums.iter().any(|entry| !entry.value.trim().is_empty())
|| self
.tolerances
.iter()
.any(|entry| !entry.value.trim().is_empty())
|| self
.datums
.iter()
.any(|entry| !entry.value.trim().is_empty())
|| !self.projected_height.trim().is_empty()
|| !self.datum_identifier.trim().is_empty()
|| self.extra_rows.iter().any(|row| !row.trim().is_empty())
@@ -332,7 +336,10 @@ impl State {
&& self.symbol.is_empty()
&& self.symbol_tail.is_empty()
&& self.tolerances[1] == ToleranceEntry::default()
&& self.datums.iter().all(|entry| *entry == DatumEntry::default());
&& self
.datums
.iter()
.all(|entry| *entry == DatumEntry::default());
let frame = if only_plain_value {
self.tolerances[0].value.clone()
} else {
@@ -407,10 +414,7 @@ fn material_picker<'a>(index: usize, datum: bool, code: &str) -> Element<'a, Mes
iced::widget::pick_list(selected, options, |choice| choice.to_string())
.on_select(move |choice| {
if datum {
Message::ToleranceDialogField(Field::DatumMaterial(
index,
choice.code.to_string(),
))
Message::ToleranceDialogField(Field::DatumMaterial(index, choice.code.to_string()))
} else {
Message::ToleranceDialogField(Field::ToleranceMaterial(
index,
@@ -442,60 +446,64 @@ pub fn view_window<'a>(
.into(),
);
let tolerance_rows = state.tolerances.iter().enumerate().fold(
column![].spacing(6),
|column, (index, entry)| {
column.push(
row![
text(format!("{} {}", t!("Tolerance"), index + 1))
.size(11)
.style(muted)
.width(Length::Fixed(82.0)),
checkbox(entry.diameter)
.on_toggle(move |value| Message::ToleranceDialogToggle(
Toggle::Diameter(index, value)
))
.size(14),
text(t!("Diameter")).size(11).width(Length::Fixed(62.0)),
text_input(EMPTY, &entry.value)
.on_input(move |value| Message::ToleranceDialogField(
Field::ToleranceValue(index, value)
))
.size(12)
.padding([3, 6])
.width(Length::Fixed(125.0)),
material_picker(index, false, &entry.material),
]
.spacing(6)
.align_y(iced::Center),
)
},
);
let tolerance_rows =
state
.tolerances
.iter()
.enumerate()
.fold(column![].spacing(6), |column, (index, entry)| {
column.push(
row![
text(format!("{} {}", t!("Tolerance"), index + 1))
.size(11)
.style(muted)
.width(Length::Fixed(82.0)),
checkbox(entry.diameter)
.on_toggle(move |value| Message::ToleranceDialogToggle(
Toggle::Diameter(index, value)
))
.size(14),
text(t!("Diameter")).size(11).width(Length::Fixed(62.0)),
text_input(EMPTY, &entry.value)
.on_input(move |value| Message::ToleranceDialogField(
Field::ToleranceValue(index, value)
))
.size(12)
.padding([3, 6])
.width(Length::Fixed(125.0)),
material_picker(index, false, &entry.material),
]
.spacing(6)
.align_y(iced::Center),
)
});
let tolerances = panel(t!("Tolerance values").into_owned(), tolerance_rows.into());
let datum_rows = state.datums.iter().enumerate().fold(
column![].spacing(6),
|column, (index, entry)| {
column.push(
row![
text(format!("{} {}", t!("Datum"), index + 1))
.size(11)
.style(muted)
.width(Length::Fixed(82.0)),
text_input(EMPTY, &entry.value)
.on_input(move |value| Message::ToleranceDialogField(
Field::DatumValue(index, value)
))
.size(12)
.padding([3, 6])
.width(Length::Fixed(212.0)),
material_picker(index, true, &entry.material),
]
.spacing(6)
.align_y(iced::Center),
)
},
);
let datum_rows =
state
.datums
.iter()
.enumerate()
.fold(column![].spacing(6), |column, (index, entry)| {
column.push(
row![
text(format!("{} {}", t!("Datum"), index + 1))
.size(11)
.style(muted)
.width(Length::Fixed(82.0)),
text_input(EMPTY, &entry.value)
.on_input(move |value| Message::ToleranceDialogField(
Field::DatumValue(index, value)
))
.size(12)
.padding([3, 6])
.width(Length::Fixed(212.0)),
material_picker(index, true, &entry.material),
]
.spacing(6)
.align_y(iced::Center),
)
});
let datums = panel(t!("Datum references").into_owned(), datum_rows.into());
let additions = panel(
@@ -512,9 +520,7 @@ pub fn view_window<'a>(
.padding([3, 6])
.width(Length::Fixed(125.0)),
checkbox(state.projected_zone)
.on_toggle(|value| Message::ToleranceDialogToggle(
Toggle::ProjectedZone(value)
))
.on_toggle(|value| Message::ToleranceDialogToggle(Toggle::ProjectedZone(value)))
.size(14),
text(t!("Projected tolerance zone")).size(11),
]
+80 -68
View File
@@ -1,13 +1,11 @@
//! Layer State Manager — native DWG/DXF named layer-state UI.
use crate::app::{LayerStateLayerFlag, LayerStateProperty, Message};
use crate::t;
use crate::ui::properties::{lw_options, LwItem};
use codec::{LayerState, LayerStateMask};
use iced::widget::{
button, checkbox, column, container, row, scrollable, text, text_input, Space,
};
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
@@ -39,14 +37,7 @@ fn transparency_options(current: Option<codec::types::Transparency>) -> Vec<Tran
fn muted(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(
theme
.palette()
.background
.base
.text
.scale_alpha(0.65),
),
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
}
}
@@ -75,9 +66,7 @@ fn divider<'a>(width: Length) -> Element<'a, Message> {
.width(width)
.height(1)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color,
)),
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
})
.into()
@@ -233,9 +222,11 @@ pub fn view_window<'a>(
} else {
column![
text(t!("New layer state")).size(13),
text(t!("Save captures the current settings of every layer in the drawing."))
.size(11)
.style(muted),
text(t!(
"Save captures the current settings of every layer in the drawing."
))
.size(11)
.style(muted),
]
.spacing(7)
};
@@ -371,26 +362,57 @@ fn bool_cell<'a>(
fn editor_header<'a>() -> Element<'a, Message> {
container(
row![
text(t!("Layer")).size(10).style(muted).width(Length::Fixed(170.0)),
text(t!("On")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("Freeze")).size(10).style(muted).width(Length::Fixed(54.0)),
text(t!("Lock")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("Plot")).size(10).style(muted).width(Length::Fixed(44.0)),
text(t!("New VP")).size(10).style(muted).width(Length::Fixed(54.0)),
text(t!("Color")).size(10).style(muted).width(Length::Fixed(135.0)),
text(t!("Linetype")).size(10).style(muted).width(Length::Fixed(150.0)),
text(t!("Lineweight")).size(10).style(muted).width(Length::Fixed(115.0)),
text(t!("Plot style")).size(10).style(muted).width(Length::Fixed(135.0)),
text(t!("Transparency")).size(10).style(muted).width(Length::Fixed(105.0)),
text(t!("Layer"))
.size(10)
.style(muted)
.width(Length::Fixed(170.0)),
text(t!("On"))
.size(10)
.style(muted)
.width(Length::Fixed(44.0)),
text(t!("Freeze"))
.size(10)
.style(muted)
.width(Length::Fixed(54.0)),
text(t!("Lock"))
.size(10)
.style(muted)
.width(Length::Fixed(44.0)),
text(t!("Plot"))
.size(10)
.style(muted)
.width(Length::Fixed(44.0)),
text(t!("New VP"))
.size(10)
.style(muted)
.width(Length::Fixed(54.0)),
text(t!("Color"))
.size(10)
.style(muted)
.width(Length::Fixed(135.0)),
text(t!("Linetype"))
.size(10)
.style(muted)
.width(Length::Fixed(150.0)),
text(t!("Lineweight"))
.size(10)
.style(muted)
.width(Length::Fixed(115.0)),
text(t!("Plot style"))
.size(10)
.style(muted)
.width(Length::Fixed(135.0)),
text(t!("Transparency"))
.size(10)
.style(muted)
.width(Length::Fixed(105.0)),
]
.spacing(4)
.align_y(iced::Center),
)
.padding([5, 8])
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weak.color,
)),
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
@@ -420,10 +442,7 @@ fn editor_layer_row<'a>(
},
move |color| Message::LayerStateEditorLayerColor(index, color),
Message::LayerStateEditorLayerColorToggle(index),
Message::OpenColorWindow(
crate::app::ColorPickTarget::LayerState(index),
layer.color,
),
Message::OpenColorWindow(crate::app::ColorPickTarget::LayerState(index), layer.color),
);
container(
@@ -442,26 +461,18 @@ fn editor_layer_row<'a>(
54.0
),
container(color).width(Length::Fixed(135.0)),
iced::widget::pick_list(
current_linetype,
linetypes,
|value| value.to_string(),
)
.on_select(move |value| Message::LayerStateEditorLayerLinetype(index, value))
.text_size(11)
.padding([3, 5])
.width(Length::Fixed(150.0)),
iced::widget::pick_list(
current_lineweight,
lw_options(),
|value| value.to_string(),
)
.on_select(move |item: LwItem| {
Message::LayerStateEditorLayerLineweight(index, item.0)
})
.text_size(11)
.padding([3, 5])
.width(Length::Fixed(115.0)),
iced::widget::pick_list(current_linetype, linetypes, |value| value.to_string(),)
.on_select(move |value| Message::LayerStateEditorLayerLinetype(index, value))
.text_size(11)
.padding([3, 5])
.width(Length::Fixed(150.0)),
iced::widget::pick_list(current_lineweight, lw_options(), |value| value.to_string(),)
.on_select(move |item: LwItem| {
Message::LayerStateEditorLayerLineweight(index, item.0)
})
.text_size(11)
.padding([3, 5])
.width(Length::Fixed(115.0)),
text_input(t!("Default").as_ref(), &layer.plot_style)
.on_input(move |value| Message::LayerStateEditorLayerPlotStyle(index, value))
.size(11)
@@ -482,9 +493,8 @@ fn editor_layer_row<'a>(
)
.padding([3, 8])
.style(move |theme: &Theme| container::Style {
background: (index % 2 == 1).then_some(Background::Color(
theme.palette().background.weak.color,
)),
background: (index % 2 == 1)
.then_some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(Fill)
@@ -566,11 +576,9 @@ pub fn view_editor<'a>(
.style(muted),
Space::new().width(sizing.width),
text(t!("Current layer")).size(10).style(muted),
iced::widget::pick_list(
Some(state.current_layer.clone()),
layer_names,
|value| value.to_string(),
)
iced::widget::pick_list(Some(state.current_layer.clone()), layer_names, |value| {
value.to_string()
},)
.on_select(Message::LayerStateEditorCurrentLayer)
.text_size(11)
.padding([3, 6])
@@ -579,7 +587,9 @@ pub fn view_editor<'a>(
.spacing(8)
.align_y(iced::Center),
divider(sizing.width),
text(t!("Properties restored by this state")).size(10).style(muted),
text(t!("Properties restored by this state"))
.size(10)
.style(muted),
mask_controls,
row![
text(t!("Saved layer values")).size(12),
@@ -606,9 +616,11 @@ pub fn view_editor<'a>(
..Default::default()
}),
row![
text(t!("Changes affect the saved state only; the drawing is unchanged until Restore."))
.size(10)
.style(muted),
text(t!(
"Changes affect the saved state only; the drawing is unchanged until Restore."
))
.size(10)
.style(muted),
Space::new().width(sizing.width),
button(text(t!("Cancel")).size(11))
.on_press(Message::LayerStateEditorCancel)
+1 -3
View File
@@ -150,9 +150,7 @@ pub fn view_window<'a>(
.on_press_maybe(can_map.then_some(Message::LayerTranslatorMap))
.padding([4, 10]),
button(text(t!("Map same")).size(12))
.on_press_maybe(
(!state.targets.is_empty()).then_some(Message::LayerTranslatorMapSame)
)
.on_press_maybe((!state.targets.is_empty()).then_some(Message::LayerTranslatorMapSame))
.padding([4, 10]),
Space::new().width(Fill),
loaded,
+166 -128
View File
@@ -1,6 +1,7 @@
//! Layer Manager — floating window.
use crate::app::Message;
use crate::t;
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
use crate::ui::style::common::muted_style;
use crate::ui::style::row_button::{cell_button_style, header_button_style};
@@ -15,7 +16,6 @@ use iced::widget::{
};
use iced::Padding;
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
// ── Per-viewport column descriptor ───────────────────────────────────────────
@@ -241,8 +241,7 @@ impl LayerPanel {
.collect();
// Re-resolve the selection against the rebuilt rows by name.
self.selected = anchor_name
.and_then(|n| self.layers.iter().position(|l| l.name == n));
self.selected = anchor_name.and_then(|n| self.layers.iter().position(|l| l.name == n));
self.selected_multi = multi_names
.iter()
.filter_map(|n| self.layers.iter().position(|l| l.name == *n))
@@ -294,9 +293,7 @@ impl LayerPanel {
LayerSortCol::Lock => a.locked.cmp(&b.locked),
LayerSortCol::Plot => a.plottable.cmp(&b.plottable),
LayerSortCol::Color => color_sort_key(a.color).cmp(&color_sort_key(b.color)),
LayerSortCol::Linetype => {
a.linetype.to_lowercase().cmp(&b.linetype.to_lowercase())
}
LayerSortCol::Linetype => a.linetype.to_lowercase().cmp(&b.linetype.to_lowercase()),
LayerSortCol::Lineweight => a.lineweight.value().cmp(&b.lineweight.value()),
LayerSortCol::Transparency => a.transparency.cmp(&b.transparency),
};
@@ -335,9 +332,7 @@ impl LayerPanel {
self.layers
.iter()
.enumerate()
.filter(|(_, layer)| {
filter.is_empty() || layer.name.to_lowercase().contains(&filter)
})
.filter(|(_, layer)| filter.is_empty() || layer.name.to_lowercase().contains(&filter))
.map(|(i, _)| i)
.collect()
}
@@ -395,9 +390,7 @@ impl LayerPanel {
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weak.color
)),
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.width(sizing.width)
@@ -408,27 +401,69 @@ impl LayerPanel {
let sa = self.sort_asc;
let mut header_row = row![
text(t!("Status")).size(10).style(muted_style).width(50),
sortable_header(t!("Name"), LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa),
sortable_header(
t!("Name"),
LayerSortCol::Name,
Length::Fixed(name_col_w),
sc,
sa
),
// Draggable divider: adjusts the Name column width (#359).
iced::widget::mouse_area(
container(iced::widget::Space::new().width(2).height(14)).style(
|theme: &Theme| container::Style {
container(iced::widget::Space::new().width(2).height(14)).style(|theme: &Theme| {
container::Style {
background: Some(Background::Color(
theme.palette().background.neutral.color
theme.palette().background.neutral.color,
)),
..Default::default()
},
),
}
},),
)
.on_press(Message::LayerNameColGrab)
.interaction(iced::mouse::Interaction::ResizingHorizontally),
sortable_header(t!("On"), LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Freeze"), LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Lock"), LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Plot"), LayerSortCol::Plot, Length::Fixed(COL_ICON), sc, sa),
sortable_header(t!("Color"), LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa),
sortable_header(t!("Linetype"), LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa),
sortable_header(t!("Lineweight"), LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa),
sortable_header(
t!("Freeze"),
LayerSortCol::Freeze,
Length::Fixed(COL_ICON),
sc,
sa
),
sortable_header(
t!("Lock"),
LayerSortCol::Lock,
Length::Fixed(COL_ICON),
sc,
sa
),
sortable_header(
t!("Plot"),
LayerSortCol::Plot,
Length::Fixed(COL_ICON),
sc,
sa
),
sortable_header(
t!("Color"),
LayerSortCol::Color,
Length::Fixed(COL_COLOR),
sc,
sa
),
sortable_header(
t!("Linetype"),
LayerSortCol::Linetype,
Length::Fixed(COL_LT),
sc,
sa
),
sortable_header(
t!("Lineweight"),
LayerSortCol::Lineweight,
Length::Fixed(COL_LW),
sc,
sa
),
sortable_header(
t!("Transparency"),
LayerSortCol::Transparency,
@@ -454,13 +489,13 @@ impl LayerPanel {
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
}
})
.padding([4, 8])
@@ -497,7 +532,6 @@ impl LayerPanel {
&self.vp_cols,
name_col_w,
));
}
let table = scrollable(rows_col)
@@ -507,9 +541,7 @@ impl LayerPanel {
// ── Full-window frame ─────────────────────────────────────────────
container(column![toolbar, col_header, table].spacing(0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(sizing.width)
@@ -561,25 +593,24 @@ fn sortable_header<'a>(
// ── Toolbar buttons ───────────────────────────────────────────────────────
fn toolbar_btn<'a>(icon: &'static [u8], label: Cow<'static, str>, msg: Message) -> Element<'a, Message> {
fn toolbar_btn<'a>(
icon: &'static [u8],
label: Cow<'static, str>,
msg: Message,
) -> Element<'a, Message> {
button(
row![
crate::ui::icons::themed(icon, 12.0),
text(label).size(11),
]
.spacing(5)
.align_y(iced::Center),
row![crate::ui::icons::themed(icon, 12.0), text(label).size(11),]
.spacing(5)
.align_y(iced::Center),
)
.on_press(msg)
.style(|theme: &Theme, status| {
let palette = theme.palette();
let pair = match status {
button::Status::Hovered | button::Status::Pressed => {
palette.background.strong
}
_ => palette.background.weak,
};
button::Style {
.style(|theme: &Theme, status| {
let palette = theme.palette();
let pair = match status {
button::Status::Hovered | button::Status::Pressed => palette.background.strong,
_ => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
border: Border {
radius: 3.0.into(),
@@ -588,10 +619,10 @@ fn toolbar_btn<'a>(icon: &'static [u8], label: Cow<'static, str>, msg: Message)
},
text_color: pair.text,
..Default::default()
}
})
.padding([4, 10])
.into()
}
})
.padding([4, 10])
.into()
}
fn toolbar_btn_cond<'a>(
@@ -610,11 +641,11 @@ fn toolbar_btn_cond<'a>(
if enabled {
text(label).size(11)
} else {
text(label).size(11).style(|theme: &Theme| iced::widget::text::Style {
color: Some(
theme.palette().background.base.text.scale_alpha(0.42)
),
})
text(label)
.size(11)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.42)),
})
},
]
.spacing(5)
@@ -627,18 +658,18 @@ fn toolbar_btn_cond<'a>(
_ => palette.background.weak,
};
button::Style {
background: Some(Background::Color(pair.color)),
border: Border {
radius: 3.0.into(),
color: palette.background.neutral.color,
width: 1.0,
},
text_color: if enabled {
pair.text
} else {
pair.text.scale_alpha(0.42)
},
..Default::default()
background: Some(Background::Color(pair.color)),
border: Border {
radius: 3.0.into(),
color: palette.background.neutral.color,
width: 1.0,
},
text_color: if enabled {
pair.text
} else {
pair.text.scale_alpha(0.42)
},
..Default::default()
}
})
.padding([4, 10]);
@@ -663,14 +694,14 @@ fn name_tip<'a>(name: &'a str) -> Element<'a, Message> {
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.strong.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(palette.background.strong.text),
..Default::default()
background: Some(Background::Color(palette.background.strong.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
text_color: Some(palette.background.strong.text),
..Default::default()
}
})
.into()
@@ -694,16 +725,16 @@ fn layer_row<'a>(
let lname = layer.name.clone();
let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
button(crate::ui::icons::semantic(bytes, ICON_SZ))
.on_press(on_press)
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
left: 4.0,
right: 4.0,
})
.height(Length::Fixed(ROW_H))
.into()
.on_press(on_press)
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
left: 4.0,
right: 4.0,
})
.height(Length::Fixed(ROW_H))
.into()
};
let vis_svg = crate::ui::icons::layer_visible(layer.visible);
@@ -757,24 +788,27 @@ fn layer_row<'a>(
} else {
// ~6 px per glyph at the 10 px row font — track the column width.
let name_budget = ((name_col_w / 6.0) as usize).max(8);
let name_btn = button(
text(crate::ui::text_util::elide(&layer.name, name_budget))
.size(FONT_SZ),
)
.on_press(Message::LayerRenameStart(lname.clone()))
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
left: 4.0,
right: 4.0,
})
.height(Length::Fixed(ROW_H))
.width(Length::Fixed(name_col_w));
let name_btn =
button(text(crate::ui::text_util::elide(&layer.name, name_budget)).size(FONT_SZ))
.on_press(Message::LayerRenameStart(lname.clone()))
.style(cell_button_style(is_selected, index))
.padding(Padding {
top: COMBO_PAD_V,
bottom: COMBO_PAD_V,
left: 4.0,
right: 4.0,
})
.height(Length::Fixed(ROW_H))
.width(Length::Fixed(name_col_w));
// When the name is truncated, reveal the full text on hover so the
// user can still read it without widening the column.
if layer.name.chars().count() > name_budget {
tooltip(name_btn, name_tip(&layer.name), tooltip::Position::FollowCursor).into()
tooltip(
name_btn,
name_tip(&layer.name),
tooltip::Position::FollowCursor,
)
.into()
} else {
name_btn.into()
}
@@ -783,24 +817,25 @@ fn layer_row<'a>(
// Color cell — looks like a combo_box input; click opens swatch dropdown below row.
// Shared colour selector. Layers carry a concrete colour (no ByLayer /
// ByBlock); true colours stay RGB instead of being collapsed to ACI 7.
let color_cell: Element<'_, Message> = container(crate::ui::color_select::color_selector_with_name(
layer.color,
layer.color_name.as_deref(),
color_picker_open,
crate::ui::color_select::ColorExtras {
by_layer: false,
by_block: false,
..Default::default()
},
Message::LayerColorSet,
Message::LayerColorPickerToggle(lname.clone()),
Message::OpenColorWindow(
crate::app::ColorPickTarget::Layer(lname.clone()),
let color_cell: Element<'_, Message> =
container(crate::ui::color_select::color_selector_with_name(
layer.color,
),
))
.width(Length::Fixed(COL_COLOR))
.into();
layer.color_name.as_deref(),
color_picker_open,
crate::ui::color_select::ColorExtras {
by_layer: false,
by_block: false,
..Default::default()
},
Message::LayerColorSet,
Message::LayerColorPickerToggle(lname.clone()),
Message::OpenColorWindow(
crate::app::ColorPickTarget::Layer(lname.clone()),
layer.color,
),
))
.width(Length::Fixed(COL_COLOR))
.into();
// Linetype cell — uses LinetypeItem (with ASCII art) same as Properties panel
let cur_lt_item = LinetypeItem {
@@ -837,9 +872,12 @@ fn layer_row<'a>(
// Lineweight cell
let cur_lw_item = LwItem(layer.lineweight);
let lw_cell: Element<'_, Message> = if let Some(state) = lw_combo_state {
combo_box(state, t!("lineweight").as_ref(), Some(&cur_lw_item), |item: LwItem| {
Message::LayerLineweightSet(item.0)
})
combo_box(
state,
t!("lineweight").as_ref(),
Some(&cur_lw_item),
|item: LwItem| Message::LayerLineweightSet(item.0),
)
.size(FONT_SZ)
.padding(Padding {
top: COMBO_PAD_V,
@@ -931,9 +969,9 @@ fn layer_row<'a>(
palette.background.weak
};
container::Style {
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
..Default::default()
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
..Default::default()
}
})
.padding(Padding {
+3 -7
View File
@@ -1,11 +1,11 @@
//! Layout Manager window — fills the entire OS window.
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{hdivider, vsep};
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Theme};
use crate::t;
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
@@ -91,9 +91,7 @@ pub fn view_window<'a>(
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color,
)),
background: Some(Background::Color(theme.palette().background.weakest.color)),
..Default::default()
})
.width(sizing.width)
@@ -200,9 +198,7 @@ pub fn view_window<'a>(
container(column![toolbar, hdivider(sizing.width), body].spacing(0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(sizing.width)
+26 -26
View File
@@ -1,36 +1,36 @@
pub mod about;
pub mod alias_editor;
pub mod annotation_data;
pub mod attdef_dialog;
pub mod field_dialog;
pub mod attribute_editor;
pub mod auto_constrain_settings;
pub mod block_definition;
pub mod block_palette;
pub mod browser;
pub mod layout_manager;
pub mod layer_state_manager;
pub mod drawing_units;
pub mod geometric_tolerance;
pub mod drafting_settings;
pub mod auto_constrain_settings;
pub mod layer_translator;
pub mod plot;
pub mod print_all;
pub mod plugin_manager;
pub mod shortcuts;
pub mod layers;
pub mod update_notice;
pub mod open_progress;
pub mod missing_fonts;
pub mod recovery;
pub mod options;
pub mod attribute_editor;
pub mod annotation_data;
pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod pc_manager;
pub mod count_palette;
pub mod sheet_set;
pub mod drafting_settings;
pub mod drawing_units;
pub mod field_dialog;
pub mod find_replace;
pub mod geometric_tolerance;
pub mod layer_state_manager;
pub mod layer_translator;
pub mod layers;
pub mod layout_manager;
pub mod missing_fonts;
pub mod named_parameters;
pub mod open_progress;
pub mod options;
pub mod pc_manager;
pub mod pdf_dialogs;
pub mod plot;
pub mod plugin_manager;
pub mod print_all;
pub mod recovery;
pub mod sheet_set;
pub mod shortcuts;
pub mod update_notice;
pub mod wblock;
pub mod xref_attach;
pub mod xref_help;
pub mod xref_manager;
pub mod wblock;
+4 -6
View File
@@ -392,12 +392,10 @@ mod tests {
ConstraintKind, ParametricRef, ParametricScope,
};
let mut scene = Scene::new();
let line = scene.add_entity(codec::EntityType::Line(
codec::entities::Line::from_points(
codec::types::Vector3::new(0.0, 0.0, 0.0),
codec::types::Vector3::new(10.0, 0.0, 0.0),
),
));
let line = scene.add_entity(codec::EntityType::Line(codec::entities::Line::from_points(
codec::types::Vector3::new(0.0, 0.0, 0.0),
codec::types::Vector3::new(10.0, 0.0, 0.0),
)));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
+33 -33
View File
@@ -3,10 +3,10 @@
//! Displays the file name, size, current phase, measured progress, and a
//! Cancel button.
use crate::t;
use iced::time::Instant;
use iced::widget::{button, column, container, row, stack, text, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::sync::atomic::Ordering;
@@ -62,17 +62,15 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.width(Length::Fixed(fill_width))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().primary.base.color
)),
border: Border {
radius: 3.0.into(),
..Default::default()
},
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().primary.base.color)),
border: Border {
radius: 3.0.into(),
..Default::default()
})
.into();
},
..Default::default()
})
.into();
let bar_value: Element<'_, Message> = row![
bar_fill,
@@ -84,17 +82,19 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
let bar_track: Element<'_, Message> = container(
stack![
container(Space::new().width(Length::Fixed(BAR_TRACK_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT)))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.strong.color
)),
border: Border {
radius: 3.0.into(),
..Default::default()
},
container(
Space::new()
.width(Length::Fixed(BAR_TRACK_WIDTH))
.height(Length::Fixed(BAR_TRACK_HEIGHT))
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.strong.color)),
border: Border {
radius: 3.0.into(),
..Default::default()
}),
},
..Default::default()
}),
bar_value,
]
.width(Length::Fixed(BAR_TRACK_WIDTH))
@@ -120,10 +120,10 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
phase_label(phase),
basis_points as f32 / 100.0
))
.size(12)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
});
.size(12)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().primary.base.color),
});
let cancel_btn: Element<'_, Message> = button(text(t!("Cancel")).size(12))
.on_press(Message::OpenCancel)
@@ -142,13 +142,13 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
}
});
@@ -156,7 +156,7 @@ pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Messag
let backdrop: Element<'_, Message> = container(Space::new().width(Fill).height(Fill))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.strong.color.scale_alpha(0.72)
theme.palette().background.strong.color.scale_alpha(0.72),
)),
..Default::default()
})
+145 -120
View File
@@ -6,9 +6,7 @@ use crate::app::settings;
use crate::app::settings::{CursorType, RightClickMode};
use crate::app::Message;
use crate::ui::style::form::{dialog_button, dialog_button_styled_opt};
use iced::widget::{
button, column, container, row, scrollable, slider, text, text_input, Space,
};
use iced::widget::{button, column, container, row, scrollable, slider, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Theme};
use std::fmt;
@@ -23,9 +21,7 @@ const DIALOG_HEIGHT: f32 = 620.0;
///
/// Persisted: with nine pages, reopening on General every time means hunting
/// for the one you were last in.
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize,
)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OptionsTab {
#[default]
@@ -298,7 +294,10 @@ pub fn view_window<'a>(
let palette = ui_theme.palette.to_iced();
let colors = [
(crate::tr!("options", "color-background"), palette.background),
(
crate::tr!("options", "color-background"),
palette.background,
),
(crate::tr!("options", "color-text"), palette.text),
(crate::tr!("options", "color-primary"), palette.primary),
(crate::tr!("options", "color-success"), palette.success),
@@ -351,12 +350,12 @@ pub fn view_window<'a>(
text(crate::tr!("options", "language-section")).size(15),
Space::new().height(10),
row![
text(crate::tr!("options", "language-label")).size(12).width(150),
iced::widget::pick_list(
selected_language,
language_options,
|choice| choice.label.clone(),
)
text(crate::tr!("options", "language-label"))
.size(12)
.width(150),
iced::widget::pick_list(selected_language, language_options, |choice| choice
.label
.clone(),)
.on_select(|choice| Message::LanguageChanged(choice.value))
.width(sizing.width),
]
@@ -366,7 +365,9 @@ pub fn view_window<'a>(
text(crate::t!("Applications")).size(15),
Space::new().height(10),
row![
text(crate::t!("Installed plugins and their sources")).size(12).width(Fill),
text(crate::t!("Installed plugins and their sources"))
.size(12)
.width(Fill),
button(text(crate::t!("Plugins…")).size(11))
.on_press(Message::PluginManagerOpen)
.padding([4, 10])
@@ -421,7 +422,6 @@ pub fn view_window<'a>(
.spacing(0)
.width(sizing.width);
// Saving preferences, gathered onto one page. The format and the file
// association were on General, which had become a page of three unrelated
// controls; the autosave interval and the backup toggle have existed since
@@ -431,7 +431,9 @@ pub fn view_window<'a>(
Space::new().height(10),
Space::new().height(10),
row![
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
text(crate::tr!("options", "default-save-format-label"))
.size(12)
.width(150),
iced::widget::pick_list(
selected_format,
// Drawings, not the template entry (the list's last).
@@ -445,15 +447,14 @@ pub fn view_window<'a>(
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options", "default-save-format-help"))
.size(11)
.width(sizing.width),
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
iced::widget::checkbox(file_assoc_enabled)
.on_toggle(Message::FileAssocChanged)
.size(15),
text(crate::t!("Open .dwg and .dxf files with Open CAD Studio"))
.size(12),
text(crate::t!("Open .dwg and .dxf files with Open CAD Studio")).size(12),
]
.spacing(8)
.align_y(iced::Center),
@@ -468,7 +469,10 @@ pub fn view_window<'a>(
iced::widget::checkbox(show_constraint_values)
.on_toggle(Message::ShowConstraintValuesChanged)
.size(15),
text(crate::t!("Show values and parameter names on constraint markers")).size(12),
text(crate::t!(
"Show values and parameter names on constraint markers"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
@@ -485,9 +489,13 @@ pub fn view_window<'a>(
Space::new().height(10),
row![
text(crate::t!("Automatic save")).size(12).width(150),
slider(0..=120, prefs.savetime_min.clamp(0, 120), Message::SaveTimeChanged)
.step(1)
.width(Fill),
slider(
0..=120,
prefs.savetime_min.clamp(0, 120),
Message::SaveTimeChanged
)
.step(1)
.width(Fill),
text(if prefs.savetime_min <= 0 {
crate::t!("Off").into_owned()
} else {
@@ -509,7 +517,10 @@ pub fn view_window<'a>(
iced::widget::checkbox(prefs.backup_on_save)
.on_toggle(Message::BackupOnSaveChanged)
.size(15),
text(crate::t!("Keep a .bak copy when overwriting a drawing (ISAVEBAK)")).size(12),
text(crate::t!(
"Keep a .bak copy when overwriting a drawing (ISAVEBAK)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
@@ -518,40 +529,42 @@ pub fn view_window<'a>(
.width(sizing.width);
let crosshair_rgb = crosshair_color.unwrap_or([255, 255, 255]);
let crosshair_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
crosshair_rgb[0],
crosshair_rgb[1],
crosshair_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let crosshair_swatch =
container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
crosshair_rgb[0],
crosshair_rgb[1],
crosshair_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let command_rgb = command_text_color.unwrap_or([220, 220, 220]);
let command_swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
command_rgb[0],
command_rgb[1],
command_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let command_swatch =
container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(
command_rgb[0],
command_rgb[1],
command_rgb[2],
))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
let snap_rgb = snap_marker_color.unwrap_or([255, 230, 26]);
let snap_swatch = container(Space::new())
@@ -571,11 +584,17 @@ pub fn view_window<'a>(
..Default::default()
});
let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
let model_bg_rgb = model_space
.custom_bg
.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
let paper_bg_rgb = model_space
.custom_paper_bg
.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
let desk_bg_rgb = model_space.custom_desk_bg.unwrap_or(crate::app::config::DEFAULT_DESK_BG);
let desk_bg_rgb = model_space
.custom_desk_bg
.unwrap_or(crate::app::config::DEFAULT_DESK_BG);
let mode_options = crate::app::config::ModelSpaceMode::ALL
.into_iter()
@@ -664,21 +683,19 @@ pub fn view_window<'a>(
text(crate::tr!("options", "theme-section")).size(15),
Space::new().height(10),
row![
text(crate::tr!("options", "theme-label")).size(12).width(150),
iced::widget::pick_list(
selected_theme,
theme_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::OptionsThemeChanged(choice.value))
.width(sizing.width),
text(crate::tr!("options", "theme-label"))
.size(12)
.width(150),
iced::widget::pick_list(selected_theme, theme_options, |choice| choice.label.clone(),)
.on_select(|choice| Message::OptionsThemeChanged(choice.value))
.width(sizing.width),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options", "theme-help"))
.size(11)
.width(sizing.width),
.size(11)
.width(sizing.width),
Space::new().height(12),
color_controls,
Space::new().height(16),
@@ -688,7 +705,9 @@ pub fn view_window<'a>(
text(crate::t!("Interface scale")).size(12).width(150),
iced::widget::pick_list(
Some(format!("{}%", prefs.ui_scale_percent)),
[75u16, 100, 125, 150, 175, 200].map(|p| format!("{p}%")).to_vec(),
[75u16, 100, 125, 150, 175, 200]
.map(|p| format!("{p}%"))
.to_vec(),
|label: &String| label.clone(),
)
.on_select(|label: String| {
@@ -711,13 +730,9 @@ pub fn view_window<'a>(
Space::new().height(10),
row![
text(crate::t!("Canvas mode")).size(12).width(140),
iced::widget::pick_list(
selected_mode,
mode_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::ModelSpaceModeChanged(choice.value))
.width(Fill),
iced::widget::pick_list(selected_mode, mode_options, |choice| choice.label.clone(),)
.on_select(|choice| Message::ModelSpaceModeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
@@ -1245,16 +1260,13 @@ pub fn view_window<'a>(
.find(|choice| choice.value == prefs.annotation_auto_scale.max(0))
.cloned();
let qdim_options = [
(0u8, "Endpoints"),
(1, "Intersections"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let qdim_options = [(0u8, "Endpoints"), (1, "Intersections")]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_qdim = qdim_options
.iter()
.find(|choice| choice.value == prefs.qdim_snap_priority)
@@ -1453,7 +1465,6 @@ pub fn view_window<'a>(
.spacing(0)
.width(sizing.width);
// Snap modes, grid and object snap stay in the Drafting Settings dialog.
// These two have no home: Drafting Settings shows the rotation angle but
// offers only Reset, and the polar increment lives solely in a status-bar
@@ -1497,7 +1508,9 @@ pub fn view_window<'a>(
.width(sizing.width),
Space::new().height(14),
row![
text(crate::t!("Polar tracking increment")).size(12).width(150),
text(crate::t!("Polar tracking increment"))
.size(12)
.width(150),
iced::widget::pick_list(Some(selected_polar), polar_options, |choice| {
choice.label.clone()
})
@@ -1524,7 +1537,6 @@ pub fn view_window<'a>(
.spacing(0)
.width(sizing.width);
let surface_type_options = [
(5i16, "Quadratic B-spline"),
(6, "Cubic B-spline"),
@@ -1541,17 +1553,13 @@ pub fn view_window<'a>(
.find(|choice| choice.value == drawing_prefs.surface_type)
.cloned();
let show_history_options = [
(0i16, "Never"),
(1, "As set per solid"),
(2, "Always"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let show_history_options = [(0i16, "Never"), (1, "As set per solid"), (2, "Always")]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_show_history = show_history_options
.iter()
.find(|choice| choice.value == drawing_prefs.show_solid_history.clamp(0, 2))
@@ -1584,7 +1592,10 @@ pub fn view_window<'a>(
iced::widget::checkbox(prefs.ucs_icon_at_origin)
.on_toggle(Message::UcsIconAtOriginChanged)
.size(15),
text(crate::t!("Place the UCS icon at the origin (UCSICON ORigin)")).size(12),
text(crate::t!(
"Place the UCS icon at the origin (UCSICON ORigin)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
@@ -1698,8 +1709,10 @@ pub fn view_window<'a>(
iced::widget::checkbox(drawing_prefs.record_solid_history)
.on_toggle(Message::SolidHistChanged)
.size(15),
text(crate::t!("Record the history of composite solids (SOLIDHIST)"))
.size(12),
text(crate::t!(
"Record the history of composite solids (SOLIDHIST)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
@@ -1723,13 +1736,14 @@ pub fn view_window<'a>(
let modeling = modeling.spacing(0).width(sizing.width);
// The application has no support-file search path, so this page does not
// pretend to offer one. It shows where things actually live and opens the
// folder — which is the question people are really asking when they go
// looking for a Files page.
let folder_row = |label: std::borrow::Cow<'a, str>, path: Option<String>| {
let shown = path.clone().unwrap_or_else(|| crate::t!("Not available").into_owned());
let shown = path
.clone()
.unwrap_or_else(|| crate::t!("Not available").into_owned());
let mut open = button(text(crate::t!("Open folder")).size(11))
.padding([4, 10])
.style(button::secondary);
@@ -1737,12 +1751,9 @@ pub fn view_window<'a>(
open = open.on_press(Message::OpenFolder(path));
}
row![
column![
text(label).size(12),
text(shown).size(11),
]
.spacing(2)
.width(Fill),
column![text(label).size(12), text(shown).size(11),]
.spacing(2)
.width(Fill),
open,
]
.spacing(10)
@@ -1807,7 +1818,11 @@ pub fn view_window<'a>(
.on_press(Message::OptionsTabChanged(tab))
.padding([7, 12])
.width(Fill)
.style(if selected { button::primary } else { button::text })
.style(if selected {
button::primary
} else {
button::text
})
};
let tabs = column![
tab_button(crate::t!("General"), OptionsTab::General),
@@ -1822,9 +1837,7 @@ pub fn view_window<'a>(
// The web build has no backend to choose, so it has no Graphics page.
#[cfg(not(target_arch = "wasm32"))]
let tabs = tabs.push(tab_button(crate::t!("Graphics"), OptionsTab::Graphics));
let tabs = tabs
.spacing(2)
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
let tabs = tabs.spacing(2).width(iced::Length::Fixed(TAB_RAIL_WIDTH));
let pane = column![
// Keep the scrollbar in its own lane instead of floating over the
@@ -1844,10 +1857,22 @@ pub fn view_window<'a>(
let main = container(body)
.style(container::rounded_box)
.padding([16, 18])
.width(if intrinsic { iced::Length::Fixed(DIALOG_WIDTH) } else { sizing.width })
.height(if intrinsic { iced::Length::Fixed(DIALOG_HEIGHT) } else { sizing.height });
.width(if intrinsic {
iced::Length::Fixed(DIALOG_WIDTH)
} else {
sizing.width
})
.height(if intrinsic {
iced::Length::Fixed(DIALOG_HEIGHT)
} else {
sizing.height
});
if !close_confirm {
return main.into();
}
crate::ui::modal::discard_guard(main, Message::OptionsCloseDiscard, Message::OptionsCloseKeep)
crate::ui::modal::discard_guard(
main,
Message::OptionsCloseDiscard,
Message::OptionsCloseKeep,
)
}
+6 -3
View File
@@ -75,9 +75,12 @@ pub(crate) fn view(prefs: GraphicsPrefs, active: ActiveGpu) -> Element<'static,
Space::new().height(20),
text(crate::t!("Next launch")).size(13),
Space::new().height(8),
row![text(crate::t!("Graphics backend")).size(12).width(150), backend]
.spacing(10)
.align_y(iced::Center),
row![
text(crate::t!("Graphics backend")).size(12).width(150),
backend
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(12),
row![
checkbox(prefs.compat_renderer)
+88 -26
View File
@@ -8,7 +8,10 @@ use crate::scene::model::point_cloud::{self, UNASSIGNED_OFF};
use crate::ui::dock::PanelId;
use codec::entities::ExtendedEntityData;
use codec::{CadDocument, EntityType, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::widget::{
button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip,
Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use std::collections::HashSet;
@@ -52,7 +55,9 @@ impl Row {
"cloud" => Some(Self::Cloud),
"unassigned" => Some(Self::Unassigned),
"scans" => Some(Self::Scans),
_ => text.strip_prefix("scan:").map(|name| Self::Scan(name.to_string())),
_ => text
.strip_prefix("scan:")
.map(|name| Self::Scan(name.to_string())),
}
}
}
@@ -90,18 +95,30 @@ pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec<Cloud> {
let common = entity.common();
let scans = point_cloud::resolve_source(document, data)
.and_then(|path| point_cloud::load(&path))
.map(|cloud| cloud.scan_ranges.iter().map(|s| (s.name.clone(), s.id.clone())).collect())
.map(|cloud| {
cloud
.scan_ranges
.iter()
.map(|s| (s.name.clone(), s.id.clone()))
.collect()
})
.unwrap_or_default();
Some(Cloud {
handle: common.handle,
name: if data.name.is_empty() { format!("{:X}", common.handle.value()) } else { data.name.clone() },
name: if data.name.is_empty() {
format!("{:X}", common.handle.value())
} else {
data.name.clone()
},
hidden: point_cloud::hidden(data),
scans,
})
})
.collect();
if all.iter().any(|cloud| selected.contains(&cloud.handle)) {
all.into_iter().filter(|cloud| selected.contains(&cloud.handle)).collect()
all.into_iter()
.filter(|cloud| selected.contains(&cloud.handle))
.collect()
} else {
all
}
@@ -155,7 +172,10 @@ pub fn toggled(hidden: &[String], scans: &[(String, String)], row: &Row) -> Vec<
/// Keys of the rows that fold: each cloud and its Scans.
pub fn folding_keys(clouds: &[Cloud]) -> Vec<String> {
clouds.iter().flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))]).collect()
clouds
.iter()
.flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))])
.collect()
}
fn key(handle: Handle) -> String {
@@ -191,23 +211,39 @@ fn tree_row<'a>(
};
let enabled = switch.is_some();
let label = text(label).size(12).width(Fill);
let label = if enabled { label } else { label.style(crate::ui::style::common::muted_style) };
let label = if enabled {
label
} else {
label.style(crate::ui::style::common::muted_style)
};
let switch: Element<'a, Message> = match switch {
// Mixed: a dash in the box, as the reference's indeterminate state.
Some((Switch::Mixed, message)) => button(crate::ui::icons::themed_primary(crate::ui::icons::MINUS, 10.0))
.on_press(message)
.style(button::secondary)
.padding(1)
.width(Length::Fixed(16.0))
.height(Length::Fixed(16.0))
Some((Switch::Mixed, message)) => button(crate::ui::icons::themed_primary(
crate::ui::icons::MINUS,
10.0,
))
.on_press(message)
.style(button::secondary)
.padding(1)
.width(Length::Fixed(16.0))
.height(Length::Fixed(16.0))
.into(),
Some((on, message)) => checkbox(on == Switch::On)
.on_toggle(move |_| message.clone())
.size(14)
.into(),
Some((on, message)) => checkbox(on == Switch::On).on_toggle(move |_| message.clone()).size(14).into(),
None => checkbox(false).size(14).into(),
};
let selected = state.selected.as_deref() == Some(key.as_str());
let cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 16.0)), arrow, icon, label, switch]
.spacing(5)
.align_y(iced::Center);
let cells = row![
Space::new().width(Length::Fixed(f32::from(depth) * 16.0)),
arrow,
icon,
label,
switch
]
.spacing(5)
.align_y(iced::Center);
mouse_area(
container(cells)
.width(Fill)
@@ -222,12 +258,18 @@ fn tree_row<'a>(
.into()
}
fn fold_button<'a>(icon: &'static [u8], tip: String, message: PcManagerMsg) -> Element<'a, Message> {
fn fold_button<'a>(
icon: &'static [u8],
tip: String,
message: PcManagerMsg,
) -> Element<'a, Message> {
let b = button(crate::ui::icons::themed_secondary(icon, 12.0))
.on_press(msg(message))
.style(button::subtle)
.padding([2, 4]);
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
tooltip(b, text(tip).size(10), tooltip::Position::Bottom)
.gap(4)
.into()
}
pub fn view<'a>(
@@ -237,8 +279,11 @@ pub fn view<'a>(
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::PointCloudManager, crate::t!("Point Cloud Manager").into_owned(), auto_collapse);
let title_bar = crate::ui::dock::title_bar(
PanelId::PointCloudManager,
crate::t!("Point Cloud Manager").into_owned(),
auto_collapse,
);
let query = state.search.trim().to_lowercase();
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
// Searching opens every node so the matches show.
@@ -250,7 +295,11 @@ pub fn view<'a>(
let mut tree = column![].spacing(1);
for cloud in clouds(document, selected) {
let root = key(cloud.handle);
let scans: Vec<&(String, String)> = cloud.scans.iter().filter(|(name, _)| matches(name)).collect();
let scans: Vec<&(String, String)> = cloud
.scans
.iter()
.filter(|(name, _)| matches(name))
.collect();
let show_scans = matches(&scans_label) || !scans.is_empty();
let children = matches(&regions) || matches(&unassigned) || show_scans;
if !matches(&cloud.name) && !children {
@@ -331,12 +380,22 @@ pub fn view<'a>(
style: iced::font::Style::Italic,
..iced::Font::DEFAULT
})
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
.style(|theme: &Theme| text::Style {
color: Some(theme.palette().primary.base.color),
})
.width(Fill);
let header = row![
heading,
fold_button(crate::ui::icons::MINUS, crate::t!("Collapse all").into_owned(), PcManagerMsg::CollapseAll),
fold_button(crate::ui::icons::PLUS, crate::t!("Expand all").into_owned(), PcManagerMsg::ExpandAll),
fold_button(
crate::ui::icons::MINUS,
crate::t!("Collapse all").into_owned(),
PcManagerMsg::CollapseAll
),
fold_button(
crate::ui::icons::PLUS,
crate::t!("Expand all").into_owned(),
PcManagerMsg::ExpandAll
),
]
.spacing(2)
.align_y(iced::Center);
@@ -346,7 +405,10 @@ pub fn view<'a>(
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
});
let search = text_input(&crate::t!("Search"), &state.search)
File diff suppressed because it is too large. Load diff
+411 -263
View File
@@ -7,15 +7,14 @@
use crate::app::Message;
use crate::io::paper_catalog::{self, CustomPaper, Margins, PaperSize, PaperUnits};
use crate::io::plot_device::PrinterCapabilities;
use crate::t;
use crate::ui::style::common::muted_style;
use crate::ui::style::form;
use crate::ui::style::form::{hdivider, vseparator as vsep};
use iced::widget::{
button, checkbox, column, container, mouse_area, row, scrollable, text, text_input,
Space,
button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fit, Length, Theme};
use crate::t;
use std::borrow::Cow;
use std::fmt;
@@ -44,11 +43,19 @@ struct PlotChoice {
impl PlotChoice {
fn raw(value: impl Into<String>) -> Self {
Self { raw: value.into(), localized: false, display: None }
Self {
raw: value.into(),
localized: false,
display: None,
}
}
fn localized(value: impl Into<String>) -> Self {
Self { raw: value.into(), localized: true, display: None }
Self {
raw: value.into(),
localized: true,
display: None,
}
}
fn paper(paper: &PaperSize) -> Self {
@@ -96,7 +103,15 @@ mod tests {
assert_eq!((custom.width, custom.height), (1500.0, 609.6));
assert_eq!(custom.units, PaperUnits::Millimeters);
// Blank or unparsable margins mean no margin, never a refusal.
assert_eq!(custom.margins, Margins { left: 5.0, bottom: 17.0, right: 0.0, top: 0.0 });
assert_eq!(
custom.margins,
Margins {
left: 5.0,
bottom: 17.0,
right: 0.0,
top: 0.0
}
);
assert_eq!(custom.canonical(), "Roll_24_(609.60_x_1500.00_MM)");
draft.width = "0".into();
assert_eq!(draft.build().unwrap_err(), CustomPaperError::Size);
@@ -113,7 +128,15 @@ mod tests {
assert!(draft.inches);
assert_eq!(draft.margins, ["0.25", "0.25", "0.25", "0.25"]);
let a4 = resolve("ISO_A4_(210.00_x_297.00_MM)").unwrap();
let draft = CustomPaperDraft::from_paper(&a4, Margins { left: 5.0, bottom: 17.0, right: 6.0, top: 18.0 });
let draft = CustomPaperDraft::from_paper(
&a4,
Margins {
left: 5.0,
bottom: 17.0,
right: 6.0,
top: 18.0,
},
);
assert!(!draft.inches);
assert_eq!(draft.margins, ["5", "17", "6", "18"]);
}
@@ -124,7 +147,10 @@ mod tests {
let choice = PlotChoice::localized(format!("View: {name}"));
assert_eq!(choice.to_string(), crate::tf!("View: {name}").as_ref());
assert_eq!(choice.raw.strip_prefix("View: "), Some(name));
assert_eq!(PlotChoice::raw("View: PDF printer").to_string(), "View: PDF printer");
assert_eq!(
PlotChoice::raw("View: PDF printer").to_string(),
"View: PDF printer"
);
}
}
@@ -199,7 +225,10 @@ pub enum PlotDlgMsg {
/// The driver options of a printer arrived (or failed to), for the
/// in-line properties editor; carries the printer's name so a late
/// answer for a printer the user has since left is ignored.
PrinterOptionsLoaded(String, Result<Vec<crate::io::print_to_printer::PrinterOption>, String>),
PrinterOptionsLoaded(
String,
Result<Vec<crate::io::print_to_printer::PrinterOption>, String>,
),
/// One option of the properties editor was set to a choice keyword.
PrinterOptionSet(String, String),
/// Keep the editor's choices for the printer and close it.
@@ -357,7 +386,11 @@ impl CustomPaperDraft {
if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
return Err(CustomPaperError::Size);
}
let margin = |text: &str| number(text).filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0);
let margin = |text: &str| {
number(text)
.filter(|v| v.is_finite() && *v >= 0.0)
.unwrap_or(0.0)
};
Ok(CustomPaper {
name: self.name.trim().to_string(),
width,
@@ -437,7 +470,8 @@ pub struct PlotDialogState {
/// Driver options chosen per printer (`printer → key → choice`), the way
/// a plotter configuration remembers its device settings; applied to
/// every job sent to that printer and kept across sessions.
pub driver_options: std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>,
pub driver_options:
std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>,
/// The in-line printer-properties editor while it is open.
#[serde(skip)]
pub printer_editor: Option<PrinterOptionsDraft>,
@@ -647,7 +681,6 @@ impl PlotDialogState {
self.show_plot_styles = o.show_plot_styles;
self.style_missing = o.style_missing;
}
}
fn legacy_fit_to_paper_default() -> bool {
@@ -710,7 +743,13 @@ fn drop_row<'a>(
ctor: fn(String) -> PlotDlgMsg,
width: Length,
) -> Element<'a, Message> {
form::labeled_pick_list(label, options, selected, move |choice| Message::PlotDlg(ctor(choice.raw)), width)
form::labeled_pick_list(
label,
options,
selected,
move |choice| Message::PlotDlg(ctor(choice.raw)),
width,
)
}
fn drop_row_enabled<'a>(
@@ -738,7 +777,12 @@ fn field_row<'a>(
ctor: fn(String) -> PlotDlgMsg,
width: u16,
) -> Element<'a, Message> {
form::labeled_field(label, value, move |s| Message::PlotDlg(ctor(s)), width as f32)
form::labeled_field(
label,
value,
move |s| Message::PlotDlg(ctor(s)),
width as f32,
)
}
fn field_row_enabled<'a>(
@@ -748,7 +792,13 @@ fn field_row_enabled<'a>(
width: u16,
enabled: bool,
) -> Element<'a, Message> {
form::labeled_field_enabled(label, value, move |s| Message::PlotDlg(ctor(s)), width as f32, enabled)
form::labeled_field_enabled(
label,
value,
move |s| Message::PlotDlg(ctor(s)),
width as f32,
enabled,
)
}
fn check<'a>(label: Cow<'static, str>, on: bool, flag: PlotFlag) -> Element<'a, Message> {
@@ -803,7 +853,9 @@ fn page_setup_import_chooser<'a>(
}
let all = !draft.setups.is_empty() && draft.setups.iter().all(|(_, _, on)| *on);
let any = draft.setups.iter().any(|(_, _, on)| *on);
let mut import = button(text(t!("Import")).size(11)).style(btn(true)).padding([4, 8]);
let mut import = button(text(t!("Import")).size(11))
.style(btn(true))
.padding([4, 8]);
if any {
import = import.on_press(msg(PageSetupImportMsg::Apply));
}
@@ -860,32 +912,33 @@ fn custom_scale_row<'a>(s: &'a PlotDialogState, enabled: bool) -> Element<'a, Me
/// the file's own problem, or where a copy would be found.
fn style_note<'a>(s: &'a PlotDialogState) -> Element<'a, Message> {
match &s.style_error {
Some(error) if s.style_missing => {
text(error.clone()).size(10).style(muted_style).into()
}
Some(error) if s.style_missing => text(error.clone()).size(10).style(muted_style).into(),
_ => Space::new().height(0).into(),
}
}
fn panel<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(content)
.width(Length::Fill)
.into()
container(content).width(Length::Fill).into()
}
fn choices(items: &[&str]) -> Vec<PlotChoice> {
items.iter().map(|&value| PlotChoice::localized(value)).collect()
items
.iter()
.map(|&value| PlotChoice::localized(value))
.collect()
}
/// The in-line printer-properties editor: one `label : choice` row per
/// driver option (media type, colour mode, quality, duplex, trays…), with
/// Reset / Cancel / Apply. Mirrors what LibreOffice's Properties… shows for a
/// CUPS printer, without leaving the plot dialog.
fn printer_options_editor<'a>(draft: &'a PrinterOptionsDraft, width: Length) -> Element<'a, Message> {
let mut rows = column![section_label(Cow::Owned(crate::tf!(
"Printer properties — {printer}",
printer = draft.printer
).into_owned()))]
fn printer_options_editor<'a>(
draft: &'a PrinterOptionsDraft,
width: Length,
) -> Element<'a, Message> {
let mut rows = column![section_label(Cow::Owned(
crate::tf!("Printer properties — {printer}", printer = draft.printer).into_owned()
))]
.spacing(6);
match (&draft.options, &draft.error) {
(Some(options), _) if options.is_empty() => {
@@ -917,7 +970,9 @@ fn printer_options_editor<'a>(draft: &'a PrinterOptionsDraft, width: Length) ->
Cow::Owned(option.label.clone()),
choices,
selected,
move |choice| Message::PlotDlg(PlotDlgMsg::PrinterOptionSet(key.clone(), choice.raw)),
move |choice| {
Message::PlotDlg(PlotDlgMsg::PrinterOptionSet(key.clone(), choice.raw))
},
Length::Fill,
));
}
@@ -932,7 +987,11 @@ fn printer_options_editor<'a>(draft: &'a PrinterOptionsDraft, width: Length) ->
);
}
(None, None) => {
rows = rows.push(text(t!("Reading the printer's options…")).size(10).style(muted_style));
rows = rows.push(
text(t!("Reading the printer's options…"))
.size(10)
.style(muted_style),
);
}
}
let mut buttons = row![
@@ -982,12 +1041,17 @@ fn custom_paper_editor<'a>(draft: &'a CustomPaperDraft, width: Length) -> Elemen
PlotChoice::localized("Millimeters"),
PlotChoice::localized("Inches"),
];
let unit_selected = Some(PlotChoice::localized(if draft.inches { "Inches" } else { "Millimeters" }));
let unit_selected = Some(PlotChoice::localized(if draft.inches {
"Inches"
} else {
"Millimeters"
}));
const SIDE_LABEL: f32 = 52.0;
const NUMBER: f32 = 64.0;
let dimension = |label: Cow<'static, str>, value: &'a str, ctor: fn(String) -> CustomPaperMsg| {
form::labeled_field_compact(label, value, move |v| msg(ctor(v)), SIDE_LABEL, NUMBER)
};
let dimension =
|label: Cow<'static, str>, value: &'a str, ctor: fn(String) -> CustomPaperMsg| {
form::labeled_field_compact(label, value, move |v| msg(ctor(v)), SIDE_LABEL, NUMBER)
};
let margin = |label: Cow<'static, str>, side: MarginSide, index: usize| {
form::labeled_field_compact(
label,
@@ -1009,7 +1073,12 @@ fn custom_paper_editor<'a>(draft: &'a CustomPaperDraft, width: Length) -> Elemen
container(
column![
section_label(t!("Custom paper size")),
form::labeled_field(t!("Name"), &draft.name, move |v| msg(CustomPaperMsg::Name(v)), 200.0),
form::labeled_field(
t!("Name"),
&draft.name,
move |v| msg(CustomPaperMsg::Name(v)),
200.0
),
form::labeled_pick_list(
t!("Units"),
unit_options,
@@ -1107,30 +1176,29 @@ pub fn view_window(
} else {
scrollable(column(rows).spacing(1)).height(height).into()
};
let new_name: Element<'_, Message> =
if !s.name_rename && s.name_input.is_some() {
row![
text_input("", rename_buf)
.on_input(|value| Message::PlotDlg(PlotDlgMsg::NameInput(value)))
.on_submit(Message::PlotDlg(PlotDlgMsg::NameCommit))
.style(form::field_style)
.size(11)
.padding([4, 8])
.width(Length::Fill),
button(text(t!("Save")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NameCommit))
.style(btn(true))
.padding([4, 8]),
button(text("×").size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NameCancel))
.style(btn(false))
.padding([4, 8]),
]
.spacing(4)
.into()
} else {
Space::new().height(0).into()
};
let new_name: Element<'_, Message> = if !s.name_rename && s.name_input.is_some() {
row![
text_input("", rename_buf)
.on_input(|value| Message::PlotDlg(PlotDlgMsg::NameInput(value)))
.on_submit(Message::PlotDlg(PlotDlgMsg::NameCommit))
.style(form::field_style)
.size(11)
.padding([4, 8])
.width(Length::Fill),
button(text(t!("Save")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NameCommit))
.style(btn(true))
.padding([4, 8]),
button(text("×").size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::NameCancel))
.style(btn(false))
.padding([4, 8]),
]
.spacing(4)
.into()
} else {
Space::new().height(0).into()
};
// While an import is being chosen, the list shows the other drawing's
// setups with a tick each instead of this drawing's.
let list_body: Element<'_, Message> = match &s.import_draft {
@@ -1192,16 +1260,11 @@ pub fn view_window(
.style(btn(false))
.padding([4, 12]);
if !print_all_options && s.import_draft.is_none() {
import_button = import_button
.on_press(Message::PlotDlg(PlotDlgMsg::Import(PageSetupImportMsg::Pick)));
import_button = import_button.on_press(Message::PlotDlg(PlotDlgMsg::Import(
PageSetupImportMsg::Pick,
)));
}
let left_bar = row![
new_button,
copy_button,
delete_button,
import_button,
]
.spacing(4);
let left_bar = row![new_button, copy_button, delete_button, import_button,].spacing(4);
// ── Printer / plotter ─────────────────────────────────────────────────
// The default entry names the printer it resolves to, when known, so the
@@ -1233,8 +1296,15 @@ pub fn view_window(
let selected_paper = paper_catalog::from_drawing(&s.paper, s.paper_width_mm, s.paper_height_mm);
let printer_sheets = (!s.to_file).then_some(()).and(s.printer_media.as_deref());
let mut paper_opts: Vec<PlotChoice> = match printer_sheets {
Some(caps) => caps.media.iter().map(|media| PlotChoice::paper(&media.paper)).collect(),
None => paper_catalog::catalog().iter().map(PlotChoice::paper).collect(),
Some(caps) => caps
.media
.iter()
.map(|media| PlotChoice::paper(&media.paper))
.collect(),
None => paper_catalog::catalog()
.iter()
.map(PlotChoice::paper)
.collect(),
};
// User-defined sheets come last, after whatever the device offers.
for custom in &s.custom_papers {
@@ -1252,20 +1322,24 @@ pub fn view_window(
.iter()
.any(|custom| custom.canonical() == selected_paper.canonical);
let paper_source_note: Element<'_, Message> = if printer_sheets.is_some() {
text(t!("Sheet sizes and printable areas reported by the printer."))
.size(10)
.style(muted_style)
.width(width)
.into()
text(t!(
"Sheet sizes and printable areas reported by the printer."
))
.size(10)
.style(muted_style)
.width(width)
.into()
} else {
Space::new().height(0).into()
};
let paper_note: Element<'_, Message> = if s.area == "Layout" {
text(t!("Layout plots the current sheet using the selected paper size."))
.size(10)
.style(muted_style)
.width(width)
.into()
text(t!(
"Layout plots the current sheet using the selected paper size."
))
.size(10)
.style(muted_style)
.width(width)
.into()
} else {
Space::new().height(0).into()
};
@@ -1273,7 +1347,10 @@ pub fn view_window(
// announce — and the picker takes the whole row so a long queue name
// stays readable; the driver's Properties… button sits beneath it.
let output_field = row![
text(t!("Printer / plotter")).size(11).style(muted_style).width(form::LABEL_WIDTH),
text(t!("Printer / plotter"))
.size(11)
.style(muted_style)
.width(form::LABEL_WIDTH),
iced::widget::pick_list(printer_sel, printer_opts, |value| value.to_string())
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Printer(choice.raw)))
.text_size(12)
@@ -1323,7 +1400,13 @@ pub fn view_window(
// The picker takes the whole row so long media names stay readable; the
// custom-sheet buttons sit right-aligned beneath it, and the editor
// unfolds in their place.
let size_row = drop_row(t!("Paper size"), paper_opts, Some(paper_sel), PlotDlgMsg::Paper, Length::Fill);
let size_row = drop_row(
t!("Paper size"),
paper_opts,
Some(paper_sel),
PlotDlgMsg::Paper,
Length::Fill,
);
let custom_controls: Element<'_, Message> = match &s.custom_editor {
Some(draft) => custom_paper_editor(draft, width),
None => {
@@ -1331,7 +1414,9 @@ pub fn view_window(
if selected_is_custom {
buttons = buttons.push(
button(text(t!("Remove")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Remove)))
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(
CustomPaperMsg::Remove,
)))
.style(btn(false))
.padding([4, 10]),
);
@@ -1339,7 +1424,9 @@ pub fn view_window(
buttons
.push(
button(text(t!("Custom…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(CustomPaperMsg::Open)))
.on_press(Message::PlotDlg(PlotDlgMsg::CustomPaper(
CustomPaperMsg::Open,
)))
.style(btn(false))
.padding([4, 10]),
)
@@ -1348,12 +1435,8 @@ pub fn view_window(
};
// Like the printer row: the section is its one picker, so heading and
// control share a line.
let paper_panel = panel(column![
size_row,
custom_controls,
paper_source_note,
paper_note,
].spacing(7));
let paper_panel =
panel(column![size_row, custom_controls, paper_source_note, paper_note,].spacing(7));
let mut area_options = if print_all_options {
choices(&["Layout"])
@@ -1371,16 +1454,19 @@ pub fn view_window(
);
}
let mut area_row = row![
text(t!("What to plot")).size(11).style(muted_style).width(92),
text(t!("What to plot"))
.size(11)
.style(muted_style)
.width(92),
iced::widget::pick_list(
Some(PlotChoice::localized(s.area.clone())),
area_options,
|value| value.to_string(),
)
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Area(choice.raw)))
.text_size(12)
.padding([3, 6])
.width(Length::Fill),
.on_select(|choice| Message::PlotDlg(PlotDlgMsg::Area(choice.raw)))
.text_size(12)
.padding([3, 6])
.width(Length::Fill),
]
.spacing(8)
.align_y(iced::Center);
@@ -1399,57 +1485,87 @@ pub fn view_window(
} else {
(t!("X (mm)"), t!("Y (mm)"))
};
let area_panel = panel(column![
section_label(t!("Plot area")),
area_row,
section_label(t!("Plot offset")),
let area_panel = panel(
column![
field_row_enabled(offset_x_label, &s.offset_x, PlotDlgMsg::OffsetX, 70, common_area && !s.center),
field_row_enabled(offset_y_label, &s.offset_y, PlotDlgMsg::OffsetY, 70, common_area && !s.center),
section_label(t!("Plot area")),
area_row,
section_label(t!("Plot offset")),
column![
field_row_enabled(
offset_x_label,
&s.offset_x,
PlotDlgMsg::OffsetX,
70,
common_area && !s.center
),
field_row_enabled(
offset_y_label,
&s.offset_y,
PlotDlgMsg::OffsetY,
70,
common_area && !s.center
),
]
.spacing(7),
check_enabled(
t!("Center the plot"),
s.center,
PlotFlag::Center,
common_area
),
]
.spacing(7),
check_enabled(t!("Center the plot"), s.center, PlotFlag::Center, common_area),
].spacing(7));
);
let scale_options = s
.scales
.iter()
.map(|(name, _)| PlotChoice::raw(name.clone()))
.collect();
let scale_panel = panel(column![
section_label(t!("Plot scale")),
check_enabled(
t!("Fit to paper"),
s.fit_to_paper,
PlotFlag::FitToPaper,
common_area,
),
// "Scale" intentionally stays untranslated: it would clash with the
// ribbon's zoom tool label under the same lookup key.
drop_row_enabled(
Cow::Borrowed("Scale"),
scale_options,
Some(PlotChoice::raw(s.scale.clone())),
PlotDlgMsg::Scale,
width,
common_area && !s.fit_to_paper,
),
// The custom scale spells the picked ratio in the page-setup unit
// and accepts one of its own; the unit picker decides that unit.
drop_row(
t!("Units"),
vec![PlotChoice::localized("Millimeters"), PlotChoice::localized("Inches")],
Some(PlotChoice::localized(if inches { "Inches" } else { "Millimeters" })),
PlotDlgMsg::PaperUnits,
width,
),
custom_scale_row(s, common_area && !s.fit_to_paper),
check_enabled(
t!("Scale lineweights"),
s.scale_lw && !s.fit_to_paper,
PlotFlag::ScaleLw,
!s.fit_to_paper,
),
].spacing(7));
let scale_panel = panel(
column![
section_label(t!("Plot scale")),
check_enabled(
t!("Fit to paper"),
s.fit_to_paper,
PlotFlag::FitToPaper,
common_area,
),
// "Scale" intentionally stays untranslated: it would clash with the
// ribbon's zoom tool label under the same lookup key.
drop_row_enabled(
Cow::Borrowed("Scale"),
scale_options,
Some(PlotChoice::raw(s.scale.clone())),
PlotDlgMsg::Scale,
width,
common_area && !s.fit_to_paper,
),
// The custom scale spells the picked ratio in the page-setup unit
// and accepts one of its own; the unit picker decides that unit.
drop_row(
t!("Units"),
vec![
PlotChoice::localized("Millimeters"),
PlotChoice::localized("Inches")
],
Some(PlotChoice::localized(if inches {
"Inches"
} else {
"Millimeters"
})),
PlotDlgMsg::PaperUnits,
width,
),
custom_scale_row(s, common_area && !s.fit_to_paper),
check_enabled(
t!("Scale lineweights"),
s.scale_lw && !s.fit_to_paper,
PlotFlag::ScaleLw,
!s.fit_to_paper,
),
]
.spacing(7),
);
// ── Style and shaded viewport settings ───────────────────────────────
let mut style_options = vec![PlotChoice::localized(STYLE_NONE)];
@@ -1466,111 +1582,147 @@ pub fn view_window(
} else {
PlotChoice::raw(s.style_name.clone())
};
let style_panel = panel(column![
section_label(t!("Plot style table (pen assignments)")),
drop_row(
t!("Table"),
style_options,
Some(style_selected),
PlotDlgMsg::Style,
width,
),
style_note(s),
check_enabled(
t!("Plot with plot styles"),
s.apply_plot_styles,
PlotFlag::PlotStyles,
!s.style_name.is_empty(),
),
check_enabled(
t!("Display plot styles"),
s.show_plot_styles,
PlotFlag::DisplayStyles,
s.paper_space && !s.style_name.is_empty(),
),
row![
button(text(t!("Load…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))
.style(btn(false))
.padding([4, 10]),
button(text(t!("Edit…")).size(11))
.on_press(Message::PlotStylePanelOpen)
.style(btn(false))
.padding([4, 10]),
let style_panel = panel(
column![
section_label(t!("Plot style table (pen assignments)")),
drop_row(
t!("Table"),
style_options,
Some(style_selected),
PlotDlgMsg::Style,
width,
),
style_note(s),
check_enabled(
t!("Plot with plot styles"),
s.apply_plot_styles,
PlotFlag::PlotStyles,
!s.style_name.is_empty(),
),
check_enabled(
t!("Display plot styles"),
s.show_plot_styles,
PlotFlag::DisplayStyles,
s.paper_space && !s.style_name.is_empty(),
),
row![
button(text(t!("Load…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))
.style(btn(false))
.padding([4, 10]),
button(text(t!("Edit…")).size(11))
.on_press(Message::PlotStylePanelOpen)
.style(btn(false))
.padding([4, 10]),
]
.spacing(6)
.align_y(iced::Center),
]
.spacing(6)
.align_y(iced::Center),
].spacing(7));
.spacing(7),
);
let shaded_panel = panel(column![
section_label(t!("Shaded viewport options")),
drop_row(
t!("Shade plot"),
choices(&[
"As displayed",
"2D Wireframe",
"3D Wireframe",
"Hidden Line",
"Flat Shaded",
"Gouraud Shaded",
"Flat Shaded + Edges",
"Gouraud Shaded + Edges",
]),
Some(PlotChoice::localized(s.shade.clone())),
PlotDlgMsg::Shade,
width,
),
drop_row(
t!("Quality"),
choices(&["Low", "Normal", "High"]),
Some(PlotChoice::localized(s.quality.clone())),
PlotDlgMsg::Quality,
width,
),
].spacing(7));
let shaded_panel = panel(
column![
section_label(t!("Shaded viewport options")),
drop_row(
t!("Shade plot"),
choices(&[
"As displayed",
"2D Wireframe",
"3D Wireframe",
"Hidden Line",
"Flat Shaded",
"Gouraud Shaded",
"Flat Shaded + Edges",
"Gouraud Shaded + Edges",
]),
Some(PlotChoice::localized(s.shade.clone())),
PlotDlgMsg::Shade,
width,
),
drop_row(
t!("Quality"),
choices(&["Low", "Normal", "High"]),
Some(PlotChoice::localized(s.quality.clone())),
PlotDlgMsg::Quality,
width,
),
]
.spacing(7),
);
// ── Output options and orientation ────────────────────────────────────
let paper_space_option = |on: bool, label: Cow<'static, str>, flag: PlotFlag| -> Element<'_, Message> {
if s.paper_space {
check(label, on, flag)
} else {
Space::new().height(0).into()
}
};
let options_panel = panel(column![
section_label(t!("Plot options")),
row![
column![
check(t!("Plot in background"), s.background, PlotFlag::Background),
check(t!("Object lineweights"), s.lineweights, PlotFlag::Lineweights),
check(t!("Plot transparency"), s.transparency, PlotFlag::Transparency),
paper_space_option(s.hide_paperspace, t!("Hide paperspace objects"), PlotFlag::HidePaperspace),
let paper_space_option =
|on: bool, label: Cow<'static, str>, flag: PlotFlag| -> Element<'_, Message> {
if s.paper_space {
check(label, on, flag)
} else {
Space::new().height(0).into()
}
};
let options_panel = panel(
column![
section_label(t!("Plot options")),
row![
column![
check(t!("Plot in background"), s.background, PlotFlag::Background),
check(
t!("Object lineweights"),
s.lineweights,
PlotFlag::Lineweights
),
check(
t!("Plot transparency"),
s.transparency,
PlotFlag::Transparency
),
paper_space_option(
s.hide_paperspace,
t!("Hide paperspace objects"),
PlotFlag::HidePaperspace
),
]
.spacing(6)
.width(width),
column![
paper_space_option(
s.paperspace_last,
t!("Paper space last"),
PlotFlag::PaperspaceLast
),
check(
t!("Merge overlapping lines"),
s.merge_lines,
PlotFlag::MergeLines
),
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
check(
t!("Save changes to layout"),
s.save_to_layout,
PlotFlag::SaveToLayout
),
]
.spacing(6)
.width(width),
]
.spacing(6)
.width(width),
column![
paper_space_option(s.paperspace_last, t!("Paper space last"), PlotFlag::PaperspaceLast),
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
check(t!("Save changes to layout"), s.save_to_layout, PlotFlag::SaveToLayout),
]
.spacing(6)
.width(width),
.spacing(10),
]
.spacing(10),
].spacing(7));
.spacing(7),
);
let orientation_panel = panel(column![
drop_row(
t!("Orientation"),
choices(&["Portrait", "Landscape"]),
Some(PlotChoice::localized(s.orientation.clone())),
PlotDlgMsg::Orientation,
width,
),
check(t!("Plot upside-down"), s.upside_down, PlotFlag::UpsideDown),
].spacing(7));
let orientation_panel = panel(
column![
drop_row(
t!("Orientation"),
choices(&["Portrait", "Landscape"]),
Some(PlotChoice::localized(s.orientation.clone())),
PlotDlgMsg::Orientation,
width,
),
check(t!("Plot upside-down"), s.upside_down, PlotFlag::UpsideDown),
]
.spacing(7),
);
let left = column![
printer_panel,
@@ -1580,8 +1732,8 @@ pub fn view_window(
hdivider(width),
area_panel,
]
.spacing(9)
.width(width);
.spacing(9)
.width(width);
let right = column![
scale_panel,
hdivider(width),
@@ -1591,32 +1743,30 @@ pub fn view_window(
hdivider(width),
options_panel,
]
.spacing(9)
.width(width);
let detail = scrollable(
container(row![left, right].spacing(18).width(width)).padding(14),
)
.width(width)
.height(height);
.spacing(9)
.width(width);
let detail = scrollable(container(row![left, right].spacing(18).width(width)).padding(14))
.width(width)
.height(height);
let body = row![list_panel, vsep(height), detail]
.width(width)
.height(height);
let mut toolbar_row = row![left_bar, Space::new().width(width)]
.align_y(iced::Center)
.push(
button(text(t!("Set current")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
.style(btn(false))
.padding([4, 12]),
)
.push(Space::new().width(6))
.push(
button(text(t!("Preview")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
.style(btn(false))
.padding([4, 12]),
)
.push(Space::new().width(6));
.align_y(iced::Center)
.push(
button(text(t!("Set current")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
.style(btn(false))
.padding([4, 12]),
)
.push(Space::new().width(6))
.push(
button(text(t!("Preview")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
.style(btn(false))
.padding([4, 12]),
)
.push(Space::new().width(6));
toolbar_row = toolbar_row.push(
button(text(action).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::Commit))
@@ -1633,9 +1783,7 @@ pub fn view_window(
container(column![toolbar, hdivider(width), body].spacing(0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(width)
+60 -74
View File
@@ -8,12 +8,12 @@
use crate::app::Message;
use crate::plugin::external::{ExternalPlugin, RegistryEntry, ReleaseInfo};
use crate::t;
use crate::ui::style::common::muted_style;
use iced::widget::{
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::t;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Cow;
@@ -61,13 +61,13 @@ fn badge<'a>(label: String) -> Element<'a, Message> {
.style(|theme: &Theme| {
let pair = theme.palette().primary.weak;
container::Style {
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
},
..Default::default()
}
})
.into()
@@ -85,7 +85,11 @@ fn toggle_button<'a>(id: &str, disabled: bool) -> Element<'a, Message> {
button(text(label).size(12))
.padding([3, 12])
.on_press(Message::SetPluginEnabled(id_owned, want_enabled))
.style(if disabled { button::success } else { button::danger })
.style(if disabled {
button::success
} else {
button::danger
})
.into()
}
@@ -110,13 +114,13 @@ fn status_badge<'a>(label: Cow<'static, str>, kind: StatusKind) -> Element<'a, M
StatusKind::Warning => palette.warning.weak,
};
container::Style {
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
background: Some(Background::Color(pair.color)),
text_color: Some(pair.text),
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
},
..Default::default()
}
})
.into()
@@ -140,10 +144,7 @@ fn card_style(theme: &Theme, selected: bool) -> container::Style {
}
}
fn repository_for_external(
plugin: &ExternalPlugin,
registry: &[RegistryEntry],
) -> Option<String> {
fn repository_for_external(plugin: &ExternalPlugin, registry: &[RegistryEntry]) -> Option<String> {
plugin.repository.clone().or_else(|| {
registry
.iter()
@@ -182,8 +183,7 @@ fn repository_is_installed(
) -> bool {
externals.iter().any(|plugin| {
plugin.repository.as_deref() == Some(repository)
|| registry_name
.is_some_and(|name| plugin.name.eq_ignore_ascii_case(name))
|| registry_name.is_some_and(|name| plugin.name.eq_ignore_ascii_case(name))
})
}
@@ -221,8 +221,8 @@ fn external_card<'a>(
) -> Element<'a, Message> {
let failed_old_api = load_error.is_some() && !p.api_compatible();
let acadrust_mismatch = p.acadrust_declared && !p.acadrust_compatible();
let rustc_mismatch = ocs_plugin_api::version_info::uses_acadrust_gate(p.api_version)
&& !p.rustc_compatible();
let rustc_mismatch =
ocs_plugin_api::version_info::uses_acadrust_gate(p.api_version) && !p.rustc_compatible();
let (status, kind) = if loaded && disabled {
(t!("Disabled"), StatusKind::Muted)
} else if loaded {
@@ -274,14 +274,11 @@ fn external_card<'a>(
} else {
t!("Load failed: %{error}", error = error)
};
info_body = info_body.push(
text(detail)
.size(11)
.width(Fill)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().danger.base.color),
}),
);
info_body = info_body.push(text(detail).size(11).width(Fill).style(|theme: &Theme| {
iced::widget::text::Style {
color: Some(theme.palette().danger.base.color),
}
}));
}
let info: Element<'a, Message> = if let Some(repo) = repository.clone() {
@@ -366,17 +363,17 @@ fn install_controls<'a>(
.iter()
.map(|release| release.tag.clone())
.collect::<Vec<_>>();
let selected_release = selected.as_ref().and_then(|selected| {
releases.iter().find(|release| release.tag == *selected)
});
let selected_release = selected
.as_ref()
.and_then(|selected| releases.iter().find(|release| release.tag == *selected));
let picker: Element<'_, Message> = if tags.is_empty() {
text(t!("no releases")).size(11).style(muted_style).into()
} else {
let r = repo_s.clone();
iced::widget::pick_list(selected, tags, |value| value.to_string())
.on_select(move |tag| Message::PluginReleaseSelect(r.clone(), tag))
.text_size(12)
.into()
.on_select(move |tag| Message::PluginReleaseSelect(r.clone(), tag))
.text_size(12)
.into()
};
let action = match selected_release {
Some(release)
@@ -504,11 +501,7 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
if let Some(error) = m.registry_error {
let (title, message) = registry_error_message(error);
let actions = row![
pill_button(
t!("Retry"),
Message::PluginRegistryRetry,
button::primary,
),
pill_button(t!("Retry"), Message::PluginRegistryRetry, button::primary,),
Space::new().width(6),
pill_button(
if m.registry_error_details_open {
@@ -565,9 +558,11 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
container(
column![
text(t!("Loading plugin catalog…")).size(13),
text(t!("Connecting securely using your system certificate settings."))
.size(11)
.style(muted_style),
text(t!(
"Connecting securely using your system certificate settings."
))
.size(11)
.style(muted_style),
]
.spacing(5)
.padding([10, 12]),
@@ -578,10 +573,7 @@ fn registry_notice<'a>(m: &MarketView) -> Option<Element<'a, Message>> {
})
}
fn marketplace_section<'a>(
m: &MarketView,
externals: &[ExternalPlugin],
) -> Element<'a, Message> {
fn marketplace_section<'a>(m: &MarketView, externals: &[ExternalPlugin]) -> Element<'a, Message> {
let mut col = column![text(t!("Available plugins")).size(13).style(primary_style)].spacing(6);
let mut visible = 0usize;
if let Some(notice) = registry_notice(m) {
@@ -656,9 +648,7 @@ fn marketplace_section<'a>(
));
}
if visible == 0
&& m.registry_error.is_none()
&& !(m.registry_loading && m.registry.is_empty())
if visible == 0 && m.registry_error.is_none() && !(m.registry_loading && m.registry.is_empty())
{
let message = if m.search.trim().is_empty() {
t!("No additional plugins are available.")
@@ -677,10 +667,7 @@ fn marketplace_section<'a>(
}
fn resolve_readme_link(repo: &str, uri: &str) -> String {
if uri.starts_with("https://")
|| uri.starts_with("http://")
|| uri.starts_with("mailto:")
{
if uri.starts_with("https://") || uri.starts_with("http://") || uri.starts_with("mailto:") {
uri.to_string()
} else if uri.starts_with('/') {
format!("https://github.com{uri}")
@@ -761,9 +748,7 @@ fn readme_panel<'a>(
readme.items(),
markdown::Settings::with_text_size(13, theme),
)
.map(move |uri| {
Message::OpenUrl(resolve_readme_link(&source_repo, &uri))
})
.map(move |uri| Message::OpenUrl(resolve_readme_link(&source_repo, &uri)))
}
Some(Err(error)) => container(
column![
@@ -831,15 +816,20 @@ pub fn view_window<'a>(
let width = sizing.width;
let height = sizing.height;
let title = text(t!("Plugins")).size(20);
let subtitle =
text(t!("Browse, install, and manage add-ons. Select one to view its README."))
.size(12)
.style(muted_style);
let subtitle = text(t!(
"Browse, install, and manage add-ons. Select one to view its README."
))
.size(12)
.style(muted_style);
let mut list = column![].spacing(10);
// Installed external packages (from the plugins folder).
if externals.is_empty() {
list = list.push(text(t!("No plugins installed yet.")).size(13).style(muted_style));
list = list.push(
text(t!("No plugins installed yet."))
.size(13)
.style(muted_style),
);
} else {
let mut visible_installed = 0usize;
for p in externals {
@@ -914,9 +904,7 @@ pub fn view_window<'a>(
.height(height),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(width)
@@ -942,9 +930,7 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
))
.center(Length::Fixed(44.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().primary.weak.color,
)),
background: Some(Background::Color(theme.palette().primary.weak.color)),
border: Border {
radius: 12.0.into(),
..Default::default()
@@ -976,11 +962,11 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
.width(Length::Fit.max(380.0));
container(notice)
.center_x(Length::Fit)
.padding([16, 20])
.width(Length::Fit)
.height(Length::Fit)
.into()
.center_x(Length::Fit)
.padding([16, 20])
.width(Length::Fit)
.height(Length::Fit)
.into()
}
#[cfg(test)]
+15 -7
View File
@@ -37,12 +37,18 @@ pub fn view_window<'a>(
})
.collect();
let list: Element<'a, Message> = if rows.is_empty() {
container(text(t!("No paper layouts are available.")).size(12).style(muted))
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
container(
text(t!("No paper layouts are available."))
.size(12)
.style(muted),
)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
} else {
scrollable(column(rows).spacing(2)).height(sizing.height).into()
scrollable(column(rows).spacing(2))
.height(sizing.height)
.into()
};
let printer = printer
.filter(|name| !name.is_empty())
@@ -81,8 +87,10 @@ pub fn view_window<'a>(
container(
column![
text(t!("Select the layouts to output. Each page is printed in Layout mode."))
.size(12),
text(t!(
"Select the layouts to output. Each page is printed in Layout mode."
))
.size(12),
row![
button(text(t!("Select all")).size(11))
.on_press(Message::PrintAllSelectAll)
+8 -6
View File
@@ -4,9 +4,7 @@ use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Fill, Length, Shrink, Theme};
fn status_style(
status: RecoveryStatus,
) -> impl Fn(&Theme) -> iced::widget::text::Style + Copy {
fn status_style(status: RecoveryStatus) -> impl Fn(&Theme) -> iced::widget::text::Style + Copy {
move |theme: &Theme| iced::widget::text::Style {
color: Some(match status {
RecoveryStatus::Recovered => theme.palette().warning.base.color,
@@ -107,9 +105,13 @@ pub fn view_window<'a>(
}
if let Some(path) = &report.log_path {
details = details.push(
text(format!("{}: {}", crate::tr!("recovery", "log-path"), path.display()))
.size(10)
.style(muted_style),
text(format!(
"{}: {}",
crate::tr!("recovery", "log-path"),
path.display()
))
.size(10)
.style(muted_style),
);
} else if let Some(error) = &report.log_error {
details = details.push(
+727 -210
View File
File diff suppressed because it is too large. Load diff
+29 -34
View File
@@ -29,18 +29,16 @@ fn danger_input_style(
let danger = theme.palette().danger.base;
iced::widget::text_input::Style {
background: Background::Color(theme.palette().danger.weak.color),
border: iced::border::rounded(4)
.color(danger.color)
.width(
if matches!(
status,
iced::widget::text_input::Status::Focused { is_hovered: _ }
) {
1.5
} else {
1.0
},
),
border: iced::border::rounded(4).color(danger.color).width(
if matches!(
status,
iced::widget::text_input::Status::Focused { is_hovered: _ }
) {
1.5
} else {
1.0
},
),
icon: danger.color,
placeholder: danger.color.scale_alpha(0.7),
value: danger.text,
@@ -74,8 +72,7 @@ pub fn view_window<'a>(
let head = container(
row![
container(text(t!("Key")).size(11).style(muted_style))
.width(Length::Fixed(180.0)),
container(text(t!("Key")).size(11).style(muted_style)).width(Length::Fixed(180.0)),
container(text(t!("Command")).size(11).style(muted_style)).width(sizing.width),
Space::new().width(Length::Fixed(30.0)),
]
@@ -119,8 +116,7 @@ pub fn view_window<'a>(
.width(Length::Fixed(180.0))
.style(button::secondary)
};
let unknown_command =
!command.is_empty() && unknown_commands.contains(command.trim());
let unknown_command = !command.is_empty() && unknown_commands.contains(command.trim());
let command_box = text_input(t!("command").as_ref(), command)
.on_input(move |value| Message::ShortcutEditorInput {
idx,
@@ -199,9 +195,11 @@ pub fn view_window<'a>(
let stats = if reset_confirm {
row![Space::new().width(Length::Fixed(0.0))]
} else {
row![text(format!("{}: {}", t!("Number of shortcuts"), rows.len()))
.size(12)
.style(muted_style)]
row![
text(format!("{}: {}", t!("Number of shortcuts"), rows.len()))
.size(12)
.style(muted_style)
]
};
// Reset asks for confirmation in place: the add/reset buttons are
// replaced by the question with Yes / No.
@@ -284,9 +282,15 @@ pub fn view_window<'a>(
Space::new().height(6),
conflict_banner,
Space::new().height(4),
row![stats, add_area, reset_area, Space::new().width(sizing.width), apply_area]
.spacing(8)
.align_y(iced::Center),
row![
stats,
add_area,
reset_area,
Space::new().width(sizing.width),
apply_area
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(6)
.width(sizing.width)
@@ -296,9 +300,7 @@ pub fn view_window<'a>(
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
});
@@ -313,12 +315,7 @@ pub fn view_window<'a>(
.height(Length::Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme
.palette()
.background
.strongest
.color
.scale_alpha(0.55),
theme.palette().background.strongest.color.scale_alpha(0.55),
)),
..Default::default()
}),
@@ -346,9 +343,7 @@ pub fn view_window<'a>(
.padding(16)
.width(Length::Fixed(320.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
border: iced::Border {
color: theme.palette().background.neutral.color,
width: 1.0,
+36 -31
View File
@@ -1,8 +1,8 @@
use crate::app::Message;
use crate::t;
use crate::ui::style::common::muted_style;
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Background, Border, Element, Theme};
use crate::t;
use std::borrow::Cow;
fn primary_style(theme: &Theme) -> iced::widget::text::Style {
@@ -50,17 +50,17 @@ fn version_card<'a>(
palette.background.base
};
container::Style {
background: Some(Background::Color(pair.color)),
border: Border {
color: if highlight {
palette.primary.base.color
} else {
palette.background.neutral.color
background: Some(Background::Color(pair.color)),
border: Border {
color: if highlight {
palette.primary.base.color
} else {
palette.background.neutral.color
},
width: 1.0,
radius: 6.0.into(),
},
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
..Default::default()
}
})
.into()
@@ -78,9 +78,7 @@ fn version_card<'a>(
fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> {
let trimmed = raw.trim_end();
if trimmed.is_empty() {
return Space::new()
.height(iced::Length::Fixed(6.0))
.into();
return Space::new().height(iced::Length::Fixed(6.0)).into();
}
if let Some(rest) = trimmed.strip_prefix("## ") {
return text(strip_inline_md(rest))
@@ -89,13 +87,18 @@ fn render_notes_line<'a>(raw: &str) -> Element<'a, Message> {
.into();
}
if let Some(rest) = trimmed.strip_prefix("### ") {
return text(strip_inline_md(rest))
.size(12)
.into();
return text(strip_inline_md(rest)).size(12).into();
}
if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")) {
if let Some(rest) = trimmed
.strip_prefix("- ")
.or_else(|| trimmed.strip_prefix("* "))
{
return row![
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOT, 5.0)).width(14),
container(crate::ui::icons::themed_secondary(
crate::ui::icons::DOT,
5.0
))
.width(14),
text(strip_inline_md(rest)).size(11),
]
.spacing(4)
@@ -129,10 +132,14 @@ pub fn view_window<'a>(
) -> Element<'a, Message> {
let header = container(
column![
text(t!("New Release Available")).size(20).style(primary_style),
text(t!("A newer Open CAD Studio version is published on GitHub."))
.size(11)
.style(muted_style),
text(t!("New Release Available"))
.size(20)
.style(primary_style),
text(t!(
"A newer Open CAD Studio version is published on GitHub."
))
.size(11)
.style(muted_style),
]
.spacing(4)
.align_x(iced::Center),
@@ -161,9 +168,9 @@ pub fn view_window<'a>(
20.0,
))
.width(iced::Length::Fixed(32.0))
.height(sizing.height)
.align_x(iced::Center)
.align_y(iced::Center);
.height(sizing.height)
.align_x(iced::Center)
.align_y(iced::Center);
let info_block = row![installed, arrow, latest_card]
.spacing(0)
.align_y(iced::Center)
@@ -192,8 +199,8 @@ pub fn view_window<'a>(
// Release notes panel. Rendered as a light-markdown column inside a
// bordered scrollable so long bodies stay contained and don't
// explode the window. Empty body → "No release notes provided."
let notes_heading = container(text(t!("What's new")).size(11).style(muted_style))
.padding(iced::Padding {
let notes_heading =
container(text(t!("What's new")).size(11).style(muted_style)).padding(iced::Padding {
top: 10.0,
right: 0.0,
bottom: 4.0,
@@ -238,9 +245,7 @@ pub fn view_window<'a>(
}),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(sizing.width)
+28 -12
View File
@@ -3,9 +3,7 @@
use std::fmt;
use codec::Handle;
use iced::widget::{
button, column, combo_box, container, pick_list, row, text, text_input, Space,
};
use iced::widget::{button, column, combo_box, container, pick_list, row, text, text_input, Space};
use iced::{Background, Border, Element, Fill, Length, Theme};
use crate::app::Message;
@@ -294,27 +292,37 @@ pub fn view_window<'a>(
qsel_btn = qsel_btn.style(button_style(false));
}
let obj_btns_row = row![sel_obj_btn, qsel_btn]
.spacing(5)
.align_y(iced::Center);
let obj_btns_row = row![sel_obj_btn, qsel_btn].spacing(5).align_y(iced::Center);
let object_radios = column![
form_radio(
t!("Retain"),
WblockObjectMode::Retain,
if is_objects { Some(state.object_mode) } else { None },
if is_objects {
Some(state.object_mode)
} else {
None
},
Message::WblockObjectMode,
),
form_radio(
t!("Convert to block"),
WblockObjectMode::Convert,
if is_objects { Some(state.object_mode) } else { None },
if is_objects {
Some(state.object_mode)
} else {
None
},
Message::WblockObjectMode,
),
form_radio(
t!("Delete from drawing"),
WblockObjectMode::Delete,
if is_objects { Some(state.object_mode) } else { None },
if is_objects {
Some(state.object_mode)
} else {
None
},
Message::WblockObjectMode,
),
]
@@ -348,8 +356,14 @@ pub fn view_window<'a>(
let objects_subgroup = group(
t!("Objects").into_owned(),
column![obj_btns_row, Space::new().height(2), object_radios, Space::new().height(2), status_row,]
.spacing(2),
column![
obj_btns_row,
Space::new().height(2),
object_radios,
Space::new().height(2),
status_row,
]
.spacing(2),
Length::Fixed(142.0),
);
@@ -389,7 +403,9 @@ pub fn view_window<'a>(
.style(button::secondary)
.padding([3, 0]);
let path_row = row![file_input, browse_btn].spacing(5).align_y(iced::Center);
let path_row = row![file_input, browse_btn]
.spacing(5)
.align_y(iced::Center);
let unit_options: Vec<UnitChoice> = units::all()
.map(|(code, label)| UnitChoice { code, label })
+14 -15
View File
@@ -152,7 +152,10 @@ fn number_field<'a>(
field = field.on_input(on_input);
}
row![
text(label).size(11).style(muted_style).width(Length::Fixed(16.0)),
text(label)
.size(11)
.style(muted_style)
.width(Length::Fixed(16.0)),
field
]
.spacing(6)
@@ -236,18 +239,12 @@ pub fn view_window<'a>(
let reference_type = group(
t!("Reference Type").into_owned(),
column![
form_radio(
t!("Attachment"),
false,
Some(state.overlay),
|v| msg(XrefAttachMsg::Overlay(v)),
),
form_radio(
t!("Overlay"),
true,
Some(state.overlay),
|v| msg(XrefAttachMsg::Overlay(v)),
),
form_radio(t!("Attachment"), false, Some(state.overlay), |v| msg(
XrefAttachMsg::Overlay(v)
),),
form_radio(t!("Overlay"), true, Some(state.overlay), |v| msg(
XrefAttachMsg::Overlay(v)
),),
]
.spacing(6),
Length::Shrink,
@@ -313,8 +310,10 @@ pub fn view_window<'a>(
// ── Path type, rotation, block unit ─────────────────────────────────
let path_type = group(
t!("Path type").into_owned(),
pick_list(Some(state.path_type), PathTypeChoice::ALL, |choice| choice.to_string())
.on_select(|v| msg(XrefAttachMsg::PathType(v)))
pick_list(Some(state.path_type), PathTypeChoice::ALL, |choice| {
choice.to_string()
})
.on_select(|v| msg(XrefAttachMsg::PathType(v)))
.text_size(11)
.padding([3, 6])
.width(Fill),
+9 -16
View File
@@ -18,9 +18,8 @@ pub fn view_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Me
// Fixed window width: paragraphs wrap against this instead of the
// toolbar, which is what made the dropdown unreadable.
let body_w = Length::Fixed(460.0);
let para = |label: String| -> Element<'static, Message> {
text(label).size(12).width(Fill).into()
};
let para =
|label: String| -> Element<'static, Message> { text(label).size(12).width(Fill).into() };
let intro = para(crate::t!("How the Reference Manager works on web").into_owned());
let list = para(
@@ -48,18 +47,14 @@ pub fn view_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Me
true,
);
let close_button = dialog_button(crate::t!("Close"), Message::CloseModal, false);
let action_row: Element<'static, Message> = row![
Space::new().width(Fill),
report_button,
close_button,
]
.spacing(8)
.align_y(iced::Center)
.into();
let action_row: Element<'static, Message> =
row![Space::new().width(Fill), report_button, close_button,]
.spacing(8)
.align_y(iced::Center)
.into();
let content = column![
text(crate::t!("Reference Manager — Web").into_owned())
.size(14),
text(crate::t!("Reference Manager — Web").into_owned()).size(14),
text(crate::t!("EXTERNALREFERENCES on the web build").into_owned())
.size(11)
.style(muted_style),
@@ -78,9 +73,7 @@ pub fn view_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Me
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.into()
+197 -179
View File
@@ -7,10 +7,12 @@
use crate::app::Message;
use crate::io::xref::collect_entries_with_prev;
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
use crate::ui::ROW_H;
use crate::io::xref_model::{
normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry,
};
use crate::ui::style::common::muted_style;
use crate::ui::style::row_button::cell_button_style;
use crate::ui::ROW_H;
use codec::CadDocument;
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
use iced::Padding;
@@ -24,8 +26,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
/// read-only list renders everywhere.
const IS_WASM: bool = cfg!(target_arch = "wasm32");
/// Font size for table cells (mirrors `layers.rs`).
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
/// Fixed table height so the details pane below keeps stable space.
@@ -380,7 +380,11 @@ impl XrefManagerPanel {
// re-ranges from the original anchor (plain click moves it).
// Moving it every time collapsed the range and dropped rows.
let from = self.anchor.unwrap_or(index);
let (lo, hi) = if from <= index { (from, index) } else { (index, from) };
let (lo, hi) = if from <= index {
(from, index)
} else {
(index, from)
};
self.selected.clear();
self.selected
.extend((lo..=hi).filter(|i| *i < self.entries.len()));
@@ -444,8 +448,10 @@ impl XrefManagerPanel {
}
if let Some(img) = reference_preview(a) {
let (w, h) = (img.width(), img.height());
self.previews
.insert(key, iced::widget::image::Handle::from_rgba(w, h, img.into_raw()));
self.previews.insert(
key,
iced::widget::image::Handle::from_rgba(w, h, img.into_raw()),
);
}
}
@@ -508,13 +514,16 @@ impl XrefManagerPanel {
};
if !self.tree {
let mut rows = vec![host];
rows.extend(self.entries.iter().enumerate().map(|(index, _)| {
DisplayRow {
index,
depth: 0,
is_nested: self.nested.contains(&index),
}
}));
rows.extend(
self.entries
.iter()
.enumerate()
.map(|(index, _)| DisplayRow {
index,
depth: 0,
is_nested: self.nested.contains(&index),
}),
);
return rows;
}
fn append_tree(
@@ -524,7 +533,9 @@ impl XrefManagerPanel {
seen: &mut HashSet<(String, String)>,
rows: &mut Vec<DisplayRow>,
) {
let Some(entry) = panel.entries.get(index) else { return; };
let Some(entry) = panel.entries.get(index) else {
return;
};
if !claim_path(seen, &entry.saved_path, &entry.name) {
return;
}
@@ -547,10 +558,7 @@ impl XrefManagerPanel {
let mut seen: HashSet<(String, String)> = HashSet::new();
// Roots first, then recursively expanded descendants.
for (index, _) in self.entries.iter().enumerate() {
let dominated = self
.children
.values()
.any(|kids| kids.contains(&index));
let dominated = self.children.values().any(|kids| kids.contains(&index));
if self.nested.contains(&index) && dominated {
continue;
}
@@ -589,7 +597,11 @@ impl XrefManagerPanel {
use crate::ui::dock::PanelId;
let title_bar = crate::ui::dock::title_bar(
PanelId::ExternalReferences,
format!("{} ({})", crate::t!("External References").as_ref(), self.entries.len()),
format!(
"{} ({})",
crate::t!("External References").as_ref(),
self.entries.len()
),
auto_collapse,
);
// Table content width: column widths plus gutters, stretched to the
@@ -600,7 +612,10 @@ impl XrefManagerPanel {
// The main button runs the default; the triangle opens the rest in an
// overlay menu. Formats without an attach command in this build
// (DWF/DGN/point clouds/coordination models) are omitted, not dead.
let web_tip = crate::t!("File attach is not available on web — the reference list below is read-only.").into_owned();
let web_tip = crate::t!(
"File attach is not available on web — the reference list below is read-only."
)
.into_owned();
let attach = if IS_WASM {
toolbar_tip(crate::t!("Attach DWG").into_owned(), web_tip.clone(), false)
} else {
@@ -703,13 +718,14 @@ impl XrefManagerPanel {
.enumerate()
.any(|(i, _)| self.selected.contains(&i) && !self.nested.contains(&i));
let single_direct_anchor = self.anchor.is_some_and(|a| {
self.selected.len() == 1
&& self.entries.get(a).is_some()
&& !self.nested.contains(&a)
self.selected.len() == 1 && self.entries.get(a).is_some() && !self.nested.contains(&a)
});
let host_saved = !self.host_path.is_empty();
let web_readonly: Option<String> = IS_WASM.then(|| {
crate::t!("Reference changes are not available on web — the reference list is read-only.").into_owned()
crate::t!(
"Reference changes are not available on web — the reference list is read-only."
)
.into_owned()
});
let path_group_gate: Option<String> = web_readonly.clone().or_else(|| {
if has_direct {
@@ -789,19 +805,12 @@ impl XrefManagerPanel {
false,
);
let toolbar = container(
row![
attach,
refresh,
change_path,
help
]
.spacing(4)
.align_y(iced::Center),
row![attach, refresh, change_path, help]
.spacing(4)
.align_y(iced::Center),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color,
)),
background: Some(Background::Color(theme.palette().background.weakest.color)),
..Default::default()
})
.width(Fill)
@@ -860,20 +869,20 @@ impl XrefManagerPanel {
}
let col_header: Element<'_, Message> = mouse_area(
container(header_row.width(Length::Fixed(table_w)))
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
}
})
.padding([4, 8])
.width(Fill),
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
}
})
.padding([4, 8])
.width(Fill),
)
.on_move(|p| Message::XrefColMove(p))
.on_release(Message::XrefColRelease)
@@ -951,7 +960,9 @@ impl XrefManagerPanel {
self.row_change_path_open,
));
}
scrollable(tree_col).height(Length::Fixed(self.table_h)).into()
scrollable(tree_col)
.height(Length::Fixed(self.table_h))
.into()
} else {
scrollable(
column![
@@ -971,9 +982,7 @@ impl XrefManagerPanel {
let table_rows: Element<'_, Message> = container(table_rows)
.padding(4)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weakest.color,
)),
background: Some(Background::Color(theme.palette().background.weakest.color)),
..Default::default()
})
.width(Fill)
@@ -1029,13 +1038,13 @@ impl XrefManagerPanel {
});
let preview_body: Element<'_, Message> = match single {
Some(e) => {
let cached = e.found_at.as_deref().and_then(|found| {
self.previews.get(&(e.key, found.to_string()))
});
let cached = e
.found_at
.as_deref()
.and_then(|found| self.previews.get(&(e.key, found.to_string())));
match cached {
Some(handle) => container(
iced::widget::image(handle.clone())
.width(Length::Fixed(220.0)),
iced::widget::image(handle.clone()).width(Length::Fixed(220.0)),
)
.center_x(Fill)
.center_y(Fill)
@@ -1045,7 +1054,9 @@ impl XrefManagerPanel {
None => container(
column![
text(e.name.as_str()).size(11),
text(crate::t!("Preview not available")).size(11).style(muted_style),
text(crate::t!("Preview not available"))
.size(11)
.style(muted_style),
]
.spacing(4)
.align_x(iced::Center),
@@ -1063,9 +1074,7 @@ impl XrefManagerPanel {
.width(Fill)
.height(Length::Fixed(120.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.weak.color,
)),
background: Some(Background::Color(theme.palette().background.weak.color)),
..Default::default()
})
.into(),
@@ -1075,8 +1084,7 @@ impl XrefManagerPanel {
.width(Fill)
.into()
} else {
let details =
details_pane(self.anchor.and_then(|i| self.entries.get(i)), doc);
let details = details_pane(self.anchor.and_then(|i| self.entries.get(i)), doc);
container(column![pane_tabs, details].spacing(2))
.padding([6, 8])
.width(Fill)
@@ -1108,9 +1116,7 @@ impl XrefManagerPanel {
// setting — and resize — actually takes effect.
container(content)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
..Default::default()
})
.width(Length::Fixed(width))
@@ -1240,7 +1246,11 @@ fn type_text(entry: &ReferenceEntry) -> std::borrow::Cow<'static, str> {
.extension()
.map(|e| e.to_string_lossy().to_ascii_uppercase())
.unwrap_or_default();
if ext == "RCS" { "RCS".into() } else { "RCP".into() }
if ext == "RCS" {
"RCS".into()
} else {
"RCP".into()
}
}
}
}
@@ -1300,9 +1310,7 @@ fn toolbar_btn(label: String, msg: Option<Message>, fill: bool) -> Element<'stat
.style(|theme: &Theme, status| {
let palette = theme.palette();
let pair = match status {
button::Status::Hovered | button::Status::Pressed => {
palette.background.strong
}
button::Status::Hovered | button::Status::Pressed => palette.background.strong,
_ => palette.background.weak,
};
button::Style {
@@ -1329,18 +1337,13 @@ fn toolbar_btn(label: String, msg: Option<Message>, fill: bool) -> Element<'stat
/// Disabled toolbar affordance with an explanatory tooltip (gated
/// operations, nested-selection blocks, or web-gated mutations).
fn toolbar_tip(label: String, tip: String, fill: bool) -> Element<'static, Message> {
let b = button(text(label).size(11).style(|theme: &Theme| {
iced::widget::text::Style {
color: Some(
theme
.palette()
.background
.base
.text
.scale_alpha(0.42),
),
}
}))
let b = button(
text(label)
.size(11)
.style(|theme: &Theme| iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.42)),
}),
)
.style(|theme: &Theme, _| {
let palette = theme.palette();
button::Style {
@@ -1385,9 +1388,7 @@ fn split_button(
move |theme: &Theme, status| {
let palette = theme.palette();
let pair = match status {
button::Status::Hovered | button::Status::Pressed => {
palette.background.strong
}
button::Status::Hovered | button::Status::Pressed => palette.background.strong,
_ => palette.background.weak,
};
let radius = if left {
@@ -1435,16 +1436,16 @@ fn split_button(
// where they meet.
let head = row![main, caret].spacing(-1.0).align_y(iced::Center);
let popup: Element<'static, Message> = container(
column(items.into_iter().map(|item| {
container(item).width(Fill).into()
}))
column(
items
.into_iter()
.map(|item| container(item).width(Fill).into()),
)
.spacing(2)
.padding(4),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
@@ -1463,7 +1464,11 @@ fn split_button(
/// One dropdown-menu row: a full-width button when the option can execute,
/// otherwise greyed text carrying the reason.
fn menu_item(label: String, msg: Option<Message>, gate: Option<String>) -> Element<'static, Message> {
fn menu_item(
label: String,
msg: Option<Message>,
gate: Option<String>,
) -> Element<'static, Message> {
match (msg, gate) {
(Some(m), None) => toolbar_btn(label, Some(m), true),
(_, reason) => toolbar_tip(label, reason.unwrap_or_default(), true),
@@ -1543,9 +1548,13 @@ impl<'a> iced_core::Widget<Message, Theme, iced::Renderer> for RightClickArea<'a
viewport: &iced::Rectangle,
renderer: &iced::Renderer,
) -> iced_core::mouse::Interaction {
self.child
.as_widget()
.mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
self.child.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn operate(
@@ -1680,7 +1689,9 @@ fn row_menu_for(id: XrefRowId, change_path_open: bool) -> Element<'static, Messa
let change_path_row: Element<'static, Message> = mouse_area(
container(
row![
text(crate::t!("Change Path Type").into_owned()).size(12).width(Fill),
text(crate::t!("Change Path Type").into_owned())
.size(12)
.width(Fill),
crate::ui::icons::themed_arrow_right(10.0),
]
.align_y(iced::Center)
@@ -1732,9 +1743,7 @@ fn row_menu_for(id: XrefRowId, change_path_open: bool) -> Element<'static, Messa
.padding(MENU_PADDING),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
@@ -1751,7 +1760,8 @@ fn row_menu_for(id: XrefRowId, change_path_open: bool) -> Element<'static, Messa
// Top offset so the flyout top border aligns directly with the "Change Path Type" row.
// 5 items * 24.0 + 5 * 1.0 (spacing) + 5.0 (separator) + 1.0 (spacing) + 4.0 (container padding) = 135.0
let flyout_offset = MENU_PADDING + 5.0 * MENU_ROW_H + 5.0 * MENU_SPACING + MENU_SEP_H + MENU_SPACING;
let flyout_offset =
MENU_PADDING + 5.0 * MENU_ROW_H + 5.0 * MENU_SPACING + MENU_SEP_H + MENU_SPACING;
// Plain container on purpose: each submenu row tracks hover itself
// (`on_enter` keeps the flyout open, `on_exit` closes it). Wrapping the
@@ -1759,23 +1769,15 @@ fn row_menu_for(id: XrefRowId, change_path_open: bool) -> Element<'static, Messa
// break click delivery inside the `ContextMenu` overlay — see `menu_row`.
let flyout: Element<'static, Message> = container(
column![
pathtype_item(
&crate::t!("Make Absolute").into_owned(),
Pathtype::Full
),
pathtype_item(
&crate::t!("Make Relative").into_owned(),
Pathtype::Relative
),
pathtype_item(&crate::t!("Make Absolute").into_owned(), Pathtype::Full),
pathtype_item(&crate::t!("Make Relative").into_owned(), Pathtype::Relative),
pathtype_item(&crate::t!("Remove Path").into_owned(), Pathtype::None),
]
.spacing(MENU_SPACING)
.padding(MENU_PADDING),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
background: Some(Background::Color(theme.palette().background.base.color)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
@@ -1813,9 +1815,8 @@ fn xref_row<'a>(
}
let mut name_cell = row![].spacing(2).align_y(iced::Center);
if display.depth > 0 {
name_cell = name_cell.push(
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
);
name_cell = name_cell
.push(iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)));
}
// Tree parents own the only interactive cell besides selection: the
// expand/collapse arrow. Nested rows never show one.
@@ -1848,13 +1849,21 @@ fn xref_row<'a>(
gutter(),
status_cell,
gutter(),
text(format_size(entry.size_bytes)).size(FONT_SZ).width(Length::Fixed(cw[2])),
text(format_size(entry.size_bytes))
.size(FONT_SZ)
.width(Length::Fixed(cw[2])),
gutter(),
text(type_text(entry)).size(FONT_SZ).width(Length::Fixed(cw[3])),
text(type_text(entry))
.size(FONT_SZ)
.width(Length::Fixed(cw[3])),
gutter(),
text(format_date(entry.modified)).size(FONT_SZ).width(Length::Fixed(cw[4])),
text(format_date(entry.modified))
.size(FONT_SZ)
.width(Length::Fixed(cw[4])),
gutter(),
text(entry.saved_path.clone()).size(FONT_SZ).width(Length::Fixed(cw[5])),
text(entry.saved_path.clone())
.size(FONT_SZ)
.width(Length::Fixed(cw[5])),
]
.spacing(0)
.width(Length::Fixed(table_w))
@@ -1923,8 +1932,11 @@ fn tree_row(
cells = cells.push(arrow);
}
cells = cells.push(
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 14.0))
.align_y(iced::Center),
container(crate::ui::icons::themed_secondary(
crate::ui::icons::DOC,
14.0,
))
.align_y(iced::Center),
);
cells = cells.push(text(entry.name.clone()).size(TREE_FONT_SZ));
let index = display.index;
@@ -1974,9 +1986,7 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
name.push('*');
let mut cells = row![].spacing(2).align_y(iced::Center);
cells = cells.push(tree_indent(display.depth));
cells = cells.push(
container(crate::ui::icons::themed_home(14.0)).align_y(iced::Center),
);
cells = cells.push(container(crate::ui::icons::themed_home(14.0)).align_y(iced::Center));
cells = cells.push(text(name).size(TREE_FONT_SZ));
container(cells.spacing(4).width(Fill))
.style(|theme: &Theme| {
@@ -1996,18 +2006,27 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
.width(Fill)
.into()
}
fn host_row<'a>(display: DisplayRow, host_name: &'a str, host_path: &'a str, table_w: f32, cw: &[f32; 6]) -> Element<'a, Message> {
fn host_row<'a>(
display: DisplayRow,
host_name: &'a str,
host_path: &'a str,
table_w: f32,
cw: &[f32; 6],
) -> Element<'a, Message> {
let name = if host_name.is_empty() {
crate::t!("Untitled").into_owned()
} else {
host_name.to_string()
};
let saved = if host_path.is_empty() { "—" } else { host_path };
let saved = if host_path.is_empty() {
"—"
} else {
host_path
};
let mut name_cell = row![].spacing(2).align_y(iced::Center);
if display.depth > 0 {
name_cell = name_cell.push(
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
);
name_cell = name_cell
.push(iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)));
}
name_cell = name_cell.push({
let name_text: Element<'_, Message> = text(name).size(FONT_SZ).into();
@@ -2017,11 +2036,15 @@ fn host_row<'a>(display: DisplayRow, host_name: &'a str, host_path: &'a str, tab
let content = row![
name_cell.width(Length::Fixed(cw[0])),
gutter(),
text(crate::t!("Opened")).size(FONT_SZ).width(Length::Fixed(cw[1])),
text(crate::t!("Opened"))
.size(FONT_SZ)
.width(Length::Fixed(cw[1])),
gutter(),
text("—").size(FONT_SZ).width(Length::Fixed(cw[2])),
gutter(),
text(crate::t!("Current")).size(FONT_SZ).width(Length::Fixed(cw[3])),
text(crate::t!("Current"))
.size(FONT_SZ)
.width(Length::Fixed(cw[3])),
gutter(),
text("—").size(FONT_SZ).width(Length::Fixed(cw[4])),
gutter(),
@@ -2050,7 +2073,10 @@ fn host_row<'a>(display: DisplayRow, host_name: &'a str, host_path: &'a str, tab
.into()
}
fn details_pane<'a>(entry: Option<&'a ReferenceEntry>, doc: &'a CadDocument) -> Element<'a, Message> {
fn details_pane<'a>(
entry: Option<&'a ReferenceEntry>,
doc: &'a CadDocument,
) -> Element<'a, Message> {
let inner: Element<'_, Message> = match entry {
None => text(crate::t!("Select a reference to inspect its details"))
.size(11)
@@ -2096,14 +2122,8 @@ fn details_pane<'a>(entry: Option<&'a ReferenceEntry>, doc: &'a CadDocument) ->
}
if let Some((w, h)) = find_image_display_size(doc, e.key) {
rows = rows
.push(detail_row(
crate::t!("Display Width"),
format!("{:.2}", w),
))
.push(detail_row(
crate::t!("Display Height"),
format!("{:.2}", h),
));
.push(detail_row(crate::t!("Display Width"), format!("{:.2}", w)))
.push(detail_row(crate::t!("Display Height"), format!("{:.2}", h)));
}
}
rows.into()
@@ -2169,14 +2189,13 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
/// Image definition backing a [`RefKind::Image`] entry, looked up by the
/// entry key (definition-object handle) for the Details pane extras.
fn find_image_def(
doc: &CadDocument,
key: u64,
) -> Option<&codec::objects::ImageDefinition> {
doc.objects.get(&codec::types::Handle::from(key)).and_then(|o| match o {
codec::objects::ObjectType::ImageDefinition(def) => Some(def),
_ => None,
})
fn find_image_def(doc: &CadDocument, key: u64) -> Option<&codec::objects::ImageDefinition> {
doc.objects
.get(&codec::types::Handle::from(key))
.and_then(|o| match o {
codec::objects::ObjectType::ImageDefinition(def) => Some(def),
_ => None,
})
}
fn find_image_display_size(doc: &CadDocument, key: u64) -> Option<(f64, f64)> {
@@ -2184,8 +2203,10 @@ fn find_image_display_size(doc: &CadDocument, key: u64) -> Option<(f64, f64)> {
for entity in doc.entities() {
if let codec::EntityType::RasterImage(img) = entity {
if img.definition_handle == Some(handle) {
let w = (img.u_vector.x.powi(2) + img.u_vector.y.powi(2) + img.u_vector.z.powi(2)).sqrt();
let h = (img.v_vector.x.powi(2) + img.v_vector.y.powi(2) + img.v_vector.z.powi(2)).sqrt();
let w = (img.u_vector.x.powi(2) + img.u_vector.y.powi(2) + img.u_vector.z.powi(2))
.sqrt();
let h = (img.v_vector.x.powi(2) + img.v_vector.y.powi(2) + img.v_vector.z.powi(2))
.sqrt();
return Some((w, h));
}
}
@@ -2200,7 +2221,10 @@ fn detail_row<'a>(
let label: std::borrow::Cow<'a, str> = label.into();
let value: std::borrow::Cow<'a, str> = value.into();
row![
text(label).size(11).style(muted_style).width(Length::Fixed(84.0)),
text(label)
.size(11)
.style(muted_style)
.width(Length::Fixed(84.0)),
text(value).size(11),
]
.spacing(6)
@@ -2226,7 +2250,9 @@ mod tests {
fn date_formats_known_days() {
assert_eq!(format_date(None), "—");
let day = |days: u64| {
format_date(Some(UNIX_EPOCH + std::time::Duration::from_secs(days * 86_400)))
format_date(Some(
UNIX_EPOCH + std::time::Duration::from_secs(days * 86_400),
))
};
assert_eq!(day(0), "1970-01-01");
assert_eq!(day(10957), "2000-01-01");
@@ -2283,11 +2309,7 @@ mod tests {
panel.entries.push(entry(5, "C", "c.dwg"));
panel.children.insert(5, vec![2]);
panel.expanded.insert(5);
let count = panel
.display_rows()
.iter()
.filter(|r| r.index == 2)
.count();
let count = panel.display_rows().iter().filter(|r| r.index == 2).count();
assert_eq!(count, 1);
}
@@ -2346,7 +2368,10 @@ mod tests {
panel.drag_col_by(2, 10000.0);
assert_eq!(panel.col_widths[2], super::COL_MAX_W);
panel.drag_col_by(99, 10.0); // no-op, no panic
assert_eq!(panel.table_content_width(), panel.col_widths.iter().sum::<f32>() + super::COL_GUTTER * 5.0);
assert_eq!(
panel.table_content_width(),
panel.col_widths.iter().sum::<f32>() + super::COL_GUTTER * 5.0
);
}
#[test]
@@ -2476,12 +2501,8 @@ mod tests {
let mut def = ImageDefinition::with_dimensions(img_path.clone(), 8, 8);
def.handle = h;
doc.objects.insert(h, ObjectType::ImageDefinition(def));
let mut ent = codec::entities::RasterImage::new(
&img_path,
codec::types::Vector3::ZERO,
8.0,
8.0,
);
let mut ent =
codec::entities::RasterImage::new(&img_path, codec::types::Vector3::ZERO, 8.0, 8.0);
ent.definition_handle = Some(h);
doc.add_entity(codec::EntityType::RasterImage(ent)).unwrap();
doc
@@ -2623,15 +2644,11 @@ mod tests {
/// user right-clicking a reference row.
fn open_row_menu(change_path_open: bool) -> MenuSimulator {
let row_id = XrefRowId::of(0, "");
let mut ui = iced_test::simulator(iced_aw::ContextMenu::new(
text("anchor"),
move || row_menu_for(row_id.clone(), change_path_open),
));
let mut ui = iced_test::simulator(iced_aw::ContextMenu::new(text("anchor"), move || {
row_menu_for(row_id.clone(), change_path_open)
}));
let anchor = ui.find("anchor").expect("anchor underlay present");
let center = anchor
.visible_bounds()
.expect("anchor visible")
.center();
let center = anchor.visible_bounds().expect("anchor visible").center();
ui.point_at(center);
ui.simulate([iced_core::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Right,
@@ -2669,10 +2686,7 @@ mod tests {
let target = ui
.find(label.as_str())
.expect("Change Path Type row present in overlay");
let center = target
.visible_bounds()
.expect("row visible")
.center();
let center = target.visible_bounds().expect("row visible").center();
ui.point_at(center);
ui.simulate([iced_core::Event::Mouse(iced::mouse::Event::CursorMoved {
position: center,
@@ -2696,7 +2710,10 @@ mod tests {
ui.click(make_absolute.as_str())
.expect("flyout row is clickable inside the overlay");
assert!(
published_ops(ui).contains(&(XrefRowId::of(0, ""), XrefPaletteOp::Pathtype(Pathtype::Full))),
published_ops(ui).contains(&(
XrefRowId::of(0, ""),
XrefPaletteOp::Pathtype(Pathtype::Full)
)),
"clicking Make Absolute must publish the Full-pathtype op"
);
}
@@ -2724,7 +2741,8 @@ mod tests {
// buttons again.
use iced_core::Widget;
let mut menu = iced_aw::ContextMenu::new(text("anchor"), || row_menu_for(XrefRowId::of(0, ""), false));
let mut menu =
iced_aw::ContextMenu::new(text("anchor"), || row_menu_for(XrefRowId::of(0, ""), false));
let mut tree = iced_core::widget::Tree::new(
&menu as &dyn Widget<Message, iced::Theme, iced::Renderer>,
);