//! 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, selected: Option, 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 = (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 = 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 = 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 = 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 }