T: Underline / Strikethrough Options — weight, offset, colour, tint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a36772023
commit
4aa76b85f9
|
|
@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Underline / Strikethrough Options: weight, offset, colour and tint (Character panel; screen, PDF and IDML).
|
||||
- Relink to Folder and Relink File Extension (`links.relinkFolder`; missing links found by name).
|
||||
- File › Package (⌥⇧⌘P): the document relinked to a Links folder, the placed files, an IDML copy, an optional PDF and a report (fonts, links, preflight, instructions); Copy Links To (`links.copyTo`).
|
||||
- Load Swatches / Save Swatches for Exchange (.ase: CMYK, RGB, Lab, Gray; spot or process) from the Swatches panel menu; `swatch.load` / `swatch.save`.
|
||||
|
|
|
|||
|
|
@ -46,12 +46,35 @@ pub struct RunStyle {
|
|||
pub stroke_weight: f64,
|
||||
pub underline: bool,
|
||||
pub strikethrough: bool,
|
||||
/// The underline and strikethrough bars (Underline / Strikethrough Options resolved).
|
||||
pub underline_rule: Rule,
|
||||
pub strike_rule: Rule,
|
||||
pub skew: f64,
|
||||
pub size: f64,
|
||||
/// The run's font isn't installed (shown in a substitute; highlighted on screen).
|
||||
pub missing_font: bool,
|
||||
}
|
||||
|
||||
/// An underline or strikethrough bar: its top edge `offset` below the baseline (negative =
|
||||
/// above), thickness and colour.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Rule {
|
||||
pub offset: f64,
|
||||
pub weight: f64,
|
||||
pub color: String,
|
||||
pub tint: f32,
|
||||
}
|
||||
|
||||
impl RunStyle {
|
||||
/// The bars this run draws between `x0` and `x1` on a line at `baseline`.
|
||||
pub fn rules(&self, x0: f64, x1: f64, baseline: f64) -> impl Iterator<Item = (&Rule, Rect)> {
|
||||
[(self.underline, &self.underline_rule), (self.strikethrough, &self.strike_rule)]
|
||||
.into_iter()
|
||||
.filter(|(on, _)| *on)
|
||||
.map(move |(_, r)| (r, Rect::new(x0, baseline + r.offset, x1, baseline + r.offset + r.weight)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A positioned glyph. `x` is absolute in frame inner space; `y` is relative to the line baseline.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlacedGlyph {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,18 @@ impl StyleTable<'_> {
|
|||
stroke_weight: p.stroke_weight,
|
||||
underline: p.underline,
|
||||
strikethrough: p.strikethrough,
|
||||
underline_rule: crate::Rule {
|
||||
offset: p.underline_offset.unwrap_or(p.size * 0.12),
|
||||
weight: p.underline_weight.unwrap_or(p.size / 14.0),
|
||||
color: if p.underline_color.is_empty() { p.fill.clone() } else { p.underline_color.clone() },
|
||||
tint: if p.underline_color.is_empty() { p.fill_tint } else { p.underline_tint },
|
||||
},
|
||||
strike_rule: crate::Rule {
|
||||
offset: -p.strikethrough_offset.unwrap_or(p.size * 0.3),
|
||||
weight: p.strikethrough_weight.unwrap_or(p.size / 14.0),
|
||||
color: if p.strikethrough_color.is_empty() { p.fill.clone() } else { p.strikethrough_color.clone() },
|
||||
tint: if p.strikethrough_color.is_empty() { p.fill_tint } else { p.strikethrough_tint },
|
||||
},
|
||||
skew: p.skew,
|
||||
size: p.size,
|
||||
missing_font: *self
|
||||
|
|
|
|||
|
|
@ -388,6 +388,17 @@ attr_set! {
|
|||
position: Position = Position::Normal,
|
||||
underline: bool = false,
|
||||
strikethrough: bool = false,
|
||||
/// Underline Options: weight and offset below the baseline (`None` = from the size),
|
||||
/// colour (empty = the text colour) and tint.
|
||||
underline_weight: Option<f64> = None,
|
||||
underline_offset: Option<f64> = None,
|
||||
underline_color: String = String::new(),
|
||||
underline_tint: f32 = 1.0,
|
||||
/// Strikethrough Options: offset above the baseline.
|
||||
strikethrough_weight: Option<f64> = None,
|
||||
strikethrough_offset: Option<f64> = None,
|
||||
strikethrough_color: String = String::new(),
|
||||
strikethrough_tint: f32 = 1.0,
|
||||
ligatures: bool = true,
|
||||
no_break: bool = false,
|
||||
language: String = "English: USA".into(),
|
||||
|
|
|
|||
|
|
@ -152,3 +152,33 @@ fn open_type_features_round_trip_through_idml() {
|
|||
let after = st.format_after(5).over.otf_features.clone().unwrap_or_default();
|
||||
assert!(!designcraft_doc::otf::is_on(&after, "frac") && designcraft_doc::otf::stylistic_sets(&after) == 0, "{after:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underline_options_render_and_round_trip() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 200], "content": "text", "text": "Underlined words"})).unwrap();
|
||||
let sid = r["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 10})).unwrap();
|
||||
s.execute("type.char", &json!({"attrs": {"underline": true, "underlineWeight": 3, "underlineOffset": 4, "underlineColor": "C=100 M=0 Y=0 K=0"}}))
|
||||
.unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let cs = designcraft_compose::compose_story(&d, designcraft_doc::StoryId(sid), &Default::default());
|
||||
let st = cs.styles.iter().find(|st| st.underline).unwrap();
|
||||
assert_eq!((st.underline_rule.weight, st.underline_rule.offset), (3.0, 4.0));
|
||||
assert_eq!(st.underline_rule.color, "C=100 M=0 Y=0 K=0");
|
||||
// Drawn in cyan, 4–7 pt under the baseline.
|
||||
let l = &cs.frames[0].lines[0];
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
rr.threads = 0;
|
||||
let img = rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
let x = l.glyphs[2].x as u32;
|
||||
let p = img.pixel(x, (l.baseline + 5.5) as u32);
|
||||
assert!(p[0] < 80 && p[2] > 150, "cyan underline: {p:?}");
|
||||
let back = designcraft_idml::import_idml(&designcraft_idml::export_idml(&d)).unwrap();
|
||||
let story = back.stories.values().find(|x| x.text.starts_with("Underlined")).unwrap();
|
||||
let f = story.format_after(0);
|
||||
assert_eq!(f.over.underline_weight, Some(Some(3.0)));
|
||||
assert_eq!(f.over.underline_offset, Some(Some(4.0)));
|
||||
assert_eq!(f.over.underline_color.as_deref(), Some("C=100 M=0 Y=0 K=0"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -875,6 +875,23 @@ impl<'a> Ex<'a> {
|
|||
if let Some(v) = a.ligatures {
|
||||
el.set("Ligatures", bool_s(v));
|
||||
}
|
||||
for (k, w, o, c, t) in [
|
||||
("Underline", a.underline_weight, a.underline_offset, &a.underline_color, a.underline_tint),
|
||||
("StrikeThru", a.strikethrough_weight, a.strikethrough_offset, &a.strikethrough_color, a.strikethrough_tint),
|
||||
] {
|
||||
if let Some(w) = w {
|
||||
el.set(&format!("{k}Weight"), num(w.unwrap_or(-9999.0)));
|
||||
}
|
||||
if let Some(o) = o {
|
||||
el.set(&format!("{k}Offset"), num(o.unwrap_or(-9999.0)));
|
||||
}
|
||||
if let Some(c) = c.as_ref().filter(|c| !c.is_empty()) {
|
||||
el.set(&format!("{k}Color"), self.sw(c));
|
||||
}
|
||||
if let Some(t) = t {
|
||||
el.set(&format!("{k}Tint"), num(t as f64 * 100.0));
|
||||
}
|
||||
}
|
||||
if let Some(list) = &a.otf_features {
|
||||
for (tag, _, attr) in designcraft_doc::otf::TOGGLES {
|
||||
el.set(attr, bool_s(designcraft_doc::otf::is_on(list, tag)));
|
||||
|
|
|
|||
|
|
@ -859,6 +859,23 @@ impl<'r> Importer<'r> {
|
|||
a.underline = e.boolean("Underline");
|
||||
a.strikethrough = e.boolean("StrikeThru");
|
||||
a.ligatures = e.boolean("Ligatures");
|
||||
// Underline / strikethrough options (-9999 = automatic).
|
||||
for k in ["Underline", "StrikeThru"] {
|
||||
let num = |n: &str| e.num(&format!("{k}{n}")).filter(|v| *v > -9000.0);
|
||||
let color = e.prop(&format!("{k}Color")).map(|r| self.swatch_ref(r.trim())).filter(|r| r != "Text Color" && !r.is_empty());
|
||||
let tint = e.num(&format!("{k}Tint")).filter(|v| *v >= 0.0).map(|v| (v / 100.0) as f32);
|
||||
if k == "Underline" {
|
||||
a.underline_weight = num("Weight").map(Some);
|
||||
a.underline_offset = num("Offset").map(Some);
|
||||
a.underline_color = color;
|
||||
a.underline_tint = tint;
|
||||
} else {
|
||||
a.strikethrough_weight = num("Weight").map(Some);
|
||||
a.strikethrough_offset = num("Offset").map(Some);
|
||||
a.strikethrough_color = color;
|
||||
a.strikethrough_tint = tint;
|
||||
}
|
||||
}
|
||||
// OpenType features (only when the element sets any).
|
||||
let mut otf_list: Vec<String> = Vec::new();
|
||||
let mut any = false;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ impl Exporter<'_> {
|
|||
if !ft.tables.is_empty() {
|
||||
self.tables(s, ft);
|
||||
}
|
||||
let mut deco: Vec<(u32, Rect)> = Vec::new();
|
||||
let mut deco: Vec<(designcraft_compose::Rule, Rect)> = Vec::new();
|
||||
for l in &ft.lines {
|
||||
let gs = &l.glyphs;
|
||||
let mut i = 0;
|
||||
|
|
@ -84,25 +84,18 @@ impl Exporter<'_> {
|
|||
// Underline / strikethrough, merged per style along the line.
|
||||
for g in gs.iter().filter(|g| g.visible) {
|
||||
let st = &cs.styles[g.style as usize];
|
||||
let w = st.size / 14.0;
|
||||
let mut add = |y: f64| {
|
||||
let r = Rect::new(g.x, y, g.x + g.adv, y + w);
|
||||
for (rule, r) in st.rules(g.x, g.x + g.adv, l.baseline) {
|
||||
match deco.last_mut() {
|
||||
Some((si, last)) if *si == g.style && (last.y0 - r.y0).abs() < 1e-6 && (r.x0 - last.x1).abs() < 0.5 => last.x1 = r.x1,
|
||||
_ => deco.push((g.style, r)),
|
||||
Some((last_rule, last)) if *last_rule == *rule && (last.y0 - r.y0).abs() < 1e-6 && (r.x0 - last.x1).abs() < 0.5 => {
|
||||
last.x1 = r.x1
|
||||
}
|
||||
_ => deco.push((rule.clone(), r)),
|
||||
}
|
||||
};
|
||||
if st.underline {
|
||||
add(l.baseline + st.size * 0.12);
|
||||
}
|
||||
if st.strikethrough {
|
||||
add(l.baseline - st.size * 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (si, r) in deco {
|
||||
let st = &cs.styles[si as usize];
|
||||
if let Some(c) = self.swatch_color(&st.fill, st.fill_tint) {
|
||||
for (rule, r) in deco {
|
||||
if let Some(c) = self.swatch_color(&rule.color, rule.tint) {
|
||||
self.fill_rect(s, r, c);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ pub(crate) struct LineGlyphs {
|
|||
pub bounds: Option<Rect>,
|
||||
/// Glyph outlines batched per run style.
|
||||
pub runs: Vec<(u32, BezPath)>,
|
||||
/// Underline / strikethrough bars.
|
||||
pub decos: Vec<(u32, Rect)>,
|
||||
/// Underline / strikethrough bars: colour swatch, tint, rectangle.
|
||||
pub decos: Vec<(String, f32, Rect)>,
|
||||
pub glyphs: usize,
|
||||
}
|
||||
|
||||
|
|
@ -123,14 +123,8 @@ fn build_line(db: &FontDb, cs: &ComposedStory, l: &Line) -> LineGlyphs {
|
|||
continue;
|
||||
}
|
||||
let style = &cs.styles[g.style as usize];
|
||||
if style.underline || style.strikethrough {
|
||||
let w = style.size / 14.0;
|
||||
if style.underline {
|
||||
decos.push((g.style, Rect::new(g.x, l.baseline + style.size * 0.12, g.x + g.adv, l.baseline + style.size * 0.12 + w)));
|
||||
}
|
||||
if style.strikethrough {
|
||||
decos.push((g.style, Rect::new(g.x, l.baseline - style.size * 0.3, g.x + g.adv, l.baseline - style.size * 0.3 + w)));
|
||||
}
|
||||
for (rule, r) in style.rules(g.x, g.x + g.adv, l.baseline) {
|
||||
decos.push((rule.color.clone(), rule.tint, r));
|
||||
}
|
||||
let outline = db.outline(&g.face, g.gid);
|
||||
if outline.elements().is_empty() {
|
||||
|
|
@ -156,7 +150,7 @@ fn build_line(db: &FontDb, cs: &ComposedStory, l: &Line) -> LineGlyphs {
|
|||
let sw = if st.stroke != designcraft_color::swatch::NONE { st.stroke_weight } else { 0.0 };
|
||||
bounds = union(bounds, bp.bounding_box().inflate(sw, sw));
|
||||
}
|
||||
for (_, r) in &decos {
|
||||
for (_, _, r) in &decos {
|
||||
bounds = union(bounds, *r);
|
||||
}
|
||||
LineGlyphs { bounds, runs, decos, glyphs }
|
||||
|
|
@ -263,9 +257,11 @@ impl Renderer {
|
|||
ctx.stroke_path(bp);
|
||||
}
|
||||
}
|
||||
for (si, r) in &lg.decos {
|
||||
ctx.set_paint(fill_of(*si).unwrap_or(peniko::Color::BLACK));
|
||||
ctx.fill_rect(r);
|
||||
for (sw, tint, r) in &lg.decos {
|
||||
if let Some(c) = doc.resolve_color(sw, *tint) {
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
ctx.fill_rect(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1188,6 +1188,37 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
c["otfFeatures"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
|
||||
ui.menu_button("OpenType", |ui| open_type_menu(app, ui, &features));
|
||||
});
|
||||
// Underline / Strikethrough Options.
|
||||
for (key, title) in [("underline", "Underline Options"), ("strikethrough", "Strikethrough Options")] {
|
||||
if !c[key].as_bool().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
egui::CollapsingHeader::new(title).id_salt(key).show(ui, |ui| {
|
||||
let size = c["size"].as_f64().unwrap_or(12.0);
|
||||
let auto_off = if key == "underline" { size * 0.12 } else { size * 0.3 };
|
||||
egui::Grid::new((key, "opts")).num_columns(4).spacing(vec2(6.0, 4.0)).show(ui, |ui| {
|
||||
caption(ui, "Weight");
|
||||
let w = c[format!("{key}Weight")].as_f64().unwrap_or(size / 14.0);
|
||||
if let Some(v) = number(ui, &format!("{key}w"), Some(w), " pt", 60.0, 2) {
|
||||
let _ = app.run("type.char", json!({"attrs": {format!("{key}Weight"): v.max(0.0)}}));
|
||||
}
|
||||
caption(ui, "Offset");
|
||||
let o = c[format!("{key}Offset")].as_f64().unwrap_or(auto_off);
|
||||
if let Some(v) = number(ui, &format!("{key}o"), Some(o), " pt", 60.0, 2) {
|
||||
let _ = app.run("type.char", json!({"attrs": {format!("{key}Offset"): v}}));
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
caption(ui, "Color");
|
||||
let cur = c[format!("{key}Color")].as_str().filter(|s| !s.is_empty()).map(str::to_string);
|
||||
super::swatch_picker(app, ui, &format!("{key}c"), cur.clone(), |app, n| {
|
||||
let _ = app.run("type.char", json!({"attrs": {format!("{key}Color"): n}}));
|
||||
});
|
||||
ui.label(cur.unwrap_or_else(|| "(Text Color)".into()));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The Character panel's OpenType menu: feature toggles, figure styles and stylistic sets.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 47 | 23 | 27 | 60% |
|
||||
| P1 | 97 | 48 | 23 | 26 | 61% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|
|
@ -24,7 +24,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Effects & transparency | 50% |
|
||||
| Placing & links | 78% |
|
||||
| Type & text frames | 80% |
|
||||
| Typography | 79% |
|
||||
| Typography | 83% |
|
||||
| Styles | 71% |
|
||||
| Tables | 73% |
|
||||
| Long documents | 58% |
|
||||
|
|
@ -216,7 +216,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Type & text frames | Math expressions | P2 | M |
|
||||
| Type & text frames | Right-to-left / World-Ready composer | P2 | M |
|
||||
| Typography | Character attributes | P0 | D |
|
||||
| Typography | Underline/Strikethrough options | P1 | M |
|
||||
| Typography | Underline/Strikethrough options | P1 | D |
|
||||
| Typography | OpenType features | P0 | D |
|
||||
| Typography | Variable fonts | P1 | M |
|
||||
| Typography | Paragraph attributes | P0 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue