forked from 1b-agent/opencadstudio-upstream
feat(ui): implement themed 5-section ribbon object color dropdown
This commit is contained in:
1 parent
5f423c966a
commit
612124b84b
7 files changed
+381
-39
No files matched your search
@@ -22,6 +22,7 @@ rustflags = [
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
rustflags = [
|
||||
"-Clink-arg=-Wl,--stack,16777216",
|
||||
"-Clink-arg=-Wl,--exclude-all-symbols",
|
||||
]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
|
||||
@@ -1669,4 +1669,15 @@ mod tests {
|
||||
Some("MTP")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_color_dropdown() {
|
||||
use crate::app::Message;
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ToggleRibbonDropdown("PROP_COLOR".to_string()));
|
||||
let _ = app.view(wid);
|
||||
}
|
||||
}
|
||||
@@ -1999,10 +1999,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
|
||||
pub(super) fn on_ribbon_color_changed(&mut self, color: AcadColor) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.ribbon.prop_color_palette_open = false;
|
||||
self.ribbon.close_dropdown();
|
||||
let handles = self.property_target_handles(i);
|
||||
let i = self.active_tab;
|
||||
self.ribbon.prop_color_palette_open = false;
|
||||
self.ribbon.close_dropdown();
|
||||
self.note_recent_color(color);
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
if self.has_property_selection(i) {
|
||||
return Task::none();
|
||||
|
||||
+18
-15
@@ -9131,27 +9131,30 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::ColorWindowPick(color) => {
|
||||
// Keep only real colours in the recent list. ByLayer / ByBlock / None
|
||||
// are logical CAD states rather than reusable colours.
|
||||
if matches!(
|
||||
&color,
|
||||
acadrust::types::Color::Index(_) | acadrust::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);
|
||||
}
|
||||
|
||||
self.recent_colors.insert(0, color.clone());
|
||||
self.recent_colors.truncate(12);
|
||||
}
|
||||
|
||||
self.note_recent_color(color);
|
||||
self.on_color_window_pick(color)
|
||||
}
|
||||
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn note_recent_color(&mut self, color: acadrust::types::Color) {
|
||||
// Keep only real colours in the recent list. ByLayer / ByBlock / None
|
||||
// are logical CAD states rather than reusable colours.
|
||||
if matches!(
|
||||
&color,
|
||||
acadrust::types::Color::Index(_) | acadrust::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);
|
||||
}
|
||||
|
||||
self.recent_colors.insert(0, color);
|
||||
self.recent_colors.truncate(12);
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a named scale into the scale-manager editor buffers (name + the
|
||||
/// paper / drawing units); blank ratios when the scale isn't found.
|
||||
fn load_scale_editor(&mut self, name: &str) {
|
||||
|
||||
@@ -2218,6 +2218,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
&history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack),
|
||||
self.win_size,
|
||||
self.tabs[self.active_tab].is_start,
|
||||
&self.recent_colors,
|
||||
)
|
||||
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into());
|
||||
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Object Color Dropdown panel.
|
||||
//!
|
||||
//! Provides a 5-section flyout ribbon color control:
|
||||
//! 1. ByLayer and ByBlock logical color entries
|
||||
//! 2. 45-swatch Quick Pick Grid (5 rows × 9 columns)
|
||||
//! 3. Standard Index Colors (ACI 1–9)
|
||||
//! 4. Recent Colors row (up to 9 swatches)
|
||||
//! 5. "Select Color..." action to open the full Select Color dialog
|
||||
|
||||
use super::widgets::{muted_text_style, popup_panel_style, popup_row_style};
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
use crate::ui::properties::acad_color_display;
|
||||
use acadrust::types::Color as AcadColor;
|
||||
use iced::widget::{button, column, container, row, text, Space};
|
||||
use iced::{Background, Border, Element, Length, Padding, Theme};
|
||||
|
||||
pub const PANEL_W: f32 = 202.0;
|
||||
const SWATCH_SIZE: f32 = 18.0;
|
||||
const SWATCH_GAP: f32 = 2.0;
|
||||
|
||||
/// Curated 45-swatch quick pick grid measured from the AutoCAD ribbon control.
|
||||
pub const QUICK_PICK_GRID: [[(u8, u8, u8); 9]; 5] = [
|
||||
[
|
||||
(0x00, 0x00, 0x00),
|
||||
(0xF8, 0xD7, 0x31),
|
||||
(0xCD, 0x69, 0x28),
|
||||
(0x99, 0x1B, 0x1E),
|
||||
(0x64, 0x21, 0x65),
|
||||
(0x29, 0x31, 0x89),
|
||||
(0x22, 0x34, 0x6E),
|
||||
(0x13, 0x67, 0x34),
|
||||
(0x72, 0x74, 0x30),
|
||||
],
|
||||
[
|
||||
(0x63, 0x64, 0x66),
|
||||
(0xF1, 0xEB, 0x1F),
|
||||
(0xF2, 0x67, 0x22),
|
||||
(0xCD, 0x20, 0x27),
|
||||
(0x93, 0x27, 0x8F),
|
||||
(0x45, 0x54, 0xA5),
|
||||
(0x10, 0x56, 0x89),
|
||||
(0x13, 0x9B, 0x48),
|
||||
(0x99, 0x9B, 0x37),
|
||||
],
|
||||
[
|
||||
(0x93, 0x95, 0x98),
|
||||
(0xF4, 0xEE, 0x51),
|
||||
(0xF8, 0x99, 0x1E),
|
||||
(0xED, 0x1F, 0x24),
|
||||
(0xA9, 0x53, 0xA0),
|
||||
(0x5E, 0x67, 0xAF),
|
||||
(0x27, 0x76, 0xBB),
|
||||
(0x58, 0xBA, 0x48),
|
||||
(0xC8, 0xC9, 0x2D),
|
||||
],
|
||||
[
|
||||
(0xC7, 0xC8, 0xCA),
|
||||
(0xF7, 0xF2, 0x81),
|
||||
(0xFE, 0xCC, 0x66),
|
||||
(0xF2, 0x71, 0x72),
|
||||
(0xD1, 0x9C, 0xC7),
|
||||
(0x98, 0x98, 0xCC),
|
||||
(0x7A, 0xAF, 0xDF),
|
||||
(0xAF, 0xD9, 0xAA),
|
||||
(0xE5, 0xE8, 0x78),
|
||||
],
|
||||
[
|
||||
(0xFF, 0xFF, 0xFF),
|
||||
(0xF8, 0xF6, 0xB0),
|
||||
(0xFE, 0xEA, 0xB9),
|
||||
(0xF7, 0xAB, 0xAE),
|
||||
(0xDD, 0xB3, 0xD4),
|
||||
(0xCE, 0xCC, 0xE6),
|
||||
(0xAD, 0xDD, 0xF7),
|
||||
(0xD7, 0xEB, 0xD2),
|
||||
(0xED, 0xEE, 0x99),
|
||||
],
|
||||
];
|
||||
|
||||
/// The standard ACI 1..=9 index colors pulled directly from the ACI color table.
|
||||
#[allow(dead_code)]
|
||||
pub fn aci_1_9() -> [(u8, u8, u8); 9] {
|
||||
std::array::from_fn(|i| {
|
||||
acadrust::types::aci_table::aci_to_rgb((i + 1) as u8).unwrap_or((128, 128, 128))
|
||||
})
|
||||
}
|
||||
|
||||
fn swatch_btn<'a>(color: AcadColor, cur_color: AcadColor) -> Element<'a, Message> {
|
||||
let (bg, _) = acad_color_display(color);
|
||||
let selected = color == cur_color;
|
||||
|
||||
button(
|
||||
Space::new()
|
||||
.width(Length::Fixed(SWATCH_SIZE))
|
||||
.height(Length::Fixed(SWATCH_SIZE)),
|
||||
)
|
||||
.on_press(Message::RibbonColorChanged(color))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
border: Border {
|
||||
color: if selected {
|
||||
palette.primary.base.color
|
||||
} else if hovered {
|
||||
palette.background.base.text
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: if selected {
|
||||
2.0
|
||||
} else if hovered {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
},
|
||||
radius: 1.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(0)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn placeholder_swatch<'a>() -> Element<'a, Message> {
|
||||
container(
|
||||
Space::new()
|
||||
.width(Length::Fixed(SWATCH_SIZE))
|
||||
.height(Length::Fixed(SWATCH_SIZE)),
|
||||
)
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color.scale_alpha(0.5))),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color.scale_alpha(0.5),
|
||||
width: 1.0,
|
||||
radius: 1.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn divider<'a>() -> Element<'a, Message> {
|
||||
container(
|
||||
Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(1.0)),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn logical_row<'a>(
|
||||
color: AcadColor,
|
||||
cur_color: AcadColor,
|
||||
label: &'static str,
|
||||
) -> Element<'a, Message> {
|
||||
let (bg, _) = acad_color_display(color);
|
||||
let selected = color == cur_color;
|
||||
|
||||
let swatch = container(
|
||||
Space::new()
|
||||
.width(Length::Fixed(SWATCH_SIZE))
|
||||
.height(Length::Fixed(SWATCH_SIZE)),
|
||||
)
|
||||
.style(move |theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
border: Border {
|
||||
color: if selected {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: if selected { 2.0 } else { 1.0 },
|
||||
radius: 1.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
button(
|
||||
row![
|
||||
swatch,
|
||||
text(t!(label)).size(11),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::RibbonColorChanged(color))
|
||||
.style(popup_row_style)
|
||||
.padding([3, 4])
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn more_colors_row<'a>(active_color: AcadColor) -> Element<'a, Message> {
|
||||
button(
|
||||
row![text(t!("Select Color...")).size(11)]
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::OpenColorWindow(
|
||||
crate::app::ColorPickTarget::Ribbon,
|
||||
active_color,
|
||||
))
|
||||
.style(popup_row_style)
|
||||
.padding([4, 4])
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Builds the complete 5-section color dropdown panel element.
|
||||
pub fn color_dropdown_panel<'a>(
|
||||
active_color: AcadColor,
|
||||
recent_colors: &[AcadColor],
|
||||
) -> Element<'a, Message> {
|
||||
// 1. ByLayer and ByBlock
|
||||
let by_layer = logical_row(AcadColor::ByLayer, active_color, "ByLayer");
|
||||
let by_block = logical_row(AcadColor::ByBlock, active_color, "ByBlock");
|
||||
let section1 = column![by_layer, by_block].spacing(2);
|
||||
|
||||
// 2. 45-swatch Quick Pick Grid (5 rows × 9 columns)
|
||||
let mut grid_rows = column![].spacing(SWATCH_GAP);
|
||||
for row_colors in QUICK_PICK_GRID {
|
||||
let mut r = row![].spacing(SWATCH_GAP);
|
||||
for (red, green, blue) in row_colors {
|
||||
let color = AcadColor::Rgb {
|
||||
r: red,
|
||||
g: green,
|
||||
b: blue,
|
||||
};
|
||||
r = r.push(swatch_btn(color, active_color));
|
||||
}
|
||||
grid_rows = grid_rows.push(r);
|
||||
}
|
||||
|
||||
// 3. Standard Index Colors (ACI 1..=9)
|
||||
let index_header = text(t!("Index Color")).size(10).style(muted_text_style);
|
||||
let mut standard_row = row![].spacing(SWATCH_GAP);
|
||||
for idx in 1u8..=9 {
|
||||
let color = AcadColor::Index(idx);
|
||||
standard_row = standard_row.push(swatch_btn(color, active_color));
|
||||
}
|
||||
let section3 = column![index_header, standard_row].spacing(4);
|
||||
|
||||
// 4. Recent Colors row (up to 9 swatches; pad with placeholders if fewer)
|
||||
let recent_header = text(t!("Recent Colors")).size(10).style(muted_text_style);
|
||||
let mut recent_row = row![].spacing(SWATCH_GAP);
|
||||
let mut count = 0;
|
||||
for &color in recent_colors.iter().take(9) {
|
||||
recent_row = recent_row.push(swatch_btn(color, active_color));
|
||||
count += 1;
|
||||
}
|
||||
while count < 9 {
|
||||
recent_row = recent_row.push(placeholder_swatch());
|
||||
count += 1;
|
||||
}
|
||||
let section4 = column![recent_header, recent_row].spacing(4);
|
||||
|
||||
// 5. Select Color...
|
||||
let section5 = more_colors_row(active_color);
|
||||
|
||||
let content = column![
|
||||
section1,
|
||||
divider(),
|
||||
grid_rows,
|
||||
divider(),
|
||||
section3,
|
||||
divider(),
|
||||
section4,
|
||||
divider(),
|
||||
section5,
|
||||
]
|
||||
.spacing(6);
|
||||
|
||||
container(content)
|
||||
.padding(Padding {
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
left: 12.0,
|
||||
right: 12.0,
|
||||
})
|
||||
.width(Length::Fixed(PANEL_W))
|
||||
.style(popup_panel_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_quick_pick_grid_dimensions() {
|
||||
assert_eq!(QUICK_PICK_GRID.len(), 5);
|
||||
for row in &QUICK_PICK_GRID {
|
||||
assert_eq!(row.len(), 9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aci_1_9() {
|
||||
let colors = aci_1_9();
|
||||
assert_eq!(colors.len(), 9);
|
||||
assert_eq!(colors[0], (255, 0, 0)); // Red
|
||||
assert_eq!(colors[1], (255, 255, 0)); // Yellow
|
||||
assert_eq!(colors[2], (0, 255, 0)); // Green
|
||||
assert_eq!(colors[3], (0, 255, 255)); // Cyan
|
||||
assert_eq!(colors[4], (0, 0, 255)); // Blue
|
||||
assert_eq!(colors[5], (255, 0, 255)); // Magenta
|
||||
assert_eq!(colors[6], (255, 255, 255)); // White
|
||||
assert_eq!(colors[7], (128, 128, 128)); // Dark Gray
|
||||
assert_eq!(colors[8], (192, 192, 192)); // Light Gray
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_panel_geometry() {
|
||||
assert_eq!(PANEL_W, 202.0);
|
||||
let grid_width = 9.0 * SWATCH_SIZE + 8.0 * SWATCH_GAP;
|
||||
assert_eq!(grid_width, 178.0);
|
||||
let padding_h = (PANEL_W - grid_width) / 2.0;
|
||||
assert_eq!(padding_h, 12.0);
|
||||
}
|
||||
}
|
||||
+12
-20
@@ -23,6 +23,7 @@ use crate::ui::properties::{linetype_display_name, lw_options, LinetypeItem};
|
||||
mod widgets;
|
||||
mod draw_panel;
|
||||
mod modify_panel;
|
||||
mod color_dropdown;
|
||||
use widgets::{StyleContext, *};
|
||||
mod collapse;
|
||||
use collapse::{CollapsePanels, Panel};
|
||||
@@ -739,6 +740,7 @@ impl Ribbon {
|
||||
redo_labels: &[String],
|
||||
win: (f32, f32),
|
||||
is_start: bool,
|
||||
recent_colors: &[AcadColor],
|
||||
) -> Option<Element<'_, Message>> {
|
||||
if is_start {
|
||||
return None;
|
||||
@@ -820,7 +822,7 @@ impl Ribbon {
|
||||
}
|
||||
|
||||
if open_id == PROP_COLOR_ID {
|
||||
return self.prop_color_overlay(win);
|
||||
return self.prop_color_overlay(win, recent_colors);
|
||||
}
|
||||
if open_id == PROP_LINETYPE_ID {
|
||||
return self.prop_linetype_overlay(win);
|
||||
@@ -1169,27 +1171,17 @@ impl Ribbon {
|
||||
)))
|
||||
}
|
||||
|
||||
fn prop_color_overlay(&self, win: (f32, f32)) -> Option<Element<'_, Message>> {
|
||||
let picker = crate::ui::color_select::color_list(
|
||||
crate::ui::color_select::ColorExtras {
|
||||
by_layer: true,
|
||||
by_block: true,
|
||||
..Default::default()
|
||||
},
|
||||
Message::RibbonColorChanged,
|
||||
Message::OpenColorWindow(
|
||||
crate::app::ColorPickTarget::Ribbon,
|
||||
self.active_color,
|
||||
),
|
||||
);
|
||||
fn prop_color_overlay(
|
||||
&self,
|
||||
win: (f32, f32),
|
||||
recent_colors: &[AcadColor],
|
||||
) -> Option<Element<'_, Message>> {
|
||||
let panel = color_dropdown::color_dropdown_panel(self.active_color, recent_colors);
|
||||
|
||||
let panel = container(picker)
|
||||
.style(popup_panel_style)
|
||||
.width(Length::Fixed(200.0));
|
||||
|
||||
let (align_right, h_pad, top) = self.dd_anchor(PROP_COLOR_ID, 200.0, win.0);
|
||||
let (align_right, h_pad, top) =
|
||||
self.dd_anchor(PROP_COLOR_ID, color_dropdown::PANEL_W, win.0);
|
||||
Some(dropdown_backdrop(position_ribbon_dropdown(
|
||||
panel.into(),
|
||||
panel,
|
||||
align_right,
|
||||
h_pad,
|
||||
top,
|
||||
|
||||
Reference in new issue
Block a user