refactor(ui): adopt iced color picker

Remove local Pin/PickList wrappers and the ACI grid.\nBlock viewport input behind the picker and load iced_aw glyphs.
This commit is contained in:
Hakan Seven 2026-07-30 15:19:58 +03:00
commit c9c3d3b1bf
25 changed files with 289 additions and 255 deletions

View file

@ -465,8 +465,8 @@ pub(super) struct OpenCADStudio {
/// OS window Id of the primary application window.
main_window: Option<window::Id>,
// ── Floating panel windows ────────────────────────────────────────────
/// Standalone "Select Color" palette window + the field it targets.
color_pick_target: Option<ColorPickTarget>,
/// Active `iced_aw` colour picker: destination plus its initial colour.
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
/// of OS windows).
active_modal: Option<ModalKind>,
@ -2503,11 +2503,11 @@ pub enum Message {
DsToggle(DsField),
/// Toggle the expanded colour palette for a DimStyle colour field.
DsColorMore(DsField),
/// Open the standalone palette window targeting the given field.
OpenColorWindow(ColorPickTarget),
/// Close the nested colour-picker modal without choosing (Plan B).
/// Open the `iced_aw` colour picker for a field and its current colour.
OpenColorWindow(ColorPickTarget, AcadColor),
/// Close the colour picker without choosing.
CloseColorPicker,
/// A colour was chosen in the standalone palette window.
/// A colour was chosen in the `iced_aw` picker.
ColorWindowPick(acadrust::types::Color),
/// Set a block/linetype Handle field on the selected dim style from a
/// dropdown of available block-records / linetypes (by name).
@ -3177,6 +3177,7 @@ pub fn run() -> iced::Result {
}
})
.theme(|state: &OpenCADStudio, _| state.active_theme.clone())
.font(iced_aw::ICED_AW_FONT_BYTES)
.run()
}
@ -3206,5 +3207,6 @@ pub fn run_web() -> iced::Result {
.subscription(OpenCADStudio::subscription)
.title(|_state: &OpenCADStudio| "Open CAD Studio".to_string())
.theme(|state: &OpenCADStudio| state.active_theme.clone())
.font(iced_aw::ICED_AW_FONT_BYTES)
.run()
}

View file

