mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Swatch menus: make the whole row (chip included) clickable and show chips in the Contextual Task Bar (#199)
Add one shared swatch_menu_row (full-width clickable chip + name, current highlight, hover) and use it in the Properties Appearance/text menus, the shared swatch_picker (Control bar, Stroke, Character, Paragraph Border/ Shading) and both Contextual Task Bar fill/stroke menus. The Task Bar popups gain a colour chip per swatch, and every swatch menu now applies on a click anywhere on the row, including the chip. Cap the popups at 280 pt so a full-width row keeps the menus InDesign-sized. Label the chip controls and rows for accessibility. Test: clicking a swatch chip in the Appearance Fill menu applies it (the Task Bar uses the same swatch_menu_row helper). Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
3 files changed
+83
-16
No files matched your search
@@ -153,6 +153,34 @@ pub fn preflight_errors(app: &DesignApp) -> usize {
|
||||
n
|
||||
}
|
||||
|
||||
/// Height of one swatch-menu row.
|
||||
const SWATCH_MENU_ROW_H: f32 = 22.0;
|
||||
|
||||
/// One swatch-menu row: a chip and the swatch name; the whole row is clickable (the chip included).
|
||||
/// `name == current` highlights it. Returns true when the row was clicked.
|
||||
pub fn swatch_menu_row(ui: &mut egui::Ui, doc: &designcraft_doc::Document, name: &str, current: &str) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), SWATCH_MENU_ROW_H), egui::Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, name == current, name));
|
||||
if name == current {
|
||||
ui.painter().rect_filled(row, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(row, 0.0, t.hover);
|
||||
}
|
||||
let (c, g) = crate::widgets::swatch_colors(doc, name, 1.0);
|
||||
let chip = egui::Rect::from_min_size(row.min + vec2(4.0, 3.0), vec2(16.0, 16.0));
|
||||
crate::widgets::paint_chip(ui.painter(), chip, c, g);
|
||||
crate::rtl::paint(
|
||||
ui.painter(),
|
||||
row.min + vec2(28.0, SWATCH_MENU_ROW_H / 2.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
name,
|
||||
egui::FontId::proportional(12.5),
|
||||
t.text,
|
||||
);
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
/// A swatch dropdown showing a chip and name; `on_pick` gets the chosen swatch name.
|
||||
pub fn swatch_picker(app: &mut DesignApp, ui: &mut egui::Ui, id: &str, current: Option<String>, on_pick: impl FnOnce(&mut DesignApp, String)) {
|
||||
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
@@ -175,15 +203,10 @@ pub fn swatch_picker(app: &mut DesignApp, ui: &mut egui::Ui, id: &str, current:
|
||||
);
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
ui.set_max_width(280.0);
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for sw in &doc.swatches {
|
||||
let (c, g) = crate::widgets::swatch_colors(&doc, &sw.name, 1.0);
|
||||
let row = ui.horizontal(|ui| {
|
||||
let (cr, _) = ui.allocate_exact_size(vec2(14.0, 14.0), egui::Sense::hover());
|
||||
crate::widgets::paint_chip(ui.painter(), cr, c, g);
|
||||
ui.add(egui::Button::new(&sw.name).frame(false).selected(sw.name == cur))
|
||||
});
|
||||
if row.inner.clicked() {
|
||||
if swatch_menu_row(ui, &doc, &sw.name, &cur) {
|
||||
picked = Some(sw.name.clone());
|
||||
ui.close();
|
||||
}
|
||||
|
||||
@@ -572,6 +572,7 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
widgets::paint_chip(ui.painter(), chip, fc, fg);
|
||||
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
let resp = ui.interact(chip, ui.id().with("fillchip"), Sense::click()).on_hover_text(format!("Fill: {}", i.fill));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Fill swatch"));
|
||||
swatch_menu(app, &resp, &doc, &i.fill, |app, n| {
|
||||
let _ = app.run("object.fill", json!({"swatch": n}));
|
||||
});
|
||||
@@ -591,6 +592,7 @@ fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
widgets::paint_stroke_chip(ui.painter(), chip, sc, egui::Color32::from_gray(20));
|
||||
ui.painter().rect_stroke(chip, 0.0, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
let resp = ui.interact(chip, ui.id().with("strokechip"), Sense::click()).on_hover_text(format!("Stroke: {}", i.stroke));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Stroke swatch"));
|
||||
swatch_menu(app, &resp, &doc, &i.stroke, |app, n| {
|
||||
let _ = app.run("object.stroke", json!({"swatch": n}));
|
||||
});
|
||||
@@ -667,15 +669,10 @@ fn swatch_menu(app: &mut DesignApp, resp: &egui::Response, doc: &designcraft_doc
|
||||
let mut picked = None;
|
||||
egui::Popup::menu(resp).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
ui.set_max_width(280.0);
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for sw in &doc.swatches {
|
||||
let (c, g) = widgets::swatch_colors(doc, &sw.name, 1.0);
|
||||
let row = ui.horizontal(|ui| {
|
||||
let (cr, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover());
|
||||
widgets::paint_chip(ui.painter(), cr, c, g);
|
||||
ui.add(egui::Button::new(&sw.name).frame(false).selected(sw.name == cur))
|
||||
});
|
||||
if row.inner.clicked() {
|
||||
if super::swatch_menu_row(ui, doc, &sw.name, cur) {
|
||||
picked = Some(sw.name.clone());
|
||||
ui.close();
|
||||
}
|
||||
@@ -2159,4 +2156,43 @@ mod tests {
|
||||
h.run_steps(2);
|
||||
assert_eq!(language(h.state_mut()), "French");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_a_swatch_chip_in_the_appearance_fill_menu_applies_it() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
// A graphic frame: the Appearance section shows a single Fill chip.
|
||||
let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
let before = app.session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch.clone();
|
||||
assert_ne!(before, "[Paper]");
|
||||
let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let id = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(id, true));
|
||||
return;
|
||||
}
|
||||
show(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
// Open the Fill menu from the Appearance chip.
|
||||
h.get_by_label("Fill swatch").click();
|
||||
h.run_steps(3);
|
||||
// Click the *chip* (the row's left edge), not the name.
|
||||
let name = "[Paper]".to_string();
|
||||
let row = h.get_by_label(&name).rect();
|
||||
// The row spans the menu (chip + name), not just the name text.
|
||||
assert!(row.width() > 150.0, "the row is the full menu width, chip included: {row:?}");
|
||||
let p = row.min + vec2(8.0, row.height() / 2.0);
|
||||
h.hover_at(p);
|
||||
h.drag_at(p);
|
||||
h.drop_at(p);
|
||||
h.run_steps(4);
|
||||
let after = h.state().session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch.clone();
|
||||
assert_eq!(after, name, "clicking the swatch chip applies it");
|
||||
}
|
||||
}
|
||||
@@ -160,11 +160,14 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) {
|
||||
let sw = c["fill"].as_str().unwrap_or("[Black]").to_string();
|
||||
let (col, g) = widgets::swatch_colors(&doc, &sw, 1.0);
|
||||
let resp = widgets::swatch_chip(ui, 17.0, col, g).on_hover_text(format!("Fill: {sw}"));
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Task Bar fill"));
|
||||
let mut picked = None;
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(180.0);
|
||||
ui.set_max_width(280.0);
|
||||
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
|
||||
for s in &doc.swatches {
|
||||
if ui.selectable_label(s.name == sw, &s.name).clicked() {
|
||||
if panels::swatch_menu_row(ui, &doc, &s.name, &sw) {
|
||||
picked = Some(s.name.clone());
|
||||
ui.close();
|
||||
}
|
||||
@@ -228,11 +231,16 @@ fn object_controls(app: &mut DesignApp, ui: &mut Ui) {
|
||||
widgets::paint_stroke_chip(ui.painter(), r, col, egui::Color32::from_gray(20));
|
||||
}
|
||||
let resp = resp.on_hover_text(format!("{}: {sw}", if k == 0 { "Fill" } else { "Stroke" }));
|
||||
resp.widget_info(|| {
|
||||
egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("Task Bar {}", if k == 0 { "fill" } else { "stroke" }))
|
||||
});
|
||||
let mut picked = None;
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(180.0);
|
||||
ui.set_max_width(280.0);
|
||||
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
|
||||
for s in &doc.swatches {
|
||||
if ui.selectable_label(s.name == sw, &s.name).clicked() {
|
||||
if panels::swatch_menu_row(ui, &doc, &s.name, &sw) {
|
||||
picked = Some(s.name.clone());
|
||||
ui.close();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user