diff --git a/Cargo.lock b/Cargo.lock index 2c94eff96..19b6a71c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2571,6 +2571,8 @@ dependencies = [ "iced_core", "iced_fonts_macros", "iced_widget", + "num-format", + "num-traits", "web-time", ] @@ -3768,6 +3770,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + [[package]] name = "num-integer" version = "0.1.46" diff --git a/Cargo.toml b/Cargo.toml index 8d5dbfc21..1805b6b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ url = "2" iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39", features = ["image", "svg", "advanced", "canvas", "smol", "markdown"] } iced_runtime = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } -iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent/fix-iced-fonts", features = ["menu", "context_menu", "color_picker", "drop_down"] } +iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent/fix-iced-fonts", features = ["menu", "context_menu", "color_picker", "drop_down", "number_input"] } bytemuck = { version = "1.25", features = ["derive"] } glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs index 70a055ff0..186545e12 100644 --- a/src/app/commands/layers.rs +++ b/src/app/commands/layers.rs @@ -3,6 +3,10 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_layers(&mut self, cmd: &str, i: usize) -> Option> { match cmd { + cmd if cmd.starts_with("GRAPHICATTRIBUTELAYER ") => { + let name = cmd.trim_start_matches("GRAPHICATTRIBUTELAYER").trim(); + return Some(self.create_graphic_attributes_layer(name)); + } // ── Layer object commands ────────────────────────────────────── "LAYOFF" => { let handles: Vec<_> = self.tabs[i] diff --git a/src/app/mod.rs b/src/app/mod.rs index a172d776e..7b26e0aec 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -769,6 +769,7 @@ pub(super) struct OpenCADStudio { /// Whether the Properties panel is shown on the left (PROPERTIES). show_properties: bool, show_graphic_attributes: bool, + graphic_attributes_header_menu_open: bool, graphic_attribute_menu_open: bool, line_color_menu_open: bool, line_linetype_menu_open: bool, @@ -2066,6 +2067,12 @@ pub enum ArrowKey { #[derive(Debug, Clone)] pub enum Message { + ToggleGraphicAttributesHeaderMenu, + CloseGraphicAttributesHeaderMenu, + GraphicAttributesSetAllByLayer, + GraphicAttributesSetAllByBlock, + GraphicAttributesRemoveReferences, + GraphicAttributesCreateLayer, ToggleGraphicAttributeDropdown, CloseGraphicAttributeDropdown, GraphicAttributeChanged(crate::ui::window::graphic_attributes::GraphicAttribute), @@ -4161,6 +4168,7 @@ impl OpenCADStudio { render_mode_preview: None, show_properties: true, show_graphic_attributes: true, + graphic_attributes_header_menu_open: false, graphic_attribute_menu_open: false, line_color_menu_open: false, line_linetype_menu_open: false, diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 2e0756498..455ab5d99 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -130,7 +130,324 @@ fn close_fill_boundary(entity: &mut acadrust::EntityType) -> bool { } } +#[derive(Clone)] +struct ConcreteGraphicAttributes { + color: acadrust::types::Color, + linetype: String, + lineweight: acadrust::types::LineWeight, + transparency: acadrust::types::Transparency, +} + +fn concrete_graphic_attributes( + color: acadrust::types::Color, + linetype: &str, + lineweight: acadrust::types::LineWeight, + transparency: acadrust::types::Transparency, + layer: Option<&acadrust::tables::Layer>, +) -> ConcreteGraphicAttributes { + let layer_color = layer.map_or(acadrust::types::Color::Index(7), |layer| { + match layer.color { + acadrust::types::Color::ByLayer | acadrust::types::Color::ByBlock => { + acadrust::types::Color::Index(7) + } + color => color, + } + }); + let layer_linetype = layer + .map(|layer| layer.line_type.as_str()) + .filter(|value| { + !value.is_empty() + && !value.eq_ignore_ascii_case("ByLayer") + && !value.eq_ignore_ascii_case("ByBlock") + }) + .unwrap_or("Continuous"); + let layer_lineweight = layer.map_or(acadrust::types::LineWeight::Default, |layer| { + match layer.line_weight { + acadrust::types::LineWeight::ByLayer | acadrust::types::LineWeight::ByBlock => { + acadrust::types::LineWeight::Default + } + value => value, + } + }); + let layer_transparency = layer.map_or_else( + || acadrust::types::Transparency::from_percent(0.0), + |layer| { + if layer.transparency.is_by_layer() || layer.transparency.is_by_block() { + acadrust::types::Transparency::from_percent(0.0) + } else { + layer.transparency + } + }, + ); + + ConcreteGraphicAttributes { + color: match color { + acadrust::types::Color::ByLayer => layer_color, + acadrust::types::Color::ByBlock => acadrust::types::Color::Index(7), + value => value, + }, + linetype: if linetype.is_empty() || linetype.eq_ignore_ascii_case("ByLayer") { + layer_linetype.to_owned() + } else if linetype.eq_ignore_ascii_case("ByBlock") { + "Continuous".to_owned() + } else { + linetype.to_owned() + }, + lineweight: match lineweight { + acadrust::types::LineWeight::ByLayer => layer_lineweight, + acadrust::types::LineWeight::ByBlock => acadrust::types::LineWeight::Default, + value => value, + }, + transparency: if transparency.is_by_layer() { + layer_transparency + } else if transparency.is_by_block() { + acadrust::types::Transparency::from_percent(0.0) + } else { + transparency + }, + } +} + +fn valid_layer_name(name: &str) -> bool { + !name.is_empty() + && name.chars().count() <= 255 + && !name.chars().any(|character| { + matches!( + character, + '<' | '>' | '/' | '\\' | '"' | ':' | ';' | '?' | '*' | '|' | '=' | '`' + ) + }) +} + impl OpenCADStudio { + fn graphic_attribute_handles(&self, tab: usize) -> Vec { + let selected = self.tabs[tab].scene.selected_handles_in_order(); + let document = &self.tabs[tab].scene.document; + let mut seen = rustc_hash::FxHashSet::default(); + crate::ui::window::graphic_attributes::line_handles(document, &selected) + .into_iter() + .chain(crate::ui::window::graphic_attributes::fill_handles( + document, &selected, + )) + .filter(|handle| seen.insert(*handle)) + .collect() + } + + fn current_graphic_attributes(&self, tab: usize) -> ConcreteGraphicAttributes { + let document = &self.tabs[tab].scene.document; + let handles = self.graphic_attribute_handles(tab); + let draw_depth = self.tabs[tab].scene.draw_depth_map(); + let entity = handles + .iter() + .filter_map(|handle| { + document.get_entity(*handle).map(|entity| { + let depth = draw_depth + .get(&handle.value()) + .map_or(0.0, |value| value[0]); + (depth, entity) + }) + }) + .max_by(|(left, _), (right, _)| left.total_cmp(right)) + .map(|(_, entity)| entity); + + if let Some(entity) = entity { + let common = entity.common(); + return concrete_graphic_attributes( + common.color, + &common.linetype, + common.line_weight, + common.transparency, + document.layers.get(&common.layer), + ); + } + + let header = &document.header; + let layer_name = if header.current_layer_name.is_empty() { + self.tabs[tab].active_layer.as_str() + } else { + header.current_layer_name.as_str() + }; + concrete_graphic_attributes( + header.current_entity_color, + &header.current_linetype_name, + acadrust::types::LineWeight::from_value(header.current_line_weight), + document.current_entity_transparency(), + document.layers.get(layer_name), + ) + } + + pub(super) fn on_graphic_attributes_set_all(&mut self, by_layer: bool) -> iced::Task { + self.graphic_attributes_header_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + let (color, linetype, lineweight, transparency) = if by_layer { + ( + acadrust::types::Color::ByLayer, + "ByLayer", + acadrust::types::LineWeight::ByLayer, + acadrust::types::Transparency::BY_LAYER, + ) + } else { + ( + acadrust::types::Color::ByBlock, + "ByBlock", + acadrust::types::LineWeight::ByBlock, + acadrust::types::Transparency::BY_BLOCK, + ) + }; + if selected.is_empty() { + let header = &mut self.tabs[i].scene.document.header; + header.current_entity_color = color; + header.current_linetype_name = linetype.to_owned(); + header.current_linetype_handle = acadrust::Handle::NULL; + header.current_line_weight = lineweight.value(); + self.tabs[i] + .scene + .document + .set_current_entity_transparency(transparency); + self.tabs[i].dirty = true; + self.sync_ribbon_from_selection(); + self.refresh_properties(); + return iced::Task::none(); + } + + let handles: Vec<_> = self + .graphic_attribute_handles(i) + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + self.apply_property_op(i, "Graphic Attributes", &handles, |app, handle| { + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_color(entity, color); + crate::scene::view::dispatch::apply_common_prop(entity, "linetype", linetype); + crate::scene::view::dispatch::apply_line_weight(entity, lineweight); + entity.common_mut().transparency = transparency; + } + }); + iced::Task::none() + } + + pub(super) fn on_graphic_attributes_remove_references(&mut self) -> iced::Task { + self.graphic_attributes_header_menu_open = false; + let i = self.active_tab; + let selected = self.tabs[i].scene.selected_handles_in_order(); + if selected.is_empty() { + let values = self.current_graphic_attributes(i); + let linetype_handle = self.tabs[i] + .scene + .document + .line_types + .iter() + .find(|linetype| linetype.name.eq_ignore_ascii_case(&values.linetype)) + .map_or(acadrust::Handle::NULL, |linetype| linetype.handle); + let header = &mut self.tabs[i].scene.document.header; + header.current_entity_color = values.color; + header.current_linetype_name = values.linetype; + header.current_linetype_handle = linetype_handle; + header.current_line_weight = values.lineweight.value(); + self.tabs[i] + .scene + .document + .set_current_entity_transparency(values.transparency); + self.tabs[i].dirty = true; + self.sync_ribbon_from_selection(); + self.refresh_properties(); + return iced::Task::none(); + } + + let handles: Vec<_> = self + .graphic_attribute_handles(i) + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + let values: rustc_hash::FxHashMap<_, _> = handles + .iter() + .filter_map(|handle| { + let entity = self.tabs[i].scene.document.get_entity(*handle)?; + let common = entity.common(); + Some(( + *handle, + concrete_graphic_attributes( + common.color, + &common.linetype, + common.line_weight, + common.transparency, + self.tabs[i].scene.document.layers.get(&common.layer), + ), + )) + }) + .collect(); + self.apply_property_op(i, "Graphic Attributes", &handles, |app, handle| { + let Some(values) = values.get(&handle) else { + return; + }; + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::view::dispatch::apply_color(entity, values.color); + crate::scene::view::dispatch::apply_common_prop( + entity, + "linetype", + &values.linetype, + ); + crate::scene::view::dispatch::apply_line_weight(entity, values.lineweight); + entity.common_mut().transparency = values.transparency; + } + }); + iced::Task::none() + } + + pub(crate) fn create_graphic_attributes_layer( + &mut self, + raw_name: &str, + ) -> iced::Task { + let i = self.active_tab; + let name = raw_name.trim(); + if !valid_layer_name(name) { + self.command_line + .push_error("Graphic Attributes: enter a valid layer name without <>/\\\":;?*|=`."); + return iced::Task::none(); + } + if self.tabs[i] + .scene + .document + .layers + .iter() + .any(|layer| layer.name.eq_ignore_ascii_case(name)) + { + self.command_line.push_error( + crate::tf!("Graphic Attributes: layer \"{name}\" already exists.").as_ref(), + ); + return iced::Task::none(); + } + + let values = self.current_graphic_attributes(i); + let layer_name = name.to_owned(); + let undo = self.begin_layer_undo( + i, + "Create layer with active settings", + std::slice::from_ref(&layer_name), + ); + let mut layer = acadrust::tables::Layer::new(name); + layer.handle = self.tabs[i].scene.document.allocate_handle(); + layer.color = values.color; + layer.line_type = values.linetype; + layer.line_weight = values.lineweight; + layer.transparency = values.transparency; + use acadrust::tables::Table; + if self.tabs[i].scene.document.layers.add(layer).is_err() { + self.command_line + .push_error("Graphic Attributes: the layer could not be created."); + return iced::Task::none(); + } + self.tabs[i].dirty = true; + self.commit_layer_undo(i, undo); + self.refresh_layer_panel(); + self.command_line.push_output( + crate::tf!("Created layer \"{name}\" with the active Graphic Attributes settings.") + .as_ref(), + ); + iced::Task::none() + } + pub(super) fn open_gradient_editor(&mut self) { use crate::ui::window::gradient_editor::GradientEditorState; let i = self.active_tab; @@ -601,7 +918,7 @@ impl OpenCADStudio { } pub(super) fn on_line_linetype_scale(&mut self, scale: f64) -> iced::Task { - let scale = scale.clamp(0.01, 1000.0); + let scale = ((scale * 10.0).round() / 10.0).clamp(0.1, 1000.0); let i = self.active_tab; let selected = self.tabs[i].scene.selected_handles_in_order(); if selected.is_empty() { @@ -1278,6 +1595,7 @@ impl OpenCADStudio { } PanelId::GraphicAttributes => { self.show_graphic_attributes = false; + self.graphic_attributes_header_menu_open = false; self.graphic_attribute_menu_open = false; } } @@ -2270,6 +2588,17 @@ mod tests { .abs() < f64::EPSILON ); + let _ = app.update(Message::LineLinetypeScaleChanged(1.2000000000000002)); + assert_eq!( + app.tabs[i] + .scene + .document + .get_entity(line) + .expect("line") + .common() + .linetype_scale, + 1.2 + ); let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.2)); assert!( (app.tabs[i] @@ -2433,6 +2762,84 @@ mod tests { ); } + #[test] + fn graphic_attributes_bulk_modes_and_remove_references_preserve_visible_values() { + let mut app = fresh(); + let i = app.active_tab; + { + let layer = app.tabs[i] + .scene + .document + .layers + .get_mut("0") + .expect("default layer"); + layer.color = acadrust::types::Color::Index(3); + layer.line_type = "Continuous".to_string(); + layer.line_weight = acadrust::types::LineWeight::Value(50); + layer.transparency = acadrust::types::Transparency::from_percent(0.25); + } + let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::new())); + app.tabs[i].scene.select_entity(handle, false); + + let _ = app.on_graphic_attributes_set_all(false); + let common = app.tabs[i] + .scene + .document + .get_entity(handle) + .expect("line") + .common(); + assert_eq!(common.color, acadrust::types::Color::ByBlock); + assert_eq!(common.linetype, "ByBlock"); + assert_eq!(common.line_weight, acadrust::types::LineWeight::ByBlock); + assert!(common.transparency.is_by_block()); + + let _ = app.on_graphic_attributes_set_all(true); + let _ = app.on_graphic_attributes_remove_references(); + let common = app.tabs[i] + .scene + .document + .get_entity(handle) + .expect("line") + .common(); + assert_eq!(common.color, acadrust::types::Color::Index(3)); + assert_eq!(common.linetype, "Continuous"); + assert_eq!(common.line_weight, acadrust::types::LineWeight::Value(50)); + assert!((common.transparency.as_percent() - 0.25).abs() < 0.01); + } + + #[test] + fn graphic_attributes_create_layer_prompt_keeps_spaces_and_active_settings() { + let mut app = fresh(); + let i = app.active_tab; + app.tabs[i].scene.deselect_all(); + let _ = app.on_line_color(acadrust::types::Color::Index(2)); + let _ = app.on_line_linetype("Continuous".to_string()); + let _ = app.on_line_lineweight(acadrust::types::LineWeight::Value(35)); + let _ = app.on_line_transparency(acadrust::types::Transparency::from_percent(0.3)); + + let _ = app.update(Message::GraphicAttributesCreateLayer); + assert_eq!( + app.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.name()), + Some("GRAPHICATTRIBUTELAYER") + ); + let _ = app.update(Message::CommandInput("Graphic Settings".to_string())); + let _ = app.update(Message::CommandFinalize); + + let layer = app.tabs[i] + .scene + .document + .layers + .get("Graphic Settings") + .expect("created layer"); + assert_eq!(layer.color, acadrust::types::Color::Index(2)); + assert_eq!(layer.line_type, "Continuous"); + assert_eq!(layer.line_weight, acadrust::types::LineWeight::Value(35)); + assert!((layer.transparency.as_percent() - 0.3).abs() < 0.01); + } + /// A foreign document: the fresh scene's document plus one model-space LINE. fn foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument { let mut doc = app.tabs[app.active_tab].scene.document.clone(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a16a13008..2047d0067 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -9577,6 +9577,29 @@ impl OpenCADStudio { Message::PlotDialogOpen => self.on_plot_dialog_open(), Message::PlotDlg(m) => self.on_plot_dlg(m), Message::BlockPalette(m) => self.on_block_palette(m), + Message::ToggleGraphicAttributesHeaderMenu => { + self.graphic_attributes_header_menu_open ^= true; + Task::none() + } + Message::CloseGraphicAttributesHeaderMenu => { + self.graphic_attributes_header_menu_open = false; + Task::none() + } + Message::GraphicAttributesSetAllByLayer => self.on_graphic_attributes_set_all(true), + Message::GraphicAttributesSetAllByBlock => self.on_graphic_attributes_set_all(false), + Message::GraphicAttributesRemoveReferences => { + self.on_graphic_attributes_remove_references() + } + Message::GraphicAttributesCreateLayer => { + self.graphic_attributes_header_menu_open = false; + let command = crate::command::FreeTextValuePromptCommand::new( + "GRAPHICATTRIBUTELAYER", + "Enter a name for the new layer:", + ); + self.command_line.push_info(&command.prompt()); + self.tabs[self.active_tab].active_cmd = Some(Box::new(command)); + self.focus_cmd_input() + } Message::ToggleGraphicAttributeDropdown => { self.graphic_attribute_menu_open ^= true; self.line_color_menu_open = false; diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 3cfddacae..add93fa4d 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2953,6 +2953,7 @@ impl OpenCADStudio { width, auto_collapse, side, + self.graphic_attributes_header_menu_open, self.graphic_attribute_menu_open, self.line_color_menu_open, self.line_linetype_menu_open, diff --git a/src/command.rs b/src/command.rs index 8719ce9d7..bfa4c19eb 100644 --- a/src/command.rs +++ b/src/command.rs @@ -347,6 +347,46 @@ impl CadCommand for ValuePromptCommand { } } +/// One-value prompt that preserves spaces and letter casing. This is intended +/// for symbol names entered through the command line, where a space belongs to +/// the value instead of submitting the current token. +pub struct FreeTextValuePromptCommand { + name: &'static str, + prompt: &'static str, +} + +impl FreeTextValuePromptCommand { + pub fn new(name: &'static str, prompt: &'static str) -> Self { + Self { name, prompt } + } +} + +impl CadCommand for FreeTextValuePromptCommand { + fn name(&self) -> &'static str { + self.name + } + + fn prompt(&self) -> String { + crate::t!(self.prompt).into_owned() + } + + fn input_kind(&self) -> InputKind { + InputKind::FreeText + } + + fn on_text_input(&mut self, text: &str) -> Option { + Some(CmdResult::Dispatch(format!("{} {}", self.name, text.trim()))) + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Dispatch(format!("{} ", self.name)) + } +} + /// Interactive pick for `UCS FACE` and `UCS OBJECT`. /// /// Hands the picked entity to the inline `UCS FACE ` / diff --git a/src/ui/window/graphic_attributes.rs b/src/ui/window/graphic_attributes.rs index 4ec42d4ea..8758985f1 100644 --- a/src/ui/window/graphic_attributes.rs +++ b/src/ui/window/graphic_attributes.rs @@ -745,6 +745,7 @@ pub fn view<'a>( width: f32, auto_collapse: bool, side: crate::app::config::DockSide, + header_menu_open: bool, menu_open: bool, line_color_menu_open: bool, line_linetype_menu_open: bool, @@ -758,6 +759,57 @@ pub fn view<'a>( gradient_editor: Option<&'a crate::ui::window::gradient_editor::GradientEditorState>, gradient_color_picker_open: bool, ) -> Element<'a, Message> { + let menu_button = button(crate::ui::icons::themed_secondary( + crate::ui::icons::MENU, + 12.0, + )) + .on_press(Message::ToggleGraphicAttributesHeaderMenu) + .style(move |theme: &Theme, status| { + if header_menu_open { + button::primary(theme, status) + } else { + button::subtle(theme, status) + } + }) + .padding([3, 5]); + let menu: Element<'a, Message> = if header_menu_open { + let item = |label: &'static str, message| { + button(text(label).size(11)) + .on_press(message) + .style(crate::ui::color_select::list_row_style) + .width(Fill) + .padding([4, 8]) + }; + let popup = container(column![ + item( + "Set all attributes ByLayer", + Message::GraphicAttributesSetAllByLayer, + ), + item( + "Set all attributes ByBlock", + Message::GraphicAttributesSetAllByBlock, + ), + item( + "Remove ByLayer / ByBlock", + Message::GraphicAttributesRemoveReferences, + ), + item( + "Create Layer with active settings", + Message::GraphicAttributesCreateLayer, + ), + ]) + .style(crate::ui::color_select::popup_panel_style) + .padding(2); + iced_aw::DropDown::new(menu_button, popup, true) + .width(Length::Fixed(220.0)) + .height(Length::Shrink) + .alignment(popup_alignment(side)) + .offset(2.0) + .on_dismiss(Message::CloseGraphicAttributesHeaderMenu) + .into() + } else { + menu_button.into() + }; let pin_icon = if auto_collapse { crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) } else { @@ -793,6 +845,7 @@ pub fn view<'a>( row![ text(crate::t!("Graphic Attributes")).size(12), Space::new().width(Fill), + menu, pin, close ] @@ -979,15 +1032,26 @@ pub fn view<'a>( )); } if let (Some(scale), Some(lineweight)) = (linetype_scale, lineweight) { - let scale_text = if scale.varies { - String::new() + let scale_field: Element<'_, Message> = if scale.varies { + text_input("*VARIES*", "") + .size(crate::ui::ROW_H * 0.42) + .padding([5, 5]) + .style(crate::ui::properties::combo_input_style) + .width(Fill) + .into() } else { - format!("{:.2}", scale.value) - }; - let mut scale_input = text_input("*VARIES*", &scale_text) - .size(crate::ui::ROW_H * 0.42) - .padding([3, 5]) - .style(move |theme: &Theme, status| { + let scale_value = ((scale.value * 10.0).round() / 10.0).clamp(0.1, 1000.0); + let mut input = iced_aw::number_input( + &scale_value, + 0.1..=1000.0, + |value: f64| { + Message::LineLinetypeScaleChanged((value * 10.0).round() / 10.0) + }, + ) + .step(0.1) + .set_size(crate::ui::ROW_H * 0.42) + .padding([5, 5]) + .input_style(move |theme: &Theme, status| { let mut style = crate::ui::properties::combo_input_style(theme, status); if continuous_linetype { let disabled = theme.palette().background.base.text.scale_alpha(0.42); @@ -996,29 +1060,26 @@ pub fn view<'a>( } style }) - .on_submit(Message::Noop) + .style(move |theme: &Theme, status| { + let text = theme.palette().background.base.text; + iced_aw::number_input::number_input::Style { + button_background: None, + icon_color: if continuous_linetype + || status == iced_aw::style::Status::Disabled + { + text.scale_alpha(0.42) + } else { + text + }, + } + }) .width(Fill); - if !continuous_linetype { - scale_input = scale_input.on_input(Message::LineLinetypeScaleInput); - } - let step = |up: bool| { - let icon: Element<'_, Message> = if up { - crate::ui::icons::themed_arrow_up(7.0) - } else { - crate::ui::icons::themed_arrow_down(7.0) - }; - let button = button(icon) - .style(button::subtle) - .height(10) - .padding([0, 3]); if continuous_linetype { - button + input = input.on_input_maybe(None:: Message>); } else { - let delta = if up { 0.1 } else { -0.1 }; - button.on_press(Message::LineLinetypeScaleChanged( - (scale.value + delta).clamp(0.01, 1000.0), - )) - } + input = input.on_submit(Message::Noop); + } + input.into() }; let scale_icon: Element<'_, Message> = if continuous_linetype { crate::ui::icons::themed_disabled( @@ -1031,28 +1092,6 @@ pub fn view<'a>( PALETTE_CONTROL_ICON_SIZE, ) }; - let spinner_divider = container(Space::new()) - .width(Fill) - .height(1) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.neutral.color)), - ..Default::default() - }); - let spinner = container(column![step(true), spinner_divider, step(false)].spacing(0)) - .height(COMPACT_BUTTON_HEIGHT) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - border: Border { - color: theme.palette().background.neutral.color, - width: 1.0, - radius: 2.0.into(), - }, - ..Default::default() - }); - let scale_field = row![scale_input, spinner] - .spacing(0) - .align_y(iced::Center) - .width(Fill); let scale = row![scale_icon, scale_field] .spacing(3) .align_y(iced::Center)