viewer: preview reassembled UI screens in the PAK browser

When an opened GP_*.pak RATC entry is a .rat build, compose the screen
(ui_layout::compose_build) and show it atop the RATC detail, labelled
'UI screen', with the sprite children below. Reuses the existing pak-open +
T8aD-decode path — no new browser/threading. Entry summaries mark UI builds.

cargo check -p sylpheed-viewer: clean.
This commit is contained in:
2026-07-29 20:33:29 +02:00
parent c1da907135
commit 9bc8c2d694
2 changed files with 49 additions and 7 deletions

View File

@@ -160,8 +160,9 @@ pub enum PakContent {
T8ad(ImageRgba), T8ad(ImageRgba),
/// An LSTA sprite list — inline T8aD frames. /// An LSTA sprite list — inline T8aD frames.
Lsta(Vec<ImageRgba>), Lsta(Vec<ImageRgba>),
/// A RATC bundle — its listed children (T8aD children carry a decoded image). /// A RATC bundle — its listed children (T8aD children carry a decoded
Ratc(Vec<RatcEntry>), /// image), plus the reassembled UI screen when the bundle is a `.rat` build.
Ratc(Vec<RatcEntry>, Option<ImageRgba>),
/// Plain-text / XML payload, with its encoding label. /// Plain-text / XML payload, with its encoding label.
Text { text: String, encoding: String }, Text { text: String, encoding: String },
} }
@@ -192,6 +193,13 @@ impl ImageRgba {
rgba: img.rgba, rgba: img.rgba,
} }
} }
fn from_composed(s: sylpheed_formats::ui_layout::ComposedScreen) -> Self {
Self {
width: s.width,
height: s.height,
rgba: s.rgba,
}
}
fn pixels(&self) -> usize { fn pixels(&self) -> usize {
(self.width as usize) * (self.height as usize) (self.width as usize) * (self.height as usize)
} }
@@ -1469,7 +1477,10 @@ fn classify_content(payload: &[u8]) -> PakContent {
}) })
.collect(); .collect();
if !entries.is_empty() { if !entries.is_empty() {
return PakContent::Ratc(entries); // If this bundle is a `.rat` UI build, reassemble the screen.
let ui_screen = sylpheed_formats::ui_layout::compose_build(payload, false)
.map(ImageRgba::from_composed);
return PakContent::Ratc(entries, ui_screen);
} }
} }
} }

View File

@@ -672,7 +672,9 @@ fn draw_pak_browser(ui: &mut egui::Ui, pak: &mut PakView) {
PakContent::Png(img) => draw_png_detail(ui, row, img, img_tex), PakContent::Png(img) => draw_png_detail(ui, row, img, img_tex),
PakContent::T8ad(img) => draw_t8ad_detail(ui, row, img, img_tex), PakContent::T8ad(img) => draw_t8ad_detail(ui, row, img, img_tex),
PakContent::Lsta(frames) => draw_lsta_detail(ui, row, frames, img_tex), PakContent::Lsta(frames) => draw_lsta_detail(ui, row, frames, img_tex),
PakContent::Ratc(children) => draw_ratc_detail(ui, row, children, img_tex), PakContent::Ratc(children, ui_screen) => {
draw_ratc_detail(ui, row, children, ui_screen.as_ref(), img_tex)
}
PakContent::Text { text, encoding } => { PakContent::Text { text, encoding } => {
draw_text_detail(ui, row, text, encoding) draw_text_detail(ui, row, text, encoding)
} }
@@ -699,7 +701,10 @@ fn row_kind(row: &crate::iso_loader::PakRow) -> String {
PakContent::Png(img) => format!("PNG {}×{}", img.width, img.height), PakContent::Png(img) => format!("PNG {}×{}", img.width, img.height),
PakContent::T8ad(img) => format!("T8aD {}×{}", img.width, img.height), PakContent::T8ad(img) => format!("T8aD {}×{}", img.width, img.height),
PakContent::Lsta(f) => format!("LSTA · {} sprite(s)", f.len()), PakContent::Lsta(f) => format!("LSTA · {} sprite(s)", f.len()),
PakContent::Ratc(c) => format!("RATC · {} item(s)", c.len()), PakContent::Ratc(c, screen) => {
let s = if screen.is_some() { " · UI screen" } else { "" };
format!("RATC · {} item(s){s}", c.len())
}
PakContent::Text { .. } => "text".into(), PakContent::Text { .. } => "text".into(),
PakContent::None => row.identity.clone(), PakContent::None => row.identity.clone(),
} }
@@ -939,21 +944,47 @@ fn draw_ratc_detail(
ui: &mut egui::Ui, ui: &mut egui::Ui,
row: &crate::iso_loader::PakRow, row: &crate::iso_loader::PakRow,
children: &[crate::iso_loader::RatcEntry], children: &[crate::iso_loader::RatcEntry],
ui_screen: Option<&ImageRgba>,
img_tex: &mut ImgCache, img_tex: &mut ImgCache,
) { ) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading("RATC bundle"); ui.heading("RATC bundle");
ui.separator(); ui.separator();
ui.label(format!("{} item(s)", children.len())); ui.label(format!("{} item(s)", children.len()));
if ui_screen.is_some() {
ui.separator();
ui.strong("🖼 UI screen");
}
ui.separator(); ui.separator();
ui.weak("colours unverified"); ui.weak("colours unverified");
}); });
ui.separator(); ui.separator();
let refs: Vec<&ImageRgba> = children.iter().filter_map(|c| c.image.as_ref()).collect(); // Cache the reassembled screen (if any) as texture 0, then child thumbnails.
let mut refs: Vec<&ImageRgba> = Vec::new();
if let Some(s) = ui_screen {
refs.push(s);
}
let child_base = refs.len();
refs.extend(children.iter().filter_map(|c| c.image.as_ref()));
ensure_textures(ui, row.hash, &refs, img_tex); ensure_textures(ui, row.hash, &refs, img_tex);
let mut img_i = 0; // The reassembled screen, scaled to fit the panel width.
if ui_screen.is_some() {
if let Some(tex) = img_tex.as_ref().and_then(|(_, t)| t.first()) {
ui.label("Reassembled from this screen's .rat layout records:");
let sz = tex.size_vec2();
let scale = (ui.available_width() / sz.x.max(1.0)).min(1.0);
ui.add(egui::Image::new(egui::load::SizedTexture::new(
tex.id(),
[sz.x * scale, sz.y * scale],
)));
ui.weak("Frame/glow decorations (no .rat) and the live 3D background are omitted.");
ui.separator();
}
}
let mut img_i = child_base;
for c in children { for c in children {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if c.image.is_some() { if c.image.is_some() {