feat(viewer): directory-tree browser + plain-text viewer
Explorer drawer was a flat list of all 353 disc files and only textures had a preview. Two additions: 1. Directory tree — the left drawer is now a collapsible `📁` tree, rebuilt each frame from the path list (ui.rs: build_tree / split_for_tree / render_dir). Filtering narrows leaves and auto-expands matches; `.p00…p04` data segments nest under a virtual `<stem>.pak` node. Selection still keys off the original file index, so the loader pipeline is untouched. 2. Plain-text viewer — config.ini and any file that sniffs as text now show read-only, selectable, monospace content with a Wrap toggle and an encoding label. Detection/decoding live in the Bevy-free formats crate: vfs::is_probably_text + vfs::decode_text (BOM-aware UTF-8/UTF-16LE/BE, BOM-less UTF-16LE heuristic, NUL-reject + 95%-printable fallback) — pure std, WASM-safe, unit-tested (6 new tests), and reused by the CLI sniffer (new cyan `txt`). New TextPreview resource (registered unconditionally so it exists on wasm); populated in apply_loaded_texture with a 2 MiB char-boundary-safe cap. Also fixes a latent bug: switching from a texture to a non-texture left stale preview state — both texture and text state now reset on every selection. ViewerState gains text_wrap. Formats tests: 22 pass; workspace compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,22 @@ pub struct FileInfo {
|
||||
pub detected_format: Option<FileFormat>,
|
||||
}
|
||||
|
||||
/// Decoded contents of the currently-selected text file, shown in the central
|
||||
/// panel. `content` is `None` whenever the selection isn't text.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct TextPreview {
|
||||
/// Decoded UTF-8 (any BOM / UTF-16 already resolved), or `None`.
|
||||
pub content: Option<String>,
|
||||
/// Encoding label for the header, e.g. "UTF-16LE (BOM)".
|
||||
pub encoding: String,
|
||||
/// True when the file was larger than `MAX_TEXT_BYTES` and got clipped.
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
/// Cap on the decoded text we hand to egui — its text layout cost grows
|
||||
/// super-linearly, and disc config files are only a few KiB anyway.
|
||||
const MAX_TEXT_BYTES: usize = 2 * 1024 * 1024;
|
||||
|
||||
// ── SystemSet label ───────────────────────────────────────────────────────────
|
||||
|
||||
/// `draw_viewer_ui` runs `.after(IsoLoaderSystemSet)` to see the frame's
|
||||
@@ -157,7 +173,10 @@ impl Plugin for IsoLoaderPlugin {
|
||||
.add_event::<FileSelected>()
|
||||
.init_resource::<IsoState>()
|
||||
.init_resource::<TexturePreview>()
|
||||
.init_resource::<FileInfo>();
|
||||
.init_resource::<FileInfo>()
|
||||
// Registered unconditionally (even on wasm, where the load systems
|
||||
// below are cfg'd out) so the UI can always read it.
|
||||
.init_resource::<TextPreview>();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
@@ -392,6 +411,7 @@ fn poll_loader_channel(
|
||||
fn apply_loaded_texture(
|
||||
mut pending: ResMut<PendingFileBytes>,
|
||||
mut preview: ResMut<TexturePreview>,
|
||||
mut text_preview: ResMut<TextPreview>,
|
||||
mut file_info: ResMut<FileInfo>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut contexts: bevy_egui::EguiContexts,
|
||||
@@ -412,6 +432,9 @@ fn apply_loaded_texture(
|
||||
images.remove(handle.id());
|
||||
}
|
||||
*preview = TexturePreview::default();
|
||||
// Clear stale text too, so switching files never shows a previous file's
|
||||
// content in the wrong panel.
|
||||
*text_preview = TextPreview::default();
|
||||
|
||||
// Populate FileInfo for the status / info panel.
|
||||
file_info.name = path
|
||||
@@ -422,6 +445,25 @@ fn apply_loaded_texture(
|
||||
file_info.size_bytes = bytes.len();
|
||||
file_info.detected_format = Some(fmt);
|
||||
|
||||
if fmt == FileFormat::Text {
|
||||
let (mut text, encoding) = sylpheed_formats::vfs::decode_text(&bytes);
|
||||
let truncated = text.len() > MAX_TEXT_BYTES;
|
||||
if truncated {
|
||||
// Cut on a char boundary so we never split a codepoint.
|
||||
let end = text
|
||||
.char_indices()
|
||||
.take_while(|(i, _)| *i < MAX_TEXT_BYTES)
|
||||
.last()
|
||||
.map(|(i, c)| i + c.len_utf8())
|
||||
.unwrap_or(0);
|
||||
text.truncate(end);
|
||||
}
|
||||
text_preview.content = Some(text);
|
||||
text_preview.encoding = encoding.to_string();
|
||||
text_preview.truncated = truncated;
|
||||
return; // FileInfo + TextPreview are sufficient for text files.
|
||||
}
|
||||
|
||||
if fmt != FileFormat::Xpr2Texture {
|
||||
return; // Not a texture — FileInfo is sufficient
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user