Files
OneCAD-Draft/src/ui/window/options.rs
T

1982 lines
70 KiB
Rust

#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod graphics;
pub(crate) mod spacemouse;
use crate::app::config::UiThemeConfig;
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::{Background, Border, Element, Fill, Theme};
use std::fmt;
/// Width of the vertical tab rail. Wide enough for the longest translated
/// page name; German and Russian are the ones that set it.
const TAB_RAIL_WIDTH: f32 = 178.0;
/// The dialog's own size when the modal is laid out intrinsically.
const DIALOG_WIDTH: f32 = 880.0;
const DIALOG_HEIGHT: f32 = 620.0;
/// Which page of the Options dialog is showing.
///
/// 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)]
#[serde(rename_all = "kebab-case")]
pub enum OptionsTab {
#[default]
General,
Files,
OpenAndSave,
Display,
Drafting,
Modeling,
Selection,
UserPreferences,
Graphics,
}
/// Application preferences the dialog reads that are plain scalars on the app.
///
/// Gathered rather than passed one by one: `view_window` already carries a
/// long positional list, and several of these are adjacent `bool`s that would
/// swap silently at the call site.
#[derive(Clone, Copy)]
pub struct AppPrefs {
/// SAVETIME, minutes between recovery saves; 0 disables.
pub savetime_min: i32,
/// ISAVEBAK: keep a `.bak` when overwriting.
pub backup_on_save: bool,
/// TEXTFILL: fill TrueType glyphs rather than drawing them hollow.
pub textfill: bool,
/// CLIPROMPTLINES: prompt lines shown above the command window.
pub cliprompt_lines: i32,
/// COMMANDLINEFADETIME: how long overlay history lines stay visible, in ms.
pub commandline_fade_ms: i32,
/// COMMANDLINEOPACITY: command-line panel opacity in percent.
pub command_line_opacity: u8,
/// MENUBAR: the pull-down menus show above the ribbon.
pub show_menu_bar: bool,
/// The ribbon shows (RIBBON / RIBBONCLOSE).
pub show_ribbon: bool,
/// HPASSOC: new hatches follow their boundary objects.
pub hatch_associative: bool,
/// ZOOMWHEEL: reverse the mouse-wheel zoom direction.
pub zoom_wheel_reversed: bool,
/// Which mouse gestures pan and orbit the model view.
pub navigation_style: crate::app::settings::NavigationStyle,
/// Scroll pans and Ctrl+scroll zooms (touchpads).
pub touchpad_pan: bool,
/// Interface scale in percent.
pub ui_scale_percent: u16,
/// ZOOMFACTOR. The slider sets the range the system variable has; the
/// field beside it reaches `settings::ZOOM_FACTOR_MAX`.
pub zoom_factor: i32,
/// TEXTEDITMODE: TEXTEDIT keeps prompting for the next object.
pub texteditmode: bool,
/// DIMCONTINUEMODE: continued dimensions inherit the base dimension's style.
pub dimension_continue_mode: i16,
/// QDIM extension-origin priority: 0 endpoints, 1 intersections.
pub qdim_snap_priority: u8,
/// ANNOAUTOSCALE, -4..=4. The sign is on/off; the magnitude selects which
/// objects a newly added scale reaches.
pub annotation_auto_scale: i8,
/// Polar tracking increment in degrees.
pub polar_increment_deg: f32,
/// NAVVCUBE: show the navigation cube.
pub show_viewcube: bool,
/// UCSICON: show the UCS icon.
pub show_ucs_icon: bool,
/// UCSICON ORigin: draw it at the origin rather than the corner.
pub ucs_icon_at_origin: bool,
/// SHORTCUTMENU: what a right-click in the drawing area does.
pub right_click_mode: RightClickMode,
/// SHORTCUTMENUDURATION: time-sensitive hold threshold, ms.
pub right_click_hold_ms: i32,
/// Open the Plot / Page Setup dialog for every new layout.
pub page_setup_on_new_layout: bool,
}
/// A Files-page location the user may move elsewhere.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MovableFolder {
Fonts,
PlotStyles,
}
/// The locations the Files page lists.
///
/// `None` where the platform could not give one — the row then says so and
/// its button is disabled rather than opening nothing.
#[derive(Clone, Default)]
pub struct Folders {
pub config: Option<String>,
pub plot_styles: Option<String>,
pub plugins: Option<String>,
pub autosave: Option<String>,
/// Where `.shx` / `.ttf` fonts dropped in by the user (or downloaded for
/// a drawing) are found.
pub fonts: Option<String>,
/// Where `.acb` colour books are kept and loaded ones copied to.
pub color_books: Option<String>,
/// The fonts / plot styles folder was chosen in Options.
pub fonts_moved: bool,
pub plot_styles_moved: bool,
}
/// Values read from the current drawing's header rather than from preferences.
///
/// These are saved in the DWG, so they follow the drawing rather than the
/// application — the page labels them as such. `available` is false when no
/// drawing is open and the controls are not drawn at all.
#[derive(Clone, Copy)]
pub struct DrawingPrefs {
pub available: bool,
/// ISOLINES.
pub isolines: i16,
/// DISPSILH.
pub display_silhouette: bool,
/// SURFU / SURFV.
pub surface_u: i16,
pub surface_v: i16,
/// SURFTYPE: 5, 6 or 8.
pub surface_type: i16,
/// SOLIDHIST.
pub record_solid_history: bool,
/// SHOWHIST, 0..=2.
pub show_solid_history: i16,
}
/// The Selection-card settings that live on `UserSettings` rather than on the
/// model-space theme.
///
/// Gathered into one value instead of four more positional parameters: the
/// function already takes seventeen, and `pick_add` / `pick_drag_rect` are two
/// adjacent `bool`s that would swap silently at the call site.
#[derive(Clone, Copy)]
pub struct SelectionPrefs {
/// PICKBOX, 0..=50.
pub pick_box: i32,
/// PICKADD: true = a click adds to the selection.
pub pick_add: bool,
/// PICKDRAG: true = press-drag draws a rectangle instead of a lasso.
pub pick_drag_rect: bool,
/// GRIPOBJLIMIT, 0..=32767; 0 = no limit.
pub grip_object_limit: i32,
/// Selection cycling: a click where objects overlap opens a picker.
pub selection_cycling: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Labelled<T> {
value: T,
label: String,
}
impl<T> fmt::Display for Labelled<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.label)
}
}
/// A colour swatch that opens the Select Color panel when clicked.
///
/// The swatch sits beside the hex field rather than replacing it: typing
/// `#1E1E1E` stays the fastest way to reproduce an exact colour, while the
/// panel is for choosing one by eye.
fn color_swatch<'a>(target: crate::app::UiColor, [r, g, b]: [u8; 3]) -> Element<'a, Message> {
let swatch = container(Space::new())
.width(28)
.height(22)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(iced::Color::from_rgb8(r, g, b))),
border: Border {
color: theme.palette().background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
});
button(swatch)
.on_press(Message::OpenColorWindow(
crate::app::ColorPickTarget::Interface(target),
codec::types::Color::Rgb { r, g, b },
))
.padding(0)
.style(button::text)
.into()
}
#[allow(clippy::too_many_arguments)]
pub fn view_window<'a>(
default_save_format: &'a str,
file_assoc_enabled: bool,
show_constraint_values: bool,
ui_theme: &'a UiThemeConfig,
theme_color_inputs: &'a [String; 6],
language: crate::i18n::Language,
active_tab: OptionsTab,
cursor_size: i32,
selection: SelectionPrefs,
prefs: AppPrefs,
spacemouse: Element<'a, Message>,
graphics: Element<'a, Message>,
snap_angle_input: &'a str,
zoom_factor_input: &'a str,
drawing_prefs: DrawingPrefs,
folders: Folders,
qnew_template: &'a str,
double_click_block_refedit: bool,
double_click_block_attedit: bool,
cursor_type: CursorType,
crosshair_color: Option<[u8; 3]>,
crosshair_color_input: &'a str,
snap_marker_color: Option<[u8; 3]>,
snap_marker_color_input: &'a str,
command_text_color: Option<[u8; 3]>,
command_text_color_input: &'a str,
lineweight_display_scale: i32,
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
model_bg_input: &'a str,
paper_bg_input: &'a str,
desk_bg_input: &'a str,
dirty: bool,
close_confirm: bool,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let selected_format = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|candidate| *candidate == default_save_format);
let theme_options = crate::app::config::all_themes()
.iter()
.map(ToString::to_string)
.chain(std::iter::once("Custom".to_string()))
.map(|value| Labelled {
label: match value.as_str() {
"Light" => crate::t!("Light").into_owned(),
"Dark" => crate::t!("Dark").into_owned(),
"Custom" => crate::t!("Custom").into_owned(),
_ => value.clone(),
},
value,
})
.collect::<Vec<_>>();
let selected_theme = theme_options
.iter()
.find(|choice| choice.value == ui_theme.name)
.cloned();
let language_options = crate::i18n::Language::ALL
.into_iter()
.map(|value| Labelled {
label: value.label(),
value,
})
.collect::<Vec<_>>();
let selected_language = language_options
.iter()
.find(|choice| choice.value == language)
.cloned();
let cursor_options = CursorType::ALL
.into_iter()
.map(|value: CursorType| Labelled {
label: crate::t!(value.label()).into_owned(),
value,
})
.collect::<Vec<_>>();
let selected_cursor = cursor_options
.iter()
.find(|choice| choice.value == cursor_type)
.cloned();
let right_click_options = RightClickMode::ALL
.into_iter()
.map(|value: RightClickMode| Labelled {
label: crate::t!(value.label()).into_owned(),
value,
})
.collect::<Vec<_>>();
let selected_right_click = right_click_options
.iter()
.find(|choice| choice.value == prefs.right_click_mode)
.cloned();
let palette = ui_theme.palette.to_iced();
let colors = [
(
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),
(crate::tr!("options", "color-warning"), palette.warning),
(crate::tr!("options", "color-danger"), palette.danger),
];
let mut color_controls = column![].spacing(8);
for (index, (label, color)) in colors.into_iter().enumerate() {
let [r, g, bl, _] = color.into_rgba8();
let swatch = color_swatch(crate::app::UiColor::Theme(index), [r, g, bl]);
color_controls = color_controls.push(
row![
text(label).size(12).width(110),
swatch,
text_input("#RRGGBB", theme_color_inputs[index].as_str())
.on_input(move |value| Message::OptionsThemeColorChanged(index, value))
.width(130),
]
.spacing(10)
.align_y(iced::Center),
);
}
// Changes show at once but are committed by OK / Apply; Close puts them
// back (asking first when there is something to lose).
let ok = dialog_button(crate::t!("OK"), Message::OptionsOk, true);
let apply = dialog_button_styled_opt(
crate::t!("Apply"),
dirty.then_some(Message::OptionsApply),
if dirty {
button::secondary
} else {
button::text
},
);
let close = dialog_button(crate::tr!("action", "close"), Message::OptionsClose, false);
let general = column![
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(),)
.on_select(|choice| Message::LanguageChanged(choice.value))
.width(sizing.width),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Applications")).size(15),
Space::new().height(10),
row![
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])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Keyboard shortcuts")).size(12).width(Fill),
button(text(crate::t!("Keyboard Shortcuts…")).size(11))
.on_press(Message::ShortcutsPanelOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Command aliases")).size(12).width(Fill),
button(text(crate::t!("Command Aliases…")).size(11))
.on_press(Message::AliasEditorOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(22),
text(crate::t!("Plotting")).size(15),
Space::new().height(10),
row![
text(crate::t!("Plot device, paper, scale and plot styles"))
.size(12)
.width(Fill),
button(text(crate::t!("Plot and Page Setup…")).size(11))
.on_press(Message::PlotDialogOpen)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.page_setup_on_new_layout)
.on_toggle(Message::PageSetupOnNewLayoutChanged)
.size(15),
text(crate::t!("Show the page setup for new layouts")).size(12),
]
.spacing(8)
.align_y(iced::Center),
]
.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
// #205 with no control at all.
let open_and_save = column![
text(crate::t!("Open and Save")).size(15),
Space::new().height(10),
Space::new().height(10),
row![
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).
&crate::io::SAVE_FORMAT_OPTIONS[..crate::io::SAVE_FORMAT_OPTIONS.len() - 1],
|value| value.to_string(),
)
.on_select(|format: &str| Message::DefaultSaveFormatChanged(format.to_string()))
.width(sizing.width),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options", "default-save-format-help"))
.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 OneCAD Draft")) /* OneBSpace (OneCAD Draft): rebranded */.size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Also installs the application and file-type icons the desktop shows."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
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),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"On by default. Turn off to show just the constraint glyph in the viewport — \
enough to see that a constraint is present — without the driven value or named \
parameter text covering nearby geometry."
))
.size(11)
.width(sizing.width),
Space::new().height(22),
text(crate::t!("File Safety")).size(15),
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),
text(if prefs.savetime_min <= 0 {
crate::t!("Off").into_owned()
} else {
format!("{} min", prefs.savetime_min)
})
.size(11)
.width(52),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Minutes between recovery saves to a .sv$ file; 0 turns it off (SAVETIME)."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
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),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
let crosshair_swatch = color_swatch(
crate::app::UiColor::Crosshair,
crosshair_color.unwrap_or([255, 255, 255]),
);
let command_swatch = color_swatch(
crate::app::UiColor::CommandText,
command_text_color.unwrap_or([220, 220, 220]),
);
let snap_swatch = color_swatch(
crate::app::UiColor::SnapMarker,
snap_marker_color.unwrap_or([255, 230, 26]),
);
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 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()
.map(|value| Labelled {
label: crate::t!(value.label()).into_owned(),
value,
})
.collect::<Vec<_>>();
let selected_mode = mode_options
.iter()
.find(|choice| choice.value == model_space.mode)
.cloned();
let color_choice_options = [
(0u8, "0: Theme Default"),
(1, "1: Red"),
(2, "2: Yellow"),
(3, "3: Green"),
(4, "4: Cyan"),
(5, "5: Blue"),
(6, "6: Magenta"),
(7, "7: White/Black"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_window_color = color_choice_options
.iter()
.find(|c| c.value == model_space.selection_window_color)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.selection_window_color,
label: format!("ACI {}", model_space.selection_window_color),
});
let selected_crossing_color = color_choice_options
.iter()
.find(|c| c.value == model_space.selection_crossing_color)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.selection_crossing_color,
label: format!("ACI {}", model_space.selection_crossing_color),
});
let selected_highlight_color = color_choice_options
.iter()
.find(|c| c.value == model_space.selection_highlight_color)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.selection_highlight_color,
label: format!("ACI {}", model_space.selection_highlight_color),
});
let selected_grip_color = color_choice_options
.iter()
.find(|c| c.value == model_space.grip_color)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.grip_color,
label: format!("ACI {}", model_space.grip_color),
});
let selected_grip_hot = color_choice_options
.iter()
.find(|c| c.value == model_space.grip_hot)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.grip_hot,
label: format!("ACI {}", model_space.grip_hot),
});
let selected_grip_hover = color_choice_options
.iter()
.find(|c| c.value == model_space.grip_hover)
.cloned()
.unwrap_or_else(|| Labelled {
value: model_space.grip_hover,
label: format!("ACI {}", model_space.grip_hover),
});
let mut display = column![
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),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::tr!("options", "theme-help"))
.size(11)
.width(sizing.width),
Space::new().height(12),
color_controls,
Space::new().height(16),
// A list, not a slider: the dialog rescales under the cursor as the
// value changes, which would drag a slider along with it.
row![
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(),
|label: &String| label.clone(),
)
.on_select(|label: String| {
Message::UiScaleChanged(label.trim_end_matches('%').parse().unwrap_or(100))
})
.width(sizing.width),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(24),
row![
text(crate::t!("Model Space Appearance")).size(15),
Space::new().width(Fill),
button(text(crate::t!("Restore Defaults")).size(11))
.on_press(Message::RestoreModelSpaceDisplayDefaults)
.padding([4, 10])
.style(button::secondary),
]
.align_y(iced::Center),
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),
]
.spacing(10)
.align_y(iced::Center),
];
if model_space.mode == crate::app::config::ModelSpaceMode::Custom {
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Model background")).size(12).width(140),
color_swatch(crate::app::UiColor::ModelBackground, model_bg_rgb),
text_input("#RRGGBB", model_bg_input)
.on_input(Message::ModelSpaceBgChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
);
}
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Paper background")).size(12).width(140),
color_swatch(crate::app::UiColor::PaperBackground, paper_bg_rgb),
text_input("#RRGGBB", paper_bg_input)
.on_input(Message::PaperSpaceBgChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
);
display = display.push(Space::new().height(10)).push(
row![
text(crate::t!("Desk surround")).size(12).width(140),
color_swatch(crate::app::UiColor::DeskBackground, desk_bg_rgb),
text_input("#RRGGBB", desk_bg_input)
.on_input(Message::DeskSpaceBgChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
);
display = display
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Grid opacity")).size(12).width(140),
slider(5..=100, model_space.grid_opacity.clamp(5, 100), Message::GridOpacityChanged)
.step(1)
.width(Fill),
text(format!("{}%", model_space.grid_opacity.clamp(5, 100)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(match model_space.mode {
crate::app::config::ModelSpaceMode::MatchTheme => {
crate::t!("Canvas background, grid, and default line colors automatically adapt to the active theme.")
}
crate::app::config::ModelSpaceMode::ClassicDark => {
crate::t!("Model space canvas remains locked to classic CAD dark charcoal (#212830).")
}
crate::app::config::ModelSpaceMode::Custom => {
crate::t!("Custom background colors set above are applied to Model and Paper space.")
}
})
.size(11)
.width(sizing.width),
)
.push(Space::new().height(24))
.push(text(crate::t!("Crosshair")).size(15))
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Crosshair size")).size(12).width(140),
slider(1..=100, cursor_size.clamp(1, 100), Message::CursorSizeChanged)
.step(1)
.width(Fill),
text(format!("{}%", cursor_size.clamp(1, 100)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Cursor type")).size(12).width(140),
iced::widget::pick_list(
selected_cursor,
cursor_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::CursorTypeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Crosshair color")).size(12).width(140),
crosshair_swatch,
text_input(crate::t!("#RRGGBB or blank").as_ref(), crosshair_color_input)
.on_input(Message::CrosshairColorChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Snap marker color")).size(12).width(140),
snap_swatch,
text_input(crate::t!("#RRGGBB or blank").as_ref(), snap_marker_color_input)
.on_input(Message::SnapMarkerColorChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Command line text color")).size(12).width(140),
command_swatch,
text_input(crate::t!("#RRGGBB or blank").as_ref(), command_text_color_input)
.on_input(Message::CommandTextColorChanged)
.width(150),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(24))
.push(text(crate::t!("Lineweight")).size(15))
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Model display scale")).size(12).width(140),
slider(
25..=200,
lineweight_display_scale.clamp(25, 200),
Message::LineweightDisplayScaleChanged,
)
.step(1)
.width(Fill),
text(format!("{}%", lineweight_display_scale.clamp(25, 200)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!("Changes the on-screen width in Model without affecting plotted output."))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(14))
.push(
row![
iced::widget::checkbox(prefs.textfill)
.on_toggle(Message::TextFillChanged)
.size(15),
text(crate::t!("Fill TrueType glyphs (TEXTFILL)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(24))
.push(text(crate::t!("Window Elements")).size(15))
.push(Space::new().height(10))
.push(
row![
iced::widget::checkbox(prefs.show_menu_bar)
.on_toggle(Message::MenuBarChanged)
.size(15),
text(crate::t!("Show the menu bar (MENUBAR)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(8))
.push(
row![
iced::widget::checkbox(prefs.show_ribbon)
.on_toggle(Message::RibbonVisibleChanged)
.size(15),
text(crate::t!("Show the ribbon (RIBBON / RIBBONCLOSE)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(24))
.push(text(crate::t!("Command Line")).size(15))
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Prompt lines")).size(12).width(140),
slider(
0..=50,
prefs.cliprompt_lines.clamp(0, 50),
Message::ClipromptLinesChanged,
)
.step(1)
.width(Fill),
text(prefs.cliprompt_lines.clamp(0, 50).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!(
"Temporary prompt lines shown above the command window (CLIPROMPTLINES)."
))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(12))
.push(
row![
text(crate::t!("History fade time")).size(12).width(140),
slider(
0..=60000,
prefs.commandline_fade_ms.clamp(0, 60000),
Message::CommandLineFadeChanged,
)
.step(250)
.width(Fill),
text(if prefs.commandline_fade_ms <= 0 {
crate::t!("Never").into_owned()
} else {
format!("{:.1} s", prefs.commandline_fade_ms as f32 / 1000.0)
})
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(6))
.push(
text(crate::t!(
"How long overlay history lines stay visible; 0 means never fade (COMMANDLINEFADETIME)."
))
.size(11)
.width(sizing.width),
)
.push(Space::new().height(12))
.push(
row![
text(crate::t!("Command line opacity")).size(12).width(140),
slider(
20..=100,
prefs.command_line_opacity.clamp(20, 100),
Message::CommandLineOpacityChanged,
)
.step(5u8)
.width(Fill),
text(format!("{}%", prefs.command_line_opacity)).size(11).width(44),
]
.spacing(10)
.align_y(iced::Center),
);
let display_element = display.spacing(0).width(sizing.width);
let selection = column![
text(crate::t!("Selection")).size(15),
Space::new().height(10),
row![
text(crate::t!("Pick box size")).size(12).width(140),
slider(0..=50, selection.pick_box.clamp(0, 50), Message::PickBoxChanged)
.step(1)
.width(Fill),
text(selection.pick_box.clamp(0, 50).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(8),
text(crate::t!(
"Controls both the visible selection box and the click aperture."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Selection Modes")).size(15),
Space::new().height(10),
row![
// Checked is the inverse of PICKADD: plain clicks replace and Shift adds.
iced::widget::checkbox(!selection.pick_add)
.on_toggle(Message::ShiftToAddToggled)
.size(15),
text(crate::t!("Use Shift to add to selection (PICKADD)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(selection.pick_drag_rect)
.on_toggle(Message::PickDragRectToggled)
.size(15),
text(crate::t!(
"Press and drag draws a rectangle instead of a lasso (PICKDRAG)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(selection.selection_cycling)
.on_toggle(Message::SelectionCyclingChanged)
.size(15),
text(crate::t!("Clicking overlapping objects opens a picker")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(24),
row![
text(crate::t!("Visual Effect Settings")).size(15),
Space::new().width(Fill),
button(text(crate::t!("Restore Defaults")).size(11))
.on_press(Message::RestoreSelectionVisualDefaults)
.padding([4, 10])
.style(button::secondary),
]
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(model_space.selection_area)
.on_toggle(Message::SelectionAreaToggled)
.size(15),
text(crate::t!("Indicate selection area with transparent fill (SELECTIONAREA)"))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Selection area opacity")).size(12).width(140),
slider(
0..=100,
model_space.selection_opacity.clamp(0, 100),
Message::SelectionOpacityChanged,
)
.step(1)
.width(Fill),
text(format!("{}%", model_space.selection_opacity.clamp(0, 100)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Transparency of the window and crossing selection areas (SELECTIONAREAOPACITY)."
))
.size(11)
.width(sizing.width),
Space::new().height(10),
row![
text(crate::t!("Window selection color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_window_color),
color_choice_options.clone(),
|choice| choice.label.clone(),
)
.on_select(|choice| Message::SelectionWindowColorChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Crossing selection color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_crossing_color),
color_choice_options.clone(),
|choice| choice.label.clone(),
)
.on_select(|choice| Message::SelectionCrossingColorChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(model_space.selection_effect)
.on_toggle(Message::SelectionEffectToggled)
.size(15),
text(crate::t!("Show selection effect (SELECTIONEFFECT)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Selection highlight color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_highlight_color),
color_choice_options.clone(),
|choice| choice.label.clone(),
)
.on_select(|choice| Message::SelectionHighlightColorChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Selection highlight opacity")).size(12).width(140),
slider(
10..=100,
model_space.selection_highlight_opacity.clamp(10, 100),
Message::SelectionHighlightOpacityChanged,
)
.step(5)
.width(Fill),
text(format!("{}%", model_space.selection_highlight_opacity.clamp(10, 100)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Selection highlight extra width")).size(12).width(140),
slider(
0..=6,
model_space.selection_highlight_widen.min(6),
Message::SelectionHighlightWidenChanged,
)
.step(1)
.width(Fill),
text(format!("{} px", model_space.selection_highlight_widen.min(6)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Preview")).size(15),
Space::new().height(10),
row![
// SELECTIONPREVIEW is a bitmask; bit 1 is the idle rollover and
// bit 2 the one that runs while a command is gathering objects.
iced::widget::checkbox(model_space.selection_preview & 1 != 0)
.on_toggle(Message::SelectionPreviewIdleToggled)
.size(15),
text(crate::t!("Preview selection when no command is active")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(model_space.selection_preview & 2 != 0)
.on_toggle(Message::SelectionPreviewCommandToggled)
.size(15),
text(crate::t!("Preview selection during a command")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Highlights the object under the cursor before it is picked (SELECTIONPREVIEW)."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Grip Settings")).size(15),
Space::new().height(10),
row![
text(crate::t!("Grip size")).size(12).width(140),
slider(
1..=25,
model_space.grip_size.clamp(1, 25),
Message::GripSizeChanged,
)
.step(1)
.width(Fill),
text(format!("{} px", model_space.grip_size.clamp(1, 25)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Object limit for grips")).size(12).width(140),
// Values above the slider range remain available through SETVAR.
slider(
0..=1000,
selection.grip_object_limit.clamp(0, 1000),
Message::GripObjectLimitChanged,
)
.step(1)
.width(Fill),
text(if selection.grip_object_limit == 0 {
crate::t!("Unlimited").into_owned()
} else {
selection.grip_object_limit.to_string()
})
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Past this many selected objects no grips are drawn; 0 removes the limit (GRIPOBJLIMIT)."
))
.size(11)
.width(sizing.width),
Space::new().height(10),
row![
text(crate::t!("Unselected grip color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_grip_color),
color_choice_options.clone(),
|choice| choice.label.clone(),
)
.on_select(|choice| Message::GripColorChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Selected/hot grip color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_grip_hot),
color_choice_options.clone(),
|choice| choice.label.clone(),
)
.on_select(|choice| Message::GripHotChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(10),
row![
text(crate::t!("Hover grip color")).size(12).width(140),
iced::widget::pick_list(
Some(selected_grip_hover),
color_choice_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::GripHoverChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
// ANNOAUTOSCALE's magnitude decides which objects a newly added scale
// reaches — 1 skips layers that are off, frozen, locked or frozen in the
// viewport, 2 keeps locked ones, 3 skips only locked, 4 takes everything.
// The sign is the on/off the status-bar pill flips, so the mode survives
// being switched off and back on.
let auto_scale_options = [
(0i8, "Off"),
(1, "Skip objects on layers that are off, frozen or locked"),
(2, "Skip objects on layers that are off or frozen"),
(3, "Skip objects on locked layers"),
(4, "All annotative objects"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: crate::t!(label).into_owned(),
})
.collect::<Vec<_>>();
let selected_auto_scale = auto_scale_options
.iter()
.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 selected_qdim = qdim_options
.iter()
.find(|choice| choice.value == prefs.qdim_snap_priority)
.cloned();
let user_prefs = column![
text(crate::t!("User Preferences")).size(15),
Space::new().height(10),
spacemouse,
Space::new().height(12),
text(crate::t!("Navigation style")).size(15),
Space::new().height(10),
iced::widget::pick_list(
Some(prefs.navigation_style),
crate::app::settings::NavigationStyle::ALL,
|style| style.to_string(),
)
.on_select(Message::NavigationStyleChanged)
.text_size(12),
Space::new().height(12),
text(crate::t!("Zoom")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.zoom_wheel_reversed)
.on_toggle(Message::ZoomWheelReversedChanged)
.size(15),
text(crate::t!("Reverse mouse wheel zoom (ZOOMWHEEL)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(8),
row![
iced::widget::checkbox(prefs.touchpad_pan)
.on_toggle(Message::TouchpadPanChanged)
.size(15),
text(crate::t!("Touchpad: scroll pans, Ctrl+scroll or pinch zooms")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("Zoom factor")).size(12).width(150),
slider(
settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_SYSVAR_MAX,
prefs
.zoom_factor
.clamp(settings::ZOOM_FACTOR_MIN, settings::ZOOM_FACTOR_SYSVAR_MAX),
Message::ZoomFactorChanged,
)
.step(1)
.width(Fill),
text_input("60", zoom_factor_input)
.on_input(Message::ZoomFactorInputChanged)
.width(52),
text(crate::tf!(
"{:.1}% a notch",
settings::zoom_notch_percent(prefs.zoom_factor)
))
.size(11)
.width(76),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!("How far one wheel notch zooms (ZOOMFACTOR)."))
.size(11)
.width(sizing.width),
Space::new().height(4),
text(crate::tf!(
"The slider covers the system variable's {} to {}; the field takes up to {} for a faster wheel.",
settings::ZOOM_FACTOR_MIN,
settings::ZOOM_FACTOR_SYSVAR_MAX,
settings::ZOOM_FACTOR_MAX
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Text and Dimensions")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.texteditmode)
.on_toggle(Message::TextEditModeChanged)
.size(15),
text(crate::t!("TEXTEDIT edits one object and ends (TEXTEDITMODE)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
iced::widget::checkbox(prefs.dimension_continue_mode == 1)
.on_toggle(Message::DimContinueModeChanged)
.size(15),
text(crate::t!(
"Continued dimensions inherit the base dimension's layer and style (DIMCONTINUEMODE)"
))
.size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("QDIM origin priority")).size(12).width(150),
iced::widget::pick_list(selected_qdim, qdim_options, |choice| choice.label.clone())
.on_select(|choice| Message::QdimSnapPriorityChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Which points QDIM measures from. Also settable inside the command."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Annotation")).size(15),
Space::new().height(10),
row![
text(crate::t!("Add scales automatically")).size(12).width(150),
iced::widget::pick_list(selected_auto_scale, auto_scale_options, |choice| {
choice.label.clone()
})
.on_select(|choice| Message::AnnoAutoScaleChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Which annotative objects pick up a newly set annotation scale (ANNOAUTOSCALE)."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Right-click Customization")).size(15),
Space::new().height(10),
row![
text(crate::t!("Right-click in drawing area")).size(12).width(150),
iced::widget::pick_list(
selected_right_click,
right_click_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::RightClickModeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(12),
row![
text(crate::t!("Hold duration")).size(12).width(150),
slider(
100..=1000,
prefs.right_click_hold_ms.clamp(100, 1000),
Message::RightClickHoldMsChanged
)
.step(50)
.width(Fill),
text(format!("{} ms", prefs.right_click_hold_ms.clamp(100, 1000)))
.size(11)
.width(52),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::t!("Block Edit")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(double_click_block_refedit)
.on_toggle(Message::DoubleClickBlockRefeditChanged)
.size(15),
text(crate::t!("Double-click to edit block in-place (REFEDIT)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(18),
row![
iced::widget::checkbox(double_click_block_attedit)
.on_toggle(Message::DoubleClickBlockAtteditChanged)
.size(15),
text(crate::t!("Double-click attributed blocks to edit attributes (ATTEDIT)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Drawing Units")).size(15),
Space::new().height(10),
row![
text(crate::t!("Length, angle and insertion units"))
.size(12)
.width(Fill),
button(text(crate::t!("Drawing Units…")).size(11))
.on_press(Message::OpenDrawingUnits)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Hatch")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.hatch_associative)
.on_toggle(Message::HatchAssociativeChanged)
.size(15),
text(crate::t!("New hatches follow their boundaries (HPASSOC)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
]
.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
// pop-up.
let polar_options = [90.0f32, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0, 1.0]
.into_iter()
.map(|value| Labelled {
label: format!("{}°", crate::app::settings::format_snap_angle(value)),
value,
})
.collect::<Vec<_>>();
let selected_polar = polar_options
.iter()
.find(|choice| (choice.value - prefs.polar_increment_deg).abs() < 1e-4)
.cloned()
.unwrap_or_else(|| Labelled {
value: prefs.polar_increment_deg,
label: format!(
"{}°",
crate::app::settings::format_snap_angle(prefs.polar_increment_deg)
),
});
let drafting = column![
text(crate::t!("Drafting")).size(15),
Space::new().height(10),
row![
text(crate::t!("Drafting rotation")).size(12).width(150),
text_input("0", snap_angle_input)
.on_input(Message::SnapAngleInputChanged)
.width(110),
text(crate::t!("degrees")).size(11),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Rotates the crosshair and the snap grid in the active UCS (SNAPANG)."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
text(crate::t!("Polar tracking increment"))
.size(12)
.width(150),
iced::widget::pick_list(Some(selected_polar), polar_options, |choice| {
choice.label.clone()
})
.on_select(|choice| Message::PolarIncrementChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(14),
row![
text(crate::t!("Snap marker size")).size(12).width(150),
slider(
1..=20,
model_space.snap_marker_size.clamp(1, 20),
Message::SnapMarkerSizeChanged,
)
.step(1)
.width(Fill),
text(format!("{} px", model_space.snap_marker_size.clamp(1, 20)))
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
Space::new().height(24),
text(crate::t!("Snap and Grid")).size(15),
Space::new().height(10),
row![
text(crate::t!("Snap modes, grid spacing and object snap"))
.size(12)
.width(Fill),
button(text(crate::t!("Drafting Settings…")).size(11))
.on_press(Message::ToggleSnapPopup)
.padding([4, 10])
.style(button::secondary),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
let surface_type_options = [
(5i16, "Quadratic B-spline"),
(6, "Cubic B-spline"),
(8, "Bezier"),
]
.into_iter()
.map(|(value, label)| Labelled {
value,
label: format!("{} ({value})", crate::t!(label)),
})
.collect::<Vec<_>>();
let selected_surface_type = surface_type_options
.iter()
.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 selected_show_history = show_history_options
.iter()
.find(|choice| choice.value == drawing_prefs.show_solid_history.clamp(0, 2))
.cloned();
let mut modeling = column![
text(crate::t!("3D Modeling")).size(15),
Space::new().height(10),
text(crate::t!("Display Tools")).size(15),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.show_viewcube)
.on_toggle(Message::ShowViewCubeChanged)
.size(15),
text(crate::t!("Show the navigation cube (NAVVCUBE)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
iced::widget::checkbox(prefs.show_ucs_icon)
.on_toggle(Message::ShowUcsIconChanged)
.size(15),
text(crate::t!("Show the UCS icon (UCSICON)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(10),
row![
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),
]
.spacing(8)
.align_y(iced::Center),
];
if drawing_prefs.available {
modeling = modeling
.push(Space::new().height(24))
.push(
row![
text(crate::t!("Display Resolution")).size(15),
Space::new().width(10),
text(crate::t!("applies to the current drawing")).size(11),
]
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Isolines per surface")).size(12).width(150),
// Retessellate once when dragging ends.
slider(
0..=64,
drawing_prefs.isolines.clamp(0, 64),
Message::IsolinesChanged,
)
.step(1i16)
.on_release(Message::IsolinesReleased)
.width(Fill),
text(drawing_prefs.isolines.clamp(0, 64).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
iced::widget::checkbox(drawing_prefs.display_silhouette)
.on_toggle(Message::DispSilhChanged)
.size(15),
text(crate::t!("Show silhouette edges on solids (DISPSILH)")).size(12),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface density U")).size(12).width(150),
slider(
0..=200,
drawing_prefs.surface_u.clamp(0, 200),
Message::SurfaceUChanged,
)
.step(1i16)
.width(Fill),
text(drawing_prefs.surface_u.clamp(0, 200).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface density V")).size(12).width(150),
slider(
0..=200,
drawing_prefs.surface_v.clamp(0, 200),
Message::SurfaceVChanged,
)
.step(1i16)
.width(Fill),
text(drawing_prefs.surface_v.clamp(0, 200).to_string())
.size(11)
.width(44),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Surface type")).size(12).width(150),
iced::widget::pick_list(
selected_surface_type,
surface_type_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::SurfaceTypeChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
)
.push(Space::new().height(24))
.push(
row![
text(crate::t!("Solid History")).size(15),
Space::new().width(10),
text(crate::t!("applies to the current drawing")).size(11),
]
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
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),
]
.spacing(8)
.align_y(iced::Center),
)
.push(Space::new().height(10))
.push(
row![
text(crate::t!("Show solid history")).size(12).width(150),
iced::widget::pick_list(
selected_show_history,
show_history_options,
|choice| choice.label.clone(),
)
.on_select(|choice| Message::ShowHistChanged(choice.value))
.width(Fill),
]
.spacing(10)
.align_y(iced::Center),
);
}
let modeling = modeling.spacing(0).width(sizing.width);
// The page shows where things live and opens the folder — which is the
// question people are really asking when they go looking for a Files
// page. Fonts and plot styles can move elsewhere, such as a share on the
// office network that a whole team uses.
let folder_row = |label: std::borrow::Cow<'a, str>,
path: Option<String>,
movable: Option<(MovableFolder, bool)>| {
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);
if let Some(path) = path {
open = open.on_press(Message::OpenFolder(path));
}
let mut buttons = row![open].spacing(6);
if let Some((folder, moved)) = movable {
buttons = buttons.push(
button(text(crate::t!("Change...")).size(11))
.padding([4, 10])
.style(button::secondary)
.on_press(Message::ChooseFolder(folder)),
);
if moved {
buttons = buttons.push(
button(text(crate::t!("Default")).size(11))
.padding([4, 10])
.style(button::secondary)
.on_press(Message::FolderChosen(folder, None)),
);
}
}
row![
column![text(label).size(12), text(shown).size(11),]
.spacing(2)
.width(Fill),
buttons,
]
.spacing(10)
.align_y(iced::Center)
};
// Browsers have no folder picker.
let movable = |folder: MovableFolder, moved: bool| {
(!cfg!(target_arch = "wasm32")).then_some((folder, moved))
};
let files = column![
text(crate::t!("Files")).size(15),
Space::new().height(6),
text(crate::t!("Where the application keeps its own files."))
.size(11)
.width(sizing.width),
Space::new().height(16),
folder_row(crate::t!("Configuration"), folders.config.clone(), None),
Space::new().height(12),
folder_row(
crate::t!("Plot styles"),
folders.plot_styles.clone(),
movable(MovableFolder::PlotStyles, folders.plot_styles_moved),
),
Space::new().height(12),
folder_row(crate::t!("Plugins"), folders.plugins.clone(), None),
Space::new().height(12),
folder_row(crate::t!("Autosave files"), folders.autosave.clone(), None),
Space::new().height(12),
folder_row(
crate::t!("Fonts"),
folders.fonts.clone(),
movable(MovableFolder::Fonts, folders.fonts_moved),
),
Space::new().height(12),
folder_row(
crate::t!("Color book locations"),
folders.color_books.clone(),
None,
),
Space::new().height(16),
text(crate::t!("Default template for new drawings")).size(12),
Space::new().height(4),
row![
text_input(crate::t!("Blank drawing").as_ref(), qnew_template)
.on_input(Message::QnewTemplateChanged)
.size(11)
.width(Fill),
button(text(crate::t!("Browse...")).size(11))
.padding([4, 10])
.style(button::secondary)
.on_press(Message::QnewTemplateBrowse),
]
.spacing(10)
.align_y(iced::Center),
]
.spacing(0)
.width(sizing.width);
let content: Element<'a, Message> = match active_tab {
OptionsTab::General => general.into(),
OptionsTab::Display => display_element.into(),
OptionsTab::Selection => selection.into(),
OptionsTab::Files => files.into(),
OptionsTab::OpenAndSave => open_and_save.into(),
OptionsTab::Drafting => drafting.into(),
OptionsTab::Modeling => modeling.into(),
OptionsTab::UserPreferences => user_prefs.into(),
OptionsTab::Graphics => graphics,
};
// A vertical rail rather than a horizontal strip: the tab names are
// translated into 21 languages, and a row of them stops fitting long
// before the list of pages is complete.
let tab_button = |label, tab| {
let selected = active_tab == tab;
button(text(label).size(12.5))
.on_press(Message::OptionsTabChanged(tab))
.padding([7, 12])
.width(Fill)
.style(if selected {
button::primary
} else {
button::text
})
};
let tabs = column![
tab_button(crate::t!("General"), OptionsTab::General),
tab_button(crate::t!("Files"), OptionsTab::Files),
tab_button(crate::t!("Open and Save"), OptionsTab::OpenAndSave),
tab_button(crate::t!("Display"), OptionsTab::Display),
tab_button(crate::t!("Drafting"), OptionsTab::Drafting),
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
tab_button(crate::t!("Selection"), OptionsTab::Selection),
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
];
// 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 pane = column![
// Keep the scrollbar in its own lane instead of floating over the
// controls at the trailing edge of the Options content.
scrollable(content).spacing(8).height(Fill),
Space::new().height(12),
row![Space::new().width(Fill), ok, apply, close].spacing(8),
]
.width(Fill)
.height(sizing.height);
let body = row![tabs, Space::new().width(18), pane]
.width(sizing.width)
.height(sizing.height);
let intrinsic = sizing.width == crate::ui::modal::ModalSizing::INTRINSIC.width;
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
});
if !close_confirm {
return main.into();
}
crate::ui::modal::discard_guard(
main,
Message::OptionsCloseDiscard,
Message::OptionsCloseKeep,
)
}