318 lines
9.9 KiB
Rust
318 lines
9.9 KiB
Rust
|
|
//! Drawing Units — how this drawing writes lengths and angles, and what one of
|
|||
|
|
//! its units measures.
|
|||
|
|
//!
|
|||
|
|
//! Three separate settings that the word "units" runs together, kept in one
|
|||
|
|
//! place because changing one usually means looking at the others. Length and
|
|||
|
|
//! angle formats decide how numbers are *written*; the insertion unit decides
|
|||
|
|
//! what they *count*, and only matters when content arrives from elsewhere.
|
|||
|
|
//! Nothing here moves geometry — DWGUNITS does that.
|
|||
|
|
//!
|
|||
|
|
//! The sample updates as the fields change, so a format is chosen by seeing it
|
|||
|
|
//! rather than by knowing what its name implies. (#668)
|
|||
|
|
|
|||
|
|
use std::fmt;
|
|||
|
|
|
|||
|
|
use iced::widget::{button, checkbox, column, container, row, text, text_input, Space};
|
|||
|
|
use iced::{Border, Element, Fill, Length, Theme};
|
|||
|
|
|
|||
|
|
use crate::app::Message;
|
|||
|
|
use crate::modules::draw::units;
|
|||
|
|
use crate::t;
|
|||
|
|
|
|||
|
|
/// The dialog's working copy. Nothing reaches the drawing until OK, so a format
|
|||
|
|
/// can be tried against the sample and abandoned.
|
|||
|
|
#[derive(Clone, PartialEq)]
|
|||
|
|
pub struct State {
|
|||
|
|
/// LUNITS / LUPREC.
|
|||
|
|
pub linear_format: i16,
|
|||
|
|
pub linear_precision: i16,
|
|||
|
|
/// AUNITS / AUPREC.
|
|||
|
|
pub angular_format: i16,
|
|||
|
|
pub angular_precision: i16,
|
|||
|
|
/// ANGDIR — positive angles run clockwise.
|
|||
|
|
pub clockwise: bool,
|
|||
|
|
/// ANGBASE — the direction angle zero points, in degrees. Held as text so
|
|||
|
|
/// a half-typed number does not snap back while it is being typed.
|
|||
|
|
pub base_angle: String,
|
|||
|
|
/// INSUNITS.
|
|||
|
|
pub insertion_units: i16,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A dropdown entry that shows a translated name but hands back a raw code.
|
|||
|
|
#[derive(Clone, PartialEq, Eq)]
|
|||
|
|
pub struct Choice {
|
|||
|
|
pub code: i16,
|
|||
|
|
label: &'static str,
|
|||
|
|
sample: &'static str,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl fmt::Display for Choice {
|
|||
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||
|
|
let name = crate::i18n::translate(self.label);
|
|||
|
|
if self.sample.is_empty() {
|
|||
|
|
f.write_str(name.as_ref())
|
|||
|
|
} else {
|
|||
|
|
write!(f, "{name} · {}", self.sample)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Which field a dialog message is for.
|
|||
|
|
#[derive(Clone, Debug)]
|
|||
|
|
pub enum Field {
|
|||
|
|
LinearFormat(i16),
|
|||
|
|
LinearPrecision(i16),
|
|||
|
|
AngularFormat(i16),
|
|||
|
|
AngularPrecision(i16),
|
|||
|
|
Clockwise(bool),
|
|||
|
|
BaseAngle(String),
|
|||
|
|
InsertionUnits(i16),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn muted(theme: &Theme) -> iced::widget::text::Style {
|
|||
|
|
iced::widget::text::Style {
|
|||
|
|
color: Some(theme.palette().background.base.text.scale_alpha(0.6)),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn group<'a>(title: String, body: Element<'a, Message>) -> Element<'a, Message> {
|
|||
|
|
container(column![text(title).size(11).style(muted), body].spacing(6))
|
|||
|
|
.padding(8)
|
|||
|
|
.width(Fill)
|
|||
|
|
.style(|theme: &Theme| container::Style {
|
|||
|
|
border: Border {
|
|||
|
|
width: 1.0,
|
|||
|
|
radius: 4.0.into(),
|
|||
|
|
color: theme.palette().background.strong.color,
|
|||
|
|
},
|
|||
|
|
..Default::default()
|
|||
|
|
})
|
|||
|
|
.into()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn drop_row<'a>(
|
|||
|
|
label: String,
|
|||
|
|
options: Vec<Choice>,
|
|||
|
|
selected: Option<Choice>,
|
|||
|
|
ctor: fn(i16) -> Field,
|
|||
|
|
) -> Element<'a, Message> {
|
|||
|
|
let picker = iced::widget::pick_list(selected, options, |choice| choice.to_string())
|
|||
|
|
.on_select(move |choice| Message::DrawingUnitsField(ctor(choice.code)))
|
|||
|
|
.text_size(12)
|
|||
|
|
.padding([3, 6])
|
|||
|
|
.width(Fill);
|
|||
|
|
row![text(label).size(11).style(muted).width(78), picker]
|
|||
|
|
.spacing(8)
|
|||
|
|
.align_y(iced::Center)
|
|||
|
|
.into()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Precision is 0–8 places everywhere it applies; fractional and architectural
|
|||
|
|
/// read the same number as a denominator power, which is why it is offered as
|
|||
|
|
/// a plain count rather than as decimals.
|
|||
|
|
fn precision_row<'a>(
|
|||
|
|
current: i16,
|
|||
|
|
fractional: bool,
|
|||
|
|
ctor: fn(i16) -> Field,
|
|||
|
|
) -> Element<'a, Message> {
|
|||
|
|
let options: Vec<Choice> = (0..=8)
|
|||
|
|
.map(|n| Choice {
|
|||
|
|
code: n,
|
|||
|
|
label: PRECISION_LABELS[n as usize],
|
|||
|
|
sample: if fractional {
|
|||
|
|
FRACTION_SAMPLES[n as usize]
|
|||
|
|
} else {
|
|||
|
|
""
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let selected = options
|
|||
|
|
.iter()
|
|||
|
|
.find(|choice| choice.code == current.clamp(0, 8))
|
|||
|
|
.cloned();
|
|||
|
|
drop_row(t!("Precision").into_owned(), options, selected, ctor)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const PRECISION_LABELS: [&str; 9] = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
|
|||
|
|
const FRACTION_SAMPLES: [&str; 9] = [
|
|||
|
|
"1", "1/2", "1/4", "1/8", "1/16", "1/32", "1/64", "1/128", "1/256",
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
pub fn view_window<'a>(
|
|||
|
|
state: &State,
|
|||
|
|
sizing: crate::ui::modal::ModalSizing,
|
|||
|
|
) -> Element<'a, Message> {
|
|||
|
|
let linear_options: Vec<Choice> = units::linear_formats()
|
|||
|
|
.map(|(code, label, sample)| Choice {
|
|||
|
|
code,
|
|||
|
|
label,
|
|||
|
|
sample,
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let linear_selected = linear_options
|
|||
|
|
.iter()
|
|||
|
|
.find(|choice| choice.code == state.linear_format)
|
|||
|
|
.cloned();
|
|||
|
|
// Architectural and fractional read precision as a fraction denominator,
|
|||
|
|
// so the list shows fractions instead of decimal places for those two.
|
|||
|
|
let fractional = matches!(state.linear_format, 4 | 5);
|
|||
|
|
|
|||
|
|
let angular_options: Vec<Choice> = units::angular_formats()
|
|||
|
|
.map(|(code, label, sample)| Choice {
|
|||
|
|
code,
|
|||
|
|
label,
|
|||
|
|
sample,
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let angular_selected = angular_options
|
|||
|
|
.iter()
|
|||
|
|
.find(|choice| choice.code == state.angular_format)
|
|||
|
|
.cloned();
|
|||
|
|
|
|||
|
|
let insertion_options: Vec<Choice> = units::all()
|
|||
|
|
.map(|(code, label)| Choice {
|
|||
|
|
code,
|
|||
|
|
label,
|
|||
|
|
sample: "",
|
|||
|
|
})
|
|||
|
|
.collect();
|
|||
|
|
let insertion_selected = insertion_options
|
|||
|
|
.iter()
|
|||
|
|
.find(|choice| choice.code == state.insertion_units)
|
|||
|
|
.cloned();
|
|||
|
|
|
|||
|
|
let length = group(
|
|||
|
|
t!("Length").into_owned(),
|
|||
|
|
column![
|
|||
|
|
drop_row(
|
|||
|
|
t!("Type").into_owned(),
|
|||
|
|
linear_options,
|
|||
|
|
linear_selected,
|
|||
|
|
Field::LinearFormat
|
|||
|
|
),
|
|||
|
|
precision_row(state.linear_precision, fractional, Field::LinearPrecision),
|
|||
|
|
]
|
|||
|
|
.spacing(6)
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let angle = group(
|
|||
|
|
t!("Angle").into_owned(),
|
|||
|
|
column![
|
|||
|
|
drop_row(
|
|||
|
|
t!("Type").into_owned(),
|
|||
|
|
angular_options,
|
|||
|
|
angular_selected,
|
|||
|
|
Field::AngularFormat
|
|||
|
|
),
|
|||
|
|
precision_row(state.angular_precision, false, Field::AngularPrecision),
|
|||
|
|
row![
|
|||
|
|
text(t!("Zero at")).size(11).style(muted).width(78),
|
|||
|
|
text_input("0", &state.base_angle)
|
|||
|
|
.on_input(|value| Message::DrawingUnitsField(Field::BaseAngle(value)))
|
|||
|
|
.size(12)
|
|||
|
|
.padding([3, 6])
|
|||
|
|
.width(Length::Fixed(80.0)),
|
|||
|
|
text(t!("degrees")).size(11).style(muted),
|
|||
|
|
]
|
|||
|
|
.spacing(8)
|
|||
|
|
.align_y(iced::Center),
|
|||
|
|
row![
|
|||
|
|
Space::new().width(Length::Fixed(78.0)),
|
|||
|
|
checkbox(state.clockwise)
|
|||
|
|
.on_toggle(|on| Message::DrawingUnitsField(Field::Clockwise(on)))
|
|||
|
|
.size(14),
|
|||
|
|
text(t!("Positive angles run clockwise")).size(11),
|
|||
|
|
]
|
|||
|
|
.spacing(6)
|
|||
|
|
.align_y(iced::Center),
|
|||
|
|
]
|
|||
|
|
.spacing(6)
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// What the insertion unit is for, said once here, because the setting is
|
|||
|
|
// invisible until content arrives from another drawing and is otherwise
|
|||
|
|
// easy to mistake for the one above it.
|
|||
|
|
let insertion = group(
|
|||
|
|
t!("Insertion scale").into_owned(),
|
|||
|
|
column![
|
|||
|
|
drop_row(
|
|||
|
|
t!("Unit").into_owned(),
|
|||
|
|
insertion_options,
|
|||
|
|
insertion_selected,
|
|||
|
|
Field::InsertionUnits
|
|||
|
|
),
|
|||
|
|
text(t!(
|
|||
|
|
"Scales blocks and drawings inserted from elsewhere. Unitless inserts them unscaled."
|
|||
|
|
))
|
|||
|
|
.size(10)
|
|||
|
|
.style(muted),
|
|||
|
|
]
|
|||
|
|
.spacing(6)
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let sample = group(
|
|||
|
|
t!("Sample output").into_owned(),
|
|||
|
|
column![
|
|||
|
|
text(sample_length(state)).size(12),
|
|||
|
|
text(sample_angle(state)).size(12),
|
|||
|
|
]
|
|||
|
|
.spacing(3)
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let actions = row![
|
|||
|
|
Space::new().width(Fill),
|
|||
|
|
button(text(t!("Cancel")).size(12))
|
|||
|
|
.on_press(Message::CloseModal)
|
|||
|
|
.padding([4, 12]),
|
|||
|
|
button(text(t!("OK")).size(12))
|
|||
|
|
.on_press(Message::DrawingUnitsApply)
|
|||
|
|
.style(button::primary)
|
|||
|
|
.padding([4, 12]),
|
|||
|
|
]
|
|||
|
|
.spacing(6)
|
|||
|
|
.align_y(iced::Center);
|
|||
|
|
|
|||
|
|
column![row![length, angle].spacing(8), insertion, sample, actions,]
|
|||
|
|
.spacing(8)
|
|||
|
|
.padding(10)
|
|||
|
|
.width(sizing.width)
|
|||
|
|
.into()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// The sample is produced by the same formatters the drawing uses, so what it
|
|||
|
|
/// shows is what the readout will show — not a second implementation that can
|
|||
|
|
/// drift from the first.
|
|||
|
|
fn sample_length(state: &State) -> String {
|
|||
|
|
with_context(state, || {
|
|||
|
|
crate::entities::common::format_length(1.5)
|
|||
|
|
+ " "
|
|||
|
|
+ &crate::entities::common::format_length(33.5)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn sample_angle(state: &State) -> String {
|
|||
|
|
with_context(state, || {
|
|||
|
|
crate::entities::common::format_angle(std::f64::consts::FRAC_PI_4)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Format under the dialog's pending settings, then put back whatever the rest
|
|||
|
|
/// of the frame is drawing with. Without the restore the sample would leak its
|
|||
|
|
/// settings into every widget drawn after it.
|
|||
|
|
fn with_context(state: &State, body: impl FnOnce() -> String) -> String {
|
|||
|
|
use crate::entities::common::{set_unit_context, unit_context, UnitContext};
|
|||
|
|
let previous = unit_context();
|
|||
|
|
set_unit_context(UnitContext {
|
|||
|
|
lunits: state.linear_format,
|
|||
|
|
luprec: state.linear_precision,
|
|||
|
|
aunits: state.angular_format,
|
|||
|
|
auprec: state.angular_precision,
|
|||
|
|
});
|
|||
|
|
let out = body();
|
|||
|
|
set_unit_context(previous);
|
|||
|
|
out
|
|||
|
|
}
|