From 85e2dda6ba0aad3f5bccf1a3ec8b8c3e3abb9f19 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 29 Sep 2026 19:06:29 +0200 Subject: [PATCH] Improve graphic attribute handling for open boundaries and varied values --- src/app/mod.rs | 6 + src/app/update/dialog.rs | 148 +++++++++++++- src/app/update/mod.rs | 41 ++++ src/ui/properties.rs | 3 + src/ui/window/graphic_attributes.rs | 304 +++++++++++++++++++++------- 5 files changed, 427 insertions(+), 75 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 6468edd00..a172d776e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -780,6 +780,11 @@ pub(super) struct OpenCADStudio { fill_transparency_menu_open: bool, gradient_editor: Option, hatch_editor_handles: Option>, + pending_graphic_fill_close: Option<( + crate::ui::window::graphic_attributes::GraphicAttribute, + Vec, + Vec, + )>, /// Docked Insert Block panel visibility. pub(crate) show_block_palette: bool, /// Docked External References panel visibility (EXTERNALREFERENCES). @@ -4167,6 +4172,7 @@ impl OpenCADStudio { fill_transparency_menu_open: false, gradient_editor: None, hatch_editor_handles: None, + pending_graphic_fill_close: None, show_block_palette: false, show_external_references: false, show_browser: false, diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 76884f72f..2e0756498 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -90,6 +90,46 @@ fn set_hatch_graphic_attribute( } } +fn is_open_fill_boundary(entity: &acadrust::EntityType) -> bool { + match entity { + acadrust::EntityType::LwPolyline(polyline) => { + !polyline.is_closed && polyline.vertices.len() >= 3 + } + acadrust::EntityType::Polyline(polyline) => { + !polyline.flags.is_closed() && polyline.vertices.len() >= 3 + } + acadrust::EntityType::Polyline2D(polyline) => { + !polyline.is_closed() && polyline.vertices.len() >= 3 + } + acadrust::EntityType::Polyline3D(polyline) => { + !polyline.flags.closed && polyline.vertices.len() >= 3 + } + _ => false, + } +} + +fn close_fill_boundary(entity: &mut acadrust::EntityType) -> bool { + match entity { + acadrust::EntityType::LwPolyline(polyline) if !polyline.is_closed => { + polyline.is_closed = true; + true + } + acadrust::EntityType::Polyline(polyline) if !polyline.flags.is_closed() => { + polyline.flags.set_closed(true); + true + } + acadrust::EntityType::Polyline2D(polyline) if !polyline.is_closed() => { + polyline.flags.set_closed(true); + true + } + acadrust::EntityType::Polyline3D(polyline) if !polyline.flags.closed => { + polyline.flags.closed = true; + true + } + _ => false, + } +} + impl OpenCADStudio { pub(super) fn open_gradient_editor(&mut self) { use crate::ui::window::gradient_editor::GradientEditorState; @@ -195,6 +235,49 @@ impl OpenCADStudio { pub(super) fn on_graphic_attribute( &mut self, value: crate::ui::window::graphic_attributes::GraphicAttribute, + ) -> iced::Task { + self.pending_graphic_fill_close = None; + self.on_graphic_attribute_impl(value, true) + } + + pub(super) fn on_graphic_fill_close_response( + &mut self, + close: bool, + ) -> iced::Task { + let Some((value, handles, _)) = self.pending_graphic_fill_close.take() else { + return iced::Task::none(); + }; + if close { + let i = self.active_tab; + let unlocked: Vec<_> = handles + .into_iter() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .collect(); + if !unlocked.is_empty() { + let pending = self.begin_undo(i, "Close fill boundaries", unlocked.len(), true); + for handle in unlocked { + let Some(mut entity) = + self.tabs[i].scene.document.get_entity(handle).cloned() + else { + continue; + }; + if close_fill_boundary(&mut entity) { + self.tabs[i].scene.update_entity(entity); + } + } + self.tabs[i].dirty = true; + if let Some(pending) = pending { + self.commit_undo_delta(i, pending); + } + } + } + self.on_graphic_attribute_impl(value, false) + } + + fn on_graphic_attribute_impl( + &mut self, + value: crate::ui::window::graphic_attributes::GraphicAttribute, + ask_to_close: bool, ) -> iced::Task { use crate::scene::model::hatch_model::{GradientKind, HatchPattern}; use crate::ui::window::graphic_attributes::GraphicAttribute; @@ -224,6 +307,22 @@ impl OpenCADStudio { .push_info(crate::t!("Select one or more closed objects first.").as_ref()); return iced::Task::none(); } + let open_boundaries: rustc_hash::FxHashSet<_> = selected + .iter() + .copied() + .filter(|handle| { + self.tabs[i] + .scene + .document + .get_entity(*handle) + .is_some_and(is_open_fill_boundary) + }) + .collect(); + if ask_to_close && value != GraphicAttribute::None && !open_boundaries.is_empty() { + self.pending_graphic_fill_close = + Some((value, open_boundaries.iter().copied().collect(), selected.clone())); + return iced::Task::none(); + } let selected_handles: rustc_hash::FxHashSet<_> = selected.iter().copied().collect(); let (associated, new_hatches, updated_hatches, skipped) = if value == GraphicAttribute::None { @@ -331,7 +430,9 @@ impl OpenCADStudio { hatches.push(hatch); changed_boundaries.insert(*handle); } else { - skipped += 1; + if !open_boundaries.contains(handle) { + skipped += 1; + } } } // Replace only fills belonging to boundaries that successfully @@ -367,11 +468,10 @@ impl OpenCADStudio { } if skipped > 0 { let message = if skipped == 1 { - "Graphic Attributes: 1 object was not changed because it is not closed or supported." - .to_owned() + "Graphic Attributes: 1 unsupported object was not changed.".to_owned() } else { format!( - "Graphic Attributes: {skipped} objects were not changed because they are not closed or supported." + "Graphic Attributes: {skipped} unsupported objects were not changed." ) }; self.command_line.push_info(&message); @@ -2007,6 +2107,12 @@ mod tests { matches!(entity, EntityType::Hatch(_)).then_some(entity.common().handle) }) .expect("gradient hatch"); + let associated_lines = crate::ui::window::graphic_attributes::line_handles( + &app.tabs[i].scene.document, + &[selected_hatch], + ); + assert_eq!(associated_lines.len(), 1); + assert!(boundaries.contains(&associated_lines[0])); assert_eq!( crate::ui::window::graphic_attributes::current( &app.tabs[i].scene.document, @@ -2261,6 +2367,40 @@ mod tests { assert_eq!(app.tabs[i].history.undo_stack.len(), undo_before + 1); } + #[test] + fn graphic_attributes_can_close_an_open_boundary_from_the_command_prompt() { + use crate::ui::window::graphic_attributes::GraphicAttribute; + use acadrust::entities::LwPolyline; + use acadrust::types::Vector2; + + let mut app = fresh(); + let i = app.active_tab; + let mut polyline = LwPolyline::new(); + for (x, y) in [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] { + polyline.add_point(Vector2::new(x, y)); + } + let boundary = app.tabs[i] + .scene + .add_entity(EntityType::LwPolyline(polyline)); + app.tabs[i].scene.select_entity(boundary, false); + + let _ = app.update(Message::GraphicAttributeChanged(GraphicAttribute::Solid)); + assert!(app.pending_graphic_fill_close.is_some()); + assert!(!app.tabs[i].scene.document.entities().any(|entity| { + matches!(entity, EntityType::Hatch(_)) + })); + + let _ = app.update(Message::CommandOptionPick("Y".to_string())); + assert!(app.pending_graphic_fill_close.is_none()); + assert!(matches!( + app.tabs[i].scene.document.get_entity(boundary), + Some(EntityType::LwPolyline(polyline)) if polyline.is_closed + )); + assert!(app.tabs[i].scene.document.entities().any(|entity| { + matches!(entity, EntityType::Hatch(hatch) if hatch.is_solid && hatch.is_associative) + })); + } + #[test] fn graphic_line_controls_update_creation_defaults_without_a_selection() { let mut app = fresh(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 681cd30f3..a16a13008 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -432,6 +432,18 @@ impl OpenCADStudio { }) { self.hatch_editor_handles = None; } + if self + .pending_graphic_fill_close + .as_ref() + .is_some_and(|(_, _, handles)| { + let selected = + self.tabs[self.active_tab].scene.selected_handles_in_order(); + selected.len() != handles.len() + || selected.iter().any(|handle| !handles.contains(handle)) + }) + { + self.pending_graphic_fill_close = None; + } self.show_next_startup_modal(); self.sync_open_command_history(); // Close the document-level first-touch transaction started by @@ -445,6 +457,19 @@ impl OpenCADStudio { .active_cmd .as_ref() .map(|c| c.prompt()) + .or_else(|| { + self.pending_graphic_fill_close.as_ref().map(|(_, handles, _)| { + if handles.len() == 1 { + "The selected object is open. Close it before creating the fill? [Yes / No]" + .to_string() + } else { + format!( + "{} selected objects are open. Close them before creating the fill? [Yes / No]", + handles.len() + ) + } + }) + }) .or_else(|| self.pending_pick_label()); self.command_line.set_step_prompt(prompt); // Mirror the step's clickable options so they render as buttons (#304). @@ -452,6 +477,14 @@ impl OpenCADStudio { .active_cmd .as_ref() .map(|c| c.options()) + .or_else(|| { + self.pending_graphic_fill_close.as_ref().map(|_| { + vec![ + crate::command::CmdOption::new("Yes", "Y"), + crate::command::CmdOption::new("No", "N"), + ] + }) + }) .unwrap_or_default(); self.command_line.set_step_options(opts); // Persist UI preferences whenever a toggle changes them (issue #68). @@ -2406,6 +2439,14 @@ impl OpenCADStudio { // finishes the step like Enter. (#304) self.command_line.input.clear(); self.command_line.close_history(); + if self.pending_graphic_fill_close.is_some() { + if kw.eq_ignore_ascii_case("Y") || kw.eq_ignore_ascii_case("YES") { + return self.on_graphic_fill_close_response(true); + } + if kw.eq_ignore_ascii_case("N") || kw.eq_ignore_ascii_case("NO") { + return self.on_graphic_fill_close_response(false); + } + } if kw.is_empty() { return self.feed_command(crate::command::StepInput::Enter); } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 7e95a2880..0b85eb8c3 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -55,6 +55,9 @@ pub fn linetype_display_name(name: &str) -> String { impl fmt::Display for LinetypeItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.name.is_empty() && !self.art.is_empty() { + return f.write_str(&self.art); + } let name = linetype_display_name(&self.name); if self.art.is_empty() { write!(f, "{name}") diff --git a/src/ui/window/graphic_attributes.rs b/src/ui/window/graphic_attributes.rs index f4e42a60a..4ec42d4ea 100644 --- a/src/ui/window/graphic_attributes.rs +++ b/src/ui/window/graphic_attributes.rs @@ -26,14 +26,16 @@ enum PropertyMode { ByLayer, ByBlock, Custom, + Varies, } impl PropertyMode { fn button_label(self) -> &'static str { match self { - Self::ByLayer => "[L]", - Self::ByBlock => "[B]", - Self::Custom => "[C]", + Self::ByLayer => "L", + Self::ByBlock => "B", + Self::Custom => "C", + Self::Varies => "V", } } } @@ -54,6 +56,7 @@ struct LinetypeScaleInfo { } const PALETTE_CONTROL_ICON_SIZE: f32 = 20.0; +const COMPACT_BUTTON_HEIGHT: f32 = 22.0; const TRANSPARENCY_ICON: &[u8] = include_bytes!("../../../assets/icons/attributes/transparency.svg"); const LINEWEIGHT_ICON: &[u8] = include_bytes!("../../../assets/icons/attributes/lineweight.svg"); @@ -156,15 +159,28 @@ pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec Vec { - selected - .iter() - .copied() - .filter(|handle| { - document - .get_entity(*handle) - .is_some_and(|entity| !matches!(entity, EntityType::Hatch(_))) - }) - .collect() + let mut handles = Vec::new(); + let mut seen = rustc_hash::FxHashSet::default(); + for handle in selected { + match document.get_entity(*handle) { + Some(EntityType::Hatch(hatch)) if hatch.is_associative => { + for boundary in hatch.paths.iter().flat_map(|path| &path.boundary_handles) { + if seen.insert(*boundary) + && document + .get_entity(*boundary) + .is_some_and(|entity| !matches!(entity, EntityType::Hatch(_))) + { + handles.push(*boundary); + } + } + } + Some(entity) if !matches!(entity, EntityType::Hatch(_)) && seen.insert(*handle) => { + handles.push(*handle); + } + _ => {} + } + } + handles } fn top_handle( @@ -276,6 +292,33 @@ fn lineweight_info( Some(LineweightInfo { value, varies }) } +fn effective_lineweight( + document: &CadDocument, + handles: &[Handle], + draw_depth: &rustc_hash::FxHashMap, + stored: acadrust::types::LineWeight, +) -> acadrust::types::LineWeight { + if !matches!(stored, acadrust::types::LineWeight::ByLayer) { + return stored; + } + let layer_name = top_handle(handles, draw_depth) + .and_then(|handle| document.get_entity(handle)) + .map(|entity| entity.common().layer.as_str()) + .unwrap_or_else(|| { + if document.header.current_layer_name.is_empty() { + "0" + } else { + document.header.current_layer_name.as_str() + } + }); + document + .layers + .get(layer_name) + .map_or(acadrust::types::LineWeight::Default, |layer| { + layer.line_weight + }) +} + fn linetype_scale_info( document: &CadDocument, handles: &[Handle], @@ -364,7 +407,12 @@ fn linetype_preview_art( } else { logical_name }; - document + if effective_name.eq_ignore_ascii_case("Continuous") + || effective_name.eq_ignore_ascii_case("Solid") + { + return "_".repeat(80); + } + let art = document .line_types .iter() .find(|line_type| line_type.name.eq_ignore_ascii_case(effective_name)) @@ -375,7 +423,26 @@ fn linetype_preview_art( crate::io::linetypes::extract_pattern(&line_type.description) } }) - .unwrap_or_default() + .unwrap_or_default(); + let pattern_start = art.split_whitespace().find_map(|part| { + (part + .chars() + .filter(|character| matches!(character, '_' | '.' | '-' | '/' | '\\' | '|' | '~')) + .count() + >= 2) + .then(|| art.find(part)) + .flatten() + }); + let pattern = pattern_start.map_or(art.as_str(), |start| &art[start..]); + if pattern.is_empty() { + return String::new(); + } + let mut full_width = pattern.to_string(); + while full_width.chars().count() < 80 { + full_width.push(' '); + full_width.push_str(pattern); + } + full_width } fn top_fill_hatch<'a>( @@ -418,6 +485,21 @@ fn top_fill_hatch<'a>( .map(|(_, hatch)| hatch) } +fn gradients_vary(document: &CadDocument, selected: &[Handle]) -> bool { + let mut gradients = fill_handles(document, selected) + .into_iter() + .filter_map(|handle| match document.get_entity(handle) { + Some(EntityType::Hatch(hatch)) if hatch.gradient_color.enabled => { + Some(&hatch.gradient_color) + } + _ => None, + }); + let Some(first) = gradients.next() else { + return false; + }; + gradients.any(|gradient| gradient != first) +} + fn popup_alignment(side: crate::app::config::DockSide) -> iced_aw::drop_down::Alignment { match side { crate::app::config::DockSide::Left => iced_aw::drop_down::Alignment::BottomEnd, @@ -425,6 +507,19 @@ fn popup_alignment(side: crate::app::config::DockSide) -> iced_aw::drop_down::Al } } +fn compact_button_content(label: &'static str) -> Element<'static, Message> { + container(text(label).size(crate::ui::ROW_H * 0.42)) + .width(12) + .height(Fill) + .align_x(iced::Center) + .align_y(iced::Center) + .into() +} + +fn mode_button_content(mode: PropertyMode) -> Element<'static, Message> { + compact_button_content(mode.button_label()) +} + fn by_mode_menu<'a>( side: crate::app::config::DockSide, mode: PropertyMode, @@ -434,7 +529,7 @@ fn by_mode_menu<'a>( by_layer: Message, by_block: Message, ) -> Element<'a, Message> { - let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + let more = button(mode_button_content(mode)) .on_press(toggle) .style(move |theme: &Theme, status| { if open { @@ -443,8 +538,8 @@ fn by_mode_menu<'a>( button::secondary(theme, status) } }) - .height(crate::ui::ROW_H) - .padding([2, 7]); + .height(COMPACT_BUTTON_HEIGHT) + .padding([1, 3]); if !open { return more.into(); } @@ -483,7 +578,7 @@ fn color_control<'a>( Some("*VARIES*") } else { match info.color { - acadrust::types::Color::ByLayer => Some("ByLayer"), + acadrust::types::Color::ByLayer => None, acadrust::types::Color::ByBlock => Some("ByBlock"), _ => None, } @@ -498,7 +593,7 @@ fn color_control<'a>( let color = button(label.unwrap_or_else(|| container(Space::new()).width(Fill).height(Fill))) .on_press(Message::OpenColorWindow(target.clone(), info.color)) .width(Fill) - .height(crate::ui::ROW_H) + .height(COMPACT_BUTTON_HEIGHT) .padding(0) .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(info.display)), @@ -521,7 +616,7 @@ fn color_control<'a>( ..Default::default() }); let mode = if info.varies { - PropertyMode::Custom + PropertyMode::Varies } else { match info.color { acadrust::types::Color::ByLayer => PropertyMode::ByLayer, @@ -529,7 +624,7 @@ fn color_control<'a>( _ => PropertyMode::Custom, } }; - let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + let more = button(mode_button_content(mode)) .on_press(toggle) .style(move |theme: &Theme, status| { if menu_open { @@ -538,8 +633,8 @@ fn color_control<'a>( button::secondary(theme, status) } }) - .height(crate::ui::ROW_H) - .padding([2, 7]); + .height(COMPACT_BUTTON_HEIGHT) + .padding([1, 3]); let more: Element<'a, Message> = if menu_open { let choice = |label, color| { button(text(label).size(11)) @@ -599,11 +694,11 @@ fn transparency_control<'a>( .align_y(iced::Center) .height(crate::ui::ROW_H); let mode = if info.varies { - PropertyMode::Custom + PropertyMode::Varies } else { info.mode }; - let more = button(text(mode.button_label()).size(crate::ui::ROW_H * 0.42)) + let more = button(mode_button_content(mode)) .on_press(toggle) .style(move |theme: &Theme, status| { if menu_open { @@ -613,7 +708,7 @@ fn transparency_control<'a>( } }) .height(crate::ui::ROW_H) - .padding([2, 7]); + .padding([1, 3]); let more: Element<'a, Message> = if menu_open { let row_button = |label, message| { button(text(label).size(11)) @@ -823,7 +918,9 @@ pub fn view<'a>( }); if let Some(info) = linetype.as_ref() { let selected = (!info.varies).then(|| crate::ui::properties::LinetypeItem { - name: info.value.clone(), + // An empty name makes only the preview art visible in the closed + // Graphic Attributes control. Menu items retain their full names. + name: String::new(), art: linetype_preview_art(document, &line_handles, draw_depth, &info.value), }); let combo = combo_box( @@ -844,7 +941,7 @@ pub fn view<'a>( let more = by_mode_menu( side, if info.varies { - PropertyMode::Custom + PropertyMode::Varies } else if info.value.eq_ignore_ascii_case("ByLayer") { PropertyMode::ByLayer } else if info.value.eq_ignore_ascii_case("ByBlock") { @@ -890,7 +987,15 @@ pub fn view<'a>( let mut scale_input = text_input("*VARIES*", &scale_text) .size(crate::ui::ROW_H * 0.42) .padding([3, 5]) - .style(crate::ui::properties::combo_input_style) + .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); + style.value = disabled; + style.placeholder = disabled; + } + style + }) .on_submit(Message::Noop) .width(Fill); if !continuous_linetype { @@ -902,7 +1007,10 @@ pub fn view<'a>( } else { crate::ui::icons::themed_arrow_down(7.0) }; - let button = button(icon).style(button::secondary).padding([0, 3]); + let button = button(icon) + .style(button::subtle) + .height(10) + .padding([0, 3]); if continuous_linetype { button } else { @@ -912,18 +1020,48 @@ pub fn view<'a>( )) } }; - let scale = row![ - crate::ui::icons::semantic(GraphicAttribute::Solid.icon(), PALETTE_CONTROL_ICON_SIZE), - scale_input, - column![step(true), step(false)].spacing(0), - ] + let scale_icon: Element<'_, Message> = if continuous_linetype { + crate::ui::icons::themed_disabled( + GraphicAttribute::Solid.icon(), + PALETTE_CONTROL_ICON_SIZE, + ) + } else { + crate::ui::icons::semantic( + GraphicAttribute::Solid.icon(), + 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) .width(Length::FillPortion(1)); - let selected = - (!lineweight.varies).then_some(crate::ui::properties::LwItem(lineweight.value)); + let selected = (!lineweight.varies).then_some(crate::ui::properties::LwItem( + effective_lineweight(document, &line_handles, draw_depth, lineweight.value), + )); let lineweight_mode = if lineweight.varies { - PropertyMode::Custom + PropertyMode::Varies } else { match lineweight.value { acadrust::types::LineWeight::ByLayer => PropertyMode::ByLayer, @@ -1038,12 +1176,25 @@ pub fn view<'a>( } else { Message::GradientEditorOpen }; - let preview = button( - image(crate::ui::window::gradient_editor::compact_preview(hatch)) - .width(Fill) - .height(Fill) - .content_fit(iced::ContentFit::Fill), - ) + let preview_image = image(crate::ui::window::gradient_editor::compact_preview(hatch)) + .width(Fill) + .height(Fill) + .content_fit(iced::ContentFit::Fill); + let preview_content: Element<'_, Message> = if gradients_vary(document, selected) { + iced::widget::stack![ + preview_image, + container(text("*VARIES*").size(10)) + .width(Fill) + .height(Fill) + .align_x(iced::Left) + .align_y(iced::Center) + .padding([0, 6]), + ] + .into() + } else { + preview_image.into() + }; + let preview = button(preview_content) .on_press(toggle.clone()) .width(Fill) .height(crate::ui::ROW_H) @@ -1063,7 +1214,7 @@ pub fn view<'a>( }, ..Default::default() }); - let edit = button(text("...").size(crate::ui::ROW_H * 0.42)) + let edit = button(compact_button_content("⋮")) .on_press(toggle) .style(move |theme: &Theme, status| { if gradient_editor.is_some() { @@ -1072,8 +1223,8 @@ pub fn view<'a>( button::secondary(theme, status) } }) - .height(crate::ui::ROW_H) - .padding([2, 7]); + .height(COMPACT_BUTTON_HEIGHT) + .padding([1, 3]); let edit = tooltip( edit, text("Edit gradient").size(10), @@ -1122,7 +1273,7 @@ pub fn view<'a>( Message::HatchEditorOpen }) .width(Fill) - .height(crate::ui::ROW_H) + .height(COMPACT_BUTTON_HEIGHT) .padding(0) .style(move |theme: &Theme, status| button::Style { background: Some(Background::Color(theme.palette().background.base.color)), @@ -1139,28 +1290,7 @@ pub fn view<'a>( }, ..Default::default() }); - let preview: Element<'_, Message> = if hatch_editor_open { - let popup = crate::ui::properties::hatch_pattern_picker_content( - hatch_pattern_search, - hatch_pattern_focus, - current_pattern, - "graphic-hatch-pattern-search", - Message::GraphicHatchPatternSearchChanged, - Message::GraphicHatchPatternConfirm, - Message::GraphicHatchPatternFocus, - Message::GraphicHatchPatternChanged, - ); - iced_aw::DropDown::new(preview, popup, true) - .width(Length::Fixed(348.0)) - .height(Length::Fixed(720.0)) - .alignment(popup_alignment(side)) - .offset(4.0) - .on_dismiss(Message::HatchEditorClose) - .into() - } else { - preview.into() - }; - let edit = button(text("...").size(crate::ui::ROW_H * 0.42)) + let edit = button(compact_button_content("⋮")) .on_press(if hatch_editor_open { Message::HatchEditorClose } else { @@ -1174,8 +1304,40 @@ pub fn view<'a>( } }) .height(crate::ui::ROW_H) - .padding([2, 7]); + .padding([1, 3]); let edit = tooltip(edit, text("Edit hatch").size(10), tooltip::Position::Bottom); + let edit: Element<'_, Message> = if hatch_editor_open { + let popup = crate::ui::properties::hatch_pattern_picker_content( + hatch_pattern_search, + hatch_pattern_focus, + current_pattern, + "graphic-hatch-pattern-search", + Message::GraphicHatchPatternSearchChanged, + Message::GraphicHatchPatternConfirm, + Message::GraphicHatchPatternFocus, + Message::GraphicHatchPatternChanged, + ); + let popup = container(popup) + .padding(1) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + color: theme.palette().primary.base.color, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }); + iced_aw::DropDown::new(edit, popup, true) + .width(Length::Fixed(352.0)) + .height(Length::Fixed(724.0)) + .alignment(popup_alignment(side)) + .offset(4.0) + .on_dismiss(Message::HatchEditorClose) + .into() + } else { + edit.into() + }; body = body.push(row![preview, edit].spacing(4).align_y(iced::Center)); } if current != GraphicAttribute::None {