feat(hatch): add visual pattern picker

Closes #566
This commit is contained in:
Hakan Seven 2026-07-29 15:46:53 +03:00
commit 6686141d3b
6 changed files with 448 additions and 51 deletions

View file

@ -1926,6 +1926,16 @@ pub enum Message {
PropVertexStep(i8),
/// User selected a hatch pattern from the pattern pick_list in Properties.
PropHatchPatternChanged(String),
/// Open or close the visual hatch-pattern picker.
PropHatchPatternPickerToggle(String),
/// Filter the visual hatch-pattern picker.
PropHatchPatternSearchChanged(String),
/// Move the visual pattern grid focus to a hovered card.
PropHatchPatternFocus(usize),
/// Move keyboard focus by one card or one two-column row.
PropHatchPatternNavigate(i8),
/// Select the keyboard-focused visual pattern card.
PropHatchPatternConfirm,
/// User selected a generic choice field in the Properties panel.
PropGeomChoiceChanged {
field: &'static str,

View file

@ -342,9 +342,6 @@ impl OpenCADStudio {
lineweight_combo: iced::widget::combo_box::State::new(
ui::properties::lw_options(),
),
hatch_pattern_combo: iced::widget::combo_box::State::new(
crate::scene::model::hatch_patterns::names(),
),
linetype_items,
..Default::default()
}
@ -1464,9 +1461,6 @@ impl OpenCADStudio {
title,
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
hatch_pattern_combo: iced::widget::combo_box::State::new(
crate::scene::model::hatch_patterns::names(),
),
lineweight_combo: iced::widget::combo_box::State::new(
ui::properties::lw_options(),
),
@ -1485,9 +1479,6 @@ impl OpenCADStudio {
lineweight_combo: iced::widget::combo_box::State::new(
ui::properties::lw_options(),
),
hatch_pattern_combo: iced::widget::combo_box::State::new(
crate::scene::model::hatch_patterns::names(),
),
linetype_items,
..Default::default()
},
@ -1528,9 +1519,6 @@ impl OpenCADStudio {
selected_group: active_group,
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()),
hatch_pattern_combo: iced::widget::combo_box::State::new(
crate::scene::model::hatch_patterns::names(),
),
lineweight_combo: iced::widget::combo_box::State::new(
ui::properties::lw_options(),
),

View file

@ -1213,6 +1213,12 @@ impl OpenCADStudio {
}
Message::CommandHistoryPrev => {
if self.tabs[self.active_tab]
.properties
.hatch_pattern_picker_open
{
return self.update(Message::PropHatchPatternNavigate(-2));
}
// Grip popup wins first — arrow keys walk its items.
if let Some(popup) = self.grip_popup.as_mut() {
if !popup.items.is_empty() {
@ -1235,6 +1241,12 @@ impl OpenCADStudio {
}
Message::CommandHistoryNext => {
if self.tabs[self.active_tab]
.properties
.hatch_pattern_picker_open
{
return self.update(Message::PropHatchPatternNavigate(2));
}
if let Some(popup) = self.grip_popup.as_mut() {
if !popup.items.is_empty() {
popup.selected = (popup.selected + 1) % popup.items.len();
@ -1349,7 +1361,17 @@ impl OpenCADStudio {
}
Message::CommandFinalize => self.on_command_finalize(),
Message::CommandEscape => self.on_command_escape(),
Message::CommandEscape => {
let panel = &mut self.tabs[self.active_tab].properties;
if panel.hatch_pattern_picker_open {
panel.hatch_pattern_picker_open = false;
panel.hatch_pattern_search.clear();
panel.hatch_pattern_focus = 0;
Task::none()
} else {
self.on_command_escape()
}
}
Message::Command(cmd) => {
// Close viewport context menu if open.
@ -2764,6 +2786,12 @@ impl OpenCADStudio {
Task::none()
}
Message::MTextCaretMove(d) => {
if self.tabs[self.active_tab]
.properties
.hatch_pattern_picker_open
{
return self.update(Message::PropHatchPatternNavigate(d as i8));
}
self.mtext_caret_move(d);
Task::none()
}
@ -3088,7 +3116,79 @@ impl OpenCADStudio {
Task::none()
}
Message::PropHatchPatternChanged(name) => self.on_prop_hatch_pattern_changed(name),
Message::PropHatchPatternChanged(name) => {
let panel = &mut self.tabs[self.active_tab].properties;
panel.hatch_pattern_picker_open = false;
panel.hatch_pattern_search.clear();
self.on_prop_hatch_pattern_changed(name)
}
Message::PropHatchPatternPickerToggle(current) => {
let panel = &mut self.tabs[self.active_tab].properties;
panel.hatch_pattern_picker_open = !panel.hatch_pattern_picker_open;
if panel.hatch_pattern_picker_open {
panel.color_picker_open = false;
panel.color_palette_open = false;
panel.open_color_field = None;
panel.edit_choice_open = false;
panel.hatch_pattern_focus =
crate::ui::properties::filtered_hatch_patterns("")
.iter()
.position(|entry| entry.name.eq_ignore_ascii_case(&current))
.unwrap_or(0);
return iced::widget::operation::focus(iced::widget::Id::new(
"hatch-pattern-search",
));
} else {
panel.hatch_pattern_search.clear();
panel.hatch_pattern_focus = 0;
}
Task::none()
}
Message::PropHatchPatternSearchChanged(search) => {
let panel = &mut self.tabs[self.active_tab].properties;
panel.hatch_pattern_search = search;
panel.hatch_pattern_focus = 0;
Task::none()
}
Message::PropHatchPatternFocus(index) => {
let panel = &mut self.tabs[self.active_tab].properties;
let len =
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
.len();
if index < len {
panel.hatch_pattern_focus = index;
}
Task::none()
}
Message::PropHatchPatternNavigate(delta) => {
let panel = &mut self.tabs[self.active_tab].properties;
let len =
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
.len();
if len > 0 {
panel.hatch_pattern_focus =
(panel.hatch_pattern_focus as isize + delta as isize)
.rem_euclid(len as isize) as usize;
}
Task::none()
}
Message::PropHatchPatternConfirm => {
let panel = &self.tabs[self.active_tab].properties;
let name =
crate::ui::properties::filtered_hatch_patterns(&panel.hatch_pattern_search)
.get(panel.hatch_pattern_focus)
.map(|entry| entry.name.clone());
if let Some(name) = name {
self.update(Message::PropHatchPatternChanged(name))
} else {
Task::none()
}
}
Message::PropBoolToggle(field) => {
let i = self.active_tab;
@ -3236,6 +3336,10 @@ impl OpenCADStudio {
Message::PropEditChoiceToggle => {
let panel = &mut self.tabs[self.active_tab].properties;
panel.edit_choice_open = !panel.edit_choice_open;
if panel.edit_choice_open {
panel.hatch_pattern_picker_open = false;
panel.hatch_pattern_search.clear();
}
Task::none()
}
@ -3255,6 +3359,8 @@ impl OpenCADStudio {
!self.tabs[i].properties.color_picker_open;
if self.tabs[i].properties.color_picker_open {
self.tabs[i].properties.color_palette_open = false;
self.tabs[i].properties.hatch_pattern_picker_open = false;
self.tabs[i].properties.hatch_pattern_search.clear();
}
Task::none()
}
@ -3382,6 +3488,8 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].properties.color_picker_open = false;
self.tabs[i].properties.color_palette_open = false;
self.tabs[i].properties.hatch_pattern_picker_open = false;
self.tabs[i].properties.hatch_pattern_search.clear();
Task::none()
}

View file

@ -45,6 +45,34 @@ fn viewcube_has_room(bar_w: f32, tile_w: f32) -> bool {
bar_w + VIEWCUBE_GAP + VIEWCUBE_REGION_PX + VIEWCUBE_PAD <= tile_w
}
fn hatch_pattern_key_event(
event: iced::Event,
status: iced::event::Status,
_window: window::Id,
) -> Option<Message> {
if !matches!(status, iced::event::Status::Captured) {
return None;
}
let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) = event else {
return None;
};
match key {
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
Some(Message::PropHatchPatternNavigate(-1))
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
Some(Message::PropHatchPatternNavigate(1))
}
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => {
Some(Message::PropHatchPatternNavigate(-2))
}
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
Some(Message::PropHatchPatternNavigate(2))
}
_ => None,
}
}
/// `ViewportRenderMode` enum carries the raw DXF integers, not a label,
/// so wrap it locally with a friendly name renderer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -1658,6 +1686,14 @@ impl OpenCADStudio {
let single_instance = crate::io::single_instance::subscribe().map(Message::OpenExternal);
#[cfg(target_arch = "wasm32")]
let single_instance = Subscription::none();
let hatch_pattern_keys = if self.tabs[self.active_tab]
.properties
.hatch_pattern_picker_open
{
event::listen_with(hatch_pattern_key_event)
} else {
Subscription::none()
};
iced::Subscription::batch([
frames,
history_tick,
@ -1668,6 +1704,7 @@ impl OpenCADStudio {
web_fonts,
autosave,
single_instance,
hatch_pattern_keys,
event::listen_with(|ev, status, win_id| {
use iced::event::Status;
match ev {