@ -2120,6 +2120,9 @@ impl OpenCADStudio {
Message::PaneClicked(pane) => self.on_pane_clicked(pane),
Message::PaneDragged(ev) => self.on_pane_dragged(ev),
Message::PaneMove(idx, local) => {
if self.color_pick_target.is_some() {
return Task::none();
}
let p = self.pane_canvas_point(idx, local);
// While dragging a pane, just track the cursor (no focus swap or
// snap) so the drop target reads cleanly.
@ -5744,8 +5747,8 @@ impl OpenCADStudio {
};
Task::none()
}
Message::OpenColorWindow(target) => {
self.color_pick_target = Some(target);
Message::OpenColorWindow(target, color) => {
self.color_pick_target = Some((target, color));
self.ds_color_open = None;
self.mls_color_open = None;
self.ts_color_open = None;
@ -5753,8 +5756,7 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].properties.color_picker_open = false;
self.tabs[i].layers.color_picker_row = None;
// Shown as a nested modal over the active dialog (Plan B):
// `color_pick_target.is_some()` drives the overlay in view_main.
// `color_pick_target.is_some()` drives the iced_aw overlay.
Task::none()
}
Message::CloseColorPicker => {

View file

@ -960,7 +960,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
pub(super) fn on_color_window_pick(&mut self, color: acadrust::types::Color) -> Task<Message> {
let s = crate::ui::color_select::color_to_aci_string(color);
let edit = match self.color_pick_target.take() {
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)),
Some(crate::app::ColorPickTarget::MLeader(f)) => {
Some(Message::MLeaderStyleEdit { field: f, value: s })
@ -986,10 +986,14 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
}
Some(crate::app::ColorPickTarget::Layer(idx)) => {
self.tabs[self.active_tab].layers.selected = Some(idx);
match color {
acadrust::types::Color::Index(i) => Some(Message::LayerColorSet(i)),
_ => None,
}
let index = match color {
acadrust::types::Color::Index(i) => i,
acadrust::types::Color::Rgb { r, g, b } => {
crate::ui::color_select::nearest_aci(r, g, b)
}
_ => 7,
};
Some(Message::LayerColorSet(index))
}
Some(crate::app::ColorPickTarget::LayerState(idx)) => {
Some(Message::LayerStateEditorLayerColor(idx, color))

View file

@ -435,6 +435,10 @@ impl OpenCADStudio {
}
pub(super) fn on_cursor_moved(&mut self, p: Point) -> Task<Message> {
if self.color_pick_target.is_some() {
return Task::none();
}
// `p` is relative to the ViewCube hit area's top-left. Map
// it back to full-canvas coordinates so ViewportClick's
// hit-test lines up. The hit area sits in the top-right of
@ -498,7 +502,7 @@ impl OpenCADStudio {
// leaks through the stack to the pane mouse_area beneath and
// would track the crosshair over the dropdown's empty areas.
// Drop the move here instead. (#227)
if self.ribbon.open_dropdown.is_some() {
if self.ribbon.open_dropdown.is_some() || self.color_pick_target.is_some() {
return Task::none();
}
let i = self.active_tab;

View file

@ -79,11 +79,12 @@ pub(super) fn viewport_controls<'a>(
};
// Render-mode picker, restyled borderless so the outer chip frames it.
let picker = crate::ui::pick_list(
render_modes,
let picker = iced::widget::pick_list(
Some(RenderModeChoice(render_mode)),
|c| Message::SetRenderMode(c.0),
render_modes,
|value| value.to_string(),
)
.on_select(|c| Message::SetRenderMode(c.0))
.text_size(11)
.padding([4, 6])
.style(move |theme: &Theme, _| {

View file

@ -788,10 +788,10 @@ impl OpenCADStudio {
.into();
// Pin the bar to the active model tile's top-left corner so it
// follows the active panel in a tiled layout.
let bar_layer = crate::ui::pin_at(
iced::Point::new(rect.x, rect.y),
let bar_layer = iced::widget::pin(
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
);
)
.position(iced::Point::new(rect.x.max(0.0), rect.y.max(0.0)));
viewport_stack = viewport_stack.push(bar_layer);
}
@ -840,7 +840,8 @@ impl OpenCADStudio {
},
..Default::default()
});
let border_layer = crate::ui::pin_at(iced::Point::new(x, y), border_frame);
let border_layer =
iced::widget::pin(border_frame).position(iced::Point::new(x, y));
viewport_stack = viewport_stack.push(border_layer);
let vp_mode = tab
@ -866,10 +867,10 @@ impl OpenCADStudio {
])
.report_width0(self.render_bar_w.clone())
.into();
let picker_layer = crate::ui::pin_at(
iced::Point::new(x + 4.0, y + 4.0),
let picker_layer = iced::widget::pin(
container(adaptive).width(iced::Length::Fixed(rect.width.max(1.0))),
);
)
.position(iced::Point::new(x + 4.0, y + 4.0));
viewport_stack = viewport_stack.push(picker_layer);
// Hide the ViewCube first — before the render bar — when they collide.
@ -877,10 +878,8 @@ impl OpenCADStudio {
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
let controls = crate::ui::pin_at(
iced::Point::new(cube_x, cube_y),
viewcube_nav_controls(),
);
let controls = iced::widget::pin(viewcube_nav_controls())
.position(iced::Point::new(cube_x, cube_y));
viewport_stack = viewport_stack.push(controls);
let ucs_current = tab
@ -896,13 +895,14 @@ impl OpenCADStudio {
.map(|u| u.name.clone())
.filter(|n| !n.is_empty())
.collect();
let picker = crate::ui::pin_at(
iced::Point::new(
let picker = iced::widget::pin(iced::widget::opaque(viewcube_ucs_picker(
ucs_current,
ucs_names,
)))
.position(iced::Point::new(
cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5,
cube_y + VIEWCUBE_HIT_SIZE + 6.0,
),
iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
);
));
viewport_stack = viewport_stack.push(picker);
}
}
@ -917,10 +917,8 @@ impl OpenCADStudio {
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
// Cube hit area + nav controls (home / roll / nudge) as one layer.
let controls = crate::ui::pin_at(
iced::Point::new(cube_x, cube_y),
viewcube_nav_controls(),
);
let controls = iced::widget::pin(viewcube_nav_controls())
.position(iced::Point::new(cube_x, cube_y));
viewport_stack = viewport_stack.push(controls);
// WCS / named-UCS selector under the cube.
@ -937,13 +935,14 @@ impl OpenCADStudio {
.map(|u| u.name.clone())
.filter(|n| !n.is_empty())
.collect();
let picker = crate::ui::pin_at(
iced::Point::new(
let picker = iced::widget::pin(iced::widget::opaque(viewcube_ucs_picker(
ucs_current,
ucs_names,
)))
.position(iced::Point::new(
cube_x + VIEWCUBE_HIT_SIZE * 0.5 - UCS_PICKER_W * 0.5,
cube_y + VIEWCUBE_HIT_SIZE + 6.0,
),
iced::widget::opaque(viewcube_ucs_picker(ucs_current, ucs_names)),
);
));
viewport_stack = viewport_stack.push(picker);
}
@ -1554,21 +1553,24 @@ impl OpenCADStudio {
}
None => composed.into(),
};
// The colour picker is a nested modal: it stacks over whichever dialog
// (style editor, properties, …) requested it.
if self.color_pick_target.is_some() {
crate::ui::modal::modal(
base,
"Select Color",
iced::widget::container(crate::ui::color_select::color_grid_window(
Message::ColorWindowPick,
))
.width(iced::Length::Fit.max(420.0))
.height(iced::Length::Fit.max(470.0)),
// iced_aw owns the colour-picker overlay and keeps it above whichever
// application modal requested it.
if let Some((_, current)) = self.color_pick_target.as_ref() {
let initial = crate::ui::properties::acad_color_display(*current).0;
let modal_base =
crate::ui::modal::backdrop(base, Message::CloseColorPicker);
iced_aw::ColorPicker::new(
true,
initial,
modal_base,
Message::CloseColorPicker,
iced::Vector::ZERO,
crate::ui::modal::ModalOptions::MOVABLE_FIXED,
|color| {
Message::ColorWindowPick(
crate::ui::color_select::iced_to_acad_color(color),
)
},
)
.into()
} else {
base
}

View file

@ -982,9 +982,12 @@ fn save_as_dialog_window<'a>(filename: &'a str, format: &'a str) -> Element<'a,
items.push(
row![
label("Format:").width(70),
crate::ui::pick_list(crate::io::SAVE_FORMAT_OPTIONS, sel_fmt, |s: &str| {
Message::SaveDialogFormatChanged(s.to_string())
})
iced::widget::pick_list(
sel_fmt,
crate::io::SAVE_FORMAT_OPTIONS,
|value| value.to_string(),
)
.on_select(|s: &str| Message::SaveDialogFormatChanged(s.to_string()))
.width(Fit),
]
.align_y(iced::Alignment::Center)

View file

@ -9,7 +9,9 @@ pub(super) fn position_canvas_overlay<'a>(
anchor: iced::Point,
panel: Element<'a, Message>,
) -> Element<'a, Message> {
crate::ui::pin_at(anchor, iced::widget::opaque(panel))
iced::widget::pin(iced::widget::opaque(panel))
.position(iced::Point::new(anchor.x.max(0.0), anchor.y.max(0.0)))
.into()
}
// ── In-place MText editor overlay ───────────────────────────────────────────
@ -369,7 +371,12 @@ pub(super) fn mtext_editor_overlay<'a>(
} else {
styles
};
let style_pl = crate::ui::pick_list(style_opts, Some(ed.style.clone()), Message::MTextStyle)
let style_pl = iced::widget::pick_list(
Some(ed.style.clone()),
style_opts,
|value| value.to_string(),
)
.on_select(Message::MTextStyle)
.text_size(11)
.width(iced::Length::Fixed(96.0));
let font_sel = if ed.font.trim().is_empty() {
@ -377,14 +384,15 @@ pub(super) fn mtext_editor_overlay<'a>(
} else {
ed.font.clone()
};
let font_pl = crate::ui::pick_list(
let font_pl = iced::widget::pick_list(
Some(font_sel),
MTEXT_FONTS
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(font_sel),
Message::MTextFont,
|value| value.to_string(),
)
.on_select(Message::MTextFont)
.text_size(11)
.width(iced::Length::Fixed(120.0));
// Same colour picker as the Properties panel (named swatches + "More…" full
@ -398,7 +406,10 @@ pub(super) fn mtext_editor_overlay<'a>(
},
Message::MTextColorChanged,
Message::MTextColorPickerToggle,
Message::OpenColorWindow(crate::app::ColorPickTarget::MText),
Message::OpenColorWindow(
crate::app::ColorPickTarget::MText,
acadrust::types::Color::from_index(ed.color_aci as i16),
),
))
.width(iced::Length::Fixed(150.0));
@ -443,11 +454,12 @@ pub(super) fn mtext_editor_overlay<'a>(
.width(width);
// ── Row 2: oblique / width / char-spacing · align · line spacing · OK ─
let justify = crate::ui::pick_list(
JustifyChoice::ALL,
let justify = iced::widget::pick_list(
Some(JustifyChoice(ed.attachment)),
|c| Message::MTextJustify(c.0),
JustifyChoice::ALL,
|value| value.to_string(),
)
.on_select(|c| Message::MTextJustify(c.0))
.text_size(11)
.width(iced::Length::Fixed(112.0));
let row2 = row![
@ -1008,7 +1020,12 @@ pub(super) fn qselect_overlay<'a>(
Space::new().height(10),
row![
label("Object type:"),
crate::ui::pick_list(type_options, Some(type_sel), |s: String| {
iced::widget::pick_list(
Some(type_sel),
type_options,
|value| value.to_string(),
)
.on_select(|s: String| {
if s == QSELECT_ANY_TYPE {
Message::QSelectSetType(None)
} else {
@ -1022,17 +1039,18 @@ pub(super) fn qselect_overlay<'a>(
Space::new().height(6),
row![
label("Property:"),
crate::ui::pick_list(
prop_options,
iced::widget::pick_list(
Some(prop_sel),
|p: crate::app::QSelectPropertyChoice| {
prop_options,
|value| value.to_string(),
)
.on_select(|p: crate::app::QSelectPropertyChoice| {
if p.field.is_empty() {
Message::QSelectSetProperty(None)
} else {
Message::QSelectSetProperty(Some(p))
}
}
)
})
.width(Fill),
]
.align_y(iced::Alignment::Center)
@ -1040,11 +1058,12 @@ pub(super) fn qselect_overlay<'a>(
Space::new().height(6),
row![
label("Operator:"),
crate::ui::pick_list(
op_options,
iced::widget::pick_list(
Some(state.operator),
Message::QSelectSetOperator
op_options,
|value| value.to_string(),
)
.on_select(Message::QSelectSetOperator)
.width(Fill),
]
.align_y(iced::Alignment::Center)

View file

@ -16,7 +16,9 @@ use iced::{Background, Border, Element, Theme};
/// Place `el` at pixel offset (x, y) inside a fill layer (top-left origin).
fn vc_place<'a>(x: f32, y: f32, el: Element<'a, Message>) -> Element<'a, Message> {
crate::ui::pin_at(iced::Point::new(x, y), el)
iced::widget::pin(el)
.position(iced::Point::new(x.max(0.0), y.max(0.0)))
.into()
}
/// Borderless square icon button used by the ViewCube nav controls.
@ -155,7 +157,8 @@ pub(super) fn viewcube_ucs_picker<'a>(current: String, names: Vec<String>) -> El
} else {
current
};
crate::ui::pick_list(options, Some(selected), Message::SetViewcubeUcs)
iced::widget::pick_list(Some(selected), options, |value| value.to_string())
.on_select(Message::SetViewcubeUcs)
.text_size(11)
.padding([2, 6])
// Fixed width so the caller can centre it under the cube centre with a