Merge branch 'pr/600' into batch1
This commit is contained in:
commit
878169130e
|
|
@ -140,6 +140,7 @@ Newest first.
|
|||
- **2026-10-12** · Each running app keeps recovery data in its own locked folder; current folder and lock ownership are checked before writes and cleanup. Failed isolation disables recovery with a warning instead of sharing document IDs. No readiness percentage change.
|
||||
- **2026-10-12** · Native engine saves and exports replace files atomically; failed writes retain the original, private staging preserves Unix permissions, and valid long filenames work. Desktop Services and CLI raster exports remain separate follow-ups. No readiness percentage change.
|
||||
- **2026-10-12** · Hyphenation Zone applies to the Single-line Composer only, as in InDesign. With the default Paragraph Composer, ragged text hyphenates where InDesign does (aper-natem), giving the same line breaks. No readiness percentage change.
|
||||
- **2026-10-12** · The Tools panel's Fill/Stroke keys work: D (default fill and stroke), X (fill or stroke in front), Shift+X (swap per object, tints included) and / (Apply None), as `color.*` commands; swap and default are one undo step (`object.swapFillStroke`, `object.defaultFillStroke`); the Color panel follows the proxy's fill/stroke choice. No readiness percentage change.
|
||||
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
|
||||
- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change.
|
||||
- **2026-10-11** · `object.fill` and `object.stroke` refuse a swatch name the document doesn't have (it painted nothing) and suggest the name meant (`Paper` → `[Paper]`). No readiness percentage change.
|
||||
|
|
|
|||
|
|
@ -185,6 +185,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
let tint = f64_or(p, "tint", 1.0) as f32;
|
||||
set_flag(s, p, move |i| i.fill = Fill { swatch: sw.clone(), tint, ..i.fill.clone() }, false)
|
||||
}),
|
||||
cmd!(
|
||||
"object.swapFillStroke",
|
||||
"Swap Fill and Stroke",
|
||||
[],
|
||||
None,
|
||||
"{ids?} — each object's fill and stroke colours (and tints) trade places; locked objects stay as they are",
|
||||
has_editable_selection,
|
||||
|s, p| edit_unlocked(s, p, swap_fill_stroke)
|
||||
),
|
||||
cmd!(
|
||||
"object.defaultFillStroke",
|
||||
"Default Fill and Stroke",
|
||||
[],
|
||||
None,
|
||||
"{ids?} — no fill and a black stroke; locked objects stay as they are",
|
||||
has_editable_selection,
|
||||
|s, p| edit_unlocked(s, p, default_fill_stroke)
|
||||
),
|
||||
cmd!(
|
||||
"object.stroke",
|
||||
"Stroke",
|
||||
|
|
@ -1466,6 +1484,60 @@ fn set_flag(s: &mut Session, p: &Value, f: impl Fn(&mut Item), deselect: bool) -
|
|||
})
|
||||
}
|
||||
|
||||
/// An object a colour command may change: neither it nor its layer is locked.
|
||||
fn unlocked(d: &Document, it: &Item) -> bool {
|
||||
!it.locked && d.layer(it.layer).is_none_or(|l| !l.locked)
|
||||
}
|
||||
|
||||
fn has_editable_selection(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
let st = s.active().ok_or_else(|| super::NOTHING_SELECTED.to_string())?;
|
||||
if st.selection.items.iter().any(|id| st.doc.item(*id).is_some_and(|it| unlocked(&st.doc, it))) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("the selected objects are locked".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `f` on each unlocked target in one edit (one undo step).
|
||||
fn edit_unlocked(s: &mut Session, p: &Value, f: impl Fn(&mut Item)) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
s.edit(|d, _| {
|
||||
let mut changed = 0;
|
||||
for id in &ids {
|
||||
if d.item(*id).is_some_and(|it| unlocked(d, it))
|
||||
&& let Some(it) = d.item_mut(*id)
|
||||
{
|
||||
f(it);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({"changed": changed}))
|
||||
})
|
||||
}
|
||||
|
||||
/// A stroke that gets a colour needs a weight to show it.
|
||||
fn visible_weight(st: &mut Stroke) {
|
||||
if st.swatch != designcraft_color::swatch::NONE && st.weight <= 0.0 {
|
||||
st.weight = Stroke::default().weight;
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_fill_stroke(it: &mut Item) {
|
||||
let fill = std::mem::take(&mut it.fill);
|
||||
it.fill = Fill { swatch: std::mem::take(&mut it.stroke.swatch), tint: it.stroke.tint, ..fill.clone() };
|
||||
it.stroke.swatch = fill.swatch;
|
||||
it.stroke.tint = fill.tint;
|
||||
visible_weight(&mut it.stroke);
|
||||
}
|
||||
|
||||
fn default_fill_stroke(it: &mut Item) {
|
||||
it.fill = Fill { swatch: designcraft_color::swatch::NONE.into(), tint: 1.0, ..it.fill.clone() };
|
||||
it.stroke.swatch = designcraft_color::swatch::BLACK.into();
|
||||
it.stroke.tint = 1.0;
|
||||
visible_weight(&mut it.stroke);
|
||||
}
|
||||
|
||||
fn all_flag(s: &mut Session, f: impl Fn(&mut Item)) -> Result<Value> {
|
||||
s.edit(|d, _| {
|
||||
for i in 0..d.spreads.len() {
|
||||
|
|
@ -2433,6 +2505,44 @@ mod arrange_tests {
|
|||
mod step_repeat_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn swap_and_default_fill_stroke_are_one_undo_step_per_object() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = |v: Value| ItemId(v["id"].as_u64().unwrap());
|
||||
let a = id(s.execute("frame.create", &json!({"rect": [0, 0, 10, 10]})).unwrap());
|
||||
let b = id(s.execute("frame.create", &json!({"rect": [20, 0, 30, 10]})).unwrap());
|
||||
let locked = id(s.execute("frame.create", &json!({"rect": [40, 0, 50, 10]})).unwrap());
|
||||
s.execute("object.fill", &json!({"ids": [a.0], "swatch": "[Paper]", "tint": 0.5})).unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [a.0], "swatch": "[Black]", "weight": 2})).unwrap();
|
||||
s.execute("object.fill", &json!({"ids": [b.0, locked.0], "swatch": "[Black]"})).unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [b.0, locked.0], "swatch": "[None]"})).unwrap();
|
||||
s.execute("object.lock", &json!({"ids": [locked.0]})).unwrap();
|
||||
s.execute("selection.set", &json!({"ids": [a.0, b.0, locked.0]})).unwrap();
|
||||
let colors = |s: &Session, i: ItemId| {
|
||||
let it = s.doc().unwrap().doc.item(i).unwrap().clone();
|
||||
(it.fill.swatch, it.fill.tint, it.stroke.swatch, it.stroke.tint)
|
||||
};
|
||||
let before = [colors(&s, a), colors(&s, b), colors(&s, locked)];
|
||||
|
||||
assert_eq!(s.execute("object.swapFillStroke", &json!({})).unwrap()["changed"], 2);
|
||||
assert_eq!(colors(&s, a), ("[Black]".into(), 1.0, "[Paper]".into(), 0.5));
|
||||
assert_eq!(colors(&s, b), ("[None]".into(), 1.0, "[Black]".into(), 1.0));
|
||||
assert_eq!(colors(&s, locked), before[2], "a locked object keeps its colours");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!([colors(&s, a), colors(&s, b), colors(&s, locked)], before, "one undo restores both");
|
||||
|
||||
s.execute("object.defaultFillStroke", &json!({})).unwrap();
|
||||
for i in [a, b] {
|
||||
assert_eq!(colors(&s, i), ("[None]".into(), 1.0, "[Black]".into(), 1.0));
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!([colors(&s, a), colors(&s, b), colors(&s, locked)], before);
|
||||
|
||||
s.execute("selection.set", &json!({"ids": [locked.0]})).unwrap();
|
||||
assert!(s.execute("object.swapFillStroke", &json!({})).is_err(), "nothing unlocked to change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_repeat_validates_counts_and_offsets() {
|
||||
assert_eq!(step_repeat_offsets(&json!({"count": MAX_STEP_REPEAT_COPIES})).unwrap().len(), MAX_STEP_REPEAT_COPIES as usize);
|
||||
|
|
|
|||
|
|
@ -648,6 +648,15 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
|||
("Fill (X)", ["Fill (X)", "Fill (X)", "Fill (X)", "Fill (X)", "填色 (X)"]),
|
||||
("Stroke (X)", ["Stroke (X)", "Stroke (X)", "Stroke (X)", "Stroke (X)", "描边 (X)"]),
|
||||
("Apply None (/)", ["Apply None (/)", "Apply None (/)", "Apply None (/)", "Apply None (/)", "应用无 (/)"]),
|
||||
(
|
||||
"Default Fill and Stroke",
|
||||
["Default Fill and Stroke", "Default Fill and Stroke", "Default Fill and Stroke", "Default Fill and Stroke", "默认填色和描边"],
|
||||
),
|
||||
("Swap Fill and Stroke", ["Swap Fill and Stroke", "Swap Fill and Stroke", "Swap Fill and Stroke", "Swap Fill and Stroke", "交换填色和描边"]),
|
||||
(
|
||||
"Toggle Fill and Stroke",
|
||||
["Toggle Fill and Stroke", "Toggle Fill and Stroke", "Toggle Fill and Stroke", "Toggle Fill and Stroke", "切换填色和描边"],
|
||||
),
|
||||
("Reverse the gradient", ["Reverse the gradient", "Reverse the gradient", "Reverse the gradient", "Reverse the gradient", "反转渐变"]),
|
||||
("Style source", ["Style source", "Style source", "Style source", "Style source", "样式源"]),
|
||||
("Open", ["Open", "Open", "Open", "Open", "打开"]),
|
||||
|
|
|
|||
|
|
@ -523,6 +523,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Superscript", "النص المرتفع"),
|
||||
("Tag the selection", "إضافة وسم للتحديد"),
|
||||
("The structure is valid.", "البنية صالحة."),
|
||||
("Toggle Fill and Stroke", "التبديل بين التعبئة والحد"),
|
||||
("Toggle double column", "تبديل التخطيط بعمودين"),
|
||||
("Type:", "النوع:"),
|
||||
("Underline", "تسطير"),
|
||||
|
|
@ -847,6 +848,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Crop to:", "القص إلى:"),
|
||||
("Cross-Reference Format", "تنسيق المرجع المتقاطع"),
|
||||
("Cursor Key:", "مفتاح المؤشر:"),
|
||||
("Default Fill and Stroke", "التعبئة والحد الافتراضيان"),
|
||||
("Default Fill and Stroke (D)", "التعبئة والحد الافتراضيان (D)"),
|
||||
("Default View:", "العرض الافتراضي:"),
|
||||
("Delete Column", "حذف العمود"),
|
||||
|
|
@ -988,6 +990,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Style Name Conflicts", "تعارض أسماء الأنماط"),
|
||||
("Stylistic Sets", "المجموعات الأسلوبية"),
|
||||
("Subdivisions:", "التقسيمات الفرعية:"),
|
||||
("Swap Fill and Stroke", "تبديل التعبئة والحد"),
|
||||
("Swap Fill and Stroke (Shift+X)", "تبديل التعبئة والحد (Shift+X)"),
|
||||
("Synchronize", "مزامنة"),
|
||||
("System default", "افتراضي النظام"),
|
||||
|
|
|
|||
|
|
@ -378,6 +378,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Fill (X)", "Riempimento (X)"),
|
||||
("Stroke (X)", "Traccia (X)"),
|
||||
("Apply None (/)", "Applica nessuno (/)"),
|
||||
("Default Fill and Stroke", "Riempimento e traccia predefiniti"),
|
||||
("Swap Fill and Stroke", "Scambia riempimento e traccia"),
|
||||
("Toggle Fill and Stroke", "Alterna riempimento e traccia"),
|
||||
("Reverse the gradient", "Inverti la sfumatura"),
|
||||
("Style source", "Origine stile"),
|
||||
("Open", "Apri"),
|
||||
|
|
|
|||
|
|
@ -380,6 +380,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Fill (X)", "塗り (X)"),
|
||||
("Stroke (X)", "線 (X)"),
|
||||
("Apply None (/)", "カラーなしを適用 (/)"),
|
||||
("Default Fill and Stroke", "塗りと線をデフォルトに戻す"),
|
||||
("Swap Fill and Stroke", "塗りと線を入れ替え"),
|
||||
("Toggle Fill and Stroke", "塗りと線を切り替え"),
|
||||
("Reverse the gradient", "グラデーションを反転"),
|
||||
("Style source", "スタイルの同期元"),
|
||||
("Open", "開く"),
|
||||
|
|
|
|||
|
|
@ -522,6 +522,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Superscript", "Sobrescrito"),
|
||||
("Tag the selection", "Marcar a seleção"),
|
||||
("The structure is valid.", "A estrutura é válida."),
|
||||
("Toggle Fill and Stroke", "Alternar entre preenchimento e traçado"),
|
||||
("Toggle double column", "Alternar coluna dupla"),
|
||||
("Type:", "Tipo:"),
|
||||
("Underline", "Sublinhado"),
|
||||
|
|
@ -849,6 +850,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Crop to:", "Cortar para:"),
|
||||
("Cross-Reference Format", "Formato de referência cruzada"),
|
||||
("Cursor Key:", "Tecla do cursor:"),
|
||||
("Default Fill and Stroke", "Preenchimento e traçado padrão"),
|
||||
("Default Fill and Stroke (D)", "Preenchimento e traçado padrão (D)"),
|
||||
("Default View:", "Exibição padrão:"),
|
||||
("Delete Column", "Excluir coluna"),
|
||||
|
|
@ -995,6 +997,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Style Name Conflicts", "Conflitos de nomes de estilos"),
|
||||
("Stylistic Sets", "Conjuntos estilísticos"),
|
||||
("Subdivisions:", "Subdivisões:"),
|
||||
("Swap Fill and Stroke", "Alternar preenchimento e traçado"),
|
||||
("Swap Fill and Stroke (Shift+X)", "Alternar preenchimento e traçado (Shift+X)"),
|
||||
("Synchronize", "Sincronizar"),
|
||||
("System default", "Padrão do sistema"),
|
||||
|
|
|
|||
|
|
@ -381,6 +381,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
|||
("Fill (X)", "Заливка (X)"),
|
||||
("Stroke (X)", "Обведення (X)"),
|
||||
("Apply None (/)", "Без кольору (/)"),
|
||||
("Default Fill and Stroke", "Заливка та обведення за замовчуванням"),
|
||||
("Swap Fill and Stroke", "Змінити заливку та обведення"),
|
||||
("Toggle Fill and Stroke", "Перемкнути заливку та обведення"),
|
||||
("Reverse the gradient", "Змінити напрямок градієнта"),
|
||||
("Style source", "Джерело стилів"),
|
||||
("Open", "Відкрити"),
|
||||
|
|
|
|||
|
|
@ -287,6 +287,10 @@ pub struct UiState {
|
|||
pub palette: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub flyout: Option<usize>,
|
||||
/// Fill/Stroke proxy (Tools and Color panels): the stroke is in front, so Apply None and the
|
||||
/// Color panel act on it (X toggles).
|
||||
#[serde(skip)]
|
||||
pub stroke_front: bool,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
|
|
@ -362,6 +366,7 @@ impl Default for UiState {
|
|||
dialog: None,
|
||||
palette: None,
|
||||
flyout: None,
|
||||
stroke_front: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,20 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||
("view.zoom", "Zoom To", None, "{zoom: 1.0 = 100%}"),
|
||||
("view.screenMode", "Screen Mode", None, "{mode: normal|preview|bleed|slug|presentation}"),
|
||||
("view.togglePreview", "Toggle Normal/Preview", Some("W"), "{}"),
|
||||
(
|
||||
"color.defaultFillStroke",
|
||||
"Default Fill and Stroke",
|
||||
Some("D"),
|
||||
"{} — the selection gets no fill and a black stroke (object.defaultFillStroke)",
|
||||
),
|
||||
(
|
||||
"color.swapFillStroke",
|
||||
"Swap Fill and Stroke",
|
||||
Some("Shift+X"),
|
||||
"{} — each selected object's fill and stroke colours (and tints) trade places (object.swapFillStroke)",
|
||||
),
|
||||
("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"),
|
||||
("view.frameEdges", "Show/Hide Frame Edges", Some("Cmd+H"), "{}"),
|
||||
("view.rulers", "Show/Hide Rulers", Some("Cmd+R"), "{}"),
|
||||
("view.guides", "Show/Hide Guides", Some("Cmd+;"), "{}"),
|
||||
|
|
@ -1116,6 +1130,10 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
|||
app.ui.screen_mode = if app.ui.screen_mode == ScreenMode::Normal { ScreenMode::Preview } else { ScreenMode::Normal };
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"color.defaultFillStroke" => crate::toolbar::default_fill_stroke(app),
|
||||
"color.swapFillStroke" => crate::toolbar::swap_fill_stroke(app),
|
||||
"color.toggleFillStroke" => crate::toolbar::toggle_fill_stroke(app),
|
||||
"color.applyNone" => crate::toolbar::apply_none(app),
|
||||
"view.frameEdges" => flag(&mut app.ui.frame_edges),
|
||||
"view.overprintPreview" => {
|
||||
app.canvas.shown = None;
|
||||
|
|
@ -2104,6 +2122,7 @@ fn parse_shortcut(sc: &str) -> Option<(egui::Modifiers, egui::Key)> {
|
|||
"," => key = Some(egui::Key::Comma),
|
||||
"." => key = Some(egui::Key::Period),
|
||||
"\\" => key = Some(egui::Key::Backslash),
|
||||
"/" => key = Some(egui::Key::Slash),
|
||||
"Delete" => key = Some(egui::Key::Delete),
|
||||
k => key = egui::Key::from_name(k),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,9 +209,8 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
return;
|
||||
};
|
||||
let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return };
|
||||
let stroke_id = egui::Id::new("color_panel_stroke");
|
||||
let mode_id = egui::Id::new("color_panel_mode");
|
||||
let mut stroke: bool = ui.data(|d| d.get_temp(stroke_id)).unwrap_or(false);
|
||||
let mut stroke = app.ui.stroke_front;
|
||||
let default_mode = if doc.settings.intent == designcraft_doc::Intent::Print { ColorMode::Cmyk } else { ColorMode::Rgb };
|
||||
let mut mode: ColorMode = ui.data(|d| d.get_temp(mode_id)).unwrap_or(default_mode);
|
||||
let target = if stroke { "stroke" } else { "fill" };
|
||||
|
|
@ -253,10 +252,8 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
});
|
||||
});
|
||||
});
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(stroke_id, stroke);
|
||||
d.insert_temp(mode_id, mode);
|
||||
});
|
||||
app.ui.stroke_front = stroke;
|
||||
ui.data_mut(|d| d.insert_temp(mode_id, mode));
|
||||
let sw = doc.swatch(&name).cloned();
|
||||
let Some(c) = doc.resolve_color(&name, 1.0) else {
|
||||
ui.label(crate::rtl::widget(
|
||||
|
|
|
|||
|
|
@ -196,9 +196,8 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let info = crate::panels::sel_info(app);
|
||||
let doc = app.session.active().map(|d| d.doc.clone());
|
||||
let stroke_front_id = egui::Id::new("proxy_stroke_front");
|
||||
let text_id = egui::Id::new("proxy_text");
|
||||
let stroke_front: bool = ui.data(|d| d.get_temp(stroke_front_id)).unwrap_or(false);
|
||||
let stroke_front = app.ui.stroke_front;
|
||||
let text_mode: bool = ui.data(|d| d.get_temp(text_id)).unwrap_or(false);
|
||||
let (fill_sw, stroke_sw) = match &info {
|
||||
Some(i) => (i.fill.clone(), i.stroke.clone()),
|
||||
|
|
@ -216,8 +215,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.fill", json!({"swatch": "[None]"}));
|
||||
let _ = app.run("object.stroke", json!({"swatch": "[Black]"}));
|
||||
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);
|
||||
|
|
@ -228,8 +226,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.fill", json!({"swatch": stroke_sw}));
|
||||
let _ = app.run("object.stroke", json!({"swatch": fill_sw}));
|
||||
let _ = app.run("color.swapFillStroke", json!({}));
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
// Proxy: 19.5 pt squares, fill top-left, stroke bottom-right.
|
||||
|
|
@ -278,7 +275,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
if fresp.clicked() || sresp.clicked() {
|
||||
let front =
|
||||
if stroke_front { !fresp.clicked() } else { sresp.clicked() && !fill_r.contains(sresp.interact_pointer_pos().unwrap_or_default()) };
|
||||
ui.data_mut(|d| d.insert_temp(stroke_front_id, front));
|
||||
app.ui.stroke_front = front;
|
||||
}
|
||||
if fresp.double_clicked() || sresp.double_clicked() {
|
||||
let _ = app.run("window.panel", json!({"panel": "swatches"}));
|
||||
|
|
@ -318,7 +315,7 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
|
|||
flyout_triangle(ui.painter(), well, t.icon);
|
||||
let target = if stroke_front { "object.stroke" } else { "object.fill" };
|
||||
if resp.clicked() {
|
||||
let _ = app.run(target, json!({"swatch": "[None]"}));
|
||||
let _ = app.run("color.applyNone", json!({}));
|
||||
}
|
||||
egui::Popup::context_menu(&resp).show(|ui| {
|
||||
for (label, sw) in [("Apply Color", "[Black]"), ("Apply None", "[None]")] {
|
||||
|
|
@ -411,6 +408,28 @@ fn tool_button(ui: &mut egui::Ui, icon: &str, active: bool, tip: &str) -> egui::
|
|||
})
|
||||
}
|
||||
|
||||
/// Default Fill and Stroke (D): no fill, a black stroke.
|
||||
pub(crate) fn default_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
|
||||
app.run("object.defaultFillStroke", json!({}))
|
||||
}
|
||||
|
||||
/// Swap Fill and Stroke (Shift+X): each selected object's fill and stroke trade places.
|
||||
pub(crate) fn swap_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
|
||||
app.run("object.swapFillStroke", json!({}))
|
||||
}
|
||||
|
||||
/// Fill/Stroke toggle (X): brings the other of fill and stroke to the front of the proxy.
|
||||
pub(crate) fn toggle_fill_stroke(app: &mut DesignApp) -> Result<Value, String> {
|
||||
app.ui.stroke_front = !app.ui.stroke_front;
|
||||
Ok(json!({"strokeFront": app.ui.stroke_front}))
|
||||
}
|
||||
|
||||
/// Apply None (/): to the fill or the stroke, whichever is in front of the proxy.
|
||||
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]"}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{pos2, vec2};
|
||||
|
|
@ -482,4 +501,42 @@ mod tests {
|
|||
wheel(&mut h, panel.center(), vec2(0.0, -2000.0));
|
||||
assert_eq!((rect(&h, LAST_TOOL), rect(&h, FILL)), before);
|
||||
}
|
||||
/// The Fill/Stroke proxy's keys run on the selection and stay out of text being typed.
|
||||
#[test]
|
||||
fn fill_stroke_proxy_keys() {
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use serde_json::json;
|
||||
fn press(app: &mut crate::DesignApp, ctx: &egui::Context, key: Key, modifiers: Modifiers) {
|
||||
let events = [true, false].map(|pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).to_vec();
|
||||
ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| crate::menus::shortcuts(app, ui.ctx())).textures_delta.clear();
|
||||
}
|
||||
fn colors(app: &crate::DesignApp) -> (String, f32, String, f32) {
|
||||
let st = app.session.active().unwrap();
|
||||
let it = st.doc.item(st.selection.items[0]).unwrap();
|
||||
(it.fill.swatch.clone(), it.fill.tint, it.stroke.swatch.clone(), it.stroke.tint)
|
||||
}
|
||||
let mut app = app(false);
|
||||
let ctx = egui::Context::default();
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let f = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hello"})).unwrap();
|
||||
app.run("selection.set", json!({"ids": [f["id"]]})).unwrap();
|
||||
app.run("object.fill", json!({"swatch": "[Paper]", "tint": 0.5})).unwrap();
|
||||
app.run("object.stroke", json!({"swatch": "[Black]", "tint": 1.0})).unwrap();
|
||||
|
||||
press(&mut app, &ctx, Key::X, Modifiers::SHIFT);
|
||||
assert_eq!(colors(&app), ("[Black]".into(), 1.0, "[Paper]".into(), 0.5), "Shift+X swaps, tints included");
|
||||
press(&mut app, &ctx, Key::D, Modifiers::NONE);
|
||||
assert_eq!(colors(&app), ("[None]".into(), 1.0, "[Black]".into(), 1.0), "D: no fill, black stroke");
|
||||
press(&mut app, &ctx, Key::X, Modifiers::NONE);
|
||||
assert!(app.ui.stroke_front, "X brings the stroke to the front");
|
||||
press(&mut app, &ctx, Key::Slash, Modifiers::NONE);
|
||||
assert_eq!(colors(&app).2, "[None]", "/ applies None to the stroke in front");
|
||||
|
||||
// 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());
|
||||
press(&mut app, &ctx, Key::X, Modifiers::NONE);
|
||||
assert!(app.ui.stroke_front, "X while typing leaves the proxy alone");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
|||
- Text engine: Knuth–Plass paragraph composer and single-line composer, dictionary hyphenation (public-domain Moby list + our own trained Liang patterns), justification with word/letter/glyph-scaling ranges, keep options (keep with next, keep lines together, widows/orphans), balance ragged lines, optical margin alignment, hyphenation zone/limit, columns, threading across frames and spreads, tabs, bullets/numbering, rules, shading, baseline grid, first-baseline options, vertical justification, wrap exclusions, overset detection, caret/hit testing.
|
||||
- FID2: IDML underline/strikethrough Text Color resets survive style inheritance and roundtrip. Raster and PDF paint underlines behind glyphs, retain strikethrough above, and center rules on their offsets; synthetic overlap tests cover both backends.
|
||||
- Rendering: vello_cpu (SIMD, multithreaded) — pages, items, images with mip levels, gradients, composed text.
|
||||
- UI (egui): application bar + menus, Control panel (object and text modes), Tools panel with flyouts, document tabs, rulers, pasteboard, guides (margins, columns, bleed, baseline grid), frame edges, selection handles, text ports and threads, Properties / Pages / Layers dock, Swatches, Styles, Character, Paragraph, Stroke, Text Wrap panels, New Document and Preferences dialogs, Quick Apply (⌘Return), 4 brightness themes.
|
||||
- UI (egui): application bar + menus, Control panel (object and text modes), Tools panel with flyouts and the Fill/Stroke proxy (D, X, Shift+X, / as `color.*` commands), document tabs, rulers, pasteboard, guides (margins, columns, bleed, baseline grid), frame edges, selection handles, text ports and threads, Properties / Pages / Layers dock, Swatches, Styles, Character, Paragraph, Stroke, Text Wrap panels, New Document and Preferences dialogs, Quick Apply (⌘Return), 4 brightness themes.
|
||||
- Pen tool, Direct Selection anchor/handle editing, rotate from corners, Rotate/Scale/Shear tools, Eyedropper, snapping & smart guides, Align/Distribute, Find/Change (text + GREP), Story Editor, Paragraph Style Options, Effects panel, hidden characters, native macOS menu bar, measured InDesign 2026 look (Medium Dark): Properties sections per selection state, 21 pt spinner fields with arithmetic, Contextual Task Bar, inverse (black) text selection, Pages panel with drag-and-drop reorder / parent apply, Layers panel with per-object rows.
|
||||
- Tools: Selection (click, marquee, move, Alt-duplicate, resize handles), Direct Selection, Type (draw frame, click caret, select, type), Rectangle/Ellipse/Polygon (+ frame variants), Line, Hand, Zoom.
|
||||
- PDF export (krilla): real selectable text with embedded font subsets, DeviceCMYK/RGB + spot Separations, bleed boxes, crop/bleed marks + page info, pages or spreads, PDF/A-2b (PDF/X-4 output intent pending) — `file.exportPdf`, File › Export PDF…, `designcraft-cli run --export out.pdf`. File › Export PDF… now opens an "Export PDF" options dialog (General / Compression / Marks and Bleeds / Advanced) and remembers the last-used settings across exports and sessions.
|
||||
|
|
|
|||
Loading…
Reference in New Issue