mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Merge PR #838
This commit is contained in:
commit
a6e7e2a0c7
4 files changed
+272
-24
No files matched your search
@@ -133,6 +133,7 @@ Newest first.
|
||||
|
||||
- **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change.
|
||||
- **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change.
|
||||
- **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -179,12 +179,35 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
)
|
||||
}
|
||||
),
|
||||
cmd!("object.fill", "Fill", [], None, "{swatch (a swatch's name, e.g. [Paper]), tint?: 0..1, ids?}", has_selection, |s, p| {
|
||||
let sw = str_param(p, "swatch").ok_or_else(|| bad("object.fill", "missing swatch"))?.to_string();
|
||||
known_swatch(&s.doc()?.doc, &sw, "object.fill", "swatch")?;
|
||||
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.fill",
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{swatch? (a swatch's name, e.g. [Paper]), tint?: 0..1, ids?} — at least one; a tint alone keeps each object's own swatch (the Swatches panel's Tint field)",
|
||||
has_selection,
|
||||
|s, p| {
|
||||
let sw = str_param(p, "swatch").map(str::to_string);
|
||||
let tint = match p.get("tint") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(v.as_f64().filter(|t| (0.0..=1.0).contains(t)).ok_or_else(|| bad("object.fill", "`tint` is 0..1"))? as f32),
|
||||
};
|
||||
match &sw {
|
||||
Some(sw) => known_swatch(&s.doc()?.doc, sw, "object.fill", "swatch")?,
|
||||
None if tint.is_none() => return Err(bad("object.fill", "missing swatch")),
|
||||
None => {}
|
||||
}
|
||||
set_flag(
|
||||
s,
|
||||
p,
|
||||
move |i| match &sw {
|
||||
Some(sw) => i.fill = Fill { swatch: sw.clone(), tint: tint.unwrap_or(1.0), ..i.fill.clone() },
|
||||
None => i.fill.tint = tint.unwrap_or(i.fill.tint),
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"object.swapFillStroke",
|
||||
"Swap Fill and Stroke",
|
||||
@@ -217,10 +240,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
}
|
||||
let p2 = p.clone();
|
||||
// A tint alone (the Swatches panel's Tint field) recolours strokes, it doesn't add one.
|
||||
let tint_only = p.as_object().is_some_and(|o| o.contains_key("tint") && o.keys().all(|k| k == "tint" || k == "ids"));
|
||||
set_flag(
|
||||
s,
|
||||
p,
|
||||
move |i| {
|
||||
if tint_only && i.stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
let mut st = if i.stroke.is_none() && p2.get("swatch").is_none() { Stroke::default() } else { i.stroke.clone() };
|
||||
if let Some(v) = p2.get("swatch").and_then(Value::as_str) {
|
||||
st.swatch = v.into();
|
||||
@@ -3327,3 +3355,36 @@ mod spread_target_tests {
|
||||
assert!(s.doc().unwrap().doc.spreads[0].items.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod fill_tint_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_tint_alone_keeps_each_objects_own_swatch() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [36, 36, 100, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("frame.create", &json!({"rect": [136, 36, 200, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.fill", &json!({"ids": [a], "swatch": "C=100 M=0 Y=0 K=0"})).unwrap();
|
||||
s.execute("object.fill", &json!({"ids": [b], "swatch": "C=0 M=100 Y=0 K=0"})).unwrap();
|
||||
s.execute("object.fill", &json!({"ids": [a, b], "tint": 0.4})).unwrap();
|
||||
let fill_of = |s: &Session, id: u64| s.doc().unwrap().doc.item(ItemId(id)).unwrap().fill.clone();
|
||||
assert_eq!((fill_of(&s, a).swatch.as_str(), fill_of(&s, a).tint), ("C=100 M=0 Y=0 K=0", 0.4));
|
||||
assert_eq!((fill_of(&s, b).swatch.as_str(), fill_of(&s, b).tint), ("C=0 M=100 Y=0 K=0", 0.4));
|
||||
// A swatch without a tint is at 100 %, as before.
|
||||
s.execute("object.fill", &json!({"ids": [a], "swatch": "[Black]"})).unwrap();
|
||||
assert_eq!(fill_of(&s, a).tint, 1.0);
|
||||
// Neither, or a tint out of range: refused.
|
||||
assert!(s.execute("object.fill", &json!({"ids": [a]})).is_err());
|
||||
assert!(s.execute("object.fill", &json!({"ids": [a], "tint": 2})).is_err());
|
||||
assert!(s.execute("object.fill", &json!({"ids": [a], "tint": "x"})).is_err());
|
||||
// A stroke tint alone: stroked objects take it, an object without a stroke keeps none.
|
||||
s.execute("object.stroke", &json!({"ids": [a], "swatch": "C=100 M=0 Y=0 K=0", "weight": 2})).unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [b], "swatch": "[None]"})).unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [a, b], "tint": 0.5})).unwrap();
|
||||
let stroke_of = |s: &Session, id: u64| s.doc().unwrap().doc.item(ItemId(id)).unwrap().stroke.clone();
|
||||
assert_eq!((stroke_of(&s, a).swatch.as_str(), stroke_of(&s, a).tint, stroke_of(&s, a).weight), ("C=100 M=0 Y=0 K=0", 0.5, 2.0));
|
||||
assert!(stroke_of(&s, b).is_none());
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let doc = st.doc.clone();
|
||||
let text = super::colors_affect_text(app);
|
||||
let fill_cur = super::color_target(app).map(|t| t.0);
|
||||
ui.horizontal(|ui| {
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, if text { "Applies to text" } else { "Applies to fill" }))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
});
|
||||
let target = super::color_target(app);
|
||||
header(app, ui, &doc, target.as_ref());
|
||||
// The row of the colour in front of the proxy is highlighted.
|
||||
let fill_cur = target.map(|(fill, _, stroke, _)| if app.ui.stroke_front { stroke } else { fill });
|
||||
// Unnamed colours (mixed in the Color panel) aren't listed until Add to Swatches; swatches in
|
||||
// colour groups are listed under their folder.
|
||||
let grouped: Vec<&str> = doc.color_groups.iter().flat_map(|g| g.swatches.iter().map(String::as_str)).collect();
|
||||
@@ -64,15 +58,25 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
// InDesign's bottom row: New Color Group, New Swatch, Delete Swatch (the one last clicked).
|
||||
let picked: Option<String> = ui.data(|d| d.get_temp(egui::Id::new(PICKED))).filter(|n: &String| doc.swatch(n).is_some_and(|w| !w.locked));
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(crate::rtl::widget(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Right-click: stroke, groups")).size(10.5).color(t.text_disabled),
|
||||
));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let del = ui
|
||||
.add_enabled_ui(picked.is_some(), |ui| icons::button(ui, "trash", 20.0, false, crate::i18n::tr(&app.ui.language, "Delete Swatch")))
|
||||
.inner;
|
||||
if del.clicked()
|
||||
&& let Some(name) = &picked
|
||||
&& app.run("swatch.delete", json!({"name": name})).is_ok()
|
||||
{
|
||||
ui.data_mut(|d| d.remove::<String>(egui::Id::new(PICKED)));
|
||||
}
|
||||
if icons::button(ui, "plus", 20.0, false, crate::i18n::tr(&app.ui.language, "New Swatch")).clicked() {
|
||||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
if folder_button(ui, crate::i18n::tr(&app.ui.language, "New Color Group")).clicked() {
|
||||
let _ = app.run("swatch.newColorGroup", json!({}));
|
||||
}
|
||||
crate::menus::menu_button(ui, "☰", |ui| {
|
||||
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Ink Manager")), |ui| {
|
||||
let Ok(l) = app.session.execute("ink.list", &json!({})) else { return };
|
||||
@@ -114,8 +118,99 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// One Swatches panel row: chip, name, type; click applies to the fill (of the frames or their
|
||||
/// text, see [`super::colors_affect_text`]), the context menu to the stroke, colour groups and delete.
|
||||
/// Temp-data key of the swatch last clicked (Delete Swatch removes it).
|
||||
const PICKED: &str = "swatches_picked";
|
||||
|
||||
/// The Swatches panel's top row, as InDesign's: a Fill/Stroke proxy (click: the other in front),
|
||||
/// Formatting Affects Container / Text, and the tint of the colour in front. `target` is
|
||||
/// [`super::color_target`]: fill and stroke swatches and tints of the text or the first object.
|
||||
fn header(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Document, target: Option<&(String, f32, String, f32)>) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (fill_sw, stroke_sw) = target.map_or(("[None]", "[Black]"), |(f, _, s, _)| (f.as_str(), s.as_str()));
|
||||
let text = super::colors_affect_text(app) || app.ui.formatting_affects_text;
|
||||
let stroke_front = app.ui.stroke_front;
|
||||
ui.add_space(3.0);
|
||||
ui.horizontal(|ui| {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(27.0, 27.0), Sense::click());
|
||||
let tip = crate::i18n::tr(&app.ui.language, if stroke_front { "Stroke" } else { "Fill" });
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
|
||||
let fill_r = egui::Rect::from_min_size(r.min + vec2(1.0, 2.0), vec2(16.0, 16.0));
|
||||
let stroke_r = egui::Rect::from_min_size(r.min + vec2(10.0, 10.0), vec2(16.0, 16.0));
|
||||
let paint = |stroke: bool| {
|
||||
let p = ui.painter();
|
||||
if stroke {
|
||||
// The stroke: a frame of its colour.
|
||||
let (c, g) = crate::widgets::swatch_colors(doc, stroke_sw, 1.0);
|
||||
crate::widgets::paint_chip(p, stroke_r, c, g);
|
||||
p.rect_filled(stroke_r.shrink(4.0), 0.0, t.panel);
|
||||
p.rect_stroke(stroke_r.shrink(4.0), 0.0, egui::Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside);
|
||||
p.rect_stroke(stroke_r, 0.0, egui::Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside);
|
||||
} else {
|
||||
let (c, g) = crate::widgets::swatch_colors(doc, fill_sw, 1.0);
|
||||
crate::widgets::paint_chip(p, fill_r, c, g);
|
||||
p.rect_stroke(fill_r, 0.0, egui::Stroke::new(1.0, t.text_dim), egui::StrokeKind::Outside);
|
||||
}
|
||||
};
|
||||
// The one in front is drawn last.
|
||||
paint(!stroke_front);
|
||||
paint(stroke_front);
|
||||
if resp
|
||||
.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, tip);
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("color.toggleFillStroke", json!({}));
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
for (k, (icon, tip)) in
|
||||
[("format-container", "Formatting affects container (J)"), ("format-text", "Formatting affects text (J)")].into_iter().enumerate()
|
||||
{
|
||||
if icons::button(ui, icon, 20.0, text == (k == 1), crate::i18n::tr(&app.ui.language, tip)).clicked() {
|
||||
let _ = app.run("app.formattingAffectsText", json!({"on": k == 1}));
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&app.ui.language, "Tint:"));
|
||||
let tint = target.map(|(_, ft, _, st)| f64::from(if stroke_front { *st } else { *ft }) * 100.0);
|
||||
if let Some(v) = crate::widgets::number(ui, "swatches_tint", tint, "", 44.0, 0) {
|
||||
let _ = set_tint(app, v);
|
||||
}
|
||||
crate::widgets::caption(ui, "%");
|
||||
});
|
||||
}
|
||||
|
||||
/// The Tint field: `percent` of the colour in front of the proxy, for the text or each selected
|
||||
/// object, which keep their own swatches.
|
||||
fn set_tint(app: &mut DesignApp, percent: f64) -> Result<serde_json::Value, String> {
|
||||
let v = percent.clamp(0.0, 100.0) / 100.0;
|
||||
let stroke = app.ui.stroke_front;
|
||||
if super::colors_affect_text(app) {
|
||||
app.run("type.char", json!({"attrs": {if stroke { "strokeTint" } else { "fillTint" }: v}}))
|
||||
} else {
|
||||
app.run(if stroke { "object.stroke" } else { "object.fill" }, json!({"tint": v}))
|
||||
}
|
||||
}
|
||||
|
||||
/// A folder button (New Color Group), drawn like the colour groups' folders.
|
||||
fn folder_button(ui: &mut egui::Ui, tip: &str) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(20.0, 20.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 3.0, t.hover);
|
||||
}
|
||||
let f = egui::Rect::from_center_size(r.center(), vec2(14.0, 10.0));
|
||||
ui.painter().rect_stroke(f, 1.5, egui::Stroke::new(1.2, t.icon), egui::StrokeKind::Inside);
|
||||
ui.painter().line_segment([f.left_top() + vec2(1.0, 3.0), f.right_top() + vec2(-1.0, 3.0)], egui::Stroke::new(1.0, t.icon));
|
||||
resp.on_hover_ui(|ui| {
|
||||
crate::rtl::label(ui, tip);
|
||||
})
|
||||
}
|
||||
|
||||
/// One Swatches panel row: chip, name, type; click applies it to the fill or stroke in front of
|
||||
/// the proxy (of the frames or their text, see [`super::colors_affect_text`]), the context menu
|
||||
/// to the stroke, colour groups and delete.
|
||||
fn swatch_row(
|
||||
app: &mut DesignApp,
|
||||
ui: &mut egui::Ui,
|
||||
@@ -126,6 +221,7 @@ fn swatch_row(
|
||||
) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &sw.name));
|
||||
if fill_cur == Some(sw.name.as_str()) {
|
||||
ui.painter().rect_filled(row, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
@@ -154,7 +250,8 @@ fn swatch_row(
|
||||
}
|
||||
ui.painter().text(row.right_center() - vec2(6.0, 0.0), egui::Align2::RIGHT_CENTER, kind, egui::FontId::proportional(10.5), t.text_dim);
|
||||
if resp.clicked() {
|
||||
let _ = super::apply_swatch(app, false, &sw.name, None);
|
||||
ui.data_mut(|d| d.insert_temp(egui::Id::new(PICKED), sw.name.clone()));
|
||||
let _ = super::apply_swatch(app, app.ui.stroke_front, &sw.name, None);
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply to Stroke"))).clicked() {
|
||||
@@ -573,3 +670,92 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod swatches_header_tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn harness(app: DesignApp) -> Harness<'static, DesignApp> {
|
||||
Harness::builder().with_size(vec2(300.0, 520.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,
|
||||
)
|
||||
}
|
||||
|
||||
fn items(app: &DesignApp) -> Vec<designcraft_doc::Item> {
|
||||
let st = app.session.doc().unwrap();
|
||||
st.selection.items.iter().map(|id| st.doc.item(*id).unwrap().clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatches_go_to_the_fill_or_stroke_in_front_and_the_bottom_row_deletes_the_one_clicked() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "unassigned"})).unwrap();
|
||||
app.run("swatch.create", json!({"name": "Brand", "color": "#108080"})).unwrap();
|
||||
let mut h = harness(app);
|
||||
h.run_steps(3);
|
||||
// Fill in front: a swatch is the fill.
|
||||
h.get_by_label("C=0 M=100 Y=0 K=0").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(items(h.state())[0].fill.swatch, "C=0 M=100 Y=0 K=0");
|
||||
// The proxy brings the stroke to the front; the next swatch is the stroke.
|
||||
h.get_by_label("Fill").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().ui.stroke_front);
|
||||
h.get_by_label("C=100 M=0 Y=0 K=0").click();
|
||||
h.run_steps(2);
|
||||
let it = &items(h.state())[0];
|
||||
assert_eq!((it.fill.swatch.as_str(), it.stroke.swatch.as_str()), ("C=0 M=100 Y=0 K=0", "C=100 M=0 Y=0 K=0"));
|
||||
// Formatting affects text, from the panel.
|
||||
h.get_by_label("Formatting affects text (J)").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().ui.formatting_affects_text);
|
||||
h.get_by_label("Formatting affects container (J)").click();
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().ui.formatting_affects_text);
|
||||
// Delete Swatch removes the swatch last clicked.
|
||||
h.get_by_label("Brand").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Delete Swatch").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().session.doc().unwrap().doc.swatch("Brand").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tint_field_keeps_each_objects_own_swatch() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [36, 36, 100, 100], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("frame.create", json!({"rect": [136, 36, 200, 100], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("object.fill", json!({"ids": [a], "swatch": "C=100 M=0 Y=0 K=0"})).unwrap();
|
||||
app.run("object.fill", json!({"ids": [b], "swatch": "C=0 M=100 Y=0 K=0"})).unwrap();
|
||||
app.run("object.stroke", json!({"ids": [a, b], "swatch": "[Black]"})).unwrap();
|
||||
app.run("selection.set", json!({"ids": [a, b]})).unwrap();
|
||||
let mut h = harness(app);
|
||||
h.run_steps(3);
|
||||
// The field shows the first selected object's fill tint.
|
||||
assert!(h.query_all_by_value("100").next().is_some());
|
||||
set_tint(h.state_mut(), 40.0).unwrap();
|
||||
h.run_steps(2);
|
||||
assert!(h.query_all_by_value("40").next().is_some(), "the field follows");
|
||||
// The stroke in front: the strokes' tints.
|
||||
h.state_mut().ui.stroke_front = true;
|
||||
set_tint(h.state_mut(), 70.0).unwrap();
|
||||
assert!(items(h.state()).iter().all(|i| i.stroke.tint == 0.7));
|
||||
let it = items(h.state());
|
||||
assert_eq!((it[0].fill.swatch.as_str(), it[0].fill.tint), ("C=100 M=0 Y=0 K=0", 0.4));
|
||||
assert_eq!((it[1].fill.swatch.as_str(), it[1].fill.tint), ("C=0 M=100 Y=0 K=0", 0.4));
|
||||
}
|
||||
}
|
||||
@@ -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 — at start/end of paragraph by default, as in InDesign — 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 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.
|
||||
- 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 (InDesign's header: Fill/Stroke proxy, Formatting Affects Container/Text, Tint; a swatch goes to the fill or stroke in front; New Color Group / New Swatch / Delete Swatch row), 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.
|
||||
|
||||
Reference in new issue
Block a user