use crate::app::config::UiThemeConfig; use crate::app::settings::CursorType; use crate::app::Message; use iced::widget::{ button, column, container, row, scrollable, slider, text, text_input, Space, }; use iced::{Background, Border, Element, Fill, Theme}; use std::fmt; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum OptionsTab { #[default] General, Display, Selection, } #[derive(Clone, Debug, PartialEq, Eq)] struct Labelled { value: T, label: String, } impl fmt::Display for Labelled { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.label) } } pub fn view_window<'a>( default_save_format: &'a str, file_assoc_enabled: bool, ui_theme: &'a UiThemeConfig, theme_color_inputs: &'a [String; 6], language: crate::i18n::Language, active_tab: OptionsTab, cursor_size: i32, pick_box: i32, cursor_type: CursorType, crosshair_color: Option<[u8; 3]>, crosshair_color_input: &'a str, 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 = Theme::ALL .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::>(); 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::>(); 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::>(); let selected_cursor = cursor_options .iter() .find(|choice| choice.value == cursor_type) .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 swatch = container(Space::new()) .width(28) .height(22) .style(move |theme: &Theme| container::Style { background: Some(Background::Color(color)), border: Border { color: theme.palette().background.strong.color, width: 1.0, radius: 3.0.into(), }, ..Default::default() }); 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), ); } let close = button(text(crate::tr!("action", "close")).size(12)) .on_press(Message::CloseModal) .padding([6, 18]) .style(button::secondary); 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(22), text(crate::tr!("options", "open-save-section")).size(15), Space::new().height(10), row![ text(crate::tr!("options", "default-save-format-label")).size(12).width(150), iced::widget::pick_list( selected_format, crate::io::SAVE_FORMAT_OPTIONS, |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 Open CAD Studio")) .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), ] .spacing(0) .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 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(24), text(crate::t!("Crosshair")).size(15), Space::new().height(10), 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), Space::new().height(10), 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), Space::new().height(10), 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), Space::new().height(6), text(crate::t!("Leave the color blank to keep automatic viewport contrast.")) .size(11) .width(sizing.width), ] .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, pick_box.clamp(0, 50), Message::PickBoxChanged) .step(1) .width(Fill), text(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), ] .spacing(0) .width(sizing.width); let content: Element<'a, Message> = match active_tab { OptionsTab::General => general.into(), OptionsTab::Display => display.into(), OptionsTab::Selection => selection.into(), }; let tab_button = |label, tab| { let selected = active_tab == tab; button(text(label).size(12)) .on_press(Message::OptionsTabChanged(tab)) .padding([6, 14]) .style(if selected { button::primary } else { button::secondary }) }; let tabs = row![ tab_button(crate::t!("General"), OptionsTab::General), tab_button(crate::t!("Display"), OptionsTab::Display), tab_button(crate::t!("Selection"), OptionsTab::Selection), ] .spacing(6); let body = column![ tabs, Space::new().height(12), // 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(sizing.height), Space::new().height(12), row![Space::new().width(sizing.width), close], ] .width(sizing.width) .height(sizing.height); container(body) .style(container::rounded_box) .padding([16, 18]) .width(sizing.width) .height(sizing.height) .into() }