forked from 1b-agent/opencadstudio-upstream
Merge pull request #1795 from MarcelP102/Select-Color-Redesign
[UX] Select Color redesign and feature parity Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
843cbcd607
56 files changed
+4716
-606
No files matched your search
@@ -358,7 +358,11 @@ impl OpenCADStudio {
|
||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||
entity.as_entity_mut().set_layer(layer);
|
||||
}
|
||||
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
||||
crate::scene::view::dispatch::apply_named_color(
|
||||
&mut entity,
|
||||
self.ribbon.active_color,
|
||||
self.ribbon.active_color_name(),
|
||||
);
|
||||
crate::scene::view::dispatch::apply_common_prop(
|
||||
&mut entity,
|
||||
"linetype",
|
||||
@@ -476,7 +480,11 @@ impl OpenCADStudio {
|
||||
if layer != "0" || entity.as_entity().layer().is_empty() {
|
||||
entity.as_entity_mut().set_layer(layer);
|
||||
}
|
||||
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
||||
crate::scene::view::dispatch::apply_named_color(
|
||||
&mut entity,
|
||||
self.ribbon.active_color,
|
||||
self.ribbon.active_color_name(),
|
||||
);
|
||||
crate::scene::view::dispatch::apply_common_prop(
|
||||
&mut entity,
|
||||
"linetype",
|
||||
|
||||
@@ -391,8 +391,8 @@ impl OpenCADStudio {
|
||||
.location(id)
|
||||
.map_or(crate::app::config::DockSide::Right, |(side, _)| side),
|
||||
matches!(
|
||||
self.color_pick_target,
|
||||
Some((crate::app::ColorPickTarget::Gradient(_), _))
|
||||
self.color_picker.as_ref().map(|picker| &picker.target),
|
||||
Some(crate::app::ColorPickTarget::Gradient(_))
|
||||
),
|
||||
),
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
@@ -290,6 +290,8 @@ pub(super) struct DocumentTab {
|
||||
/// Model-space corners of the last crossing window drawn with no command
|
||||
/// running: STRETCH on that selection stretches through it (#1068).
|
||||
pub(super) last_crossing_window: Option<([f64; 2], [f64; 2])>,
|
||||
/// Colours picked in this drawing this session, newest first. Not saved.
|
||||
pub(super) recent_colors: Vec<codec::types::Color>,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
@@ -697,6 +699,7 @@ impl DocumentTab {
|
||||
suspended_cmd: None,
|
||||
transparent_resume: false,
|
||||
last_crossing_window: None,
|
||||
recent_colors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-36
@@ -952,9 +952,6 @@ pub(super) struct OpenCADStudio {
|
||||
pub(crate) sheet_set_pending_layout: Option<(String, String)>,
|
||||
/// The subset / set Import Layout as Sheet adds to, while its file picker is open.
|
||||
pub(crate) sheet_set_import_parent: Option<String>,
|
||||
/// Which viewport background the colour wheel is editing, or `None` when
|
||||
/// it is closed. One slot, because only one wheel can be open at a time.
|
||||
pub(crate) bg_picker: Option<BgTarget>,
|
||||
/// General edge-stack dock layout for the side panels.
|
||||
pub(crate) dock: crate::ui::dock::DockState,
|
||||
/// Pallet revealed although it auto-hides (its edge's group, or its
|
||||
@@ -1074,13 +1071,14 @@ pub(super) struct OpenCADStudio {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
web_autosave_clear_after: Vec<String>,
|
||||
// ── Floating panel windows ────────────────────────────────────────────
|
||||
/// Active `iced_aw` colour picker: destination plus its initial colour.
|
||||
color_pick_target: Option<(ColorPickTarget, AcadColor)>,
|
||||
/// Visible page in the shared CAD colour picker.
|
||||
color_picker_tab: ColorPickerTab,
|
||||
/// The open Select Color panel, if any.
|
||||
color_picker: Option<crate::ui::color_select::ColorPicker>,
|
||||
/// The built-in colour book and those in the colour book folder, read
|
||||
/// lazily when the Select Color panel first opens.
|
||||
color_books: std::sync::Arc<Vec<crate::ui::color_select::book::ColorBook>>,
|
||||
color_books_read: bool,
|
||||
/// Recently committed real colours, newest first.
|
||||
/// ACI and True Color remain semantically distinct even when their RGB matches.
|
||||
recent_colors: Vec<AcadColor>,
|
||||
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
|
||||
/// of OS windows).
|
||||
active_modal: Option<ModalKind>,
|
||||
@@ -1788,21 +1786,19 @@ pub struct SaveOutcome {
|
||||
refreshed_preview: Option<Option<codec::Preview>>,
|
||||
result: Result<(), crate::io::SaveFailure>,
|
||||
}
|
||||
/// Which viewport background a colour-wheel session is editing.
|
||||
/// Which Options colour the interface colour picker is editing.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BgTarget {
|
||||
Model,
|
||||
Paper,
|
||||
Desk,
|
||||
pub enum UiColor {
|
||||
ModelBackground,
|
||||
PaperBackground,
|
||||
DeskBackground,
|
||||
/// One of the six theme colours, by its row in Options.
|
||||
Theme(usize),
|
||||
Crosshair,
|
||||
SnapMarker,
|
||||
CommandText,
|
||||
}
|
||||
|
||||
/// Active page in the shared CAD colour picker.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ColorPickerTab {
|
||||
#[default]
|
||||
Index,
|
||||
TrueColor,
|
||||
}
|
||||
/// Where a colour chosen in the standalone palette window should be applied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ColorPickTarget {
|
||||
@@ -1828,6 +1824,8 @@ pub enum ColorPickTarget {
|
||||
LayerState(usize),
|
||||
/// The MText editor's selection (or global) colour.
|
||||
MText,
|
||||
/// An Options interface colour (true colour only).
|
||||
Interface(UiColor),
|
||||
/// A colour of the view Background dialog: 0 solid, 1 top, 2 middle, 3 bottom.
|
||||
ViewBackground(u8),
|
||||
}
|
||||
@@ -2596,12 +2594,6 @@ pub enum Message {
|
||||
ModelSpaceModeChanged(config::ModelSpaceMode),
|
||||
/// Change Model Space custom background color as hex or empty for default.
|
||||
ModelSpaceBgChanged(String),
|
||||
/// Open the colour wheel on one of the viewport backgrounds.
|
||||
BgPickerOpen(BgTarget),
|
||||
/// Dismiss the wheel, changing nothing.
|
||||
BgPickerCancel,
|
||||
/// Accept the wheel's colour for whichever background it was opened on.
|
||||
BgPickerSubmit(iced::Color),
|
||||
/// Change Paper Space custom sheet background color as hex or empty for default.
|
||||
PaperSpaceBgChanged(String),
|
||||
/// Change Paper Space desk surround background (#RRGGBB).
|
||||
@@ -4344,14 +4336,16 @@ pub enum Message {
|
||||
DsColorMore(DsField),
|
||||
/// Open the shared CAD colour picker for a field and its current colour.
|
||||
OpenColorWindow(ColorPickTarget, AcadColor),
|
||||
/// Switch between Index Color and True Color.
|
||||
ColorPickerTabChanged(ColorPickerTab),
|
||||
/// Change the pending colour without committing it yet.
|
||||
ColorPickerColorChanged(AcadColor),
|
||||
/// Interaction inside the Select Color panel.
|
||||
ColorPicker(crate::ui::color_select::ColorPickerMsg),
|
||||
/// Close the colour picker without choosing.
|
||||
CloseColorPicker,
|
||||
/// Commit the colour chosen in the shared picker.
|
||||
ColorWindowPick(codec::types::Color),
|
||||
/// Commit the colour chosen in the shared picker, with its `BOOK$NAME`
|
||||
/// when it comes from a colour book.
|
||||
ColorWindowPick(codec::types::Color, Option<String>),
|
||||
/// An `.acb` file was read (and, on desktop, copied into the first
|
||||
/// colour book folder).
|
||||
ColorBookLoaded(Result<crate::ui::color_select::book::ColorBook, String>),
|
||||
/// Set a block/linetype Handle field on the selected dim style from a
|
||||
/// dropdown of available block-records / linetypes (by name).
|
||||
DsSetHandle {
|
||||
@@ -4756,7 +4750,6 @@ impl OpenCADStudio {
|
||||
sheet_set: Default::default(),
|
||||
sheet_set_pending_layout: None,
|
||||
sheet_set_import_parent: None,
|
||||
bg_picker: None,
|
||||
block_palette: Default::default(),
|
||||
xref_manager: Default::default(),
|
||||
dock: Default::default(),
|
||||
@@ -4805,9 +4798,9 @@ impl OpenCADStudio {
|
||||
web_autosave_consumed: None,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
web_autosave_clear_after: Vec::new(),
|
||||
color_pick_target: None,
|
||||
color_picker_tab: ColorPickerTab::Index,
|
||||
recent_colors: Vec::new(),
|
||||
color_picker: None,
|
||||
color_books: Default::default(),
|
||||
color_books_read: false,
|
||||
active_modal: None,
|
||||
hyperlink_editor_handles: Vec::new(),
|
||||
hyperlink_editor_url: String::new(),
|
||||
|
||||
@@ -3160,7 +3160,11 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
}
|
||||
|
||||
if !preserve_entity_style {
|
||||
crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color);
|
||||
crate::scene::view::dispatch::apply_named_color(
|
||||
&mut entity,
|
||||
self.ribbon.active_color,
|
||||
self.ribbon.active_color_name(),
|
||||
);
|
||||
crate::scene::view::dispatch::apply_common_prop(
|
||||
&mut entity,
|
||||
"linetype",
|
||||
|
||||
+267
-102
@@ -436,6 +436,41 @@ impl OpenCADStudio {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
(started, tab.id, tab.scene.geometry_epoch)
|
||||
});
|
||||
// The Select Color panel can open on top of another dialog, so it
|
||||
// takes the keyboard first: Esc cancels, Enter confirms, the arrows
|
||||
// walk the index palette, and every other keystroke is swallowed.
|
||||
if self.color_picker.is_some() {
|
||||
use crate::ui::color_select::ColorPickerMsg;
|
||||
match &msg {
|
||||
Message::CommandEscape => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
|
||||
}
|
||||
Message::CommandFinalize => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
|
||||
}
|
||||
Message::ShortcutPressed(key) => match key.rsplit('+').next() {
|
||||
Some("ESCAPE") => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Cancel));
|
||||
}
|
||||
Some("ENTER" | "RETURN") => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Ok));
|
||||
}
|
||||
Some("TAB") => {
|
||||
return crate::ui::color_select::picker::focus_step(
|
||||
!key.contains("SHIFT+"),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Message::ArrowKeyPressed { direction, .. } => {
|
||||
return self.update(Message::ColorPicker(ColorPickerMsg::Arrow(*direction)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if is_modal_blocked_key_msg(&msg) {
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
// A modal dialog must capture the keyboard the same way it already
|
||||
// captures the mouse. Otherwise keystrokes from the global key
|
||||
// subscription leak past the modal into the command line and fire as
|
||||
@@ -4330,38 +4365,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::LayerColorSet(color) => {
|
||||
let i = self.active_tab;
|
||||
// Apply to every selected layer (multi-select), not just one.
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.color = color;
|
||||
dl.color_name = None;
|
||||
dl.book_name = None;
|
||||
}
|
||||
}
|
||||
for pl in self.tabs[i].layers.layers.iter_mut() {
|
||||
if names.contains(&pl.name) {
|
||||
pl.color = color;
|
||||
pl.color_name = None;
|
||||
pl.book_name = None;
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// ByLayer color is baked into the cached wires at
|
||||
// tessellation time, so bump the geometry epoch to
|
||||
// invalidate the wire cache and repaint with the new color.
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].layers.color_picker_row = None;
|
||||
self.tabs[i].layers.color_full_palette = false;
|
||||
self.sync_ribbon_layers();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerColorSet(color) => self.set_layer_color(color, None),
|
||||
|
||||
Message::LayerLinetypeSet(lt) => {
|
||||
let i = self.active_tab;
|
||||
@@ -4473,7 +4477,7 @@ 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() {
|
||||
if self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
let p = self.pane_canvas_point(idx, local);
|
||||
@@ -7432,22 +7436,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropColorChanged(color) => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
let task = self.on_ribbon_color_changed(color);
|
||||
self.refresh_properties();
|
||||
return task;
|
||||
}
|
||||
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
crate::scene::view::dispatch::apply_color(entity, color);
|
||||
}
|
||||
});
|
||||
self.tabs[i].properties.color_picker_open = false;
|
||||
Task::none()
|
||||
}
|
||||
Message::PropColorChanged(color) => self.set_property_color(color, None),
|
||||
|
||||
Message::PropLwChanged(lw) => {
|
||||
let i = self.active_tab;
|
||||
@@ -8958,39 +8947,6 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::BgPickerOpen(target) => {
|
||||
self.bg_picker = Some(target);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::BgPickerCancel => {
|
||||
self.bg_picker = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::BgPickerSubmit(color) => {
|
||||
// Hand the result to the same handler the typed hex field
|
||||
// uses, so the wheel and the field cannot drift apart on
|
||||
// validation, persistence or the MatchTheme fallback.
|
||||
let hex = crate::app::config::rgb_to_hex([
|
||||
(color.r * 255.0).round() as u8,
|
||||
(color.g * 255.0).round() as u8,
|
||||
(color.b * 255.0).round() as u8,
|
||||
]);
|
||||
match self.bg_picker.take() {
|
||||
Some(crate::app::BgTarget::Model) => {
|
||||
Task::done(Message::ModelSpaceBgChanged(hex))
|
||||
}
|
||||
Some(crate::app::BgTarget::Paper) => {
|
||||
Task::done(Message::PaperSpaceBgChanged(hex))
|
||||
}
|
||||
Some(crate::app::BgTarget::Desk) => {
|
||||
Task::done(Message::DeskSpaceBgChanged(hex))
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
Message::ModelSpaceBgChanged(hex) => {
|
||||
self.model_bg_input = hex.clone();
|
||||
if hex.trim().is_empty() {
|
||||
@@ -11922,10 +11878,21 @@ impl OpenCADStudio {
|
||||
) {
|
||||
self.graphic_attributes.open_menu = None;
|
||||
}
|
||||
self.color_pick_target = Some((target, color));
|
||||
|
||||
// Always open the shared CAD colour picker on the indexed ACI page.
|
||||
self.color_picker_tab = crate::app::ColorPickerTab::Index;
|
||||
let (current, varies, name) = self.color_picker_current(&target, color);
|
||||
let mut picker = crate::ui::color_select::ColorPicker::new(target, current, varies);
|
||||
if !matches!(picker.target, crate::app::ColorPickTarget::Interface(_)) {
|
||||
self.read_color_books();
|
||||
let by_layer = self.color_picker_layer_color(&picker.target);
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
picker = picker.with_drawing(
|
||||
crate::ui::color_select::document::collect(&tab.scene.document),
|
||||
&tab.recent_colors,
|
||||
std::sync::Arc::clone(&self.color_books),
|
||||
name,
|
||||
by_layer,
|
||||
);
|
||||
}
|
||||
self.color_picker = Some(picker);
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
self.ds_color_open = None;
|
||||
self.mls_color_open = None;
|
||||
@@ -11940,29 +11907,226 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ColorPickerTabChanged(tab) => {
|
||||
self.color_picker_tab = tab;
|
||||
Task::none()
|
||||
}
|
||||
Message::ColorPickerColorChanged(color) => {
|
||||
if let Some((_, current)) = self.color_pick_target.as_mut() {
|
||||
*current = color;
|
||||
Message::ColorPicker(msg) => {
|
||||
let Some(picker) = self.color_picker.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
match picker.update(msg) {
|
||||
Some(crate::ui::color_select::Outcome::Commit(color, name)) => {
|
||||
self.update(Message::ColorWindowPick(color, name))
|
||||
}
|
||||
Some(crate::ui::color_select::Outcome::LoadBook) => Task::perform(
|
||||
crate::ui::color_select::book::load_from_dialog(),
|
||||
Message::ColorBookLoaded,
|
||||
),
|
||||
Some(crate::ui::color_select::Outcome::Cancel) => {
|
||||
self.color_picker = None;
|
||||
Task::none()
|
||||
}
|
||||
Some(crate::ui::color_select::Outcome::Screenshot) => {
|
||||
use crate::ui::color_select::{eyedropper::EyedropperMsg, ColorPickerMsg};
|
||||
match self.main_window {
|
||||
Some(id) => iced::window::screenshot(id).map(|shot| {
|
||||
Message::ColorPicker(ColorPickerMsg::Eyedropper(
|
||||
EyedropperMsg::Shot(shot),
|
||||
))
|
||||
}),
|
||||
// Nothing to capture: leave the eyedropper instead
|
||||
// of waiting for a screenshot that never comes.
|
||||
None => self.update(Message::ColorPicker(ColorPickerMsg::Cancel)),
|
||||
}
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::CloseColorPicker => {
|
||||
self.color_pick_target = None;
|
||||
self.color_picker = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ColorWindowPick(color) => {
|
||||
self.note_recent_color(color);
|
||||
self.on_color_window_pick(color)
|
||||
Message::ColorWindowPick(color, name) => {
|
||||
// Interface colours are not drawing colours: keep them out of
|
||||
// the recent list.
|
||||
if !matches!(
|
||||
self.color_picker.as_ref().map(|picker| &picker.target),
|
||||
Some(crate::app::ColorPickTarget::Interface(_))
|
||||
) {
|
||||
self.note_recent_color(color);
|
||||
}
|
||||
self.on_color_window_pick(color, name)
|
||||
}
|
||||
Message::ColorBookLoaded(Ok(book)) => {
|
||||
// A book loaded again replaces the earlier copy.
|
||||
let index = crate::ui::color_select::book::add_or_replace(
|
||||
std::sync::Arc::make_mut(&mut self.color_books),
|
||||
book,
|
||||
);
|
||||
if let Some(picker) = self.color_picker.as_mut() {
|
||||
picker.show_book(std::sync::Arc::clone(&self.color_books), index);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ColorBookLoaded(Err(error)) => {
|
||||
if let Some(picker) = self.color_picker.as_mut().filter(|_| error != "Cancelled") {
|
||||
picker.set_status(error);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour the Select Color panel shows as "Current", and whether the
|
||||
/// selection it edits has mixed colours. A mixed selection starts from the
|
||||
/// colour of its most recently drawn object (the highest handle).
|
||||
/// Also returns the colour-book identity (`BOOK$NAME`) of that colour.
|
||||
fn color_picker_current(
|
||||
&self,
|
||||
target: &crate::app::ColorPickTarget,
|
||||
color: codec::types::Color,
|
||||
) -> (codec::types::Color, bool, Option<String>) {
|
||||
let i = self.active_tab;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let name_of = |handle| {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| entity.common().color_name.clone())
|
||||
};
|
||||
match target {
|
||||
crate::app::ColorPickTarget::Properties => {}
|
||||
crate::app::ColorPickTarget::Ribbon => {
|
||||
return (color, false, self.ribbon.active_color_name());
|
||||
}
|
||||
crate::app::ColorPickTarget::Layer(layer) => {
|
||||
let name = document
|
||||
.layers
|
||||
.get(layer)
|
||||
.and_then(crate::ui::color_select::document::layer_identity);
|
||||
return (color, false, name);
|
||||
}
|
||||
_ => return (color, false, None),
|
||||
}
|
||||
let mut colors = self
|
||||
.property_target_handles(i)
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.map(|entity| (handle, entity.common().color))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let Some(&(first_handle, first)) = colors.first() else {
|
||||
return (color, false, None);
|
||||
};
|
||||
if colors.iter().all(|&(_, c)| c == first) {
|
||||
return (first, false, name_of(first_handle));
|
||||
}
|
||||
colors.sort_by_key(|&(handle, _)| handle);
|
||||
let (newest_handle, newest) = colors.last().copied().unwrap_or((first_handle, color));
|
||||
(newest, true, name_of(newest_handle))
|
||||
}
|
||||
|
||||
/// The colour ByLayer stands for in the Select Color panel: the layer of
|
||||
/// the newest selected object, or the current layer for new objects.
|
||||
fn color_picker_layer_color(
|
||||
&self,
|
||||
target: &crate::app::ColorPickTarget,
|
||||
) -> Option<codec::types::Color> {
|
||||
let i = self.active_tab;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let layer = match target {
|
||||
crate::app::ColorPickTarget::Properties => self
|
||||
.property_target_handles(i)
|
||||
.into_iter()
|
||||
.max()
|
||||
.and_then(|handle| document.get_entity(handle))
|
||||
.map_or(self.tabs[i].active_layer.as_str(), |entity| {
|
||||
entity.as_entity().layer()
|
||||
}),
|
||||
crate::app::ColorPickTarget::Ribbon => self.tabs[i].active_layer.as_str(),
|
||||
_ => return None,
|
||||
};
|
||||
document.layers.get(layer).map(|layer| layer.color)
|
||||
}
|
||||
|
||||
/// Read the built-in colour book and every `.acb` in the colour book
|
||||
/// folder, once per session. A book in the folder replaces a built-in
|
||||
/// one of the same name.
|
||||
fn read_color_books(&mut self) {
|
||||
if std::mem::replace(&mut self.color_books_read, true) {
|
||||
return;
|
||||
}
|
||||
self.color_books = std::sync::Arc::new(crate::ui::color_select::book::read_all());
|
||||
}
|
||||
|
||||
/// Colour the selected objects (or set the current colour when nothing
|
||||
/// is selected). `name` is a colour-book identity, `BOOK$NAME`.
|
||||
pub(super) fn set_property_color(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<String>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
let task = self.on_ribbon_color_changed(color);
|
||||
self.ribbon.active_color_book = name.map(|name| (color, name));
|
||||
self.refresh_properties();
|
||||
return task;
|
||||
}
|
||||
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
crate::scene::view::dispatch::apply_named_color(entity, color, name.clone());
|
||||
}
|
||||
});
|
||||
self.tabs[i].properties.color_picker_open = false;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Colour every selected layer (multi-select), not just one. `name` is a
|
||||
/// colour-book identity, `BOOK$NAME`.
|
||||
pub(super) fn set_layer_color(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<&str>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
// Layers keep the book and the colour name apart.
|
||||
let (book_name, color_name) = match name.map(|name| name.split_once('$')) {
|
||||
Some(Some((book, name))) => (Some(book), Some(name)),
|
||||
Some(None) => (None, name),
|
||||
None => (None, None),
|
||||
};
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.color = color;
|
||||
dl.color_name = color_name.map(str::to_string);
|
||||
dl.book_name = book_name.map(str::to_string);
|
||||
}
|
||||
}
|
||||
for pl in self.tabs[i].layers.layers.iter_mut() {
|
||||
if names.contains(&pl.name) {
|
||||
pl.color = color;
|
||||
pl.color_name = color_name.map(str::to_string);
|
||||
pl.book_name = book_name.map(str::to_string);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// ByLayer color is baked into the cached wires at
|
||||
// tessellation time, so bump the geometry epoch to
|
||||
// invalidate the wire cache and repaint with the new color.
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].layers.color_picker_row = None;
|
||||
self.tabs[i].layers.color_full_palette = false;
|
||||
self.sync_ribbon_layers();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) {
|
||||
// Keep only real colours in the recent list. ByLayer / ByBlock / None
|
||||
// are logical CAD states rather than reusable colours.
|
||||
@@ -11971,12 +12135,13 @@ impl OpenCADStudio {
|
||||
codec::types::Color::Index(_) | codec::types::Color::Rgb { .. }
|
||||
) {
|
||||
// No duplicates: selecting an existing colour moves it to the front.
|
||||
if let Some(pos) = self.recent_colors.iter().position(|c| c == &color) {
|
||||
self.recent_colors.remove(pos);
|
||||
let recent = &mut self.tabs[self.active_tab].recent_colors;
|
||||
if let Some(pos) = recent.iter().position(|c| c == &color) {
|
||||
recent.remove(pos);
|
||||
}
|
||||
|
||||
self.recent_colors.insert(0, color);
|
||||
self.recent_colors.truncate(12);
|
||||
recent.insert(0, color);
|
||||
recent.truncate(12);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-27
@@ -1136,42 +1136,60 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
|
||||
pub(super) fn on_color_window_pick(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<String>,
|
||||
) -> Task<Message> {
|
||||
if let Some(crate::app::ColorPickTarget::Interface(target)) = self
|
||||
.color_picker
|
||||
.as_ref()
|
||||
.map(|picker| picker.target.clone())
|
||||
{
|
||||
self.color_picker = None;
|
||||
let Some((r, g, b)) = crate::ui::color_select::math::color_rgb(color) else {
|
||||
return Task::none();
|
||||
};
|
||||
// Hand the result to the same handler the typed hex field uses, so
|
||||
// the picker and the field cannot drift apart on validation or
|
||||
// persistence.
|
||||
let hex = crate::app::config::rgb_to_hex([r, g, b]);
|
||||
use crate::app::UiColor;
|
||||
return Task::done(match target {
|
||||
UiColor::ModelBackground => Message::ModelSpaceBgChanged(hex),
|
||||
UiColor::PaperBackground => Message::PaperSpaceBgChanged(hex),
|
||||
UiColor::DeskBackground => Message::DeskSpaceBgChanged(hex),
|
||||
UiColor::Theme(index) => Message::OptionsThemeColorChanged(index, hex),
|
||||
UiColor::Crosshair => Message::CrosshairColorChanged(hex),
|
||||
UiColor::SnapMarker => Message::SnapMarkerColorChanged(hex),
|
||||
UiColor::CommandText => Message::CommandTextColorChanged(hex),
|
||||
});
|
||||
}
|
||||
if matches!(
|
||||
self.color_pick_target.as_ref().map(|(target, _)| target),
|
||||
self.color_picker.as_ref().map(|picker| &picker.target),
|
||||
Some(crate::app::ColorPickTarget::PlotStyle)
|
||||
) {
|
||||
self.color_pick_target = None;
|
||||
self.color_picker = None;
|
||||
|
||||
let rgb = match color {
|
||||
codec::types::Color::Rgb { r, g, b } => Some((r, g, b)),
|
||||
codec::types::Color::Index(index) => codec::types::aci_table::aci_to_rgb(index),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((r, g, b)) = rgb {
|
||||
self.ps_color_buf = format!("#{r:02X}{g:02X}{b:02X}");
|
||||
if let Some((r, g, b)) = crate::ui::color_select::math::color_rgb(color) {
|
||||
self.ps_color_buf = crate::ui::color_select::math::hex_string(r, g, b);
|
||||
}
|
||||
|
||||
return self.on_plot_style_panel_apply();
|
||||
}
|
||||
let s = crate::ui::color_select::color_to_aci_string(color);
|
||||
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
|
||||
let edit = match self.color_picker.take().map(|picker| picker.target) {
|
||||
Some(crate::app::ColorPickTarget::Gradient(index)) => {
|
||||
Some(Message::GraphicAttributes(GraphicAttributesMsg::Gradient(
|
||||
crate::ui::window::gradient_editor::GradientMsg::Color(index, color),
|
||||
)))
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::ViewBackground(slot)) => {
|
||||
let rgb = match color {
|
||||
codec::types::Color::Rgb { r, g, b } => Some([r, g, b]),
|
||||
codec::types::Color::Index(index) => {
|
||||
codec::types::aci_table::aci_to_rgb(index).map(|(r, g, b)| [r, g, b])
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
rgb.map(|rgb| {
|
||||
Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(slot, rgb))
|
||||
crate::ui::color_select::math::color_rgb(color).map(|(r, g, b)| {
|
||||
Message::PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg::BgColor(
|
||||
slot,
|
||||
[r, g, b],
|
||||
))
|
||||
})
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some(
|
||||
@@ -1189,7 +1207,9 @@ impl OpenCADStudio {
|
||||
field: f,
|
||||
value: s,
|
||||
}),
|
||||
Some(crate::app::ColorPickTarget::Properties) => Some(Message::PropColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Properties) => {
|
||||
return self.set_property_color(color, name);
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::PropertiesBg) => {
|
||||
Some(Message::PropBgColorChanged(color))
|
||||
}
|
||||
@@ -1197,13 +1217,17 @@ impl OpenCADStudio {
|
||||
Some(Message::PropColorFieldChanged { field, color })
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::MText) => Some(Message::MTextColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Ribbon) => Some(Message::RibbonColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Layer(name)) => {
|
||||
Some(crate::app::ColorPickTarget::Ribbon) => {
|
||||
let task = self.update(Message::RibbonColorChanged(color));
|
||||
self.ribbon.active_color_book = name.map(|name| (color, name));
|
||||
return task;
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::Layer(layer)) => {
|
||||
let i = self.active_tab;
|
||||
match self.layer_panel_index(i, &name) {
|
||||
match self.layer_panel_index(i, &layer) {
|
||||
Some(idx) => {
|
||||
self.tabs[i].layers.selected = Some(idx);
|
||||
Some(Message::LayerColorSet(color))
|
||||
return self.set_layer_color(color, name.as_deref());
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
@@ -1211,7 +1235,10 @@ impl OpenCADStudio {
|
||||
Some(crate::app::ColorPickTarget::LayerState(idx)) => {
|
||||
Some(Message::LayerStateEditorLayerColor(idx, color))
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::PlotStyle) => None,
|
||||
// Handled before this match.
|
||||
Some(
|
||||
crate::app::ColorPickTarget::PlotStyle | crate::app::ColorPickTarget::Interface(_),
|
||||
) => None,
|
||||
None => None,
|
||||
};
|
||||
if let Some(m) = edit {
|
||||
|
||||
@@ -1144,7 +1144,7 @@ impl OpenCADStudio {
|
||||
p: Point,
|
||||
expected_viewport: Option<codec::Handle>,
|
||||
) -> Task<Message> {
|
||||
if self.color_pick_target.is_some() {
|
||||
if self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -1232,7 +1232,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() || self.color_pick_target.is_some() {
|
||||
if self.ribbon.open_dropdown.is_some() || self.color_picker.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
|
||||
+14
-36
@@ -2303,7 +2303,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
&redo_labels,
|
||||
self.win_size,
|
||||
self.tabs[self.active_tab].is_start,
|
||||
&self.recent_colors,
|
||||
&self.tabs[self.active_tab].recent_colors,
|
||||
)
|
||||
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into())
|
||||
};
|
||||
@@ -2431,42 +2431,20 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, current)) = self.color_pick_target.as_ref() {
|
||||
match self.color_picker_tab {
|
||||
super::ColorPickerTab::Index => {
|
||||
let content =
|
||||
crate::ui::color_select::index_color_page(*current, &self.recent_colors);
|
||||
|
||||
crate::ui::modal::modal(
|
||||
base,
|
||||
t!("Select Color"),
|
||||
content,
|
||||
Message::CloseColorPicker,
|
||||
self.modal_offset,
|
||||
crate::ui::modal::ModalOptions::NOTICE,
|
||||
)
|
||||
}
|
||||
|
||||
super::ColorPickerTab::TrueColor => {
|
||||
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,
|
||||
// Cancel inside True Color returns to the indexed page instead
|
||||
// of closing the whole CAD colour picker.
|
||||
Message::ColorPickerTabChanged(super::ColorPickerTab::Index),
|
||||
|color| {
|
||||
Message::ColorWindowPick(crate::ui::color_select::iced_to_acad_color(
|
||||
color,
|
||||
))
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
if let Some(picker) = self.color_picker.as_ref() {
|
||||
// While the eyedropper is picking, the panel steps aside so the
|
||||
// whole window can be sampled.
|
||||
if let Some(eyedropper) = picker.eyedropper_view() {
|
||||
return iced::widget::stack![base, eyedropper].into();
|
||||
}
|
||||
crate::ui::modal::modal(
|
||||
base,
|
||||
t!("Select Color"),
|
||||
picker.view(),
|
||||
Message::CloseColorPicker,
|
||||
self.modal_offset,
|
||||
crate::ui::modal::ModalOptions::NOTICE,
|
||||
)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
|
||||
@@ -373,6 +373,8 @@ impl OpenCADStudio {
|
||||
.map(|_| std::env::temp_dir().display().to_string()),
|
||||
fonts: crate::io::font_repo::fonts_dir()
|
||||
.map(|p| p.display().to_string()),
|
||||
color_books: crate::ui::color_select::book::folder()
|
||||
.map(|p| p.display().to_string()),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -395,7 +397,6 @@ impl OpenCADStudio {
|
||||
&self.model_bg_input,
|
||||
&self.paper_bg_input,
|
||||
&self.desk_bg_input,
|
||||
self.bg_picker,
|
||||
dirty,
|
||||
close_confirm,
|
||||
flow,
|
||||
|
||||
Reference in new issue
Block a user