parent
a717572d65
commit
6686141d3b
6 changed files with 448 additions and 51 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(¤t))
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@ pub fn find(name: &str) -> Option<&'static PatternEntry> {
|
|||
catalog().iter().find(|e| e.name.eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
pub fn names() -> Vec<String> {
|
||||
catalog().iter().map(|e| e.name.clone()).collect()
|
||||
}
|
||||
|
||||
// ── DXF export ────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn build_dxf_pattern(entry: &PatternEntry) -> DxfPattern {
|
||||
|
|
|
|||
|
|
@ -8,18 +8,24 @@
|
|||
//! • Geometry → text_input per coordinate / dimension field
|
||||
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
use std::fmt;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use crate::ui::ROW_H;
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::Handle;
|
||||
use iced::widget::{button, column, combo_box, container, row, scrollable, text, text_input};
|
||||
use iced::{Background, Border, Color, Element, Length, Padding, Theme};
|
||||
use iced::widget::{
|
||||
button, canvas, column, combo_box, container, mouse_area, row, scrollable, text, text_input,
|
||||
};
|
||||
use iced::{
|
||||
mouse, Background, Border, Color, Element, Length, Padding, Point, Rectangle, Size, Theme,
|
||||
};
|
||||
|
||||
// ── Row-height-derived constants ─────────────────────────────────────────
|
||||
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px
|
||||
const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo to ROW_H
|
||||
const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch
|
||||
const PATTERN_CARD_W: f32 = 158.0;
|
||||
const PATTERN_PREVIEW_H: f32 = 58.0;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::scene::model::object::{PropSection, PropValue};
|
||||
|
|
@ -72,6 +78,124 @@ impl fmt::Display for SelectionGroup {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HatchPatternPreview {
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern,
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for HatchPatternPreview {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &(),
|
||||
renderer: &iced::Renderer,
|
||||
theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
|
||||
|
||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
let palette = theme.extended_palette();
|
||||
let pad = 4.0;
|
||||
let sample = canvas::Path::rectangle(
|
||||
Point::new(pad, pad),
|
||||
Size::new(
|
||||
(bounds.width - pad * 2.0).max(0.0),
|
||||
(bounds.height - pad * 2.0).max(0.0),
|
||||
),
|
||||
);
|
||||
frame.fill(&sample, palette.background.base.color);
|
||||
|
||||
match &self.pattern {
|
||||
HatchPattern::Solid => {
|
||||
frame.fill(&sample, palette.background.base.text.scale_alpha(0.72));
|
||||
}
|
||||
HatchPattern::Gradient { .. } => {
|
||||
frame.fill(&sample, palette.primary.weak.color);
|
||||
}
|
||||
HatchPattern::Pattern(_) => {
|
||||
let model = HatchModel {
|
||||
world_origin: [0.0, 0.0],
|
||||
boundary: Arc::new(vec![
|
||||
[pad, pad],
|
||||
[bounds.width - pad, pad],
|
||||
[bounds.width - pad, bounds.height - pad],
|
||||
[pad, bounds.height - pad],
|
||||
]),
|
||||
boundary_wcs: None,
|
||||
pattern: self.pattern.clone(),
|
||||
name: String::new(),
|
||||
color: [1.0; 4],
|
||||
angle_offset: 0.0,
|
||||
scale: hatch_preview_scale(&self.pattern),
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
let stroke = canvas::Stroke::default()
|
||||
.with_color(palette.background.base.text)
|
||||
.with_width(1.0);
|
||||
for segment in model.pattern_segments() {
|
||||
frame.stroke(
|
||||
&canvas::Path::line(
|
||||
Point::new(segment[0][0] as f32, bounds.height - segment[0][1] as f32),
|
||||
Point::new(segment[1][0] as f32, bounds.height - segment[1][1] as f32),
|
||||
),
|
||||
stroke.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frame.stroke(
|
||||
&sample,
|
||||
canvas::Stroke::default()
|
||||
.with_color(palette.background.neutral.color)
|
||||
.with_width(1.0),
|
||||
);
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
||||
fn hatch_preview_scale(pattern: &crate::scene::model::hatch_model::HatchPattern) -> f32 {
|
||||
use crate::scene::model::hatch_model::HatchPattern;
|
||||
|
||||
let HatchPattern::Pattern(families) = pattern else {
|
||||
return 1.0;
|
||||
};
|
||||
let spacing = families
|
||||
.iter()
|
||||
.filter_map(|family| {
|
||||
let spacing = family.dy.abs();
|
||||
(spacing > 1.0e-4).then_some(spacing)
|
||||
})
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
if spacing.is_finite() {
|
||||
(8.0 / spacing).clamp(0.01, 100.0)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
fn hatch_pattern_matches(
|
||||
entry: &crate::scene::model::hatch_patterns::PatternEntry,
|
||||
search: &str,
|
||||
) -> bool {
|
||||
let query = search.trim();
|
||||
query.is_empty()
|
||||
|| entry.name.to_lowercase().contains(&query.to_lowercase())
|
||||
|| entry.description.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
pub(crate) fn filtered_hatch_patterns(
|
||||
search: &str,
|
||||
) -> Vec<&'static crate::scene::model::hatch_patterns::PatternEntry> {
|
||||
crate::scene::model::hatch_patterns::catalog()
|
||||
.iter()
|
||||
.filter(|entry| hatch_pattern_matches(entry, search))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// All standard CAD lineweight options for the combobox.
|
||||
pub fn lw_options() -> Vec<LwItem> {
|
||||
[
|
||||
|
|
@ -131,7 +255,12 @@ pub struct PropertiesPanel {
|
|||
pub layer_combo: combo_box::State<String>,
|
||||
pub lineweight_combo: combo_box::State<LwItem>,
|
||||
pub linetype_combo: combo_box::State<LinetypeItem>,
|
||||
pub hatch_pattern_combo: combo_box::State<String>,
|
||||
/// Whether the visual hatch-pattern picker is open.
|
||||
pub hatch_pattern_picker_open: bool,
|
||||
/// Case-insensitive filter for the visual hatch-pattern picker.
|
||||
pub hatch_pattern_search: String,
|
||||
/// Keyboard/hover focus inside the filtered visual pattern grid.
|
||||
pub hatch_pattern_focus: usize,
|
||||
/// In-progress text edits keyed by `field` name.
|
||||
pub edit_buf: HashMap<String, String>,
|
||||
/// Entity handles this panel was built for. `refresh_properties` compares
|
||||
|
|
@ -177,7 +306,9 @@ impl Default for PropertiesPanel {
|
|||
layer_combo: combo_box::State::new(vec![]),
|
||||
lineweight_combo: combo_box::State::new(lw_options()),
|
||||
linetype_combo: combo_box::State::new(vec![]),
|
||||
hatch_pattern_combo: combo_box::State::new(crate::scene::model::hatch_patterns::names()),
|
||||
hatch_pattern_picker_open: false,
|
||||
hatch_pattern_search: String::new(),
|
||||
hatch_pattern_focus: 0,
|
||||
edit_buf: HashMap::default(),
|
||||
source_handles: vec![],
|
||||
color_picker_open: false,
|
||||
|
|
@ -865,38 +996,142 @@ impl PropertiesPanel {
|
|||
label: &'a str,
|
||||
current: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
let selected = if current == VARIES_LABEL {
|
||||
None
|
||||
} else {
|
||||
Some(current.to_string())
|
||||
};
|
||||
// iced hides `selected` while the combo has focus and shows its
|
||||
// placeholder instead. Use the real pattern name for a single hatch;
|
||||
// a fixed VARIES placeholder made a correct ANSI31 value look mixed as
|
||||
// soon as the dropdown opened (#476).
|
||||
let placeholder = if current == VARIES_LABEL {
|
||||
VARIES_LABEL
|
||||
} else {
|
||||
current
|
||||
};
|
||||
let combo = combo_box(
|
||||
&self.hatch_pattern_combo,
|
||||
placeholder,
|
||||
selected.as_ref(),
|
||||
Message::PropHatchPatternChanged,
|
||||
let head = button(
|
||||
row![
|
||||
text(crate::ui::text_util::elide(current, 16))
|
||||
.size(FONT_SZ)
|
||||
.width(Length::Fill),
|
||||
if self.hatch_pattern_picker_open {
|
||||
crate::ui::icons::themed_arrow_up(FONT_SZ)
|
||||
} else {
|
||||
crate::ui::icons::themed_arrow_down(FONT_SZ)
|
||||
},
|
||||
]
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.size(FONT_SZ)
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
.on_press(Message::PropHatchPatternPickerToggle(current.to_string()))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.extended_palette();
|
||||
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
button::Style {
|
||||
background: Some(Background::Color(if hovered {
|
||||
palette.background.weak.color
|
||||
} else {
|
||||
palette.background.base.color
|
||||
})),
|
||||
text_color: palette.background.base.text,
|
||||
border: Border {
|
||||
color: if self.hatch_pattern_picker_open {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.input_style(combo_input_style)
|
||||
.on_open(Message::PropColorPickerClose)
|
||||
.padding([COMBO_PAD_V, 6.0])
|
||||
.width(Length::Fill);
|
||||
|
||||
prop_row_widget(label, combo.into())
|
||||
if !self.hatch_pattern_picker_open {
|
||||
return prop_row_widget(label, head.into());
|
||||
}
|
||||
|
||||
let search = text_input("Search patterns…", &self.hatch_pattern_search)
|
||||
.id(iced::widget::Id::new("hatch-pattern-search"))
|
||||
.on_input(Message::PropHatchPatternSearchChanged)
|
||||
.on_submit(Message::PropHatchPatternConfirm)
|
||||
.size(FONT_SZ)
|
||||
.padding([5, 7])
|
||||
.width(Length::Fill);
|
||||
|
||||
let mut grid = column![].spacing(6);
|
||||
let visible = filtered_hatch_patterns(&self.hatch_pattern_search);
|
||||
for (row_index, pair) in visible.chunks(2).enumerate() {
|
||||
let mut cards = row![].spacing(6);
|
||||
for (column_index, entry) in pair.iter().enumerate() {
|
||||
let index = row_index * 2 + column_index;
|
||||
let selected = current.eq_ignore_ascii_case(&entry.name);
|
||||
let focused = self.hatch_pattern_focus == index;
|
||||
let name = entry.name.clone();
|
||||
let preview = canvas(HatchPatternPreview {
|
||||
pattern: entry.gpu.clone(),
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(PATTERN_PREVIEW_H);
|
||||
let card = button(
|
||||
column![
|
||||
preview,
|
||||
container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ))
|
||||
.width(Length::Fill)
|
||||
.align_x(iced::Center),
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.on_press(Message::PropHatchPatternChanged(name))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.extended_palette();
|
||||
let hovered =
|
||||
matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
let pair = if selected {
|
||||
palette.primary.weak
|
||||
} else if hovered || focused {
|
||||
palette.background.strong
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: if selected || focused {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: if selected || focused { 2.0 } else { 1.0 },
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(5)
|
||||
.width(PATTERN_CARD_W);
|
||||
cards = cards.push(
|
||||
mouse_area(card).on_enter(Message::PropHatchPatternFocus(index)),
|
||||
);
|
||||
}
|
||||
grid = grid.push(cards);
|
||||
}
|
||||
|
||||
let results: Element<'_, Message> = if visible.is_empty() {
|
||||
container(
|
||||
text("No matching patterns")
|
||||
.size(FONT_SZ)
|
||||
.style(hint_text_style),
|
||||
)
|
||||
.padding(12)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
scrollable(grid)
|
||||
.height(Length::Fixed(300.0))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
let popup = container(column![search, results].spacing(7))
|
||||
.style(container::bordered_box)
|
||||
.padding(8)
|
||||
.width(348)
|
||||
.max_height(360.0);
|
||||
|
||||
prop_row_widget(
|
||||
label,
|
||||
crate::ui::color_select::floating_below(head.into(), popup.into()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1491,3 +1726,26 @@ fn hint_text_style(theme: &Theme) -> iced::widget::text::Style {
|
|||
color: Some(theme.extended_palette().background.base.text.scale_alpha(0.48)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{hatch_pattern_matches, hatch_preview_scale};
|
||||
|
||||
#[test]
|
||||
fn hatch_picker_filters_names_and_descriptions() {
|
||||
let ansi31 = crate::scene::model::hatch_patterns::find("ANSI31").unwrap();
|
||||
|
||||
assert!(hatch_pattern_matches(ansi31, "ansi"));
|
||||
assert!(hatch_pattern_matches(ansi31, &ansi31.description));
|
||||
assert!(!hatch_pattern_matches(ansi31, "definitely-not-a-pattern"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hatch_preview_scale_is_finite_and_visible() {
|
||||
let ansi31 = crate::scene::model::hatch_patterns::find("ANSI31").unwrap();
|
||||
let scale = hatch_preview_scale(&ansi31.gpu);
|
||||
|
||||
assert!(scale.is_finite());
|
||||
assert!((0.01..=100.0).contains(&scale));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue