Integration: Fill/Stroke keys follow Formatting Affects Text (fill-stroke-keys, #84)

color.defaultFillStroke, color.swapFillStroke and color.applyNone act on
what the Tools panel proxy shows: with colours going to text
(panels::colors_affect_text) they set the text's fill and stroke through
type.char in one step (Default: black fill, no stroke; Swap keeps tints;
Apply None through panels::apply_swatch), otherwise they run
object.defaultFillStroke, object.swapFillStroke and object.fill/stroke.
The proxy's Default, Swap and Apply None buttons run these commands.
fill_stroke_proxy_keys covers J with a text frame selected.
This commit is contained in:
aiongg
2026-10-12 14:51:14 +00:00
parent 1ebefe4db4
commit 58d64f8084
2 changed files with 40 additions and 16 deletions
+3 -3
View File
@@ -141,16 +141,16 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
"color.defaultFillStroke",
"Default Fill and Stroke",
Some("D"),
"{} — the selection gets no fill and a black stroke (object.defaultFillStroke)",
"{} — the selection gets no fill and a black stroke (object.defaultFillStroke); text colour edits (Formatting Affects Text) get a black fill and no stroke",
),
(
"color.swapFillStroke",
"Swap Fill and Stroke",
Some("Shift+X"),
"{} — each selected object's fill and stroke colours (and tints) trade places (object.swapFillStroke)",
"{} — each selected object's fill and stroke colours (and tints) trade places (object.swapFillStroke), or the text's with Formatting Affects Text",
),
("color.toggleFillStroke", "Toggle Fill and Stroke", Some("X"), "{} → {strokeFront} — which of fill and stroke the proxy has in front"),
("color.applyNone", "Apply None", Some("/"), "{} — [None] for the fill or stroke in front of the proxy"),
("color.applyNone", "Apply None", Some("/"), "{} — [None] for the fill or stroke in front of the proxy, of the objects or the text (Formatting Affects Text)"),
("view.frameEdges", "Show/Hide Frame Edges", Some("Cmd+H"), "{}"),
("view.rulers", "Show/Hide Rulers", Some("Cmd+R"), "{}"),
("view.guides", "Show/Hide Guides", Some("Cmd+;"), "{}"),
+37 -13
View File
@@ -215,10 +215,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
})
.clicked()
{
// Text defaults to a black fill and no stroke; objects to no fill and a black stroke.
let (fill, stroke) = if crate::panels::colors_affect_text(app) { ("[Black]", "[None]") } else { ("[None]", "[Black]") };
let _ = crate::panels::apply_swatch(app, false, fill, None);
let _ = crate::panels::apply_swatch(app, true, stroke, None);
let _ = app.run("color.defaultFillStroke", json!({}));
}
let swap = egui::Rect::from_min_size(egui::pos2(x0 + 19.0, row.min.y), vec2(11.0, 11.0));
icons::paint(ui.painter(), swap, "swap", t.icon);
@@ -229,8 +226,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
})
.clicked()
{
let _ = crate::panels::apply_swatch(app, false, &stroke_sw, None);
let _ = crate::panels::apply_swatch(app, true, &fill_sw, None);
let _ = app.run("color.swapFillStroke", json!({}));
}
ui.add_space(2.0);
// Proxy: 19.5 pt squares, fill top-left, stroke bottom-right.
@@ -330,7 +326,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
crate::widgets::paint_chip(ui.painter(), chip, None, None);
flyout_triangle(ui.painter(), well, t.icon);
if resp.clicked() {
let _ = crate::panels::apply_swatch(app, stroke_front, "[None]", None);
let _ = app.run("color.applyNone", json!({}));
}
egui::Popup::context_menu(&resp).show(|ui| {
for (label, sw) in [("Apply Color", "[Black]"), ("Apply None", "[None]")] {
@@ -423,13 +419,24 @@ fn tool_button(ui: &mut egui::Ui, icon: &str, active: bool, tip: &str) -> egui::
})
}
/// Default Fill and Stroke (D): no fill, a black stroke.
/// Default Fill and Stroke (D): text gets a black fill and no stroke, objects no fill and a black
/// stroke. Colour keys act on what the proxy shows (see `panels::colors_affect_text`).
pub(crate) fn default_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
if crate::panels::colors_affect_text(app) {
let attrs = json!({"fill": "[Black]", "fillTint": 1.0, "stroke": "[None]", "strokeTint": 1.0});
return app.run("type.char", json!({ "attrs": attrs }));
}
app.run("object.defaultFillStroke", json!({}))
}
/// Swap Fill and Stroke (Shift+X): each selected object's fill and stroke trade places.
/// Swap Fill and Stroke (Shift+X): the text's fill and stroke, or each selected object's, trade
/// places, tints included.
pub(crate) fn swap_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
if crate::panels::colors_affect_text(app) {
let (fill, fill_tint, stroke, stroke_tint) = crate::panels::color_target(app).ok_or("no text to color")?;
let attrs = json!({"fill": stroke, "fillTint": stroke_tint, "stroke": fill, "strokeTint": fill_tint});
return app.run("type.char", json!({ "attrs": attrs }));
}
app.run("object.swapFillStroke", json!({}))
}
@@ -439,10 +446,10 @@ pub(crate) fn toggle_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
Ok(json!({"strokeFront": app.ui.stroke_front}))
}
/// Apply None (/): to the fill or the stroke, whichever is in front of the proxy.
/// Apply None (/): to the fill or the stroke, whichever is in front of the proxy, of the text or
/// the objects.
pub(crate) fn apply_none(app: &mut DesignApp) -> Result<Value, String> {
let target = if app.ui.stroke_front { "object.stroke" } else { "object.fill" };
app.run(target, json!({"swatch": "[None]"}))
crate::panels::apply_swatch(app, app.ui.stroke_front, "[None]", None)
}
#[cfg(test)]
@@ -608,11 +615,28 @@ mod tests {
press(&mut app, &ctx, Key::Slash, Modifiers::NONE);
assert_eq!(colors(&app).2, "[None]", "/ applies None to the stroke in front");
// Formatting Affects Text (J) with the frame selected: the keys colour its text, as the
// proxy shows it, and leave the frame alone.
press(&mut app, &ctx, Key::X, Modifiers::NONE);
press(&mut app, &ctx, Key::J, Modifiers::NONE);
assert!(crate::panels::colors_affect_text(&app));
let frame = colors(&app);
app.run("type.char", json!({"attrs": {"fill": "[Paper]", "fillTint": 0.5, "stroke": "[Black]", "strokeTint": 1.0}})).unwrap();
let text = |app: &mut crate::DesignApp| crate::panels::color_target(app).unwrap();
press(&mut app, &ctx, Key::X, Modifiers::SHIFT);
assert_eq!(text(&mut app), ("[Black]".into(), 1.0, "[Paper]".into(), 0.5), "Shift+X swaps the text's colours");
press(&mut app, &ctx, Key::D, Modifiers::NONE);
assert_eq!(text(&mut app), ("[Black]".into(), 1.0, "[None]".into(), 1.0), "D: text gets a black fill and no stroke");
press(&mut app, &ctx, Key::Slash, Modifiers::NONE);
assert_eq!(text(&mut app).0, "[None]", "/ applies None to the text's fill in front");
assert_eq!(colors(&app), frame, "the frame keeps its colours");
// Typing in a story: the keys are text, not shortcuts.
app.session.execute("text.select", &json!({"story": f["story"], "anchor": 5})).unwrap();
app.select_tool("type");
assert!(app.session.wants_text());
let front = app.ui.stroke_front;
press(&mut app, &ctx, Key::X, Modifiers::NONE);
assert!(app.ui.stroke_front, "X while typing leaves the proxy alone");
assert_eq!(app.ui.stroke_front, front, "X while typing leaves the proxy alone");
}
}