mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 00:11:46 +00:00
Type: add OpenType Stylistic Sets submenu (Set 1–20) (#10304)
* Type: add OpenType Stylistic Sets submenu (Set 1–20) Type > OpenType > Stylistic Sets toggles ss01–ss20 through new type.openType.stylisticSets.setN commands on the usual formatting scope (selected characters while editing, else the selected type layers). The toggle direction and the menu checkmark follow the style at the selection start. Updates TYPE-218-5 and G8. Closes #10285 * Stylistic Sets: round-trip test through a full PSD; scorecard parity count * Stylistic Sets: test PSD round trip on an opened file; record the new-document PSD gap Documents made in PhotoCraft get no Txt2 block on PSD save, and the set mask lives only there, so their sets are not saved to PSD yet. The engine test keeps the .pcraft round trip, an io test covers the PSD round trip of a file with type objects, and TYPE-218-5 and G8 record the gap. * Stylistic Sets: share one tag table between the engine and the menu The engine exports STYLISTIC_SET_TAGS; the Set 1–20 commands index it and the menu checkmarks read it, so the 20 tags are listed once. * Stylistic Sets: check malformed menu ids through the resolver, not the menu list * Scorecard: TYPE-218-5 notes PSD save of stylistic sets in documents made here (#10308) --------- Co-authored-by: FredApps <FredApps@users.noreply.github.com>
This commit is contained in:
1 parent
efbe97bd53
commit
ad59d9962c
12 files changed
+282
-17
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! The Type menu: anti-aliasing, orientation, point ⇄ paragraph conversion, Create Work Path,
|
||||
//! Convert to Shape, Rasterize, Warp Text, the OpenType toggles (with the Numerals submenu),
|
||||
//! Paste Lorem Ipsum, Update All Text Layers, missing-font replacement and the default type
|
||||
//! styles.
|
||||
//! Convert to Shape, Rasterize, Warp Text, the OpenType toggles (with the Numerals submenu and the 20
|
||||
//! Stylistic Sets), Paste Lorem Ipsum, Update All Text Layers, missing-font replacement and the
|
||||
//! default type styles.
|
||||
//!
|
||||
//! Everything edits the typed [`TextLayer`] model and re-renders through `type_cmds::refresh`,
|
||||
//! so the PSD `TySh` data (and with it warp, anti-aliasing and the OpenType flags) is regenerated
|
||||
@@ -503,6 +503,12 @@ fn warp_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// ---------- OpenType toggles ----------
|
||||
|
||||
/// The OpenType tags of Type › OpenType › Stylistic Sets: `STYLISTIC_SET_TAGS[n - 1]` is Set n.
|
||||
pub const STYLISTIC_SET_TAGS: [&str; 20] = [
|
||||
"ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09", "ss10", "ss11", "ss12", "ss13", "ss14", "ss15", "ss16", "ss17", "ss18", "ss19",
|
||||
"ss20",
|
||||
];
|
||||
|
||||
/// OpenType figure tags Type › OpenType › Numerals clears or sets (`lnum`/`onum`/`pnum`/`tnum`).
|
||||
pub const NUMERAL_FEATURE_TAGS: [&str; 4] = ["lnum", "onum", "pnum", "tnum"];
|
||||
|
||||
@@ -879,7 +885,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
const LP: &str = r##"{"layer":id?}"##;
|
||||
const FP: &str = r##"{"layer":id? or "layers":[id,…]? (default selected Type layers)}"##;
|
||||
const OT: &str = r##"{"layer":id? or "layers":[id,…]? (default selected Type layers),"on":bool? (default: toggle representative),"range":[startChar,endChar]? (single layer; default all)}"##;
|
||||
vec![
|
||||
// One toggle per stylistic set; a run fn can't capture, so each indexes the tag table itself.
|
||||
macro_rules! stylistic_sets {
|
||||
($($n:literal),* $(,)?) => {
|
||||
[$(spec!(concat!("type.openType.stylisticSets.set", $n), concat!("Set ", $n), &["Type", "OpenType", "Stylistic Sets"], OT, has_format_target, |s, p| toggle_opentype(s, p, STYLISTIC_SET_TAGS[$n - 1]))),*]
|
||||
};
|
||||
}
|
||||
let sets = stylistic_sets![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
|
||||
let mut all = vec![
|
||||
spec!("type.antiAlias.none", "None", &["Type", "Anti-Alias"], FP, has_format_target, |s, p| set_aa(s, p, AntiAlias::None)),
|
||||
spec!("type.antiAlias.sharp", "Sharp", &["Type", "Anti-Alias"], FP, has_format_target, |s, p| set_aa(s, p, AntiAlias::Sharp)),
|
||||
spec!("type.antiAlias.crisp", "Crisp", &["Type", "Anti-Alias"], FP, has_format_target, |s, p| set_aa(s, p, AntiAlias::Crisp)),
|
||||
@@ -995,7 +1008,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
|s, _| save_defaults(s)
|
||||
),
|
||||
spec!("type.loadDefaultTypeStyles", "Load Default Type Styles", &["Type"], FP, has_defaults, load_defaults),
|
||||
]
|
||||
];
|
||||
all.extend(sets);
|
||||
all
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -375,6 +375,152 @@ fn numerals_tabular_changes_inter_digit_widths() {
|
||||
assert!((width(&s) - plain).abs() < 0.5, "default restores the advances");
|
||||
}
|
||||
|
||||
const SET_IDS: [&str; 3] = ["type.openType.stylisticSets.set1", "type.openType.stylisticSets.set3", "type.openType.stylisticSets.set20"];
|
||||
|
||||
#[test]
|
||||
fn stylistic_sets_are_registered_with_their_menu_path() {
|
||||
for n in 1..=20 {
|
||||
let spec = crate::commands::find(&format!("type.openType.stylisticSets.set{n}")).unwrap();
|
||||
assert_eq!(spec.label, format!("Set {n}"));
|
||||
assert_eq!(spec.menu, ["Type", "OpenType", "Stylistic Sets"]);
|
||||
}
|
||||
assert!(crate::commands::find("type.openType.stylisticSets.set0").is_none());
|
||||
assert!(crate::commands::find("type.openType.stylisticSets.set21").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_sets_toggle_independently_and_keep_other_features() {
|
||||
let mut s = session(8);
|
||||
let id = text_layer(&mut s, "office 1/2");
|
||||
let before = text(&s, id).char_runs()[0].style.clone();
|
||||
s.execute("type.openType.stylisticAlternates", json!({})).unwrap();
|
||||
s.execute(SET_IDS[0], json!({})).unwrap();
|
||||
s.execute(SET_IDS[1], json!({})).unwrap();
|
||||
s.execute(SET_IDS[2], json!({"on": true})).unwrap();
|
||||
for tag in ["ss01", "ss03", "ss20", "salt"] {
|
||||
assert!(on_at(&s, id, 0, tag), "{tag}");
|
||||
}
|
||||
assert!(!on_at(&s, id, 0, "ss02"));
|
||||
s.execute(SET_IDS[0], json!({})).unwrap();
|
||||
assert!(!on_at(&s, id, 0, "ss01"), "toggled off");
|
||||
for tag in ["ss03", "ss20", "salt"] {
|
||||
assert!(on_at(&s, id, 0, tag), "{tag} stays");
|
||||
}
|
||||
let st = text(&s, id).char_runs()[0].style.clone();
|
||||
assert!(st.ligatures, "liga untouched");
|
||||
assert_eq!((st.size_pt, st.font_family.clone(), st.color), (before.size_pt, before.font_family, before.color));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_set_range_edits_only_those_characters_and_direction_follows_the_range_start() {
|
||||
let mut s = session(8);
|
||||
let id = text_layer(&mut s, "abcdef");
|
||||
s.execute("type.openType.stylisticSets.set2", json!({"range": [3, 6]})).unwrap();
|
||||
let on: Vec<bool> = (0..6).map(|i| on_at(&s, id, i, "ss02")).collect();
|
||||
assert_eq!(on, [false, false, false, true, true, true]);
|
||||
// The range starts on the set, so the default direction is off.
|
||||
let r = s.execute("type.openType.stylisticSets.set2", json!({"range": [3, 6]})).unwrap();
|
||||
assert_eq!(r["on"], false);
|
||||
assert!((0..6).all(|i| !on_at(&s, id, i, "ss02")));
|
||||
s.execute("type.openType.stylisticSets.set2", json!({"on": true, "range": [3, 6]})).unwrap();
|
||||
// A range starting before the styled characters turns it on (start is off), for all of them.
|
||||
let r = s.execute("type.openType.stylisticSets.set2", json!({"range": [1, 6]})).unwrap();
|
||||
assert_eq!(r["on"], true);
|
||||
let on: Vec<bool> = (0..6).map(|i| on_at(&s, id, i, "ss02")).collect();
|
||||
assert_eq!(on, [false, true, true, true, true, true]);
|
||||
// And a range starting on a set character toggles off even though it begins mid-text.
|
||||
let r = s.execute("type.openType.stylisticSets.set2", json!({"range": [4, 5]})).unwrap();
|
||||
assert_eq!(r["on"], false);
|
||||
let on: Vec<bool> = (0..6).map(|i| on_at(&s, id, i, "ss02")).collect();
|
||||
assert_eq!(on, [false, true, true, true, false, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_sets_apply_to_every_listed_layer() {
|
||||
let mut s = session(8);
|
||||
let a = text_layer(&mut s, "one");
|
||||
let b = text_layer(&mut s, "two");
|
||||
s.execute("type.openType.stylisticSets.set7", json!({"layers": [a.0, b.0]})).unwrap();
|
||||
assert!(on_at(&s, a, 0, "ss07") && on_at(&s, b, 2, "ss07"));
|
||||
s.execute("type.openType.stylisticSets.set7", json!({"layers": [a.0, b.0]})).unwrap();
|
||||
assert!(!on_at(&s, a, 0, "ss07") && !on_at(&s, b, 0, "ss07"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_set_toggles_are_one_undo_step_each() {
|
||||
let mut s = session(8);
|
||||
let id = text_layer(&mut s, "abc");
|
||||
s.execute(SET_IDS[0], json!({})).unwrap();
|
||||
s.execute(SET_IDS[1], json!({})).unwrap();
|
||||
assert!(s.undo());
|
||||
assert!(on_at(&s, id, 0, "ss01") && !on_at(&s, id, 0, "ss03"), "one step undone");
|
||||
assert!(s.undo());
|
||||
assert!(!on_at(&s, id, 0, "ss01"));
|
||||
assert!(s.redo());
|
||||
assert!(on_at(&s, id, 0, "ss01") && !on_at(&s, id, 0, "ss03"), "one step redone");
|
||||
assert!(s.redo());
|
||||
assert!(on_at(&s, id, 0, "ss01") && on_at(&s, id, 0, "ss03"));
|
||||
}
|
||||
|
||||
/// The sets survive a `.pcraft` save. In PSD they live only in the file's `Txt2` block, which a
|
||||
/// document made here doesn't get yet; the PSD round trip of an opened file is tested in
|
||||
/// `photocraft-io` (`psd_kerning.rs`, `edited_text_layer_keeps_its_stylistic_sets`).
|
||||
#[test]
|
||||
fn stylistic_sets_survive_a_pcraft_round_trip() {
|
||||
let mut s = session(8);
|
||||
let id = text_layer(&mut s, "Il36");
|
||||
s.execute("type.openType.stylisticSets.set1", json!({})).unwrap();
|
||||
s.execute("type.openType.stylisticSets.set5", json!({})).unwrap();
|
||||
let bytes = photocraft_format::save_to_bytes(&s.active().unwrap().doc, &Default::default()).unwrap();
|
||||
let doc = photocraft_format::load_from_bytes(&bytes).unwrap();
|
||||
let Some(LayerContent::Text(t)) = doc.layer(id).map(|l| &l.content) else { panic!("text layer lost") };
|
||||
let st = &t.char_runs()[0].style;
|
||||
assert!(feature_on(st, "ss01") && feature_on(st, "ss05") && !feature_on(st, "ss02"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_sets_fail_gracefully_on_bad_params() {
|
||||
let mut s = session(8);
|
||||
let bg = s.active().unwrap().active_layer.unwrap();
|
||||
let id = text_layer(&mut s, "abc");
|
||||
let cmd = "type.openType.stylisticSets.set9";
|
||||
assert!(s.execute(cmd, json!({"layer": 987_654})).is_err(), "unknown layer");
|
||||
assert!(s.execute(cmd, json!({"layer": bg.0})).is_err(), "not a type layer");
|
||||
assert!(s.execute(cmd, json!({"layers": ["x"]})).is_err());
|
||||
// Same as the other toggles: an unusable `on` falls back to toggling, a reversed range is
|
||||
// read in order, and a range past the end clamps (nothing to style).
|
||||
s.execute(cmd, json!({"on": "x"})).unwrap();
|
||||
assert!(on_at(&s, id, 0, "ss09"));
|
||||
s.execute(cmd, json!({"range": [5, 2]})).unwrap();
|
||||
s.execute(cmd, json!({"range": [50, 90]})).unwrap();
|
||||
s.execute(cmd, json!({"range": [u64::MAX, 0]})).unwrap();
|
||||
s.execute(cmd, json!({"range": "all"})).unwrap();
|
||||
assert!(s.execute(cmd, json!({"layer": id.0, "range": [0, 1], "on": null})).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_sets_change_inter_glyphs() {
|
||||
let mut s = session(8);
|
||||
let r = s.execute("type.create", json!({"x": 20, "y": 70, "text": "Il36", "font": "Inter", "size": 36, "color": "#000000"})).unwrap();
|
||||
let id = LayerId(r["layer"].as_u64().unwrap());
|
||||
let glyphs = |s: &Session| {
|
||||
let t = text(s, id).clone();
|
||||
let mut e = photocraft_text::shared().lock().unwrap_or_else(|e| e.into_inner());
|
||||
e.layout(&t, 72.0).glyphs.iter().map(|g| g.id).collect::<Vec<_>>()
|
||||
};
|
||||
let plain = glyphs(&s);
|
||||
s.execute("type.openType.stylisticSets.set1", json!({"range": [2, 4]})).unwrap();
|
||||
let one = glyphs(&s);
|
||||
assert_ne!(one, plain, "ss01 redraws the digits");
|
||||
s.execute("type.openType.stylisticSets.set2", json!({"range": [0, 2]})).unwrap();
|
||||
let two = glyphs(&s);
|
||||
assert_ne!(two, one, "ss02 redraws I and l");
|
||||
assert_eq!(two.len(), plain.len());
|
||||
s.execute("type.openType.stylisticSets.set1", json!({"range": [2, 4]})).unwrap();
|
||||
s.execute("type.openType.stylisticSets.set2", json!({"range": [0, 2]})).unwrap();
|
||||
assert_eq!(glyphs(&s), plain, "turning both off restores the glyphs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lorem_update_fonts_and_default_styles() {
|
||||
let mut s = session(16);
|
||||
|
||||
@@ -359,6 +359,33 @@ fn edited_text_layer_takes_its_own_style_runs() {
|
||||
assert!(t.char_runs().iter().all(|r| r.style.features.iter().all(|f| f.tag == "calt")), "the old text's salt came back");
|
||||
}
|
||||
|
||||
/// Stylistic Sets (#10285) on a type layer of an opened file save into its `Txt2` object as the
|
||||
/// set mask and come back on reopen: Set 1 and Set 5 on "Il", none on "36".
|
||||
#[test]
|
||||
fn edited_text_layer_keeps_its_stylistic_sets() {
|
||||
let mut doc = Document::new("k", photocraft_geom::Size::new(64, 32), photocraft_color::ColorMode::Rgb, photocraft_color::SampleType::U8);
|
||||
let mut t = layer("Il36", &[(2, Kerning::Metrics, 0.0), (2, Kerning::Metrics, 0.0)]);
|
||||
if let Some(run) = t.runs.first_mut() {
|
||||
run.style.features = ["ss01", "ss05"].iter().map(|tag| photocraft_doc::text::FontFeature { tag: (*tag).into(), value: 1 }).collect();
|
||||
}
|
||||
t.sync_summary();
|
||||
photocraft_text::TextEngine::new().render_layer(&mut t, doc.resolution_dpi, doc.pixel_format());
|
||||
let raw = photocraft_text::psd::build_tysh(&t, doc.resolution_dpi, None);
|
||||
t.psd_raw = Some(std::sync::Arc::new(photocraft_text::psd::set_text_index(&raw, 0).unwrap()));
|
||||
doc.layers.push(Layer::new("sets", LayerContent::Text(t)));
|
||||
doc.metadata.psd_global_blocks = vec![(*b"8BIM", *b"Txt2", std::sync::Arc::new(photoshop_txt2(&[("Il36", "")])))];
|
||||
let bytes = photocraft_io::export(&doc, "k.psd", &Default::default()).unwrap().bytes;
|
||||
let back = photocraft_io::import("k.psd", &bytes).unwrap().document;
|
||||
let LayerContent::Text(t) = &back.layers[0].content else { panic!("not text") };
|
||||
assert_eq!(t.text, "Il36");
|
||||
let on = |r: &TextRun, tag: &str| r.style.features.iter().any(|f| f.tag == tag && f.value > 0);
|
||||
let runs = t.char_runs();
|
||||
let first = runs.first().unwrap();
|
||||
assert!(on(first, "ss01") && on(first, "ss05") && !on(first, "ss02"), "{runs:?}");
|
||||
assert_eq!(first.len, 2, "{runs:?}");
|
||||
assert!(runs.iter().skip(1).all(|r| !on(r, "ss01") && !on(r, "ss05")), "{runs:?}");
|
||||
}
|
||||
|
||||
/// A file-controlled `TextIndex` must not size the save: `i32::MAX` is ignored (the regenerated
|
||||
/// `Txt2` is sized by real text objects, never by a file's numbers) and the document still saves
|
||||
/// and reopens with its text.
|
||||
|
||||
@@ -483,6 +483,26 @@ pub static CATALOG: &[(&[&str], &str, Option<&str>, &str)] = &[
|
||||
(&["Type", "OpenType", "Numerals"], "Tabular Lining", None, "type.openType.numerals.tabularLining"),
|
||||
(&["Type", "OpenType", "Numerals"], "Proportional Oldstyle", None, "type.openType.numerals.proportionalOldstyle"),
|
||||
(&["Type", "OpenType", "Numerals"], "Tabular Oldstyle", None, "type.openType.numerals.tabularOldstyle"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 1", None, "type.openType.stylisticSets.set1"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 2", None, "type.openType.stylisticSets.set2"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 3", None, "type.openType.stylisticSets.set3"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 4", None, "type.openType.stylisticSets.set4"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 5", None, "type.openType.stylisticSets.set5"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 6", None, "type.openType.stylisticSets.set6"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 7", None, "type.openType.stylisticSets.set7"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 8", None, "type.openType.stylisticSets.set8"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 9", None, "type.openType.stylisticSets.set9"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 10", None, "type.openType.stylisticSets.set10"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 11", None, "type.openType.stylisticSets.set11"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 12", None, "type.openType.stylisticSets.set12"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 13", None, "type.openType.stylisticSets.set13"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 14", None, "type.openType.stylisticSets.set14"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 15", None, "type.openType.stylisticSets.set15"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 16", None, "type.openType.stylisticSets.set16"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 17", None, "type.openType.stylisticSets.set17"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 18", None, "type.openType.stylisticSets.set18"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 19", None, "type.openType.stylisticSets.set19"),
|
||||
(&["Type", "OpenType", "Stylistic Sets"], "Set 20", None, "type.openType.stylisticSets.set20"),
|
||||
(&["Type"], "---", None, "---"),
|
||||
(&["Type"], "Create Work Path", None, "type.createWorkPath"),
|
||||
(&["Type"], "Convert to Shape", None, "type.convertToShape"),
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::menu_catalog::CATALOG;
|
||||
use crate::menus::is_live;
|
||||
|
||||
/// Minimum number of live catalog items. Raise it when parity grows; never lower it.
|
||||
pub const FLOOR: usize = 640;
|
||||
pub const FLOOR: usize = 660;
|
||||
|
||||
/// One top-level menu's coverage.
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
|
||||
@@ -205,11 +205,19 @@ fn type_checked(app: &PhotocraftApp, id: &str) -> Option<bool> {
|
||||
"ornaments" => "ornm",
|
||||
"ordinals" => "ordn",
|
||||
"fractions" => "frac",
|
||||
_ => return None,
|
||||
set => stylistic_set_tag(set.strip_prefix("stylisticSets.set")?)?,
|
||||
};
|
||||
Some(photocraft_engine::type_extra_cmds::feature_on_at(t, start, tag))
|
||||
}
|
||||
|
||||
/// `ss01`..`ss20` for the digits of a Stylistic Sets item id (`1`..`20`, no leading zeros).
|
||||
fn stylistic_set_tag(n: &str) -> Option<&'static str> {
|
||||
if n.starts_with('0') || !n.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
photocraft_engine::type_extra_cmds::STYLISTIC_SET_TAGS.get(n.parse::<usize>().ok()?.checked_sub(1)?).copied()
|
||||
}
|
||||
|
||||
/// Engine commands that get a dialog or file picker here when invoked without parameters.
|
||||
fn wraps(id: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
@@ -298,6 +298,26 @@ fn numerals_checkmark_follows_the_selection() {
|
||||
assert_eq!(checked(&app, "type.openType.numerals.notAStyle"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylistic_set_checkmark_follows_the_toggle() {
|
||||
let (mut app, ctx) = app_with(1);
|
||||
let id = "type.openType.stylisticSets.set4";
|
||||
assert!(!crate::menus::is_enabled(&app, id));
|
||||
assert_eq!(checked(&app, id), None, "no type layer");
|
||||
app.run("type.create", json!({"x": 10, "y": 50, "text": "Type", "size": 24})).unwrap();
|
||||
assert!(crate::menus::is_enabled(&app, id));
|
||||
assert_eq!(checked(&app, id), Some(false));
|
||||
menu(&mut app, &ctx, id, json!({})).unwrap();
|
||||
assert_eq!(checked(&app, id), Some(true));
|
||||
assert_eq!(checked(&app, "type.openType.stylisticSets.set5"), Some(false));
|
||||
menu(&mut app, &ctx, id, json!({})).unwrap();
|
||||
assert_eq!(checked(&app, id), Some(false));
|
||||
for bad in ["set0", "set21", "set04", "set", "set-1", "setx", "set100"] {
|
||||
// Not through `checked`, which only finds catalog items: these ids must reach the resolver.
|
||||
assert_eq!(super::dispatch::checked(&app, &format!("type.openType.stylisticSets.{bad}")), None, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_menu_requires_a_recorded_action() {
|
||||
let (mut app, ctx) = app_with(1);
|
||||
|
||||
@@ -2445,6 +2445,34 @@ mod multi_layer_formatting {
|
||||
assert_eq!(text(&app, ids[0]).antialias, AntiAlias::Sharp);
|
||||
}
|
||||
|
||||
/// Type > OpenType > Stylistic Sets (#10285): while editing, a set goes on for the selected
|
||||
/// characters only, and its checkmark follows the style at the selection start.
|
||||
#[test]
|
||||
fn stylistic_set_menu_toggles_the_selection_and_checks_its_start() {
|
||||
use photocraft_engine::type_extra_cmds::feature_on;
|
||||
let (mut app, ids) = fixture();
|
||||
let ctx = egui::Context::default();
|
||||
let id = "type.openType.stylisticSets.set2";
|
||||
let checked = |app: &PhotocraftApp| crate::menus::menu_items(app).into_iter().find(|i| i.id == id).and_then(|i| i.checked);
|
||||
editing(&mut app, ids[0]);
|
||||
assert_eq!(checked(&app), Some(false));
|
||||
crate::menus::invoke(&mut app, &ctx, id, json!({})).unwrap();
|
||||
let on: Vec<bool> = (0..5).map(|ci| feature_on(&style(&app, ids[0], ci), "ss02")).collect();
|
||||
assert_eq!(on, [false, true, true, false, false], "only the selected characters");
|
||||
assert!((0..5).all(|ci| !feature_on(&style(&app, ids[1], ci), "ss02")), "the other layer is untouched");
|
||||
assert_eq!(checked(&app), Some(true), "the selection start has the set");
|
||||
// A selection starting after the styled characters shows it off.
|
||||
if let Some(ed) = app.ui.text_edit.as_mut() {
|
||||
ed.anchor = 3;
|
||||
ed.caret = 5;
|
||||
}
|
||||
assert_eq!(checked(&app), Some(false));
|
||||
crate::menus::invoke(&mut app, &ctx, id, json!({})).unwrap();
|
||||
let on: Vec<bool> = (0..5).map(|ci| feature_on(&style(&app, ids[0], ci), "ss02")).collect();
|
||||
assert_eq!(on, [false, true, true, true, true]);
|
||||
assert_eq!(checked(&app), Some(true));
|
||||
}
|
||||
|
||||
/// The Character panel's OpenType buttons (TYPE-218-5, #3752): Photoshop's eight, in its
|
||||
/// order; each shows pressed while its feature is on and toggles it on the selected type
|
||||
/// layers, or on the selected characters while typing.
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
# Where PhotoCraft falls short of Photoshop
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (G8: PhotoCraft-native PSDs keep stylistic sets and other Txt2-only OpenType flags through save/reopen, #10308; G10: TIFF Options dialog on Save As and Save a Copy, compression, Save Image Pyramid, Save Transparency, #8827; G11: Batch Esc Continue/Stop and read-only input previews; full live tabs remain open, #4579; G11: action recording folds Move-tool nudges, records relative keyboard document switches and scales point text placement; paragraph boxes remain open, #4514; G11: Batch measured output naming and UTF-16 log corrections; remaining progress and cancel prompt parity tracked, #4579; G11: Render ATN events mapped; Stylize/Pixelate ATN events mapped, current counts 128 supported/89 unmapped; G9: 11 of 183 preferences do nothing: startup reads Graphics Adapter through the typed Performance section, so the scorecard audit sees it; selected colour sets edit drawing, tone and supported filters; geometry remains open; G11: `.atn` document offsets preserve opening order after tab reordering, #7103; embedded AI/PDF/EPS save-back retains native format with a source-bound SVG display preview; Smooth and Feather command masks cropped to selection bounds plus blur reach; G1: Halftone Pattern Filter Gallery settings survive PSD/PSB saves (#8940); cropped selection morphology and command mask conversion performance evidence added; G6: Art History Brush landed as partial, TOOL-213-20, #1610, so no toolbar tool is missing; G4: five hard-round built-in presets use 5% spacing to remove stacked-tip outlines and reduce pressure-opacity bands, #8842; G9: Crop tool canvas menu, #6561; G2: Content-Aware Scale seam carving 1.65–3.6× faster with unchanged output, P28 row 3.3 → 1.6 s on a 4-core VM, #5236; G9: UI scale presets done, 105–120% steps, #3222; G14: lossy DNG, raw sidecars, Camera Raw Defaults live, Calibration and Auto, CR3 assessed as not clean-room feasible; G8: PSD type on a path opens on its path and saves back into the file's `Txt2`, #7962; G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve mLine truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (G8: Type › OpenType › Stylistic Sets submenu, Set 1–20, #10285; G8: PhotoCraft-native PSDs keep stylistic sets and other Txt2-only OpenType flags through save/reopen, #10308; G10: TIFF Options dialog on Save As and Save a Copy, compression, Save Image Pyramid, Save Transparency, #8827; G11: Batch Esc Continue/Stop and read-only input previews; full live tabs remain open, #4579; G11: action recording folds Move-tool nudges, records relative keyboard document switches and scales point text placement; paragraph boxes remain open, #4514; G11: Batch measured output naming and UTF-16 log corrections; remaining progress and cancel prompt parity tracked, #4579; G11: Render ATN events mapped; Stylize/Pixelate ATN events mapped, current counts 128 supported/89 unmapped; G9: 11 of 183 preferences do nothing: startup reads Graphics Adapter through the typed Performance section, so the scorecard audit sees it; selected colour sets edit drawing, tone and supported filters; geometry remains open; G11: `.atn` document offsets preserve opening order after tab reordering, #7103; embedded AI/PDF/EPS save-back retains native format with a source-bound SVG display preview; Smooth and Feather command masks cropped to selection bounds plus blur reach; G1: Halftone Pattern Filter Gallery settings survive PSD/PSB saves (#8940); cropped selection morphology and command mask conversion performance evidence added; G6: Art History Brush landed as partial, TOOL-213-20, #1610, so no toolbar tool is missing; G4: five hard-round built-in presets use 5% spacing to remove stacked-tip outlines and reduce pressure-opacity bands, #8842; G9: Crop tool canvas menu, #6561; G2: Content-Aware Scale seam carving 1.65–3.6× faster with unchanged output, P28 row 3.3 → 1.6 s on a 4-core VM, #5236; G9: UI scale presets done, 105–120% steps, #3222; G14: lossy DNG, raw sidecars, Camera Raw Defaults live, Calibration and Auto, CR3 assessed as not clean-room feasible; G8: PSD type on a path opens on its path and saves back into the file's `Txt2`, #7962; G11: a recorded Save As imports with its format and file (`file.saveAs`, `file.saveACopy` for As a Copy) instead of as a plain Save that wrote over the played file, #7102; folder destinations stop playback until #5430; adjustment clipping stays visible in compact Properties, #5231; G9: Color panel modes, ramp and copy commands, the same in every theme, UI-217-17, #6000; G17: Help › Check for Updates and an opt-out check once per launch, DIST-220-1 partial, #5485; G10: `.asl` style libraries landed, #1051; G8: area type (TYPE-218-10); G1: smart objects keep Photoshop's Cylinder warp, #4112; G8: paragraph direction buttons in the Paragraph panel and options bar, TYPE-218-2 partial; G1: 32-bit Hue/Saturation and Colorize as Photoshop does them, Photoshop-oracle floor 190/258, #5781; G11: compound layer properties export as separate Photoshop Set events, rename last, #8304; G11: Batch and droplets take the result at the action's Save step, so actions ending in Save As and Close work; G11: `.atn` steps that switch to another open document import, play and save back, #7103; G14: the Nikon D4 white level follows the sample depth, so 12-bit D4 NEFs no longer open 2.6 stops dark, #5978; G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E), #5987; Apply Layer Comp action mapping, #7101; G10: JPEG Options dialog landed, FILE-215-19 partial (no Matte, progressive Scans or size preview); G10: Cineon write; G10: DPX read and write, #1049, Cineon and Amiga IFF read; Classical Object Finder improvement; learned segmentation remains open; G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYKLine truncated
|
||||
|
||||
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
|
||||
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
|
||||
@@ -98,11 +98,11 @@ what an agent can't finish alone.
|
||||
and saves back into the file's `Txt2` since 2026-10-21, #7962),
|
||||
the visual RTL caret and PSD storage of the paragraph direction (TYPE-218-2; the paragraph
|
||||
direction buttons landed 2026-10-22), complex-script fallback fonts and golden tests
|
||||
(TYPE-218-3), OpenType Stylistic Sets submenu and greying out features the font lacks
|
||||
(TYPE-218-5; Character panel buttons 2026-10-11, #3752; Numerals submenu 2026-11-03 —
|
||||
(TYPE-218-3), greying out features the font lacks (TYPE-218-5; Character panel buttons 2026-10-11, #3752;
|
||||
Stylistic Sets submenu, Set 1–20, 2026-11-05, #10285; Numerals submenu 2026-11-03 —
|
||||
`.pcraft` keeps all figure tags; PSD EngineData only `OldStyle`/`onum` until a Photoshop-authored
|
||||
oracle lands; PhotoCraft-native PSDs keep stylistic sets and other Txt2-only flags through
|
||||
save/reopen since 2026-11-04, #10308; the Type › OpenType › Stylistic Sets menu is #10285 / PR #10304), text free transform
|
||||
save/reopen since 2026-11-04, #10308), text free transform
|
||||
(#2630), area type in PSD (TYPE-218-10). Done since the re-measure: manual and optical kerning with PSD round trip
|
||||
(TYPE-218-6, #206), variable-font axes (TYPE-218-4, #2985), Warp on live type without a
|
||||
rasterize prompt, as in Photoshop (TYPE-218-9, #3004), and the live Warp Text preview that opens
|
||||
@@ -449,6 +449,7 @@ what an agent can't finish alone.
|
||||
| 2026-10-31 | minor | G11: four Render ATN events mapped; current counts 120 supported/97 unmapped. Other Actions gaps, readiness percentages and estimates unchanged. |
|
||||
| 2026-10-31 | minor | G11: Group F action steps map to existing commands; 127 supported event IDs; action-file support remains partial; estimates unchanged |
|
||||
| 2026-10-31 | minor | G11: twelve Stylize/Pixelate ATN events mapped; current counts 128 supported/89 unmapped. Other Actions gaps, readiness percentages and estimates unchanged. |
|
||||
| 2026-11-05 | minor | G8: Type > OpenType > Stylistic Sets submenu (Set 1–20) landed (TYPE-218-5, #10285); font-capability greying still missing; PhotoCraft-native PSDs already keep the sets since #10308 |
|
||||
| 2026-10-27 | minor | Smooth/Feather command reads and replacement use selection bounds plus blur reach; full-canvas reference and release comparisons extended; percentages and scorecard budgets unchanged |
|
||||
| 2026-11-01 | minor | G9: 11 of 183 preferences do nothing (was 12): Graphics Adapter (`performance.gpuAdapter`) always worked, but startup read it by raw JSON key, which the scorecard prefs audit can't see; it now reads the typed Performance section; no estimate changed |
|
||||
| 2026-10-27 | minor | G9: 12 of 181 preferences do nothing, the scorecard's count with integration batch 9 (#9775) on main; no estimate changed |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Photoshop's menu tree) and the live command registry. Do not edit by hand.
|
||||
|
||||
**640 of 640 menu items live (100.0%).**
|
||||
**660 of 660 menu items live (100.0%).**
|
||||
|
||||
| Menu | Live | Total | % |
|
||||
|---|---:|---:|---:|
|
||||
@@ -10,7 +10,7 @@ Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Pho
|
||||
| Edit | 75 | 75 | 100% |
|
||||
| Image | 61 | 61 | 100% |
|
||||
| Layer | 163 | 163 | 100% |
|
||||
| Type | 54 | 54 | 100% |
|
||||
| Type | 74 | 74 | 100% |
|
||||
| Select | 25 | 25 | 100% |
|
||||
| Filter | 75 | 75 | 100% |
|
||||
| View | 74 | 74 | 100% |
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
|
||||
| [Tools](#tools) | 36 | 9 | 0 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 15 | 6 | 2 | settings that do nothing: 10 of 187 (target 0) |
|
||||
| [Type](#type) | 5 | 5 | 0 | |
|
||||
| [Automation](#automation) | 5 | 3 | 3 | 640 of 640 menu items live (100.0%) |
|
||||
| [Automation](#automation) | 5 | 3 | 3 | 660 of 660 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 30 of 30 crates; panic_hunt present |
|
||||
| [Distribution](#distribution) | 5 | 5 | 1 | |
|
||||
| [Open bugs](#open-bugs) | 31 | 1 | 0 | |
|
||||
@@ -241,7 +241,7 @@ Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.
|
||||
| TYPE-218-2 | Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Paragraph section (Window > Paragraph and Properties) and the options bar have Auto / Left-to-right / Right-to-left buttons setting the paragraph direction through type.setStyle; the options bar's choice is also the direction new type starts with (type_tool.rs direction_items; paragraph_direction_buttons_set_and_show_the_direction, options_bar_direction_sets_selected_layers_and_new_type). Still missing: visual RTL caret (the arrow keys move in logical order), and PSD doesn't store the direction (crates/text psd.rs neither reads nor writes it, so it reopens as Auto) |
|
||||
| TYPE-218-3 | Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Fallback fonts are covered for Thai, Khmer, Lao, Myanmar (THAI_FAMILIES, KHMER_FAMILIES, LAO_FAMILIES, MYANMAR_FAMILIES in fonts.rs; #1909, #3259), the nine Indic scripts (INDIC_FAMILIES: Nirmala UI, Kohinoor and Sangam MN, Noto Sans) and Arabic and Hebrew. Still missing: per-script golden shaping tests against reference output (only smoke tests that an installed font shapes without .notdef, plus a bidi test) |
|
||||
| TYPE-218-4 | Variable font axis sliders from the font's axes; opsz follows the point size | done | [#218](https://github.com/storytold/photocraft/issues/218) | One slider per axis of the style's font in the Character section (Properties and Window > Character), setting type.setStyle variations; opsz follows the type size unless set (layout.rs variation_list); a style or weight resets the wght axis (#2985). PSD does not round-trip axis values yet |
|
||||
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Type > OpenType toggles (type_extra_cmds.rs). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Type > OpenType > Numerals (Default, Lining, Oldstyle, Proportional, Tabular, and the four height×width combinations) landed 2026-11-03: each sets lnum/onum/pnum/tnum as a radio style on the selection or layer, round-trips .pcraft, and shows its checkmark from the style at the selection start. Still missing: the Stylistic Sets submenu, and greying out features the font lacks |
|
||||
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Type > OpenType toggles (type_extra_cmds.rs). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Type > OpenType > Numerals (Default, Lining, Oldstyle, Proportional, Tabular, and the four height×width combinations) landed 2026-11-03: each sets lnum/onum/pnum/tnum as a radio style on the selection or layer, round-trips .pcraft, and shows its checkmark from the style at the selection start. Type > OpenType > Stylistic Sets (Set 1-20, type.openType.stylisticSets.setN) landed for #10285: each toggles ssNN on the selection or layer, round-trips .pcraft and PSD (opened files and, since #10308, documents made here), and shows its checkmark from the style at the selection start. Still missing: greying out features the font lacks |
|
||||
| TYPE-218-6 | Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) | done | [#218](https://github.com/storytold/photocraft/issues/218) | Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/it/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning) |
|
||||
| TYPE-218-7 | Warp Text dialog previews live and opens with the layer's current warp | done | [#218](https://github.com/storytold/photocraft/issues/218) | Type > Warp Text opens with the shown layer's warp, or None, never remembered values (filter_dialog.rs:503 warp_defaults; warp_text_dialog_tests.rs:80 warp_text_opens_with_the_type_layers_current_warp, :99 warp_text_opens_at_none_for_a_layer_without_warp, :108 remembered_warp_values_never_override_the_layers_warp). The dialog previews live without editing the document, Cancel leaves no step and OK is one undoable step (:138 warp_text_dialog_previews_the_warp_without_editing_the_document, :160 cancelling_warp_text_leaves_the_document_and_history_untouched, :179 confirming_warp_text_is_one_undoable_step); the reduced-proxy preview matches the full-size result (filter_dialog.rs:2150 warp_text_preview_matches_the_full_size_result_on_a_reduced_proxy) |
|
||||
| TYPE-218-8 | Text editing: select all, double/triple/quadruple-click selection, paste, with tests | done | [#218](https://github.com/storytold/photocraft/issues/218) | Like Photoshop, 2 clicks select the word, 3 the laid-out line (a column in vertical type), 4 the paragraph up to the hard return, 5 all text, and the next quick click starts over (#3493; type_tool.rs next_click_count, click_selection, multi_click). Tested through real canvas clicks in point, paragraph and vertical type, with paste replacing the selection and Cmd+A selecting all (type_tool_tests.rs multi_click_selects_word_line_paragraph_and_all_in_point_text, multi_click_separates_wrapped_lines_from_paragraphs, click_counts_cycle_and_restart, click_selection_without_layout_and_on_hostile_input, multi_click_without_a_session_does_nothing) |
|
||||
@@ -250,7 +250,7 @@ Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.
|
||||
|
||||
## Automation
|
||||
|
||||
Menu wiring (`docs/parity-checklist.md`): 640 of 640 menu items live (100.0%). This counts dispatch, not behaviour.
|
||||
Menu wiring (`docs/parity-checklist.md`): 660 of 660 menu items live (100.0%). This counts dispatch, not behaviour.
|
||||
|
||||
| Id | Target | Status | Issue | Note |
|
||||
|---|---|---|---|---|
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ id = "TYPE-218-5"
|
||||
target = "OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus"
|
||||
status = "partial"
|
||||
issue = 218
|
||||
note = "Type > OpenType toggles (type_extra_cmds.rs). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Type > OpenType > Numerals (Default, Lining, Oldstyle, Proportional, Tabular, and the four height×width combinations) landed 2026-11-03: each sets lnum/onum/pnum/tnum as a radio style on the selection or layer, round-trips .pcraft, and shows its checkmark from the style at the selection start. Still missing: the Stylistic Sets submenu, and greying out features the font lacks"
|
||||
note = "Type > OpenType toggles (type_extra_cmds.rs). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Type > OpenType > Numerals (Default, Lining, Oldstyle, Proportional, Tabular, and the four height×width combinations) landed 2026-11-03: each sets lnum/onum/pnum/tnum as a radio style on the selection or layer, round-trips .pcraft, and shows its checkmark from the style at the selection start. Type > OpenType > Stylistic Sets (Set 1-20, type.openType.stylisticSets.setN) landed for #10285: each toggles ssNN on the selection or layer, round-trips .pcraft and PSD (opened files and, since #10308, documents made here), and shows its checkmark from the style at the selection start. Still missing: greying out features the font lacks"
|
||||
|
||||
[[item]]
|
||||
id = "TYPE-218-6"
|
||||
|
||||
Reference in new issue
Block a user