diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 60702f49..c44f1a28 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -113,7 +113,9 @@ pub struct Element { pub keyframes: Vec, /// `opt ` link to another record — the focused state of a button. pub focus_link: Option, - /// This element is itself a focused-state record. + /// This element is itself a focused-state record: its name is another + /// element's name plus a trailing `f`, and that other element is present. + /// The pairing is required — see `mark_focused_states`. pub focused: bool, /// A `loopN` sprite animation rather than a placed element. pub animated: bool, @@ -253,13 +255,46 @@ fn parse_decls(bundle: &[u8]) -> Option> { pivot_y: be32(e, 52), keyframes: Vec::new(), focus_link: None, - focused: lname.ends_with("f.rat") || lname.ends_with("f.t32"), + focused: false, // needs the whole table — see below animated: lname.contains("loop"), }); } + mark_focused_states(&mut elements); Some(elements) } +/// Flag the elements that are a **focused variant of another element present in +/// the same build** — `pgmenu_btn00f.t32` next to `pgmenu_btn00.t32`. +/// +/// The pairing is not decoration, it is the whole rule. A trailing `f` alone +/// flags **2 458** elements on the disc and only **54** of them have a base to +/// be the focused version of; all 54 are `pgmenu_btnNNf.t32`. The other 2 404, +/// spread over 864 bundles, are `_eff` glow layers whose names merely end in the +/// same letter — `pb_name_eff.t32` (1 122 elements), `pbmwindow_eff.t32`, +/// `pghud_range_eff.t32`, `palogo_gamearts_eff.t32`. `compose` drops focused +/// records by default, so the unpaired rule was deleting a glow layer from +/// nearly every screen that has one. +/// +/// The draw capture settles it independently: on the developer-logo splash the +/// three `_eff` glows carry layer key `0xa100` and are **painted**, before their +/// logos (`docs/re/structures/ui-paint-order-key.md`). They are not focus states. +fn mark_focused_states(elements: &mut [Element]) { + let names: std::collections::HashSet = elements + .iter() + .map(|e| e.name.to_ascii_lowercase()) + .collect(); + for el in elements.iter_mut() { + let l = el.name.to_ascii_lowercase(); + if !(l.ends_with("f.rat") || l.ends_with("f.t32")) { + continue; + } + let Some((stem, ext)) = l.rsplit_once('.') else { + continue; + }; + el.focused = names.contains(&format!("{}.{}", &stem[..stem.len() - 1], ext)); + } +} + /// Read the placement region that follows the declaration table, filling in each /// element's keyframe group. fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec { @@ -328,7 +363,8 @@ pub fn parse_build(bundle: &[u8]) -> Option { // say. Elements without a record (eff*/deli*/msg) are then missing, so // callers are told via `from_fallback`. None => { - let els = fallback_elements(bundle, &records); + let mut els = fallback_elements(bundle, &records); + mark_focused_states(&mut els); let order = (0..els.len()).collect(); (els, true, order) } @@ -405,7 +441,7 @@ fn fallback_elements(bundle: &[u8], records: &HashMap) - time: Some(0), }], focus_link: opt_link(rec), - focused: lname.ends_with("f.rat"), + focused: false, // set by `mark_focused_states` once the set is known animated: lname.contains("loop"), }); } diff --git a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs index 9d5034c6..a598eb4c 100644 --- a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs +++ b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs @@ -515,3 +515,54 @@ fn every_composite_paints_in_layer_key_order() { to carry the weight the write-up puts on it" ); } + +/// A focused-state record is a **pair**, and the unpaired reading was deleting +/// glow layers from most of the disc. +/// +/// `compose` skips focused records by default, so whatever this flag matches +/// disappears from every composite. Matching a trailing `f` alone flagged 2 458 +/// elements; only 54 of them — all `pgmenu_btnNNf.t32` — have the base element +/// they would be the focused version of. The rest are `_eff` glows that merely +/// end in the same letter, and the draw capture shows them being painted. +#[test] +fn a_focused_state_always_has_the_element_it_is_the_focused_state_of() { + skip_without_disc!(root); + let (mut total, mut flagged, mut eff) = (0usize, 0usize, 0usize); + for_each_build(&root, |pak, bytes| { + let Some(b) = ui_layout::parse_build(bytes) else { + return; + }; + let names: Vec = b + .elements + .iter() + .map(|e| e.name.to_ascii_lowercase()) + .collect(); + for el in &b.elements { + total += 1; + let l = el.name.to_ascii_lowercase(); + if l.ends_with("_eff.t32") { + eff += 1; + assert!( + !el.focused, + "{pak}: {} is flagged as a focused state — it is a glow layer", + el.name + ); + } + if !el.focused { + continue; + } + flagged += 1; + let (stem, ext) = l.rsplit_once('.').expect("an extension"); + let base = format!("{}.{}", &stem[..stem.len() - 1], ext); + assert!( + names.contains(&base), + "{pak}: {} is flagged as a focused state but {base} is not in \ + the build — the pairing requirement has been lost", + el.name + ); + } + }); + assert!(total > 5000, "only {total} elements — the sweep did not run"); + assert!(eff > 100, "only {eff} `_eff` elements, expected the disc's glows"); + eprintln!("focused states: {flagged} of {total} elements; {eff} `_eff` glows kept"); +}