Full Code of ggand0/viewskater for AI

main 0f7b001a4ad0 cached
80 files
1.1 MB
251.8k tokens
991 symbols
1 requests
Download .txt
Showing preview only (1,197K chars total). Download the full file or copy to clipboard to get everything.
Repository: ggand0/viewskater
Branch: main
Commit: 0f7b001a4ad0
Files: 80
Total size: 1.1 MB

Directory structure:
gitextract_szegwn3j/

├── .gitignore
├── Cargo.toml
├── README.md
├── assets/
│   └── ViewSkater.icns
├── build.rs
├── docs/
│   ├── bundling.md
│   └── replay.md
├── resources/
│   ├── linux/
│   │   ├── appimage.desktop
│   │   └── viewskater.desktop
│   └── macos/
│       ├── Info.plist
│       ├── entitlements.plist
│       └── viewskater_wrapper.sh
└── src/
    ├── app/
    │   ├── keyboard_handlers.rs
    │   ├── message.rs
    │   ├── message_handlers.rs
    │   ├── replay_handlers.rs
    │   └── settings_widget.rs
    ├── app.rs
    ├── archive_cache.rs
    ├── build_info.rs
    ├── cache/
    │   ├── cache_utils.rs
    │   ├── compression.rs
    │   ├── cpu_img_cache.rs
    │   ├── gpu_img_cache.rs
    │   ├── img_cache.rs
    │   ├── mod.rs
    │   └── texture_cache.rs
    ├── coco/
    │   ├── annotation_manager.rs
    │   ├── mod.rs
    │   ├── overlay/
    │   │   ├── bbox_overlay.rs
    │   │   ├── bbox_shader.rs
    │   │   ├── bbox_shader.wgsl
    │   │   ├── mask_shader.rs
    │   │   ├── mask_shader.wgsl
    │   │   ├── mod.rs
    │   │   ├── polygon_shader.rs
    │   │   └── polygon_shader.wgsl
    │   ├── parser.rs
    │   ├── rle_decoder.rs
    │   └── widget.rs
    ├── config.rs
    ├── exif_utils.rs
    ├── file_io.rs
    ├── loading_handler.rs
    ├── loading_status.rs
    ├── logging.rs
    ├── macos_file_access.rs
    ├── main.rs
    ├── menu.rs
    ├── navigation_keyboard.rs
    ├── navigation_slider.rs
    ├── pane.rs
    ├── replay.rs
    ├── selection_manager.rs
    ├── settings.rs
    ├── settings_modal.rs
    ├── ui.rs
    ├── utils/
    │   ├── mem.rs
    │   ├── mod.rs
    │   ├── save.rs
    │   └── timing.rs
    ├── widgets/
    │   ├── circular.rs
    │   ├── dualslider.rs
    │   ├── easing.rs
    │   ├── mod.rs
    │   ├── modal.rs
    │   ├── selection_widget.rs
    │   ├── shader/
    │   │   ├── atlas_texture.wgsl
    │   │   ├── cpu_scene.rs
    │   │   ├── image_shader.rs
    │   │   ├── mod.rs
    │   │   ├── scene.rs
    │   │   ├── texture.wgsl
    │   │   ├── texture_pipeline.rs
    │   │   └── texture_scene.rs
    │   ├── split.rs
    │   ├── synced_image_split.rs
    │   ├── toggler.rs
    │   └── viewer.rs
    └── window_state.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
CLAUDE.md
.DS_Store
icon.png
assets_dev/
benchmarks/
devlogs/
docs/plans/
logs/
tmp/

# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb/target


================================================
FILE: Cargo.toml
================================================
[package]
name = "viewskater"
version = "0.3.1"
edition = "2021"
description = "A fast image viewer for browsing large collections of images."

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
env_logger = "0.10"
console_log = "1.0"
log = "0.4.20"
tokio = { version = "1.32", features = ["rt", "sync", "macros", "time", "io-util", "fs"] }
smol = "2.0"
rfd = { version = "0.12", default-features = false, features = ["xdg-portal"] }
native-dialog = "0.7"
num-traits = "0.2"
alphanumeric-sort = "1.5.3"
image = { version = "0.25", default-features = false, features = [
    "jpeg", "png", "gif", "bmp", "ico", "tiff", "webp", "pnm", "qoi", "tga"
] }
futures = "0.3"
once_cell = "1.16"
smol_str = "0.2.2"
backtrace = "0.3"
dirs = "5.0"
webbrowser = "0.7"
bytemuck = { version = "1.0", features = ["derive"] }
earcutr = "0.4"
lyon_algorithms = "1.0"
chrono = { version = "0.4", features = ["clock"] }
memmap2 = "0.9.5"
rayon = "1.8"
texpresso = { version = "2.0.1", features = ["rayon"] }
sysinfo = "0.33.1"
libc = "0.2"
clap = { version = "4.0", features = ["derive"] }
zip = "4"
unrar = "0.5"
sevenz-rust2 = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
regex = "1.10"
arboard = { version = "3", features = ["image-data"] }
jpeg2k = { version = "0.10", optional = true, features = ["image"] }

# Custom iced (direct deps)
iced_custom = { package = "iced", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13", features = [
    "image", "tokio", "svg", "lazy", "wgpu"
] }
iced_winit = { package = "iced_winit", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_wgpu = { package = "iced_wgpu", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_widget = { package = "iced_widget", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13", features = ["wgpu", "canvas"] }
iced_core = { package = "iced_core", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_runtime = { package = "iced_runtime", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_futures = { package = "iced_futures", git = "https://github.com/ggand0/iced.git", branch = "custom-0.13", features = ["tokio"] }
iced_graphics = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
# Upstream iced_aw -- iced deps overridden via [patch.crates-io] below
iced_aw = { package = "iced_aw", git = "https://github.com/iced-rs/iced_aw.git", tag = "v.0.11.0", default-features = false, features = [
    "menu", "quad", "tabs"
] }

# Override iced crates from crates.io (used by iced_aw and iced_fonts) with custom fork
[patch.crates-io]
iced = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_core = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_widget = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_runtime = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_renderer = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_futures = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_graphics = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_wgpu = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_tiny_skia = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
iced_winit = { git = "https://github.com/ggand0/iced.git", branch = "custom-0.13" }
# Local iced development: uncomment below, comment above
#iced = { path = "../iced" }
#iced_core = { path = "../iced/core" }
#iced_widget = { path = "../iced/widget" }
#iced_runtime = { path = "../iced/runtime" }
#iced_renderer = { path = "../iced/renderer" }
#iced_futures = { path = "../iced/futures" }
#iced_graphics = { path = "../iced/graphics" }
#iced_wgpu = { path = "../iced/wgpu" }
#iced_tiny_skia = { path = "../iced/tiny_skia" }
#iced_winit = { path = "../iced/winit" }

[features]
# Image selection/curation features for dataset preparation (disabled by default)
selection = []
# COCO dataset visualization (disabled by default)
coco = []
# JPEG 2000 support (disabled by default)
jp2 = ["dep:jpeg2k"]

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = { version = "0.5.2", features = ["relax-sign-encoding"] }
block2 = "0.5"
objc2-foundation = { version = "0.2.2", default-features = false, features = [
    "std",
    "NSUserDefaults",
    "NSNotification",
    "NSString",
    "NSOperation",
    "block2",
] }
objc2-app-kit = { version = "0.2.2", default-features = false, features = [
    "std",
    "NSApplication",
    "NSWindow",
    "NSView",
    "NSResponder",
    "NSScreen",
] }


# Used on macOS for generating .app bundles via `cargo bundle`
[target.'cfg(target_os = "macos")'.dev-dependencies]
cargo-bundle = "0.6.0"

[build-dependencies]
winres = "0.1.12"
chrono = { version = "0.4", features = ["clock"] }

[package.metadata.winres]
OriginalFilename = "view_skater.exe"
FileDescription = "A fast image viewer for browsing large collections of images."

[package.metadata.bundle]
name = "ViewSkater"
identifier = "com.ggando.viewskater"
icon = ["assets/ViewSkater.icns"]
short_description = "A fast image viewer for browsing large collections of images."

[package.metadata.appimage]
desktop_entry = "./resources/linux/appimage.desktop"

[profile.release]
lto = "fat"
codegen-units = 1

# Use an optimized dev profile for faster runtime during development.
[profile.dev]
opt-level = 3
lto = false
codegen-units = 16

[profile.opt-dev]
inherits = "release"
opt-level = 3
lto = false
codegen-units = 16

================================================
FILE: README.md
================================================
<img src="https://github.com/user-attachments/assets/4c410f1b-1103-4b84-87f1-7278aa3a46f9" alt="Alt text" width="600"/>

# ViewSkater
ViewSkater is a fast, cross-platform image viewer written in Rust & Iced.
It aims to alleviate the challenges of exploring and comparing numerous images. Linux, macOS and Windows are currently supported.

> **Note:** This (iced) version is in maintenance mode. Active development is moving to the [egui version](https://github.com/ggand0/viewskater-egui), which offers better performance and a simpler codebase.

## Features
- GPU-based image rendering powered by wgpu
- Dynamic image caching on CPU or GPU memory
- Continuous image rendering via key presses and the slider UI
- Dual pane view for side-by-side image comparison
- Supports image formats supported by the image crate (JPG, PNG, GIF, BMP, TIFF, WebP, QOI, TGA, etc.)
- **JPEG 2000 support** (optional feature): View JP2, J2K, and J2C files
- Supports viewing images inside ZIP, RAR, and 7z (LZMA2 codec) files
- Renders images up to 8192×8192 px (larger images are resized to fit)
- **COCO annotation support** (optional feature): Display bounding boxes and segmentation masks with dual rendering modes (polygon/pixel)
- **Selection feature** (optional feature): Select and export subsets of images from large datasets

## Installation
Download the pre-built binaries from the [releases page](https://github.com/ggand0/viewskater/releases), or build it locally:

```sh
cargo run
```

To see debug logs while running, set the `RUST_LOG` environment variable:
```sh
RUST_LOG=viewskater=debug cargo run
```

To build a full release binary for packaging or distribution:
```sh
cargo build --release
```

**Building with optional features:**
```sh
# Build with COCO annotation support
cargo build --release --features coco

# Build with selection feature
cargo build --release --features selection

# Build with JPEG 2000 support
cargo build --release --features jp2

# Build with multiple features
cargo build --release --features coco,selection,jp2
```

See [docs/bundling.md](./docs/bundling.md) for full packaging instructions.

### Linux icon setup

On GNOME 46+ (Ubuntu 24.04+), the taskbar icon requires installing a `.desktop` file and icon:

```bash
mkdir -p ~/.local/share/icons/hicolor/256x256/apps
cp assets/icon_256.png ~/.local/share/icons/hicolor/256x256/apps/viewskater.png
gtk-update-icon-cache -f ~/.local/share/icons/hicolor/
cp resources/linux/viewskater.desktop ~/.local/share/applications/
```

Edit the `Exec=` line in the installed `.desktop` file to point to your binary:
```bash
sed -i "s|Exec=.*|Exec=/path/to/viewskater %f|" \
    ~/.local/share/applications/viewskater.desktop
```

For the AppImage, use the AppImage path instead:
```bash
sed -i "s|Exec=.*|Exec=/path/to/ViewSkater.AppImage %f|" \
    ~/.local/share/applications/viewskater.desktop
```

## Usage
Drag and drop an image or a directory of images onto a pane, and navigate through the images using the **A / D** keys or the slider UI.
Use the mouse wheel to zoom in/out of an image.

In dual-pane mode (**Ctrl + 2**), the slider syncs images in both panes by default.
You can switch to per-pane sliders by selecting the "Controls -> Controls -> Toggle Slider" menu item or pressing the **Space** bar.

**COCO Annotations** (when built with `--features coco`):
Drag and drop a COCO-format JSON annotation file onto the app. The app will automatically search for the image directory in common locations:
- Same directory as the JSON file
- `images/`, `img/`, `val2017/`, or `train2017/` subdirectories
- Single subdirectory if only one exists in the JSON's parent directory

If the image directory is not found automatically, a folder picker will prompt you to select the image directory manually.

**Image Selection** (when built with `--features selection`):
Mark images for dataset curation while browsing. Press **S** to mark an image as selected (green badge), **X** to exclude it (red badge), or **U** to clear the mark. Export your selections to JSON using **Cmd+E** (macOS) or **Ctrl+E** (Windows/Linux). Selection states are automatically saved and persist across sessions.

## Shortcuts
| Action                             | macOS Shortcut      | Windows/Linux Shortcut |
|------------------------------------|----------------------|-------------------------|
| Show previous / next image         | Left / Right or A / D | Left / Right or A / D  |
| Continuous scroll ("skate" mode)   | Shift + Left / Right or Shift + A / D | Shift + Left / Right or Shift + A / D |
| Jump to first / last image         | Cmd + Left / Right   | Ctrl + Left / Right    |
| Toggle UI (slider + footer)        | Tab                  | Tab                    |
| Toggle single / dual slider        | Space                | Space                  |
| Select Pane 1 / 2 (Dual slider)    | 1 / 2                | 1 / 2                  |
| Open folder in Pane 1 / 2          | Alt + 1 / 2          | Alt + 1 / 2            |
| Open file in Pane 1 / 2            | Shift + Alt + 1 / 2  | Shift + Alt + 1 / 2    |
| Open file (Single pane)            | Cmd + O              | Ctrl + O               |
| Open folder (Single pane)          | Cmd + Shift + O      | Ctrl + Shift + O       |
| Toggle single / dual pane mode     | Cmd + 1 / 2          | Ctrl + 1 / 2           |
| Toggle fullscreen mode             | F11                  | F11                    |
| Close all panes                    | Cmd + W              | Ctrl + W               |
| Exit                               | Cmd + Q              | Ctrl + Q               |


## Documentation

- [Bundling & Packaging](./docs/bundling.md) - Build for Linux, macOS, Windows
- [Replay Mode](./docs/replay.md) - Automated benchmarking with CLI options

## Resources
- [Website](https://viewskater.com/)
- [egui version](https://github.com/ggand0/viewskater-egui)

## Acknowledgments
ViewSkater's slider UI was inspired by the open-source project [emulsion](https://github.com/ArturKovacs/emulsion).

## License
ViewSkater is licensed under either of
- Apache License, Version 2.0
  ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license
  ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)

at your option.



================================================
FILE: build.rs
================================================
use {
    std::{
        env,
        io,
        process::Command,
        fs,
    },
    winres::WindowsResource,
};

fn main() -> io::Result<()> {
    // Capture build information
    capture_build_info();

    // Windows resource setup
    if env::var_os("CARGO_CFG_WINDOWS").is_some() {
        WindowsResource::new()
            // This path can be absolute, or relative to your crate root.
            // When building on Windows, comment out the first 3 lines of this block (just call set_icon)
            //.set_toolkit_path("/usr/bin")
            //.set_windres_path("x86_64-w64-mingw32-windres")
            //.set_ar_path("x86_64-w64-mingw32-ar")
            .set_icon("./assets/icon.ico")
            .compile()?;
    }
    Ok(())
}

fn capture_build_info() {
    // Uncomment to override version string if needed:
    //println!("cargo:rustc-env=CARGO_PKG_VERSION={}", env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "unknown".to_string()));

    // Generate build timestamp
    let build_timestamp = chrono::Utc::now().format("%Y%m%d.%H%M%S").to_string();
    println!("cargo:rustc-env=BUILD_TIMESTAMP={}", build_timestamp);

    // Get git commit hash
    let git_hash = get_git_hash().unwrap_or_else(|| "unknown".to_string());
    println!("cargo:rustc-env=GIT_HASH={}", git_hash);

    // Get git commit hash (short version)
    let git_hash_short = if git_hash.len() >= 7 {
        git_hash[0..7].to_string()
    } else {
        git_hash.clone()
    };
    println!("cargo:rustc-env=GIT_HASH_SHORT={}", git_hash_short);

    // Target platform info
    let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_else(|_| "unknown".to_string());
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_else(|_| "unknown".to_string());
    println!("cargo:rustc-env=TARGET_PLATFORM={}-{}", target_arch, target_os);

    // Build profile
    let profile = env::var("PROFILE").unwrap_or_else(|_| "unknown".to_string());
    println!("cargo:rustc-env=BUILD_PROFILE={}", profile);

    // Create a combined build string
    let build_string = format!("{}.{}", env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string()), build_timestamp);
    println!("cargo:rustc-env=BUILD_STRING={}", build_string);

    // For macOS, automatically update Info.plist with the build timestamp
    if target_os == "macos" {
        update_info_plist(&build_timestamp);
        println!("cargo:rustc-env=BUNDLE_VERSION={}", build_timestamp);
    } else {
        // For non-macOS, still set the bundle version but don't update plist
        println!("cargo:rustc-env=BUNDLE_VERSION={}", build_timestamp);
    }

    // Tell cargo to rerun this if git changes or Info.plist changes
    println!("cargo:rerun-if-changed=.git/HEAD");
    println!("cargo:rerun-if-changed=.git/refs/heads/");
    println!("cargo:rerun-if-changed=resources/macos/Info.plist");
}

fn get_git_hash() -> Option<String> {
    let output = Command::new("git")
        .args(["rev-parse", "HEAD"])
        .output()
        .ok()?;

    if output.status.success() {
        let hash = String::from_utf8(output.stdout).ok()?;
        Some(hash.trim().to_string())
    } else {
        None
    }
}

fn update_info_plist(build_timestamp: &str) {
    let plist_path = "resources/macos/Info.plist";

    // Check if Info.plist exists
    if !std::path::Path::new(plist_path).exists() {
        println!("cargo:warning=Info.plist not found at {}, skipping update", plist_path);
        return;
    }

    // Read the current Info.plist
    let content = match fs::read_to_string(plist_path) {
        Ok(content) => content,
        Err(e) => {
            println!("cargo:warning=Failed to read Info.plist: {}", e);
            return;
        }
    };

    // Update CFBundleVersion using regex replacement
    let updated_content = if let Some(start) = content.find("<key>CFBundleVersion</key>") {
        if let Some(value_start) = content[start..].find("<string>") {
            if let Some(value_end) = content[start + value_start + 8..].find("</string>") {
                let before = &content[..start + value_start + 8];
                let after = &content[start + value_start + 8 + value_end..];
                format!("{}{}{}", before, build_timestamp, after)
            } else {
                content
            }
        } else {
            content
        }
    } else {
        content
    };

    // Write back the updated content
    if let Err(e) = fs::write(plist_path, updated_content) {
        println!("cargo:warning=Failed to write updated Info.plist: {}", e);
    } else {
        println!("cargo:warning=Updated CFBundleVersion in Info.plist to {}", build_timestamp);
    }
}

================================================
FILE: docs/bundling.md
================================================
# Building and Packaging ViewSkater

This guide explains how to build and package ViewSkater for different operating systems.

## Linux

To build and package ViewSkater as an AppImage:

```sh
cargo install cargo-appimage
cargo appimage
```

This will generate a standalone `.AppImage` in the `target/appimage/` directory.


## Windows

To build the release binary:

```sh
cargo build --release
```
This will generate `viewskater.exe` in the `target/release/` directory.
You can wrap it in an installer using tools like [Inno Setup](https://github.com/jrsoftware/issrc) if needed.


## macOS

To bundle the `.app` and create a `.dmg`:

```sh
cargo bundle --release
```

Then:

```sh
cd target/release/bundle/osx
hdiutil create -volname "ViewSkater" -srcfolder "ViewSkater.app" -ov -format UDZO "view_skater.dmg"
```

This will generate a `.dmg` file suitable for distribution.


## Notes

- Tested on Rust 1.85.1
- macOS bundling uses [`cargo-bundle`](https://github.com/burtonageo/cargo-bundle)
- Linux packaging uses [`cargo-appimage`](https://github.com/linuxwolf/cargo-appimage)

================================================
FILE: docs/replay.md
================================================
# Replay Mode

Replay mode automates image navigation for performance benchmarking. It loads test directories, navigates through images, and records FPS metrics.

## Quick Start

```bash
# Basic benchmark (keyboard mode)
cargo run --profile opt-dev -- --replay --test-dir /path/to/images --duration 5 --auto-exit

# Slider mode benchmark
cargo run --profile opt-dev -- --replay --test-dir /path/to/images --duration 5 --nav-mode slider --auto-exit

# Multiple directories with JSON output
cargo run --profile opt-dev -- --replay \
    --test-dir /path/to/small_images \
    --test-dir /path/to/large_images \
    --duration 10 \
    --output results.json \
    --output-format json \
    --auto-exit
```

## CLI Arguments

| Argument | Default | Description |
|----------|---------|-------------|
| `--replay` | - | Enable replay mode |
| `--test-dir` | - | Directory to benchmark (can specify multiple) |
| `--duration` | `10` | Seconds to navigate each direction |
| `--directions` | `right` | Navigation direction: `right`, `left`, or `both` |
| `--iterations` | `1` | Number of times to repeat the benchmark |
| `--nav-mode` | `keyboard` | Navigation mechanism: `keyboard` or `slider` |
| `--nav-interval` | `50` | Milliseconds between navigation actions |
| `--slider-step` | `1` | Images to skip per navigation (slider mode only) |
| `--skip-initial` | `0` | Skip first N images from metrics (excludes cache warmup) |
| `--output` | - | Output file path |
| `--output-format` | `markdown` | Output format: `json` or `markdown` |
| `--auto-exit` | `false` | Exit automatically when benchmark completes |
| `--verbose` | `false` | Print detailed metrics during execution |

## Navigation Modes

### Keyboard Mode (default)

Simulates holding down navigation keys. Sets `skate_right`/`skate_left` flags for continuous frame-by-frame navigation.

- Movement: Continuous (every frame)
- Image loading: Incremental, cache-aware
- Typical Image FPS: 200+

### Slider Mode

Simulates dragging the navigation slider. Sends `SliderChanged` messages at regular intervals.

- Movement: Stepped (one position per interval)
- Image loading: Direct jump with async preview loading
- Typical Image FPS: 20-50 (depends on image size)

## Speed Control

Navigation speed is controlled by two parameters:

### Navigation Interval (`--nav-interval`)

Controls how frequently navigation actions are triggered.

| nav-interval | Actions/sec |
|--------------|-------------|
| 50ms (default) | 20 |
| 25ms | 40 |
| 20ms | 50 |
| 100ms | 10 |

### Slider Step (`--slider-step`, slider mode only)

Controls how many images to skip per navigation action.

| nav-interval | slider-step | Images/sec |
|--------------|-------------|------------|
| 50ms | 1 | 20 |
| 50ms | 5 | 100 |
| 20ms | 1 | 50 |

**Formula:** `images_per_second = (1000 / nav_interval) * slider_step`

### Matching Mouse Speed

For MX Master 3 (1000 DPI) or similar mice with typical drag speed:

```bash
# ~50 images/sec to match typical mouse drag
--nav-mode slider --nav-interval 20
```

## Output Formats

### JSON

```json
{
  "results": [
    {
      "directory": "/path/to/images",
      "direction": "right",
      "duration_secs": 5.02,
      "total_frames": 251,
      "ui_fps": { "avg": 60.1, "min": 58.0, "max": 62.0 },
      "image_fps": { "avg": 45.2, "min": 40.0, "max": 50.0, "last": 48.0 },
      "memory_mb": { "avg": 512.0, "min": 400.0, "max": 600.0 }
    }
  ],
  "iterations": 2
}
```

### Markdown

Generates a formatted table with per-directory results and summary statistics.

## Metrics Collected

| Metric | Description |
|--------|-------------|
| UI FPS | Main application frame rate |
| Image FPS | Rate of new images displayed |
| Memory | Process memory usage (Linux/macOS) |
| Duration | Actual time spent navigating |
| Total Frames | Number of UI frames rendered |

### Image FPS Sources

- **Keyboard mode**: Uses `IMAGE_RENDER_FPS` (incremental cache loading)
- **Slider mode**: Uses `iced_wgpu::get_image_fps()` (async preview loading)

## Tips

1. **Use `--skip-initial`** to exclude warmup frames where cache is cold
2. **Use `--directions right`** to avoid cache advantage on reverse navigation
3. **Run multiple `--iterations`** for more reliable averages
4. **Use `--profile opt-dev`** for optimized builds with debug symbols
5. **Set `--auto-exit`** for scripted benchmarks


================================================
FILE: resources/linux/appimage.desktop
================================================
[Desktop Entry]
Name=ViewSkater
Exec=viewskater
Icon=icon
Type=Application
Categories=Graphics;Viewer;
StartupWMClass=viewskater


================================================
FILE: resources/linux/viewskater.desktop
================================================
[Desktop Entry]
Name=ViewSkater
Exec=/path/to/viewskater %f
Icon=viewskater
Type=Application
Categories=Graphics;Viewer;
StartupWMClass=viewskater
MimeType=image/png;image/jpeg;image/webp;image/tiff;image/bmp;


================================================
FILE: resources/macos/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>English</string>
  <key>CFBundleDisplayName</key>
  <string>ViewSkater</string>
  <key>CFBundleIconFile</key>
  <string>ViewSkater.icns</string>
  <key>CFBundleIdentifier</key>
  <string>com.ggando.viewskater</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>ViewSkater</string>
  <key>CFBundleExecutable</key>
  <string>viewskater</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>0.3.1</string>
  <key>CFBundleVersion</key>
  <string>20251027.113543</string>
  <key>CSResourcesFileMapped</key>
  <true/>
  <key>LSApplicationCategoryType</key>
  <string>public.app-category.productivity</string>
  <key>LSArchitecturePriority</key>
  <array>
    <string>arm64</string>
  </array>
  <key>LSMinimumSystemVersion</key>
  <string>12.0</string>
  <key>NSHighResolutionCapable</key>
  <true/>
  <key>NSPrincipalClass</key>
  <string>NSApplication</string>

  <key>CFBundleDocumentTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeName</key>
      <string>JPEG Image</string>
      <key>CFBundleTypeRole</key>
      <string>Viewer</string>
      <key>LSHandlerRank</key>
      <string>Alternate</string>
      <key>CFBundleTypeExtensions</key>
      <array>
        <string>jpg</string>
        <string>jpeg</string>
      </array>
      <key>LSItemContentTypes</key>
      <array>
        <string>public.jpeg</string>
      </array>
      <key>CFBundleTypeIconFile</key>
      <string>ViewSkater.icns</string>
    </dict>
    <dict>
      <key>CFBundleTypeName</key>
      <string>PNG Image</string>
      <key>CFBundleTypeRole</key>
      <string>Viewer</string>
      <key>LSHandlerRank</key>
      <string>Alternate</string>
      <key>CFBundleTypeExtensions</key>
      <array>
        <string>png</string>
      </array>
      <key>LSItemContentTypes</key>
      <array>
        <string>public.png</string>
      </array>
      <key>CFBundleTypeIconFile</key>
      <string>ViewSkater.icns</string>
    </dict>
  </array>

  <key>UTImportedTypeDeclarations</key>
  <array>
    <dict>
      <key>UTTypeConformsTo</key>
      <array>
        <string>public.image</string>
        <string>public.data</string>
      </array>
      <key>UTTypeDescription</key>
      <string>Image File</string>
      <key>UTTypeIdentifier</key>
      <string>com.ggando.viewskater.image</string>
      <key>UTTypeTagSpecification</key>
      <dict>
        <key>public.filename-extension</key>
        <array>
          <string>jpg</string>
          <string>jpeg</string>
          <string>png</string>
        </array>
        <key>public.mime-type</key>
        <array>
          <string>image/jpeg</string>
          <string>image/png</string>
        </array>
      </dict>
    </dict>
  </array>
</dict>
</plist>


================================================
FILE: resources/macos/entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- CRITICAL: App Sandbox must be enabled for App Store apps -->
    <key>com.apple.security.app-sandbox</key>
    <true/>
    
    <!-- App identifier -->
    <key>com.apple.application-identifier</key>
    <string>YOUR_TEAM_ID.YOUR_BUNDLE_ID</string>
    
    <!-- Essential file access entitlements for document-based apps -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    
    <!-- Security-scoped bookmark support (essential for App Store document apps) -->
    <key>com.apple.security.files.bookmarks.app-scope</key>
    <true/>
    <key>com.apple.security.files.bookmarks.document-scope</key>
    <true/>
    
    <!-- Additional entitlements for better file access handling -->
    <!-- Allow reading files opened via system integration (Open With, double-click) -->
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
</dict>
</plist>



================================================
FILE: resources/macos/viewskater_wrapper.sh
================================================
#!/bin/bash
#
# viewskater_wrapper.sh
#
# Purpose:
# This wrapper script is intended for debugging macOS file association
# events (e.g., when opening a file with ViewSkater from Finder) and
# for capturing early startup logs or errors from the main application binary.
#
# Usage:
# Revise Info.plist's CFBundleExecutable to point to this script.
# e.g.,
# <key>CFBundleExecutable</key>
# <string>viewskater_wrapper.sh</string>
#
# How it works:
# - Sets up a log file at $HOME/Library/Logs/ViewSkater/open_events.log.
# - Logs invocation arguments and environment details.
# - Executes the main "viewskater" binary located in the same directory
#   as this script.
# - Redirects stderr of the main binary to the log file.
#
BASEDIR=$(dirname "$0")
LOG_FILE="$HOME/Library/Logs/ViewSkater/open_events.log"
mkdir -p "$(dirname "$LOG_FILE")"

# Log launch information with more detail
echo "$(date): ViewSkater wrapper launched with args: $@" >> "$LOG_FILE"
echo "$(date): Current directory: $(pwd)" >> "$LOG_FILE"
echo "$(date): Executable path: $BASEDIR/viewskater" >> "$LOG_FILE"

# Add direct console output
echo "ViewSkater wrapper starting..."
echo "Arguments: $@"
echo "Current directory: $(pwd)"
echo "Executable path: $BASEDIR/viewskater"

# Check if the executable exists
if [ ! -f "$BASEDIR/viewskater" ]; then
    echo "ERROR - Executable not found at $BASEDIR/viewskater"
    echo "$(date): ERROR - Executable not found at $BASEDIR/viewskater" >> "$LOG_FILE"
    exit 1
fi

# Execute with error logging
echo "Launching ViewSkater executable..."
exec "$BASEDIR/viewskater" "$@" 2>> "$LOG_FILE"

================================================
FILE: src/app/keyboard_handlers.rs
================================================
use log::debug;
use iced_core::keyboard::{self, Key, key::Named};
use iced_winit::runtime::Task;

use crate::app::{DataViewer, Message};
use crate::menu::PaneLayout;
use crate::file_io;
use crate::navigation_keyboard::{move_right_all, move_left_all};

// Helper function to check for the platform-appropriate modifier key
fn is_platform_modifier(modifiers: &keyboard::Modifiers) -> bool {
    #[cfg(target_os = "macos")]
    return modifiers.logo(); // Use Command key on macOS

    #[cfg(not(target_os = "macos"))]
    return modifiers.control(); // Use Control key on other platforms
}

impl DataViewer {
    pub(crate) fn handle_key_pressed_event(&mut self, key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Vec<Task<Message>> {
        let mut tasks = Vec::new();

        match key.as_ref() {
            Key::Named(Named::Tab) => {
                debug!("Tab pressed");
                self.toggle_footer();
            }

            Key::Named(Named::Space) | Key::Character("b") => {
                debug!("Space pressed");
                self.toggle_slider_type();
            }

            Key::Character("h") | Key::Character("H") => {
                debug!("H key pressed");
                // Only toggle split orientation in dual pane mode
                if self.pane_layout == PaneLayout::DualPane {
                    self.toggle_split_orientation();
                }
            }

            Key::Character("1") => {
                debug!("Key1 pressed");
                if self.pane_layout == PaneLayout::DualPane && self.is_slider_dual {
                    self.panes[0].is_selected = !self.panes[0].is_selected;
                }

                // If shift+alt is pressed, load a file into pane0
                if modifiers.shift() && modifiers.alt() {
                    debug!("Key1 Shift+Alt pressed");
                    tasks.push(Task::perform(file_io::pick_file(), move |result| {
                        Message::FolderOpened(result, 0)
                    }));
                }

                // If alt is pressed, load a folder into pane0
                else if modifiers.alt() {
                    debug!("Key1 Alt pressed");
                    tasks.push(Task::perform(file_io::pick_folder(), move |result| {
                        Message::FolderOpened(result, 0)
                    }));
                }

                // If platform_modifier is pressed, switch to single pane layout
                else if is_platform_modifier(&modifiers) {
                    self.toggle_pane_layout(PaneLayout::SinglePane);
                }
            }
            Key::Character("2") => {
                debug!("Key2 pressed");
                if self.pane_layout == PaneLayout::DualPane && self.is_slider_dual {
                    self.panes[1].is_selected = !self.panes[1].is_selected;
                }

                // If shift+alt is pressed, load a file into pane1
                if self.pane_layout == PaneLayout::DualPane && modifiers.shift() && modifiers.alt() {
                    debug!("Key2 Shift+Alt pressed");
                    tasks.push(Task::perform(file_io::pick_file(), move |result| {
                        Message::FolderOpened(result, 1)
                    }));
                }

                // If alt is pressed, load a folder into pane1
                else if self.pane_layout == PaneLayout::DualPane && modifiers.alt() {
                    debug!("Key2 Alt pressed");
                    tasks.push(Task::perform(file_io::pick_folder(), move |result| {
                        Message::FolderOpened(result, 1)
                    }));
                }

                // If platform_modifier is pressed, switch to dual pane with synced slider
                else if is_platform_modifier(&modifiers) {
                    debug!("Key2 Ctrl pressed");
                    self.toggle_pane_layout(PaneLayout::DualPane);
                    if self.is_slider_dual {
                        self.toggle_slider_type();
                    }
                }
            }

            Key::Character("c") |
            Key::Character("w") => {
                // Close the selected panes
                if is_platform_modifier(&modifiers) {
                    self.reset_state(-1);
                }
            }

            Key::Character("q") => {
                // Terminate the app
                if is_platform_modifier(&modifiers) {
                    tasks.push(Task::done(Message::SaveWindowState));
                    tasks.push(Task::done(Message::Quit));
                }
            }

            Key::Character("s") if is_platform_modifier(&modifiers) => {
                {
                        debug!("Save file with platform_modifier+s");
                        if self.panes[0].current_image.len() > 0 {
                            tasks.push(Task::perform(file_io::pick_save_file(), move |result| {
                                Message::ReadySaveImage(result)
                            }));
                        }
                }
            }

            Key::Character("o") => {
                // If platform_modifier is pressed, open a file or folder
                if is_platform_modifier(&modifiers) {
                    let pane_index = if self.pane_layout == PaneLayout::SinglePane {
                        0 // Use first pane in single-pane mode
                    } else {
                        self.last_opened_pane as usize // Use last opened pane in dual-pane mode
                    };
                    debug!("o key pressed pane_index: {}", pane_index);

                    // If shift is pressed or we have uppercase O, open folder
                    if modifiers.shift() {
                        debug!("Opening folder with platform_modifier+shift+o");
                        tasks.push(Task::perform(file_io::pick_folder(), move |result| {
                            Message::FolderOpened(result, pane_index)
                        }));
                    } else {
                        // Otherwise open file
                        debug!("Opening file with platform_modifier+o");
                        tasks.push(Task::perform(file_io::pick_file(), move |result| {
                            Message::FolderOpened(result, pane_index)
                        }));
                    }
                }
            }

            Key::Named(Named::ArrowLeft) | Key::Character("a") => {
                // Check for first image navigation with platform modifier or Fn key
                if is_platform_modifier(&modifiers) {
                    debug!("Navigating to first image");
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when navigating to first image
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }

                    // Find which panes need to be updated
                    let mut operations = Vec::new();

                    for (idx, pane) in self.panes.iter_mut().enumerate() {
                        if pane.dir_loaded && (pane.is_selected || self.is_slider_dual) {
                            // Navigate to the first image (index 0)
                            if pane.img_cache.current_index > 0 {
                                let new_pos = 0;
                                pane.slider_value = new_pos as u16;
                                self.slider_value = new_pos as u16;

                                // Save the operation for later execution
                                operations.push((idx as isize, new_pos));
                            }
                        }
                    }

                    // Now execute all operations after the loop is complete
                    for (pane_idx, new_pos) in operations {
                        tasks.push(crate::navigation_slider::load_remaining_images(
                            &self.device,
                            &self.queue,
                            self.is_gpu_supported,
                            self.cache_strategy,
                            self.compression_strategy,
                            &mut self.panes,
                            &mut self.loading_status,
                            pane_idx,
                            new_pos,
                        ));
                    }

                    return tasks;
                }

                // Existing left-arrow logic
                if self.skate_right {
                    self.skate_right = false;

                    // Discard all queue items that are LoadNext or ShiftNext
                    self.loading_status.reset_load_next_queue_items();
                }

                if self.pane_layout == PaneLayout::DualPane && self.is_slider_dual && !self.panes.iter().any(|pane| pane.is_selected) {
                    debug!("No panes selected");
                }

                if self.skate_left {
                    // will be handled at the end of update() to run move_left_all
                } else if modifiers.shift() {
                    self.skate_left = true;
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when entering skate mode
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }
                } else {
                    self.skate_left = false;
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when keyboard navigation starts
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }

                    debug!("move_left_all from handle_key_pressed_event()");
                    let task = move_left_all(
                        &self.device,
                        &self.queue,
                        self.cache_strategy,
                        self.compression_strategy,
                        &mut self.panes,
                        &mut self.loading_status,
                        &mut self.slider_value,
                        &self.pane_layout,
                        self.is_slider_dual,
                        self.last_opened_pane as usize);
                    tasks.push(task);
                }
            }
            Key::Named(Named::ArrowRight) | Key::Character("d") => {
                // Check for last image navigation with platform modifier or Fn key
                if is_platform_modifier(&modifiers) {
                    debug!("Navigating to last image");
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when navigating to last image
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }

                    // Find which panes need to be updated
                    let mut operations = Vec::new();

                    for (idx, pane) in self.panes.iter_mut().enumerate() {
                        if pane.dir_loaded && (pane.is_selected || self.is_slider_dual) {
                            // Get the last valid index
                            if let Some(last_index) = pane.img_cache.image_paths.len().checked_sub(1) {
                                if pane.img_cache.current_index < last_index {
                                    let new_pos = last_index;
                                    pane.slider_value = new_pos as u16;
                                    self.slider_value = new_pos as u16;

                                    // Save the operation for later execution
                                    operations.push((idx as isize, new_pos));
                                }
                            }
                        }
                    }

                    // Now execute all operations after the loop is complete
                    for (pane_idx, new_pos) in operations {
                        tasks.push(crate::navigation_slider::load_remaining_images(
                            &self.device,
                            &self.queue,
                            self.is_gpu_supported,
                            self.cache_strategy,
                            self.compression_strategy,
                            &mut self.panes,
                            &mut self.loading_status,
                            pane_idx,
                            new_pos,
                        ));
                    }

                    return tasks;
                }

                // Existing right-arrow logic
                debug!("Right key or 'D' key pressed!");
                if self.skate_left {
                    self.skate_left = false;

                    // Discard all queue items that are LoadPrevious or ShiftPrevious
                    self.loading_status.reset_load_previous_queue_items();
                }

                if self.pane_layout == PaneLayout::DualPane && self.is_slider_dual && !self.panes.iter().any(|pane| pane.is_selected) {
                    debug!("No panes selected");
                }

                if modifiers.shift() {
                    self.skate_right = true;
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when entering skate mode
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }
                } else {
                    self.skate_right = false;
                    self.use_slider_image_for_render = false;

                    // Clear slider_image_position when keyboard navigation starts
                    for pane in self.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }

                    let task = move_right_all(
                        &self.device,
                        &self.queue,
                        self.cache_strategy,
                        self.compression_strategy,
                        &mut self.panes,
                        &mut self.loading_status,
                        &mut self.slider_value,
                        &self.pane_layout,
                        self.is_slider_dual,
                        self.last_opened_pane as usize);
                    tasks.push(task);
                    debug!("handle_key_pressed_event() - tasks count: {}", tasks.len());
                }
            }

            Key::Named(Named::F3)  => {
                self.show_fps = !self.show_fps;
                debug!("Toggled debug FPS display: {}", self.show_fps);
            }

            Key::Named(Named::Super) => {
                #[cfg(target_os = "macos")] {
                    self.set_ctrl_pressed(true);
                }
            }
            Key::Named(Named::Control) => {
                #[cfg(not(target_os = "macos"))] {
                    self.set_ctrl_pressed(true);
                }
            }
            _ => {
                // Check if selection module wants to handle this key
                #[cfg(feature = "selection")]
                if let Some(task) = crate::widgets::selection_widget::handle_keyboard_event(
                    key,
                    modifiers,
                    &self.pane_layout,
                    self.last_opened_pane,
                ) {
                    tasks.push(task);
                }

                // Check if COCO module wants to handle this key
                #[cfg(feature = "coco")]
                if let Some(task) = crate::coco::widget::handle_keyboard_event(
                    key,
                    modifiers,
                    &self.pane_layout,
                    self.last_opened_pane,
                ) {
                    tasks.push(task);
                }
            }
        }

        tasks
    }

    pub(crate) fn handle_key_released_event(&mut self, key_code: &keyboard::Key, _modifiers: keyboard::Modifiers) -> Vec<Task<Message>> {
        #[allow(unused_mut)]
        let mut tasks = Vec::new();

        match key_code.as_ref() {
            Key::Named(Named::Tab) => {
                debug!("Tab released");
            }
            Key::Named(Named::Enter) | Key::Character("NumpadEnter")  => {
                debug!("Enter key released!");

            }
            Key::Named(Named::Escape) => {
                debug!("Escape key released!");

            }
            Key::Named(Named::ArrowLeft) | Key::Character("a") => {
                debug!("Left key or 'A' key released!");
                self.skate_left = false;
            }
            Key::Named(Named::ArrowRight) | Key::Character("d") => {
                debug!("Right key or 'D' key released!");
                self.skate_right = false;
            }
            Key::Named(Named::Super) => {
                #[cfg(target_os = "macos")] {
                    self.set_ctrl_pressed(false);
                }
            }
            Key::Named(Named::Control) => {
                #[cfg(not(target_os = "macos"))] {
                    self.set_ctrl_pressed(false);
                }
            }
            _ => {},
        }

        tasks
    }
}


================================================
FILE: src/app/message.rs
================================================
use std::path::PathBuf;

use iced_core::Event;
use iced_core::image::Handle;
use iced_core::Color;
use iced_winit::winit::dpi::{PhysicalPosition, PhysicalSize};

use crate::cache::img_cache::{CachedData, CacheStrategy, ImageMetadata, LoadOperation};
use crate::menu::PaneLayout;
use crate::file_io;
use iced_wgpu::engine::CompressionStrategy;

/// Result of async directory enumeration
#[derive(Debug, Clone)]
pub struct DirectoryEnumResult {
    pub file_paths: Vec<PathBuf>,
    pub directory_path: String,
    pub initial_index: usize,
}

/// Error type for async directory enumeration
#[derive(Debug, Clone)]
pub enum DirectoryEnumError {
    NoImagesFound,
    DirectoryError(String),
    NotFound,
}

/// Result type for slider image widget loading: (pane_idx, position, handle, dimensions, file_size)
pub type SliderImageWidgetResult = Result<(usize, usize, Handle, (u32, u32), u64), (usize, usize)>;

/// Result type for batch image loading: (cached_data, metadata, load_operation)
pub type ImagesLoadedResult = Result<(Vec<Option<CachedData>>, Vec<Option<ImageMetadata>>, Option<LoadOperation>), std::io::ErrorKind>;

#[derive(Debug, Clone)]
pub enum Message {
    Debug(String),
    Nothing,
    ShowAbout,
    HideAbout,
    ShowOptions,
    HideOptions,
    SaveWindowState,
    SaveSettings,
    ClearSettingsStatus,
    SettingsTabSelected(usize),
    ShowLogs,
    OpenSettingsDir,
    ExportDebugLogs,
    ExportAllLogs,
    OpenWebLink(String),
    // Note: Changed from font::Error to () since the error is never used
    #[allow(dead_code)]
    FontLoaded(Result<(), ()>),
    OpenFolder(usize),
    OpenFile(usize),
    FileDropped(isize, String),
    Close,
    Quit,
    ReplayKeepAlive,
    FolderOpened(Result<String, file_io::Error>, usize),
    DirectoryEnumerated(Result<DirectoryEnumResult, DirectoryEnumError>, usize),
    SliderChanged(isize, u16),
    SliderReleased(isize, u16),
    #[allow(dead_code)]
    SliderImageLoaded(Result<(usize, CachedData), usize>),
    SliderImageWidgetLoaded(SliderImageWidgetResult),
    Event(Event),
    ImagesLoaded(ImagesLoadedResult),
    OnSplitResize(u16),
    ResetSplit(u16),
    ToggleSliderType(bool),
    TogglePaneLayout(PaneLayout),
    ToggleFooter(bool),
    PaneSelected(usize, bool),
    CopyFilename(usize),
    CopyFilePath(usize),
    CopyImage(usize),
    #[allow(dead_code)]
    BackgroundColorChanged(Color),
    #[allow(dead_code)]
    TimerTick,
    SetCacheStrategy(CacheStrategy),
    SetCompressionStrategy(CompressionStrategy),
    ToggleFpsDisplay(bool),
    ToggleSplitOrientation(bool),
    ToggleSyncedZoom(bool),
    ToggleMouseWheelZoom(bool),
    ToggleCopyButtons(bool),
    ToggleMetadataDisplay(bool),
    ToggleNearestNeighborFilter(bool),
    SetSpinnerLocation(crate::settings::SpinnerLocation),
    #[cfg(feature = "coco")]
    ToggleCocoSimplification(bool),
    #[cfg(feature = "coco")]
    SetCocoMaskRenderMode(crate::settings::CocoMaskRenderMode),
    ToggleFullScreen(bool),
    CursorOnTop(bool),
    CursorOnMenu(bool),
    CursorOnFooter(bool),
    #[cfg(feature = "selection")]
    SelectionAction(crate::widgets::selection_widget::SelectionMessage),
    #[cfg(feature = "coco")]
    CocoAction(crate::coco::widget::CocoMessage),
    // Advanced settings input
    AdvancedSettingChanged(String, String),  // (field_name, value)
    ResetAdvancedSettings,
    // Window resize
    WindowResized(f32, PhysicalSize<u32>, bool), // (new width, window size, is window maximized)
    PositionChanged(
        PhysicalPosition<i32>,
        Option<iced_winit::winit::monitor::MonitorHandle>,
    ), // (window position, current monitor)
    RequestSaveImage,
    ReadySaveImage(Result<PathBuf, file_io::Error>),
    HideSuccessSaveModal,
    HideFailureSaveModal,
}


================================================
FILE: src/app/message_handlers.rs
================================================
// Comprehensive message handler module that routes different message categories
// This significantly reduces the size of app.rs update() method

use std::path::PathBuf;
use std::sync::Arc;
use log::{info, warn, error, debug};
use iced_winit::runtime::Task;
use iced_wgpu::engine::CompressionStrategy;
use iced_core::Event;

use iced_runtime::clipboard;

use crate::app::{DataViewer, Message};
use crate::cache::img_cache::{CacheStrategy, CachedData, LoadOperation};
use crate::exif_utils::decode_with_exif_orientation;
use crate::settings::{UserSettings, WindowState};
use crate::utils::save::extract_gpu_image;
use crate::{file_io, window_state::get_window_visible};
use crate::loading_handler;
use crate::navigation_slider;
use crate::navigation_keyboard::{move_left_all, move_right_all};
use crate::menu::PaneLayout;
use crate::pane::{IMAGE_RENDER_TIMES, IMAGE_RENDER_FPS};
use crate::widgets::shader::{scene::Scene, cpu_scene::CpuScene};

#[allow(unused_imports)]
use std::time::Instant;

/// Main entry point for handling all messages
/// Routes messages to appropriate handler functions
pub fn handle_message(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        // Simple inline messages
        Message::Nothing => Task::none(),
        Message::Debug(s) => {
            app.title = s;
            Task::none()
        }
        Message::BackgroundColorChanged(color) => {
            app.background_color = color;
            Task::none()
        }
        Message::FontLoaded(_) => Task::none(),
        Message::TimerTick => {
            debug!("TimerTick received");
            Task::none()
        }
        Message::Quit => {
            let _ = handle_save_window_state(app);
            std::process::exit(0);
        }
        Message::ReplayKeepAlive => {
            // This message is sent periodically during replay mode to keep the update loop active
            debug!("ReplayKeepAlive received - keeping replay update loop active");
            // Reset pending flag so a new keep-alive can be scheduled
            app.replay_keep_alive_pending = false;
            Task::none()
        }

        // UI state messages (About, Options, Logs)
        Message::ShowLogs | Message::OpenSettingsDir | Message::ExportDebugLogs |
        Message::ExportAllLogs | Message::ShowAbout | Message::HideAbout |
        Message::ShowOptions | Message::HideOptions | Message::OpenWebLink(_) => {
            handle_ui_messages(app, message)
        }

        // Settings messages
        Message::SaveWindowState | Message::SaveSettings | Message::ClearSettingsStatus |
        Message::SettingsTabSelected(_) | Message::AdvancedSettingChanged(_, _) |
        Message::ResetAdvancedSettings => {
            handle_settings_messages(app, message)
        }

        // File operation messages
        Message::OpenFolder(_) | Message::OpenFile(_) | Message::FileDropped(_, _) |
        Message::Close | Message::FolderOpened(_, _) | Message::DirectoryEnumerated(_, _) |
        Message::CopyFilename(_) | Message::CopyFilePath(_) | Message::CopyImage(_) => {
            handle_file_messages(app, message)
        }

        // Image loading messages
        Message::ImagesLoaded(_) | Message::SliderImageWidgetLoaded(_) | Message::SliderImageLoaded(_) => {
            handle_image_loading_messages(app, message)
        }

        // Slider and navigation messages
        Message::SliderChanged(_, _) | Message::SliderReleased(_, _) => {
            handle_slider_messages(app, message)
        }
        
        Message::RequestSaveImage | Message::ReadySaveImage(_) => handle_save_image(app, message),

        // Toggle and UI control messages
        Message::OnSplitResize(_) | Message::ResetSplit(_) | Message::ToggleSliderType(_) |
        Message::TogglePaneLayout(_) | Message::ToggleFooter(_) | Message::ToggleSyncedZoom(_) |
        Message::ToggleMouseWheelZoom(_) | Message::ToggleCopyButtons(_) | Message::ToggleMetadataDisplay(_) | Message::ToggleNearestNeighborFilter(_) |
        Message::SetSpinnerLocation(_) |
        Message::ToggleFullScreen(_) | Message::ToggleFpsDisplay(_) | Message::ToggleSplitOrientation(_) |
        Message::CursorOnTop(_) | Message::CursorOnMenu(_) | Message::CursorOnFooter(_) |
        Message::PaneSelected(_, _) | Message::SetCacheStrategy(_) | Message::SetCompressionStrategy(_) |
        Message::WindowResized(_, _, _) | Message::PositionChanged(_, _)
        | Message::HideSuccessSaveModal
        | Message::HideFailureSaveModal =>
        {

            handle_toggle_messages(app, message)
        }

        #[cfg(feature = "coco")]
        Message::ToggleCocoSimplification(_) => {
            handle_toggle_messages(app, message)
        }

        #[cfg(feature = "coco")]
        Message::SetCocoMaskRenderMode(_) => {
            handle_toggle_messages(app, message)
        }

        // Event messages (mouse, keyboard, file drops)
        Message::Event(event) => {
            handle_event_messages(app, event)
        }

        // Feature-specific messages
        #[cfg(feature = "selection")]
        Message::SelectionAction(msg) => {
            crate::widgets::selection_widget::handle_selection_message(
                msg,
                &app.panes,
                &mut app.selection_manager,
            )
        }

        #[cfg(feature = "coco")]
        Message::CocoAction(coco_msg) => {
            crate::coco::widget::handle_coco_message(
                coco_msg,
                &mut app.panes,
                &mut app.annotation_manager,
            )
        }
    }
}

/// Routes UI state messages (About, Options, Logs, etc.)
pub fn handle_ui_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::ShowLogs => {
            let app_name = "viewskater";
            let log_dir_path = crate::logging::get_log_directory(app_name);
            let _ = std::fs::create_dir_all(log_dir_path.clone());
            crate::logging::open_in_file_explorer(log_dir_path.to_string_lossy().as_ref());
            Task::none()
        }
        Message::OpenSettingsDir => {
            let settings_path = UserSettings::settings_path();
            if let Some(settings_dir) = settings_path.parent() {
                let _ = std::fs::create_dir_all(settings_dir);
                crate::logging::open_in_file_explorer(settings_dir.to_string_lossy().as_ref());
            }
            Task::none()
        }
        Message::ExportDebugLogs => {
            let app_name = "viewskater";
            if let Some(log_buffer) = crate::get_shared_log_buffer() {
                crate::logging::export_and_open_debug_logs(app_name, log_buffer);
            } else {
                warn!("Log buffer not available for export");
            }
            Task::none()
        }
        Message::ExportAllLogs => {
            handle_export_all_logs();
            Task::none()
        }
        Message::ShowAbout => {
            app.show_about = true;
            Task::perform(async {
                std::thread::sleep(std::time::Duration::from_millis(5));
            }, |_| Message::Nothing)
        }
        Message::HideAbout => {
            app.show_about = false;
            Task::none()
        }
        Message::ShowOptions => {
            app.settings.show();
            Task::perform(async {
                std::thread::sleep(std::time::Duration::from_millis(5));
            }, |_| Message::Nothing)
        }
        Message::HideOptions => {
            app.settings.hide();
            Task::none()
        }
        Message::OpenWebLink(url) => {
            if let Err(e) = webbrowser::open(&url) {
                warn!("Failed to open link: {}, error: {:?}", url, e);
            }
            Task::none()
        }
        _ => Task::none()
    }
}

/// Routes settings-related messages
pub fn handle_settings_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::SaveWindowState => handle_save_window_state(app),
        Message::SaveSettings => handle_save_settings(app),
        Message::ClearSettingsStatus => {
            app.settings.clear_save_status();
            Task::none()
        }
        Message::SettingsTabSelected(index) => {
            app.settings.set_active_tab(index);
            Task::none()
        }
        Message::AdvancedSettingChanged(field_name, value) => {
            app.settings.set_advanced_input(field_name, value);
            Task::none()
        }
        Message::ResetAdvancedSettings => {
            handle_reset_advanced_settings(app);
            Task::none()
        }
        _ => Task::none()
    }
}

/// Routes file operation messages
pub fn handle_file_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::OpenFolder(pane_index) => {
            Task::perform(file_io::pick_folder(), move |result| {
                Message::FolderOpened(result, pane_index)
            })
        }
        Message::OpenFile(pane_index) => {
            Task::perform(file_io::pick_file(), move |result| {
                Message::FolderOpened(result, pane_index)
            })
        }
        Message::FileDropped(pane_index, dropped_path) => {
            handle_file_dropped(app, pane_index, dropped_path)
        }
        Message::Close => {
            app.reset_state(-1);
            debug!("directory_path: {:?}", app.directory_path);
            debug!("self.current_image_index: {}", app.current_image_index);
            for pane in app.panes.iter_mut() {
                let img_cache = &mut pane.img_cache;
                debug!("img_cache.current_index: {}", img_cache.current_index);
                debug!("img_cache.image_paths.len(): {}", img_cache.image_paths.len());
            }
            Task::none()
        }
        Message::FolderOpened(result, pane_index) => {
            match result {
                Ok(dir) => {
                    debug!("Folder opened: {}", dir);
                    if pane_index > 0 && app.pane_layout == PaneLayout::SinglePane {
                        debug!("Ignoring request to open folder in pane {} while in single-pane mode", pane_index);
                        Task::none()
                    } else {
                        app.initialize_dir_path(&PathBuf::from(dir), pane_index)
                    }
                }
                Err(err) => {
                    debug!("Folder open failed: {:?}", err);
                    Task::none()
                }
            }
        }
        Message::DirectoryEnumerated(result, pane_index) => {
            use crate::app::DirectoryEnumError;
            match result {
                Ok(enum_result) => {
                    debug!("Directory enumerated: {} images found", enum_result.file_paths.len());
                    app.complete_dir_initialization(enum_result, pane_index)
                }
                Err(DirectoryEnumError::NoImagesFound) => {
                    error!("No supported images found in directory");
                    Task::none()
                }
                Err(DirectoryEnumError::DirectoryError(e)) => {
                    error!("Directory enumeration error: {}", e);
                    Task::none()
                }
                Err(DirectoryEnumError::NotFound) => {
                    error!("Path not found");
                    Task::none()
                }
            }
        }
        Message::CopyFilename(pane_index) => {
            let path = &app.panes[pane_index].img_cache.image_paths[app.panes[pane_index].img_cache.current_index];
            let filename_str = path.file_name().to_string();
            if let Some(filename) = file_io::get_filename(&filename_str) {
                debug!("Copying filename to clipboard: {}", filename);
                return clipboard::write(filename);
            }
            Task::none()
        }
        Message::CopyFilePath(pane_index) => {
            let path = &app.panes[pane_index].img_cache.image_paths[app.panes[pane_index].img_cache.current_index];
            let img_path = path.file_name().to_string();
            if let Some(dir_path) = app.panes[pane_index].directory_path.as_ref() {
                let full_path = PathBuf::from(dir_path).join(img_path);
                debug!("Copying full path to clipboard: {}", full_path.display());
                return clipboard::write(full_path.to_string_lossy().to_string());
            }
            Task::none()
        }
        Message::CopyImage(pane_index) => {
            let cache = &app.panes[pane_index].img_cache;
            // Try CPU cache first, fall back to reading from disk for GPU-cached images
            let bytes = if let Ok(cached_data) = cache.get_current_image() {
                cached_data.as_vec().ok()
            } else {
                None
            };
            let bytes = bytes.or_else(|| {
                let path_source = &cache.image_paths[cache.current_index];
                match path_source {
                    crate::cache::img_cache::PathSource::Filesystem(path) => {
                        std::fs::read(path).ok()
                    }
                    _ => {
                        error!("Cannot copy image: archive images require CPU cache");
                        None
                    }
                }
            });
            if let Some(bytes) = bytes {
                std::thread::spawn(move || {
                    match image::load_from_memory(&bytes) {
                        Ok(img) => {
                            let rgba = img.to_rgba8();
                            let (w, h) = rgba.dimensions();
                            let img_data = arboard::ImageData {
                                width: w as usize,
                                height: h as usize,
                                bytes: std::borrow::Cow::Owned(rgba.into_raw()),
                            };
                            match arboard::Clipboard::new() {
                                Ok(mut clip) => {
                                    if let Err(e) = clip.set_image(img_data) {
                                        error!("Failed to copy image to clipboard: {}", e);
                                    } else {
                                        debug!("Image copied to clipboard ({}x{})", w, h);
                                    }
                                }
                                Err(e) => error!("Failed to open clipboard: {}", e),
                            }
                        }
                        Err(e) => error!("Failed to decode image for clipboard: {}", e),
                    }
                });
            }
            Task::none()
        }
        _ => Task::none()
    }
}

/// Routes image loading messages
pub fn handle_image_loading_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::ImagesLoaded(result) => {
            debug!("ImagesLoaded");
            match result {
                Ok((image_data, metadata, operation)) => {
                    if let Some(op) = operation {
                        let cloned_op = op.clone();
                        match op {
                            LoadOperation::LoadNext((ref pane_indices, ref target_indices))
                            | LoadOperation::LoadPrevious((ref pane_indices, ref target_indices))
                            | LoadOperation::ShiftNext((ref pane_indices, ref target_indices))
                            | LoadOperation::ShiftPrevious((ref pane_indices, ref target_indices)) => {
                                let operation_type = cloned_op.operation_type();

                                loading_handler::handle_load_operation_all(
                                    &mut app.panes,
                                    &mut app.loading_status,
                                    pane_indices,
                                    target_indices,
                                    &image_data,
                                    &metadata,
                                    &cloned_op,
                                    operation_type,
                                );

                                // Clear loading timer for the panes that completed
                                // (clear per-pane, not based on global queue state)
                                for &pane_idx in pane_indices {
                                    if let Some(pane) = app.panes.get_mut(pane_idx) {
                                        pane.loading_started_at = None;
                                    }
                                }
                            }
                            LoadOperation::LoadPos((pane_index, target_indices_and_cache)) => {
                                loading_handler::handle_load_pos_operation(
                                    &mut app.panes,
                                    &mut app.loading_status,
                                    pane_index,
                                    &target_indices_and_cache,
                                    &image_data,
                                    &metadata,
                                );

                                // Clear loading timer for this pane
                                if let Some(pane) = app.panes.get_mut(pane_index) {
                                    pane.loading_started_at = None;
                                }

                                // Signal replay controller that initial load is complete
                                if let Some(ref mut replay_controller) = app.replay_controller {
                                    if matches!(replay_controller.state, crate::replay::ReplayState::WaitingForReady { .. }) {
                                        debug!("LoadPos complete - signaling replay controller that app is ready to navigate");

                                        // Set image count for slider mode navigation
                                        if let Some(pane) = app.panes.get(pane_index) {
                                            replay_controller.set_image_count(pane.img_cache.image_paths.len());
                                        }

                                        // Reset FPS trackers right before navigation starts
                                        // This ensures no stale data from image loading contaminates metrics
                                        if let Ok(mut fps) = crate::CURRENT_FPS.lock() { *fps = 0.0; }
                                        if let Ok(mut fps) = IMAGE_RENDER_FPS.lock() { *fps = 0.0; }
                                        if let Ok(mut times) = crate::FRAME_TIMES.lock() { times.clear(); }
                                        if let Ok(mut times) = IMAGE_RENDER_TIMES.lock() { times.clear(); }
                                        iced_wgpu::reset_image_fps();

                                        replay_controller.on_ready_to_navigate();
                                    }
                                }
                            }
                        }
                    }
                }
                Err(err) => {
                    debug!("Image load failed: {:?}", err);
                }
            }
            Task::none()
        }
        Message::SliderImageWidgetLoaded(result) => {
            match result {
                Ok((pane_idx, pos, handle, dimensions, file_size)) => {
                    crate::track_async_delivery();

                    if let Some(pane) = app.panes.get_mut(pane_idx) {
                        pane.slider_image = Some(handle);
                        pane.slider_image_dimensions = Some(dimensions);
                        pane.slider_image_position = Some(pos);
                        // Update metadata for footer display during slider dragging
                        pane.current_image_metadata = Some(crate::cache::img_cache::ImageMetadata::new(
                            dimensions.0, dimensions.1, file_size
                        ));
                        // BUGFIX: Don't update current_index here! It causes desyncs when stale slider images
                        // load after slider release. The slider position is tracked in slider_image_position instead.
                        // pane.img_cache.current_index = pos;

                        debug!("Slider image loaded for pane {} at position {} with dimensions {:?}", pane_idx, pos, dimensions);
                    } else {
                        warn!("SliderImageWidgetLoaded: Invalid pane index {}", pane_idx);
                    }
                },
                Err((pane_idx, pos)) => {
                    warn!("SLIDER: Failed to load image widget for pane {} at position {}", pane_idx, pos);
                }
            }
            Task::none()
        }
        Message::SliderImageLoaded(result) => {
            match result {
                Ok((pos, cached_data)) => {
                    let pane = &mut app.panes[0];

                    if let CachedData::Cpu(bytes) = &cached_data {
                        debug!("SliderImageLoaded: loaded data: {:?}", bytes.len());

                        pane.current_image = CachedData::Cpu(bytes.clone());
                        pane.current_image_index = Some(pos);
                        pane.slider_scene = Some(Scene::CpuScene(CpuScene::new(
                            bytes.clone(), true)));

                        if let Some(device) = &pane.device {
                            if let Some(queue) = &pane.queue {
                                if let Some(scene) = &mut pane.slider_scene {
                                    scene.ensure_texture(device, queue, pane.pane_id);
                                }
                            }
                        }
                    }
                },
                Err(pos) => {
                    warn!("SLIDER: Failed to load image for position {}", pos);
                }
            }
            Task::none()
        }
        _ => Task::none()
    }
}

/// Routes slider and navigation messages
pub fn handle_slider_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::SliderChanged(pane_index, value) => {
            app.is_slider_moving = true;
            app.use_slider_image_for_render = true;
            app.last_slider_update = Instant::now();

            // Reset COCO zoom state when slider starts moving
            #[cfg(feature = "coco")]
            {
                if pane_index == -1 {
                    // Reset all panes
                    for pane in app.panes.iter_mut() {
                        pane.zoom_scale = 1.0;
                        pane.zoom_offset = iced_core::Vector::default();
                    }
                } else {
                    // Reset specific pane
                    if let Some(pane) = app.panes.get_mut(pane_index as usize) {
                        pane.zoom_scale = 1.0;
                        pane.zoom_offset = iced_core::Vector::default();
                    }
                }
            }

            let use_async = true;

            #[cfg(target_os = "linux")]
            let use_throttle = true;
            #[cfg(not(target_os = "linux"))]
            let use_throttle = false;

            if pane_index == -1 {
                app.prev_slider_value = app.slider_value;
                app.slider_value = value;

                if app.panes[0].slider_image.is_none() {
                    for pane in app.panes.iter_mut() {
                        pane.slider_scene = None;
                    }
                }
            } else {
                let pane_index_usize = pane_index as usize;

                if app.is_slider_dual && app.pane_layout == PaneLayout::DualPane {
                    for idx in 0..app.panes.len() {
                        if idx != pane_index_usize {
                            app.panes[idx].slider_image = None;
                            app.panes[idx].slider_image_position = None;
                        }
                    }
                }

                let pane = &mut app.panes[pane_index_usize];
                pane.prev_slider_value = pane.slider_value;
                pane.slider_value = value;

                if pane.slider_image.is_none() {
                    pane.slider_scene = None;
                }
            }

            navigation_slider::update_pos(
                &mut app.panes,
                pane_index,
                value as usize,
                use_async,
                use_throttle,
            )
        }
        Message::SliderReleased(pane_index, value) => {
            debug!("SLIDER_DEBUG: SliderReleased event received");
            app.is_slider_moving = false;

            let final_image_fps = iced_wgpu::get_image_fps();
            let upload_timestamps = iced_wgpu::get_image_upload_timestamps();

            if !upload_timestamps.is_empty() {
                if let Ok(mut render_times) = IMAGE_RENDER_TIMES.lock() {
                    *render_times = upload_timestamps.into_iter().collect();

                    if let Ok(mut fps) = IMAGE_RENDER_FPS.lock() {
                        *fps = final_image_fps as f32;
                        debug!("SLIDER_DEBUG: Synced image fps tracking, final FPS: {:.1}", final_image_fps);
                    }
                }
            }

            // Use the position of the currently displayed slider_image if available,
            // otherwise fall back to the slider value
            let pos = if pane_index >= 0 {
                app.panes.get(pane_index as usize)
                    .and_then(|pane| pane.slider_image_position)
                    .unwrap_or(value as usize)
            } else {
                // For pane_index == -1 (all panes), use slider_image_position from pane 0
                app.panes.first()
                    .and_then(|pane| pane.slider_image_position)
                    .unwrap_or(value as usize)
            };

            debug!("SliderReleased: Using position {} (slider_image_position) instead of slider value {}", pos, value);

            navigation_slider::load_remaining_images(
                &app.device,
                &app.queue,
                app.is_gpu_supported,
                app.cache_strategy,
                app.compression_strategy,
                &mut app.panes,
                &mut app.loading_status,
                pane_index,
                pos)
        }
        _ => Task::none()
    }
}

/// Routes toggle and UI control messages
pub fn handle_toggle_messages(app: &mut DataViewer, message: Message) -> Task<Message> {
    match message {
        Message::OnSplitResize(position) => {
            app.divider_position = Some(position);
            Task::none()
        }
        Message::ResetSplit(_position) => {
            app.divider_position = None;
            Task::none()
        }
        Message::ToggleSliderType(_bool) => {
            app.toggle_slider_type();
            Task::none()
        }
        Message::TogglePaneLayout(pane_layout) => {
            app.toggle_pane_layout(pane_layout);
            Task::none()
        }
        Message::ToggleFooter(_bool) => {
            app.toggle_footer();
            Task::none()
        }
        Message::ToggleSyncedZoom(enabled) => {
            app.synced_zoom = enabled;
            Task::none()
        }
        Message::ToggleMouseWheelZoom(enabled) => {
            app.mouse_wheel_zoom = enabled;
            for pane in app.panes.iter_mut() {
                pane.mouse_wheel_zoom = enabled;
            }
            Task::none()
        }
        Message::ToggleCopyButtons(enabled) => {
            app.show_copy_buttons = enabled;
            Task::none()
        }
        Message::ToggleMetadataDisplay(enabled) => {
            app.show_metadata = enabled;
            Task::none()
        }

        Message::HideSuccessSaveModal => {
            app.toggle_success_save_modal();

            Task::none()
        }
        Message::HideFailureSaveModal => {
            app.set_failure_save_modal(None);
            Task::none()
        }


        Message::ToggleNearestNeighborFilter(enabled) => {
            debug!("ToggleNearestNeighborFilter: setting to {}", enabled);
            app.nearest_neighbor_filter = enabled;

            // Force reload of current directories to apply the new filter immediately
            let mut tasks = Vec::new();
            for pane_index in 0..app.panes.len() {
                if let Some(dir_path) = app.panes[pane_index].directory_path.clone() {
                    debug!("Reloading directory for pane {}: {:?}", pane_index, dir_path);
                    tasks.push(app.initialize_dir_path(&PathBuf::from(dir_path), pane_index));
                }
            }

            Task::batch(tasks)
        }
        Message::SetSpinnerLocation(location) => {
            debug!("SetSpinnerLocation: setting to {:?}", location);
            app.spinner_location = location;
            Task::none()
        }
        #[cfg(feature = "coco")]
        Message::ToggleCocoSimplification(enabled) => {
            app.coco_disable_simplification = enabled;
            Task::none()
        }
        #[cfg(feature = "coco")]
        Message::SetCocoMaskRenderMode(mode) => {
            app.coco_mask_render_mode = mode;
            Task::none()
        }
        Message::ToggleFullScreen(enabled) => {
            if enabled {
                app.window_state = WindowState::FullScreen;
            } else {
                app.window_state = WindowState::Window;
            }
            Task::none()
        }
        Message::ToggleFpsDisplay(value) => {
            app.show_fps = value;
            Task::none()
        }
        Message::ToggleSplitOrientation(_bool) => {
            app.toggle_split_orientation();
            Task::none()
        }
        Message::CursorOnTop(value) => {
            app.cursor_on_top = value;
            Task::none()
        }
        Message::CursorOnMenu(value) => {
            app.cursor_on_menu = value;
            Task::none()
        }
        Message::CursorOnFooter(value) => {
            app.cursor_on_footer = value;
            Task::none()
        }
        Message::PaneSelected(pane_index, is_selected) => {
            app.panes[pane_index].is_selected = is_selected;
            for (index, pane) in app.panes.iter_mut().enumerate() {
                debug!("pane_index: {}, is_selected: {}", index, pane.is_selected);
            }
            Task::none()
        }
        Message::SetCacheStrategy(strategy) => {
            app.update_cache_strategy(strategy);
            Task::none()
        }
        Message::SetCompressionStrategy(strategy) => {
            app.update_compression_strategy(strategy);
            Task::none()
        }
        Message::WindowResized(width, size, is_maximized) => {
            app.window_width = width;
            app.window_size = size;

            // Track the largest size seen while maximized (used by Linux X11 un-maximize workaround)
            if is_maximized {
                let should_update = app.maximized_size.map_or(true, |max_size| {
                    size.width > max_size.width || size.height > max_size.height
                });
                if should_update {
                    app.maximized_size = Some(size);
                }
            }

            // macOS: use is_maximized (winit's isZoomed() wrapper), same as other platforms.
            // isZoomed() may be unreliable mid-animation, but save_window_state_to_disk
            // queries it authoritatively post-animation as a safety net.
            #[cfg(target_os = "macos")]
            match app.window_state {
                WindowState::Window => {
                    if is_maximized {
                        app.window_state = WindowState::Maximized;
                        app.last_windowed_position = app.position_before_transition;
                    }
                },
                WindowState::Maximized => {
                    if !is_maximized {
                        app.window_state = WindowState::Window;
                    }
                },
                _ => {},
            }

            // Windows/Linux: use winit's is_maximized() (reliable on these platforms)
            #[cfg(not(target_os = "macos"))]
            match app.window_state {
                WindowState::Window => {
                    if is_maximized {
                        app.window_state = WindowState::Maximized;
                        // Windows workaround: PositionChanged(0,0) fires before this event,
                        // corrupting last_windowed_position. Restore from backup.
                        app.last_windowed_position = app.position_before_transition;
                    }
                },
                WindowState::Maximized => {
                    if !is_maximized {
                        // Primary detection: is_maximized() returned false (works on Windows/macOS)
                        app.window_state = WindowState::Window;
                    } else {
                        // X11 workaround: is_maximized() returns stale true during un-maximize transition
                        // On X11, maximized windows cannot be resized - any size change means un-maximize
                        #[cfg(target_os = "linux")]
                        {
                            let size_changed = app.maximized_size.map_or(false, |max_size| size != max_size);
                            if size_changed {
                                app.window_state = WindowState::Window;
                                app.maximized_size = None;
                            }
                        }
                    }
                },
                _ => {},
            }

            Task::none()
        }
        Message::PositionChanged(position, monitor) => {
            app.window_position = position;
            let is_same_monitor = app.last_monitor == monitor;
            // Only track last_windowed_position when in windowed state or moving across different monitors
            // Save previous value first (Windows workaround: PositionChanged fires before WindowResized
            // during maximize, so we need to be able to restore if transition is detected)
            if app.window_state == WindowState::Window || !is_same_monitor {
                app.position_before_transition = app.last_windowed_position;
                app.last_windowed_position = position;
                app.last_monitor = monitor;
            }
            Task::none()
        }
        _ => Task::none()
    }
}

/// Routes event messages (mouse wheel, keyboard, file drops)
pub fn handle_event_messages(app: &mut DataViewer, event: Event) -> Task<Message> {
    match event {
        Event::Mouse(iced_core::mouse::Event::WheelScrolled { delta }) => {
            if !app.ctrl_pressed && !app.mouse_wheel_zoom && !app.settings.is_visible() && !app.show_about {
                match delta {
                    iced_core::mouse::ScrollDelta::Lines { y, .. }
                    | iced_core::mouse::ScrollDelta::Pixels { y, .. } => {
                        if y > 0.0 {
                            // Clear slider state when using mouse wheel navigation
                            app.use_slider_image_for_render = false;
                            for pane in app.panes.iter_mut() {
                                pane.slider_image_position = None;
                            }

                            return move_left_all(
                                &app.device,
                                &app.queue,
                                app.cache_strategy,
                                app.compression_strategy,
                                &mut app.panes,
                                &mut app.loading_status,
                                &mut app.slider_value,
                                &app.pane_layout,
                                app.is_slider_dual,
                                app.last_opened_pane as usize);
                        } else if y < 0.0 {
                            // Clear slider state when using mouse wheel navigation
                            app.use_slider_image_for_render = false;
                            for pane in app.panes.iter_mut() {
                                pane.slider_image_position = None;
                            }

                            return move_right_all(
                                &app.device,
                                &app.queue,
                                app.cache_strategy,
                                app.compression_strategy,
                                &mut app.panes,
                                &mut app.loading_status,
                                &mut app.slider_value,
                                &app.pane_layout,
                                app.is_slider_dual,
                                app.last_opened_pane as usize
                            );
                        }
                    }
                };
            } else {
                // Mouse wheel with ctrl pressed or mouse_wheel_zoom enabled = zoom mode
                // Clear slider state to switch to ImageShader widget which handles zoom
                if app.use_slider_image_for_render {
                    app.use_slider_image_for_render = false;
                    for pane in app.panes.iter_mut() {
                        pane.slider_image_position = None;
                    }
                }
            }
            Task::none()
        }

        Event::Keyboard(iced_core::keyboard::Event::KeyPressed { key, modifiers, .. }) => {
            debug!("KeyPressed - Key pressed: {:?}, modifiers: {:?}", key, modifiers);
            debug!("modifiers.shift(): {}", modifiers.shift());
            let tasks = app.handle_key_pressed_event(&key, modifiers);

            if !tasks.is_empty() {
                return Task::batch(tasks);
            }
            Task::none()
        }

        Event::Keyboard(iced_core::keyboard::Event::KeyReleased { key, modifiers, .. }) => {
            let tasks = app.handle_key_released_event(&key, modifiers);
            if !tasks.is_empty() {
                return Task::batch(tasks);
            }
            Task::none()
        }

        #[cfg(any(target_os = "macos", target_os = "windows"))]
        Event::Window(iced_core::window::Event::FileDropped(dropped_paths, _position)) => {
            handle_window_file_drop(app, &dropped_paths[0])
        }

        #[cfg(target_os = "linux")]
        Event::Window(iced_core::window::Event::FileDropped(dropped_path, _)) => {
            handle_window_file_drop(app, &dropped_path[0])
        }

        _ => Task::none()
    }
}

// ============================================================================
// Helper functions
// ============================================================================

fn handle_window_file_drop(app: &mut DataViewer, path: &std::path::Path) -> Task<Message> {
    if app.pane_layout != PaneLayout::SinglePane {
        return Task::none();
    }

    // Check if it's a JSON file that might be COCO format
    #[cfg(feature = "coco")]
    if path.extension().and_then(|s| s.to_str()) == Some("json") {
        debug!("JSON file detected in window event, checking if it's COCO format: {}", path.display());
        match std::fs::read_to_string(path) {
            Ok(content) => {
                if crate::coco::parser::CocoDataset::is_coco_format(&content) {
                    info!("✓ Detected COCO JSON file: {}", path.display());
                    return Task::done(Message::CocoAction(
                        crate::coco::widget::CocoMessage::LoadCocoFile(path.to_path_buf())
                    ));
                } else {
                    debug!("JSON file is not COCO format, treating as regular file");
                }
            }
            Err(e) => {
                warn!("Failed to read JSON file: {}", e);
            }
        }
    }

    app.reset_state(-1);
    debug!("File dropped: {:?}", path);
    app.initialize_dir_path(&path.to_path_buf(), 0)
}

fn handle_file_dropped(app: &mut DataViewer, pane_index: isize, dropped_path: String) -> Task<Message> {
    let path = PathBuf::from(&dropped_path);

    #[cfg(feature = "coco")]
    debug!("COCO FEATURE IS ENABLED");
    #[cfg(not(feature = "coco"))]
    debug!("COCO FEATURE IS DISABLED");

    #[cfg(feature = "coco")]
    if path.extension().and_then(|s| s.to_str()) == Some("json") {
        debug!("JSON file detected, checking if it's COCO format: {}", path.display());
        match std::fs::read_to_string(&path) {
            Ok(content) => {
                if crate::coco::parser::CocoDataset::is_coco_format(&content) {
                    info!("✓ Detected COCO JSON file: {}", path.display());
                    return Task::none();
                } else {
                    debug!("JSON file is not COCO format, treating as regular file");
                }
            }
            Err(e) => {
                warn!("Failed to read JSON file: {}", e);
            }
        }
    }

    debug!("Message::FileDropped - Resetting state");
    app.reset_state(pane_index);

    debug!("File dropped: {:?}, pane_index: {}", dropped_path, pane_index);
    debug!("self.dir_loaded, pane_index, last_opened_pane: {:?}, {}, {}",
        app.panes[pane_index as usize].dir_loaded, pane_index, app.last_opened_pane);
    app.initialize_dir_path(&path, pane_index as usize)
}

fn handle_save_settings(app: &mut DataViewer) -> Task<Message> {
    let parse_value = |key: &str, _default: u64| -> Result<u64, String> {
        app.settings.advanced_input
            .get(key)
            .ok_or_else(|| format!("Missing value for {}", key))?
            .parse::<u64>()
            .map_err(|_| format!("Invalid number for {}", key))
    };

    let cache_size = match parse_value("cache_size", 5) {
        Ok(v) if v > 0 && v <= 100 => v as usize,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Cache size must be between 1 and 100".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing cache_size: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let max_loading_queue_size = match parse_value("max_loading_queue_size", 3) {
        Ok(v) if v > 0 && v <= 50 => v as usize,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Max loading queue size must be between 1 and 50".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing max_loading_queue_size: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let max_being_loaded_queue_size = match parse_value("max_being_loaded_queue_size", 3) {
        Ok(v) if v > 0 && v <= 50 => v as usize,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Max being loaded queue size must be between 1 and 50".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing max_being_loaded_queue_size: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let atlas_size = match parse_value("atlas_size", 2048) {
        Ok(v) if (256..=8192).contains(&v) && v.is_power_of_two() => v as u32,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Atlas size must be a power of 2 between 256 and 8192".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing atlas_size: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let double_click_threshold_ms = match parse_value("double_click_threshold_ms", 250) {
        Ok(v) if (50..=1000).contains(&v) => v as u16,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Double-click threshold must be between 50 and 1000 ms".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing double_click_threshold_ms: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let archive_cache_size = match parse_value("archive_cache_size", 200) {
        Ok(v) if (10..=10000).contains(&v) => v,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Archive cache size must be between 10 and 10000 MB".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing archive_cache_size: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let archive_warning_threshold_mb = match parse_value("archive_warning_threshold_mb", 500) {
        Ok(v) if (10..=10000).contains(&v) => v,
        Ok(_) => {
            app.settings.set_save_status(Some("Error: Archive warning threshold must be between 10 and 10000 MB".to_string()));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
        Err(e) => {
            app.settings.set_save_status(Some(format!("Error parsing archive_warning_threshold_mb: {}", e)));
            return Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus);
        }
    };

    let settings = UserSettings {
        show_fps: app.show_fps,
        show_footer: app.show_footer,
        is_horizontal_split: app.is_horizontal_split,
        synced_zoom: app.synced_zoom,
        mouse_wheel_zoom: app.mouse_wheel_zoom,
        show_copy_buttons: app.show_copy_buttons,
        show_metadata: app.show_metadata,
        nearest_neighbor_filter: app.nearest_neighbor_filter,
        cache_strategy: match app.cache_strategy {
            CacheStrategy::Cpu => "cpu".to_string(),
            CacheStrategy::Gpu => "gpu".to_string(),
        },
        compression_strategy: match app.compression_strategy {
            CompressionStrategy::None => "none".to_string(),
            CompressionStrategy::Bc1 => "bc1".to_string(),
        },
        is_slider_dual: app.is_slider_dual,
        cache_size,
        max_loading_queue_size,
        max_being_loaded_queue_size,
        window_width: app.window_size.width,
        window_height: app.window_size.height,
        atlas_size,
        double_click_threshold_ms,
        archive_cache_size,
        archive_warning_threshold_mb,
        #[cfg(feature = "coco")]
        coco_disable_simplification: app.coco_disable_simplification,
        #[cfg(not(feature = "coco"))]
        coco_disable_simplification: false,
        #[cfg(feature = "coco")]
        coco_mask_render_mode: app.coco_mask_render_mode,
        #[cfg(not(feature = "coco"))]
        coco_mask_render_mode: crate::settings::CocoMaskRenderMode::default(),
        use_binary_size: app.use_binary_size,
        spinner_location: app.spinner_location,
        window_state: app.window_state,
        window_position_x: app.window_position.x,
        window_position_y: app.window_position.y,
    };

    let old_settings = UserSettings::load(None);
    let window_settings_changed = atlas_size != old_settings.atlas_size;

    match settings.save() {
        Ok(_) => {
            info!("Settings saved successfully");

            app.archive_cache_size = archive_cache_size * 1_048_576;
            app.archive_warning_threshold_mb = archive_warning_threshold_mb;
            info!("Archive settings applied immediately: cache_size={}MB, warning_threshold={}MB",
                archive_cache_size, archive_warning_threshold_mb);

            if cache_size != app.cache_size {
                info!("Cache size changed from {} to {}, reloading all panes", app.cache_size, cache_size);
                app.cache_size = cache_size;

                let pane_file_lengths: Vec<usize> = app.panes.iter()
                    .map(|p| p.img_cache.num_files)
                    .collect();

                let cache_size = app.cache_size;
                let archive_cache_size = app.archive_cache_size;
                let archive_warning_threshold_mb = app.archive_warning_threshold_mb;

                for (i, pane) in app.panes.iter_mut().enumerate() {
                    if let Some(dir_path) = &pane.directory_path.clone() {
                        if pane.dir_loaded {
                            let path = PathBuf::from(dir_path);

                            let _ = pane.initialize_dir_path(
                                &Arc::clone(&app.device),
                                &Arc::clone(&app.queue),
                                app.is_gpu_supported,
                                app.cache_strategy,
                                app.compression_strategy,
                                &app.pane_layout,
                                &pane_file_lengths,
                                i,
                                &path,
                                app.is_slider_dual,
                                &mut app.slider_value,
                                cache_size,
                                archive_cache_size,
                                archive_warning_threshold_mb,
                            );
                        }
                    }
                }
            }

            if max_loading_queue_size != app.max_loading_queue_size || max_being_loaded_queue_size != app.max_being_loaded_queue_size {
                info!("Queue size settings changed: max_loading_queue_size={}, max_being_loaded_queue_size={}", max_loading_queue_size, max_being_loaded_queue_size);
                app.max_loading_queue_size = max_loading_queue_size;
                app.max_being_loaded_queue_size = max_being_loaded_queue_size;

                for pane in app.panes.iter_mut() {
                    pane.max_loading_queue_size = max_loading_queue_size;
                    pane.max_being_loaded_queue_size = max_being_loaded_queue_size;
                }
            }

            if double_click_threshold_ms != app.double_click_threshold_ms {
                info!("Double-click threshold changed from {} to {} ms", app.double_click_threshold_ms, double_click_threshold_ms);
                app.double_click_threshold_ms = double_click_threshold_ms;
            }

            app.settings.set_save_status(Some(if window_settings_changed {
                "Settings saved! Window settings require restart, other changes applied immediately.".to_string()
            } else {
                "Settings saved! All changes applied immediately.".to_string()
            }));

            Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus)
        }
        Err(e) => {
            error!("Failed to save settings: {}", e);
            app.settings.set_save_status(Some(format!("Error: {}", e)));

            Task::perform(async {
                tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
            }, |_| Message::ClearSettingsStatus)
        }
    }
}

fn handle_save_window_state(app: &mut DataViewer) -> Task<Message> {
    let mut old_settings = UserSettings::load(None);
    let tuple = get_window_visible(app.last_windowed_position, app.window_size,
        app.last_monitor.clone());
    // Prevents the saved position from being outside of the monitor
    if !tuple.0 {
        app.last_windowed_position = tuple.1;
    }
    // Use last_windowed_position to avoid saving maximized position (0,0) on Windows
    old_settings.window_position_x = app.last_windowed_position.x;
    old_settings.window_position_y = app.last_windowed_position.y;
    if app.window_state == WindowState::Window {
        old_settings.window_width = app.window_size.width;
        old_settings.window_height = app.window_size.height;
    }
    old_settings.window_state = app.window_state;
    if let Err(e) = old_settings.save() {
        error!("Failed to save window state: {e}");
    }
    Task::none()
}

fn handle_reset_advanced_settings(app: &mut DataViewer) {
    use crate::config;

    app.show_fps = false;
    app.show_footer = true;
    app.is_horizontal_split = false;
    app.synced_zoom = true;
    app.mouse_wheel_zoom = false;
    app.cache_strategy = CacheStrategy::Gpu;
    app.compression_strategy = CompressionStrategy::None;
    app.is_slider_dual = false;

    app.settings.advanced_input.insert("cache_size".to_string(), config::DEFAULT_CACHE_SIZE.to_string());
    app.settings.advanced_input.insert("max_loading_queue_size".to_string(), config::DEFAULT_MAX_LOADING_QUEUE_SIZE.to_string());
    app.settings.advanced_input.insert("max_being_loaded_queue_size".to_string(), config::DEFAULT_MAX_BEING_LOADED_QUEUE_SIZE.to_string());
    app.settings.advanced_input.insert("window_width".to_string(), config::DEFAULT_WINDOW_WIDTH.to_string());
    app.settings.advanced_input.insert("window_height".to_string(), config::DEFAULT_WINDOW_HEIGHT.to_string());
    app.settings.advanced_input.insert("atlas_size".to_string(), config::DEFAULT_ATLAS_SIZE.to_string());
    app.settings.advanced_input.insert("double_click_threshold_ms".to_string(), config::DEFAULT_DOUBLE_CLICK_THRESHOLD_MS.to_string());
    app.settings.advanced_input.insert("archive_cache_size".to_string(), config::DEFAULT_ARCHIVE_CACHE_SIZE.to_string());
    app.settings.advanced_input.insert("archive_warning_threshold_mb".to_string(), config::DEFAULT_ARCHIVE_WARNING_THRESHOLD_MB.to_string());
}

fn handle_export_all_logs() {
    println!("DEBUG: ExportAllLogs message received");
    let app_name = "viewskater";
    if let Some(log_buffer) = crate::get_shared_log_buffer() {
        println!("DEBUG: Got log buffer, starting export...");
        if let Some(stdout_buffer) = crate::get_shared_stdout_buffer() {
            println!("DEBUG: Got stdout buffer, calling export_and_open_all_logs...");
            crate::logging::export_and_open_all_logs(app_name, log_buffer, stdout_buffer);
            println!("DEBUG: export_and_open_all_logs completed");
        } else {
            println!("DEBUG: Stdout buffer not available, exporting debug logs only");
            match crate::logging::export_debug_logs(app_name, log_buffer) {
                Ok(debug_log_path) => {
                    println!("DEBUG: Export successful to: {}", debug_log_path.display());
                    info!("Debug logs successfully exported to: {}", debug_log_path.display());
                }
                Err(e) => {
                    println!("DEBUG: Export failed: {}", e);
                    error!("Failed to export debug logs: {}", e);
                    eprintln!("Failed to export debug logs: {}", e);
                }
            }
        }
        println!("DEBUG: Export operation completed");
    } else {
        println!("DEBUG: Log buffer not available");
        warn!("Log buffer not available for export");
    }
    println!("DEBUG: ExportAllLogs handler finished");
}

pub fn handle_save_image(app: &mut DataViewer, message: Message) -> Task<Message> {
    let current_image = &app.panes.first().as_ref().unwrap().current_image;

    if current_image.len() == 0 {
        Task::none()
    } else {
        match message {
            Message::ReadySaveImage(result) => {
                match result {
                    Ok(path) => {
                        let format = path
                            .extension()
                            .and_then(image::ImageFormat::from_extension);

                        if let Some(format) = format {
                            let (width, height) = current_image.dimensions();

                            let save_result = match current_image {
                                CachedData::Cpu(items) => {
                                    match decode_with_exif_orientation(items) {
                                        Ok(image) => image.save_with_format(path, format),
                                        Err(e) => Err(std::io::Error::from(e).into()),
                                    }
                                },
                                CachedData::Gpu(texture) => {
                                    let texture = texture.clone();
                                    let buf=  extract_gpu_image(app, &texture);

                                    match format {
                                        image::ImageFormat::Jpeg => {
                                            let rgb: Vec<u8> = buf
                                                .chunks_exact(4)
                                                .flat_map(|p| [p[0], p[1], p[2]])
                                                .collect();
                                            image::save_buffer_with_format(&path, &rgb, width, height, image::ColorType::Rgb8, format)
                                            }
                                            _ => {
                                            image::save_buffer_with_format(&path, &buf, width, height, image::ColorType::Rgba8, format)
                                            }
                                     }
                                }
                                CachedData::BC1(_texture) => {
                                    app.set_failure_save_modal(Some("BC1 Saving is currently unsupported".into()));
                                    return Task::none()
                                },
                            };

                            match save_result {
                                Ok(_) => app.toggle_success_save_modal(),
                                Err(e) => app.set_failure_save_modal(Some(e.to_string())),
                            }

                            Task::none()
                        } else {
                            app.set_failure_save_modal(Some(
                                "Wrong file extension, cannot determine format".into(),
                            ));

                            Task::none()
                        }

                    }

                    Err(err) => {
                        if let file_io::Error::InvalidExtension = err {
                            app.set_failure_save_modal(Some("Error selecting save file - invalid extension".into()));
                        }

                        debug!("Save file select error: {:?}", err);
                        Task::none()
                    }
                }
            }

            Message::RequestSaveImage => Task::perform(file_io::pick_save_file(), move |result| {
                Message::ReadySaveImage(result)
            }),

            _ => Task::none(),
        }
    }
}


================================================
FILE: src/app/replay_handlers.rs
================================================
//! Replay mode integration for DataViewer
//!
//! Handles replay controller updates and action processing for automated benchmarking.

use std::time::Duration;
use iced_winit::runtime::Task;
use log::{debug, info, warn};

use super::{DataViewer, Message};

/// Reset all FPS counters and timing history for fresh measurements
fn reset_fps_trackers() {
    if let Ok(mut fps) = crate::CURRENT_FPS.lock() { *fps = 0.0; }
    if let Ok(mut fps) = crate::pane::IMAGE_RENDER_FPS.lock() { *fps = 0.0; }
    if let Ok(mut times) = crate::FRAME_TIMES.lock() { times.clear(); }
    if let Ok(mut times) = crate::pane::IMAGE_RENDER_TIMES.lock() { times.clear(); }
    iced_wgpu::reset_image_fps();
}

impl DataViewer {
    /// Update replay mode logic and return any action that should be processed
    pub(crate) fn update_replay_mode(&mut self) -> Option<crate::replay::ReplayAction> {
        let replay_controller = self.replay_controller.as_mut()?;

        if !replay_controller.is_active() && !replay_controller.is_completed()
           && !replay_controller.config.test_directories.is_empty() {
            // Start replay if we have a controller but it's not active yet and not completed
            replay_controller.start();
            // Get the first directory for loading
            return replay_controller.get_current_directory().map(|dir| {
                crate::replay::ReplayAction::LoadDirectory(dir.clone())
            });
        }

        if !replay_controller.is_active() {
            return None;
        }

        debug!("App update called during active replay mode, state: {:?}", replay_controller.state);

        // Update metrics with current FPS and memory values
        let ui_fps = crate::CURRENT_FPS.lock().map(|fps| *fps).unwrap_or(0.0);
        let image_fps = if self.is_slider_moving {
            iced_wgpu::get_image_fps() as f32
        } else {
            crate::pane::IMAGE_RENDER_FPS.lock().map(|fps| *fps).unwrap_or(0.0)
        };
        let memory_mb = crate::CURRENT_MEMORY_USAGE.lock()
            .map(|mem| if *mem == u64::MAX { -1.0 } else { *mem as f64 / 1024.0 / 1024.0 })
            .unwrap_or(0.0);

        replay_controller.update_metrics(ui_fps, image_fps, memory_mb);

        // Extract state info before mutable operations (to satisfy borrow checker)
        let (state_type, start_time_elapsed) = match &replay_controller.state {
            crate::replay::ReplayState::NavigatingRight { start_time, .. } => ("right", Some(start_time.elapsed())),
            crate::replay::ReplayState::NavigatingLeft { start_time, .. } => ("left", Some(start_time.elapsed())),
            _ => ("other", None),
        };
        let duration_limit = replay_controller.config.duration_per_directory + Duration::from_secs(1);

        // Synchronize app navigation state with replay controller state
        // In slider mode, boundary detection is handled by the replay controller (position tracking)
        // In keyboard mode, we sync skate flags with app state
        let is_slider_mode = replay_controller.config.navigation_mode == crate::replay::NavigationMode::Slider;

        // Keyboard mode only: sync skate flags with app state
        // Slider mode handles boundary detection via position tracking in replay controller
        if !is_slider_mode {
            match state_type {
                "right" => {
                    let at_end = self.panes.iter().any(|pane| {
                        pane.is_selected && pane.dir_loaded &&
                        pane.img_cache.current_index >= pane.img_cache.image_paths.len().saturating_sub(1)
                    });

                    replay_controller.set_at_boundary(at_end);

                    if at_end && self.skate_right {
                        debug!("Reached end of images, stopping right navigation");
                        self.skate_right = false;
                    } else if !at_end && !self.skate_right {
                        debug!("Syncing app state: setting skate_right = true");
                        self.skate_right = true;
                        self.skate_left = false;
                    }

                    if let Some(elapsed) = start_time_elapsed {
                        if elapsed > duration_limit {
                            warn!("Replay seems stuck in NavigatingRight state, forcing progress");
                            self.skate_right = false;
                        }
                    }
                }
                "left" => {
                    let at_beginning = self.panes.iter().any(|pane| {
                        pane.is_selected && pane.dir_loaded && pane.img_cache.current_index == 0
                    });

                    replay_controller.set_at_boundary(at_beginning);

                    if at_beginning && self.skate_left {
                        debug!("Reached beginning of images, stopping left navigation");
                        self.skate_left = false;
                    } else if !at_beginning && !self.skate_left {
                        debug!("Syncing app state: setting skate_left = true");
                        self.skate_left = true;
                        self.skate_right = false;
                    }

                    if let Some(elapsed) = start_time_elapsed {
                        if elapsed > duration_limit {
                            warn!("Replay seems stuck in NavigatingLeft state, forcing progress");
                            self.skate_left = false;
                        }
                    }
                }
                _ => {
                    if self.skate_right || self.skate_left {
                        debug!("Syncing app state: clearing navigation flags");
                        self.skate_right = false;
                        self.skate_left = false;
                    }
                }
            }
        }

        // Get action from replay controller
        let action = replay_controller.update();
        if let Some(ref a) = action {
            debug!("Replay controller returned action: {:?}", a);
        }

        // Schedule keep-alive task if replay is active and we don't already have one in flight
        // This prevents accumulating many delayed messages when update() is called rapidly
        // Use navigation_interval to ensure we poll fast enough for the desired speed
        if replay_controller.is_active() && !self.replay_keep_alive_pending {
            let interval_ms = replay_controller.config.navigation_interval.as_millis() as u64;
            self.replay_keep_alive_task = Some(Task::perform(
                async move { tokio::time::sleep(tokio::time::Duration::from_millis(interval_ms)).await; },
                |_| Message::ReplayKeepAlive
            ));
        }

        action
    }

    /// Process a replay action and return the appropriate task
    pub(crate) fn process_replay_action(&mut self, action: crate::replay::ReplayAction) -> Option<Task<Message>> {
        match action {
            crate::replay::ReplayAction::LoadDirectory(path) => {
                info!("Loading directory for replay: {}", path.display());
                self.reset_state(-1);
                reset_fps_trackers();

                // Initialize directory and get the image loading task
                let load_task = self.initialize_dir_path(&path, 0);

                // Notify replay controller that directory loading started
                // on_ready_to_navigate() will be called when ImagesLoaded (LoadPos) completes
                if let Some(ref mut replay_controller) = self.replay_controller {
                    if let Some(directory_index) = replay_controller.config.test_directories.iter().position(|p| p == &path) {
                        replay_controller.on_directory_loaded(directory_index);
                    }
                }

                // Return the load task so images actually get loaded
                Some(load_task)
            }
            crate::replay::ReplayAction::RestartIteration(path) => {
                // Restart iteration by fully reloading the first directory
                // This ensures pane state is properly reset to the beginning
                info!("Restarting iteration - loading directory: {}", path.display());
                self.reset_state(-1);
                reset_fps_trackers();

                // Initialize directory and get the image loading task
                let load_task = self.initialize_dir_path(&path, 0);

                // Notify replay controller that directory loading started
                if let Some(ref mut replay_controller) = self.replay_controller {
                    if let Some(directory_index) = replay_controller.config.test_directories.iter().position(|p| p == &path) {
                        replay_controller.on_directory_loaded(directory_index);
                    }
                }

                Some(load_task)
            }
            crate::replay::ReplayAction::NavigateRight => {
                self.skate_right = true;
                if let Some(ref mut replay_controller) = self.replay_controller {
                    replay_controller.on_navigation_performed();
                }
                None
            }
            crate::replay::ReplayAction::NavigateLeft => {
                self.skate_left = true;
                if let Some(ref mut replay_controller) = self.replay_controller {
                    replay_controller.on_navigation_performed();
                }
                None
            }
            crate::replay::ReplayAction::StartNavigatingLeft => {
                reset_fps_trackers();
                self.skate_right = false;
                self.skate_left = true;
                if let Some(ref mut replay_controller) = self.replay_controller {
                    replay_controller.on_navigation_performed();
                }
                None
            }
            crate::replay::ReplayAction::SliderNavigate { position } => {
                // Slider mode navigation: send SliderChanged message to simulate slider drag
                debug!("Slider navigate to position {}", position);
                // Use pane index -1 to affect the selected pane (same as global slider)
                Some(Task::done(Message::SliderChanged(-1, position)))
            }
            crate::replay::ReplayAction::SliderStartNavigatingLeft => {
                reset_fps_trackers();
                // Slider mode doesn't use skate flags - position tracking is in replay controller
                None
            }
            crate::replay::ReplayAction::Finish => {
                info!("Replay mode finished");
                if let Some(ref controller) = self.replay_controller {
                    if controller.config.auto_exit {
                        info!("Auto-exit enabled, exiting application");
                        std::process::exit(0);
                    }
                }
                None
            }
        }
    }
}


================================================
FILE: src/app/settings_widget.rs
================================================
//! Settings widget module
//! Manages all settings-related state and UI for the application

use std::collections::HashMap;
use crate::settings::UserSettings;

/// Runtime-configurable settings that can be applied immediately without restart
#[derive(Debug, Clone)]
pub struct RuntimeSettings {
    pub mouse_wheel_zoom: bool,                         // Flag to change mouse scroll wheel behavior
    pub show_copy_buttons: bool,                        // Show copy filename/filepath buttons in footer
    pub show_metadata: bool,                            // Show image metadata (resolution, file size) in footer
    pub cache_size: usize,                              // Image cache window size (number of images to cache)
    pub archive_cache_size: u64,                        // Archive cache size in bytes (for preload decision)
    pub archive_warning_threshold_mb: u64,              // Warning threshold for large solid archives (MB)
    pub max_loading_queue_size: usize,                  // Max size for loading queue
    pub max_being_loaded_queue_size: usize,             // Max size for being loaded queue
    pub double_click_threshold_ms: u16,                 // Double-click threshold in milliseconds
}

impl RuntimeSettings {
    pub fn from_user_settings(settings: &UserSettings) -> Self {
        Self {
            mouse_wheel_zoom: settings.mouse_wheel_zoom,
            show_copy_buttons: settings.show_copy_buttons,
            show_metadata: settings.show_metadata,
            cache_size: settings.cache_size,
            archive_cache_size: settings.archive_cache_size * 1_048_576,  // Convert MB to bytes
            archive_warning_threshold_mb: settings.archive_warning_threshold_mb,
            max_loading_queue_size: settings.max_loading_queue_size,
            max_being_loaded_queue_size: settings.max_being_loaded_queue_size,
            double_click_threshold_ms: settings.double_click_threshold_ms,
        }
    }
}

/// Settings widget state
pub struct SettingsWidget {
    pub show_options: bool,                             // Settings modal visibility
    pub save_status: Option<String>,                    // Save feedback message
    pub active_tab: usize,                              // Which tab is selected
    pub advanced_input: HashMap<String, String>,        // Text input state for advanced settings
    pub runtime_settings: RuntimeSettings,              // Runtime-configurable settings
}

impl SettingsWidget {
    pub fn new(settings: &UserSettings) -> Self {
        // Initialize advanced settings input with current values
        let mut advanced_input = HashMap::new();
        advanced_input.insert("cache_size".to_string(), settings.cache_size.to_string());
        advanced_input.insert("max_loading_queue_size".to_string(), settings.max_loading_queue_size.to_string());
        advanced_input.insert("max_being_loaded_queue_size".to_string(), settings.max_being_loaded_queue_size.to_string());
        advanced_input.insert("window_width".to_string(), settings.window_width.to_string());
        advanced_input.insert("window_height".to_string(), settings.window_height.to_string());
        advanced_input.insert("atlas_size".to_string(), settings.atlas_size.to_string());
        advanced_input.insert("double_click_threshold_ms".to_string(), settings.double_click_threshold_ms.to_string());
        advanced_input.insert("archive_cache_size".to_string(), settings.archive_cache_size.to_string());
        advanced_input.insert("archive_warning_threshold_mb".to_string(), settings.archive_warning_threshold_mb.to_string());

        Self {
            show_options: false,
            save_status: None,
            active_tab: 0,
            advanced_input,
            runtime_settings: RuntimeSettings::from_user_settings(settings),
        }
    }

    pub fn show(&mut self) {
        self.show_options = true;
    }

    pub fn hide(&mut self) {
        self.show_options = false;
    }

    pub fn set_save_status(&mut self, status: Option<String>) {
        self.save_status = status;
    }

    pub fn clear_save_status(&mut self) {
        self.save_status = None;
    }

    pub fn set_active_tab(&mut self, tab: usize) {
        self.active_tab = tab;
    }

    pub fn set_advanced_input(&mut self, key: String, value: String) {
        self.advanced_input.insert(key, value);
    }

    pub fn is_visible(&self) -> bool {
        self.show_options
    }
}


================================================
FILE: src/app.rs
================================================
// Submodules
mod message;
mod message_handlers;
mod keyboard_handlers;
mod replay_handlers;
mod settings_widget;

use iced_core::Length;
use iced_core::alignment::Horizontal;
// Re-exports
pub use message::{Message, DirectoryEnumResult, DirectoryEnumError};
pub use settings_widget::{RuntimeSettings, SettingsWidget};

#[warn(unused_imports)]
#[cfg(target_os = "linux")]
mod other_os {
    pub use iced_custom as iced;
}

#[cfg(not(target_os = "linux"))]
mod macos {
    pub use iced_custom as iced;
}

use iced_winit::winit::dpi::{ PhysicalPosition, PhysicalSize };
#[cfg(target_os = "linux")]
use other_os::*;

#[cfg(not(target_os = "linux"))]
use macos::*;

use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use once_cell::sync::Lazy;

#[allow(unused_imports)]
use std::time::{Duration, Instant};

#[allow(unused_imports)]
use log::{Level, debug, info, warn, error};

use iced::{
    widget::button,
    font::Font,
    Task,
};
use iced_widget::{row, column, container, text};
use iced_wgpu::{wgpu, Renderer};
use iced_wgpu::engine::CompressionStrategy;
use iced_winit::core::Theme as WinitTheme;
use iced_winit::core::{Color, Element};


use crate::navigation_keyboard::{move_right_all, move_left_all};
use crate::cache::img_cache::CacheStrategy;
use crate::menu::PaneLayout;
use crate::pane::{self, Pane};
use crate::settings::WindowState;
use crate::ui;
use crate::widgets;
use crate::loading_status;
use crate::utils::timing::TimingStats;
use crate::RendererRequest;
use crate::build_info::BuildInfo;
#[cfg(feature = "selection")]
use crate::selection_manager::SelectionManager;
use crate::settings::UserSettings;
use crate::widgets::modal;

use std::sync::mpsc::{Sender, Receiver};

#[allow(dead_code)]
static APP_UPDATE_STATS: Lazy<Mutex<TimingStats>> = Lazy::new(|| {
    Mutex::new(TimingStats::new("App Update"))
});

pub struct DataViewer {
    pub background_color: Color,//debug
    pub title: String,
    pub directory_path: Option<String>,
    pub current_image_index: usize,
    pub slider_value: u16,                              // for master slider
    pub prev_slider_value: u16,                         // for master slider
    pub divider_position: Option<u16>,
    pub is_slider_dual: bool,
    pub show_footer: bool,
    pub pane_layout: PaneLayout,
    pub last_opened_pane: isize,
    pub panes: Vec<pane::Pane>,                         // Each pane has its own image cache
    pub loading_status: loading_status::LoadingStatus,  // global loading status for all panes
    pub skate_right: bool,
    pub skate_left: bool,
    pub update_counter: u32,
    pub show_about: bool,
    pub settings: SettingsWidget,                       // Settings widget (modal, tabs, runtime settings)
    pub device: Arc<wgpu::Device>,                     // Shared ownership using Arc
    pub queue: Arc<wgpu::Queue>,                       // Shared ownership using Arc
    pub is_gpu_supported: bool,
    pub cache_strategy: CacheStrategy,
    pub last_slider_update: Instant,
    pub is_slider_moving: bool,
    pub use_slider_image_for_render: bool,             // Keep using Viewer widget after slider release until keyboard nav
    pub backend: wgpu::Backend,
    pub show_fps: bool,
    pub compression_strategy: CompressionStrategy,
    pub renderer_request_sender: Sender<RendererRequest>,
    pub is_horizontal_split: bool,
    pub file_receiver: Receiver<String>,
    pub synced_zoom: bool,
    pub nearest_neighbor_filter: bool,
    pub replay_controller: Option<crate::replay::ReplayController>,
    pub replay_keep_alive_task: Option<Task<Message>>,
    pub replay_keep_alive_pending: bool,  // Track if a keep-alive is in flight to prevent flooding
    pub window_state: WindowState,
    pub cursor_on_top: bool,
    pub cursor_on_menu: bool,                           // Flag to show menu when fullscreen
    pub cursor_on_footer: bool,                         // Flag to show footer when fullscreen
    pub(crate) ctrl_pressed: bool,                                 // Flag to save ctrl/cmd(macOS) press state
    pub use_binary_size: bool,                          // Use binary (KiB/MiB) vs decimal (KB/MB) for file sizes
    pub spinner_location: crate::settings::SpinnerLocation,  // Where to show loading spinner
    pub window_width: f32,                              // Current window width for responsive layout
    #[cfg(feature = "selection")]
    pub selection_manager: SelectionManager,            // Manages image selections/exclusions
    #[cfg(feature = "coco")]
    pub annotation_manager: crate::coco::annotation_manager::AnnotationManager,  // Manages COCO annotations
    #[cfg(feature = "coco")]
    pub coco_disable_simplification: bool,              // COCO: Disable polygon simplification for RLE masks
    #[cfg(feature = "coco")]
    pub coco_mask_render_mode: crate::settings::CocoMaskRenderMode,  // COCO: Mask rendering mode (Polygon or Pixel)
    pub window_size: PhysicalSize<u32>,
    pub maximized_size: Option<PhysicalSize<u32>>,  // Tracks size when maximized (for X11 un-maximize detection)
    pub window_position: PhysicalPosition<i32>,
    pub last_windowed_position: PhysicalPosition<i32>,  // Tracks position when in windowed mode
    pub position_before_transition: PhysicalPosition<i32>,  // Backup for Windows maximize fix
    pub last_monitor: Option<iced_winit::winit::monitor::MonitorHandle>, // Track position when not in windowed mode with multiple monitors
    pub show_success_save_modal: bool,
    pub show_failure_save_modal: Option<String>,
}

// Implement Deref to expose RuntimeSettings fields directly on DataViewer
impl std::ops::Deref for DataViewer {
    type Target = RuntimeSettings;

    fn deref(&self) -> &Self::Target {
        &self.settings.runtime_settings
    }
}

// Implement DerefMut to allow mutable access to RuntimeSettings fields
impl std::ops::DerefMut for DataViewer {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.settings.runtime_settings
    }
}

impl DataViewer {
    pub fn new(
        device: Arc<wgpu::Device>,
        queue: Arc<wgpu::Queue>,
        backend: wgpu::Backend,
        renderer_request_sender: Sender<RendererRequest>,
        file_receiver: Receiver<String>,
        settings_path: Option<&str>,
        replay_config: Option<crate::replay::ReplayConfig>,
    ) -> Self {
        // Load user settings from YAML file
        let settings = UserSettings::load(settings_path);
        let cache_strategy = settings.get_cache_strategy();
        let compression_strategy = settings.get_compression_strategy();

        info!("Initializing DataViewer with settings:");
        info!("  show_fps: {}", settings.show_fps);
        info!("  show_footer: {}", settings.show_footer);
        info!("  is_horizontal_split: {}", settings.is_horizontal_split);
        info!("  synced_zoom: {}", settings.synced_zoom);
        info!("  mouse_wheel_zoom: {}", settings.mouse_wheel_zoom);
        info!("  show_copy_buttons: {}", settings.show_copy_buttons);
        info!("  nearest_neighbor_filter: {}", settings.nearest_neighbor_filter);
        info!("  cache_strategy: {:?}", cache_strategy);
        info!("  compression_strategy: {:?}", compression_strategy);
        info!("  is_slider_dual: {}", settings.is_slider_dual);

        Self {
            title: String::from("ViewSkater"),
            directory_path: None,
            current_image_index: 0,
            slider_value: 0,
            prev_slider_value: 0,
            divider_position: None,
            is_slider_dual: settings.is_slider_dual,
            show_footer: settings.show_footer,
            pane_layout: PaneLayout::SinglePane,
            last_opened_pane: -1,
            panes: vec![pane::Pane::new(Arc::clone(&device), Arc::clone(&queue), backend, 0, compression_strategy)],
            loading_status: loading_status::LoadingStatus::default(),
            skate_right: false,
            skate_left: false,
            update_counter: 0,
            show_about: false,
            settings: SettingsWidget::new(&settings),
            device,
            queue,
            is_gpu_supported: true,
            background_color: Color::WHITE,
            last_slider_update: Instant::now(),
            is_slider_moving: false,
            use_slider_image_for_render: false,
            backend,
            cache_strategy,
            show_fps: settings.show_fps,
            compression_strategy,
            renderer_request_sender,
            is_horizontal_split: settings.is_horizontal_split,
            file_receiver,
            synced_zoom: settings.synced_zoom,
            nearest_neighbor_filter: settings.nearest_neighbor_filter,
            replay_controller: replay_config.map(crate::replay::ReplayController::new),
            replay_keep_alive_task: None,
            replay_keep_alive_pending: false,
            window_state: crate::config::CONFIG.window_state,
            cursor_on_top: false,
            cursor_on_menu: false,
            cursor_on_footer: false,
            ctrl_pressed: false,
            use_binary_size: settings.use_binary_size,
            spinner_location: settings.spinner_location,
            window_width: settings.window_width as f32,
            #[cfg(feature = "selection")]
            selection_manager: SelectionManager::new(),
            #[cfg(feature = "coco")]
            annotation_manager: crate::coco::annotation_manager::AnnotationManager::new(),
            #[cfg(feature = "coco")]
            coco_disable_simplification: settings.coco_disable_simplification,
            #[cfg(feature = "coco")]
            coco_mask_render_mode: settings.coco_mask_render_mode,
            window_position: PhysicalPosition { x: crate::config::CONFIG.window_position_x, y: crate::config::CONFIG.window_position_y },
            last_windowed_position: PhysicalPosition { x: crate::config::CONFIG.window_position_x, y: crate::config::CONFIG.window_position_y },
            position_before_transition: PhysicalPosition { x: crate::config::CONFIG.window_position_x, y: crate::config::CONFIG.window_position_y },
            window_size: PhysicalSize { width: settings.window_width,
                height: settings.window_height },
            maximized_size: None,
            last_monitor: None,
            show_success_save_modal: false,
            show_failure_save_modal: None,
        }
    }

    pub fn clear_primitive_storage(&self) {
        if let Err(e) = self.renderer_request_sender.send(RendererRequest::ClearPrimitiveStorage) {
            error!("Failed to send ClearPrimitiveStorage request: {:?}", e);
        }
    }

    /// Ensure panes vector has at least `pane_index + 1` panes, creating new ones as needed
    fn ensure_pane_exists(&mut self, pane_index: usize) {
        while self.panes.len() <= pane_index {
            let new_pane_id = self.panes.len();
            debug!("Creating new pane at index {}", new_pane_id);
            self.panes.push(pane::Pane::new(
                Arc::clone(&self.device),
                Arc::clone(&self.queue),
                self.backend,
                new_pane_id,
                self.compression_strategy
            ));
        }
    }

    /// Clear cached slider images from all panes to prevent displaying stale images
    fn clear_slider_images(&mut self) {
        for pane in self.panes.iter_mut() {
            pane.slider_image = None;
            pane.slider_image_position = None;
            pane.slider_scene = None;
        }
    }

    /// Start loading neighbor images after directory initialization completes
    /// Sets last_opened_pane, loads selection state, and kicks off async neighbor loading
    fn start_neighbor_loading(&mut self, pane_index: usize) -> Task<Message> {
        self.last_opened_pane = pane_index as isize;

        #[cfg(feature = "selection")]
        if let Some(dir_path) = &self.panes[pane_index].directory_path {
            if let Err(e) = self.selection_manager.load_for_directory(dir_path) {
                warn!("Failed to load selection state for {}: {}", dir_path, e);
            }
        }

        // Set loading timer for spinner display during neighbor loading
        // The first image is already displayed, now we load the rest in background
        if let Some(pane) = self.panes.get_mut(pane_index) {
            pane.loading_started_at = Some(std::time::Instant::now());
            debug!("SPINNER: Set loading_started_at for neighbor loading (pane {})", pane_index);
        }

        let current_index = self.panes[pane_index].img_cache.current_index;
        let load_task = crate::navigation_slider::load_initial_neighbors(
            &self.device,
            &self.queue,
            self.is_gpu_supported,
            self.cache_strategy,
            self.compression_strategy,
            &mut self.panes,
            &mut self.loading_status,
            pane_index,
            current_index,
        );

        load_task
    }

    pub fn reset_state(&mut self, pane_index: isize) {
        // Reset loading status
        self.loading_status = loading_status::LoadingStatus::default();

        if pane_index == -1 {
            // Reset all panes
            for pane in &mut self.panes {
                pane.reset_state();
            }

            // Reset app-level viewer state only when resetting all panes
            self.title = String::from("ViewSkater");
            self.directory_path = None;
            self.current_image_index = 0;
            self.slider_value = 0;
            self.prev_slider_value = 0;
            self.last_opened_pane = 0;

            self.skate_right = false;
            self.update_counter = 0;
            self.show_about = false;
            self.last_slider_update = Instant::now();
            self.is_slider_moving = false;
            self.use_slider_image_for_render = false;

            // Clear primitive storage
            self.clear_primitive_storage();
        } else {
            // Reset only the specified pane
            self.panes[pane_index as usize].reset_state();
        }

        crate::utils::mem::log_memory("DataViewer::reset_state: After reset_state");
    }

    pub(crate) fn initialize_dir_path(&mut self, path: &PathBuf, pane_index: usize) -> Task<Message> {
        debug!("last_opened_pane: {}", self.last_opened_pane);

        // Check if this is a compressed file - use sync path for archives
        if path.extension().is_some_and(|ex| {
            crate::file_io::ALLOWED_COMPRESSED_FILES.contains(&ex.to_ascii_lowercase().to_str().unwrap_or(""))
        }) {
            return self.initialize_dir_path_sync(path, pane_index);
        }

        self.ensure_pane_exists(pane_index);
        self.reset_state(pane_index as isize);
        self.panes[pane_index].slider_image = None;
        self.panes[pane_index].slider_image_position = None;
        self.panes[pane_index].slider_scene = None;

        // Dispatch async directory enumeration (Issue #73 - NFS performance fix)
        // Note: Loading spinner will be shown during neighbor loading phase (after first image displays)
        let path_clone = path.clone();
        Task::perform(
            crate::file_io::enumerate_directory_async(path_clone),
            move |result| Message::DirectoryEnumerated(result, pane_index)
        )
    }

    /// Sync initialization path for compressed files (zip/rar/7z)
    /// Archives are typically local so sync loading is acceptable
    fn initialize_dir_path_sync(&mut self, path: &PathBuf, pane_index: usize) -> Task<Message> {
        debug!("Sync initialization for compressed file: {}", path.display());

        self.ensure_pane_exists(pane_index);
        self.clear_slider_images();

        let pane_file_lengths = self.panes.iter().map(
            |pane| pane.img_cache.image_paths.len()).collect::<Vec<usize>>();

        let cache_size = self.cache_size;
        let archive_cache_size = self.archive_cache_size;
        let archive_warning_threshold_mb = self.archive_warning_threshold_mb;

        let pane = &mut self.panes[pane_index];
        debug!("pane_file_lengths: {:?}", pane_file_lengths);

        // Load first image synchronously (archives are local, so this is fast)
        let _ = pane.initialize_dir_path(
            &Arc::clone(&self.device),
            &Arc::clone(&self.queue),
            self.is_gpu_supported,
            self.cache_strategy,
            self.compression_strategy,
            &self.pane_layout,
            &pane_file_lengths,
            pane_index,
            path,
            self.is_slider_dual,
            &mut self.slider_value,
            cache_size,
            archive_cache_size,
            archive_warning_threshold_mb,
        );

        // start_neighbor_loading will set loading timer for neighbor loading phase
        self.start_neighbor_loading(pane_index)
    }

    /// Complete directory initialization after async enumeration
    /// Called when DirectoryEnumerated message arrives
    pub(crate) fn complete_dir_initialization(
        &mut self,
        result: crate::app::message::DirectoryEnumResult,
        pane_index: usize,
    ) -> Task<Message> {
        debug!("Completing directory initialization: {} images found", result.file_paths.len());

        let pane_file_lengths = self.panes.iter().map(
            |pane| pane.img_cache.image_paths.len()).collect::<Vec<usize>>();

        let cache_size = self.cache_size;

        let pane = &mut self.panes[pane_index];

        // Initialize pane with pre-enumerated paths (loads first image synchronously)
        pane.initialize_with_paths(
            &Arc::clone(&self.device),
            &Arc::clone(&self.queue),
            self.is_gpu_supported,
            self.cache_strategy,
            self.compression_strategy,
            &self.pane_layout,
            &pane_file_lengths,
            pane_index,
            result.file_paths,
            result.directory_path,
            result.initial_index,
            self.is_slider_dual,
            &mut self.slider_value,
            cache_size,
        );

        // start_neighbor_loading will set loading timer for neighbor loading phase
        debug!("SPINNER: complete_dir_initialization calling start_neighbor_loading for pane {}", pane_index);
        self.start_neighbor_loading(pane_index)
    }

    fn set_ctrl_pressed(&mut self, enabled: bool) {
        self.ctrl_pressed = enabled;
        for pane in self.panes.iter_mut() {
            pane.ctrl_pressed = enabled;
        }
    }

    pub(crate) fn toggle_success_save_modal(&mut self) {
        self.show_success_save_modal = !self.show_success_save_modal;
    }

    pub(crate) fn set_failure_save_modal(&mut self, error_message: Option<String>) {
        self.show_failure_save_modal = error_message;
    }

    fn save_result_modal(
        title: &str,
        detail: Option<String>,
        on_dismiss: Message,
    ) -> container::Container<'_, Message, WinitTheme, Renderer> {
        let mut col = column![
            text(title.to_owned()).size(25).font(Font {
                family: iced_winit::core::font::Family::Name("Roboto"),
                weight: iced_winit::core::font::Weight::Bold,
                stretch: iced_winit::core::font::Stretch::Normal,
                style: iced_winit::core::font::Style::Normal,
            }),
        ].spacing(15).align_x(Horizontal::Center).width(Length::Fill);

        if let Some(detail) = detail {
            col = col.push(
                text(detail)
                    .size(12)
                    .style(|theme: &WinitTheme| {
                        iced_widget::text::Style {
                            color: Some(theme.extended_palette().background.weak.color),
                        }
                    }),
            );
        }

        col = col.push(button(text("OK")).on_press(on_dismiss));

        container(col)
            .width(300)
            .padding(20)
            .style(|theme: &WinitTheme| iced_widget::container::Style {
                background: Some(theme.extended_palette().background.base.color.into()),
                text_color: Some(theme.extended_palette().primary.weak.text),
                border: iced_winit::core::Border {
                    color: theme.extended_palette().background.strong.color,
                    width: 1.0,
                    radius: iced_winit::core::border::Radius::from(8.0),
                },
                ..Default::default()
            })
    }

    pub(crate) fn toggle_slider_type(&mut self) {
        // When toggling from dual to single, reset pane.is_selected to true
        if self.is_slider_dual {
            for pane in self.panes.iter_mut() {
                pane.is_selected_cache = pane.is_selected;
                pane.is_selected = true;
                pane.is_next_image_loaded = false;
                pane.is_prev_image_loaded = false;
            }

            let panes_refs: Vec<&mut pane::Pane> = self.panes.iter_mut().collect();
            self.slider_value = pane::get_master_slider_value(&panes_refs, &self.pane_layout, self.is_slider_dual, self.last_opened_pane as usize) as u16;
        } else {
            // Single to dual slider: give slider.value to each slider
            for pane in self.panes.iter_mut() {
                pane.slider_value = pane.img_cache.current_index as u16;
                pane.is_selected = pane.is_selected_cache;
            }
        }
        self.is_slider_dual = !self.is_slider_dual;
    }

    pub(crate) fn toggle_pane_layout(&mut self, pane_layout: PaneLayout) {
        match pane_layout {
            PaneLayout::SinglePane => {
                Pane::resize_panes(&mut self.panes, 1);

                debug!("self.panes.len(): {}", self.panes.len());

                if self.pane_layout == PaneLayout::DualPane {
                    // Reset the slider value to the first pane's current index
                    let panes_refs: Vec<&mut pane::Pane> = self.panes.iter_mut().collect();
                    self.slider_value = pane::get_master_slider_value(&panes_refs, &pane_layout, self.is_slider_dual, self.last_opened_pane as usize) as u16;
                    self.panes[0].is_selected = true;
                }
            }
            PaneLayout::DualPane => {
                Pane::resize_panes(&mut self.panes, 2);
                debug!("self.panes.len(): {}", self.panes.len());
            }
        }
        self.pane_layout = pane_layout;
    }

    pub(crate) fn toggle_footer(&mut self) {
        self.show_footer = !self.show_footer;
    }

    pub fn title(&self) -> String {
        match self.pane_layout  {
            PaneLayout::SinglePane => {
                if self.panes[0].dir_loaded {
                    let path = &self.panes[0].img_cache.image_paths[self.panes[0].img_cache.current_index];
                    path.file_name().to_string()
                } else {
                    self.title.clone()
                }
            }
            PaneLayout::DualPane => {
                // Select labels based on split orientation
                let (first_label, second_label) = if self.is_horizontal_split {
                    ("Top", "Bottom")
                } else {
                    ("Left", "Right")
                };

                let first_pane_filename = if self.panes[0].dir_loaded {
                    let path = &self.panes[0].img_cache.image_paths[self.panes[0].img_cache.current_index];
                    path.file_name().to_string()
                } else {
                    String::from("No File")
                };

                let second_pane_filename = if self.panes[1].dir_loaded {
                    let path = &self.panes[1].img_cache.image_paths[self.panes[1].img_cache.current_index];
                    path.file_name().to_string()
                } else {
                    String::from("No File")
                };

                format!("{}: {} | {}: {}", first_label, first_pane_filename, second_label, second_pane_filename)
            }
        }
    }

    /// Returns true if any pane has active loading (for animation loop)
    /// This returns true as soon as loading starts, to keep the redraw loop active
    pub fn is_any_pane_loading(&self) -> bool {
        self.panes.iter().any(|pane| pane.loading_started_at.is_some())
    }

    pub(crate) fn update_cache_strategy(&mut self, strategy: CacheStrategy) {
        debug!("Changing cache strategy from {:?} to {:?}", self.cache_strategy, strategy);
        self.cache_strategy = strategy;

        // Get current pane file lengths
        let pane_file_lengths: Vec<usize> = self.panes.iter()
            .map(|p| p.img_cache.num_files)
            .collect();

        // Capture runtime settings before mutable borrow
        let cache_size = self.cache_size;
        let archive_cache_size = self.archive_cache_size;
        let archive_warning_threshold_mb = self.archive_warning_threshold_mb;

        // Reinitialize all loaded panes with the new cache strategy
        for (i, pane) in self.panes.iter_mut().enumerate() {
            if let Some(dir_path) = &pane.directory_path.clone() {
                if pane.dir_loaded {
                    let path = PathBuf::from(dir_path);

                    // Reinitialize the pane with the current directory
                    let _ = pane.initialize_dir_path(
                        &Arc::clone(&self.device),
                        &Arc::clone(&self.queue),
                        self.is_gpu_supported,
                        self.cache_strategy,
                        self.compression_strategy,
                        &self.pane_layout,
                        &pane_file_lengths,
                        i,
                        &path,
                        self.is_slider_dual,
                        &mut self.slider_value,
                        cache_size,
                        archive_cache_size,
                        archive_warning_threshold_mb,
                    );
                }
            }
        }
    }

    pub(crate) fn update_compression_strategy(&mut self, strategy: CompressionStrategy) {
        if self.compression_strategy != strategy {
            self.compression_strategy = strategy;

            debug!("Queuing compression strategy change to {:?}", strategy);

            // Instead of trying to lock renderer directly, send a request to the main thread
            if let Err(e) = self.renderer_request_sender.send(
                RendererRequest::UpdateCompressionStrategy(strategy)
            ) {
                error!("Failed to queue compression strategy change: {:?}", e);
            } else {
                debug!("Compression strategy change request sent successfully");

                // Get current pane file lengths
                let pane_file_lengths: Vec<usize> = self.panes.iter()
                .map(|p| p.img_cache.num_files)
                .collect();

                // Capture runtime settings before mutable borrow
                let cache_size = self.cache_size;
                let archive_cache_size = self.archive_cache_size;
                let archive_warning_threshold_mb = self.archive_warning_threshold_mb;

                // Recreate image cache
                for (i, pane) in self.panes.iter_mut().enumerate() {
                    if let Some(dir_path) = &pane.directory_path.clone() {
                        if pane.dir_loaded {
                            let path = PathBuf::from(dir_path);

                            // Reinitialize the pane with the current directory
                            let _ = pane.initialize_dir_path(
                                &Arc::clone(&self.device),
                                &Arc::clone(&self.queue),
                                self.is_gpu_supported,
                                self.cache_strategy,
                                self.compression_strategy,
                                &self.pane_layout,
                                &pane_file_lengths,
                                i,
                                &path,
                                self.is_slider_dual,
                                &mut self.slider_value,
                                cache_size,
                                archive_cache_size,
                                archive_warning_threshold_mb,
                            );
                        }
                    }
                }
            }
        }
    }

    pub(crate) fn toggle_split_orientation(&mut self) {
        self.is_horizontal_split = !self.is_horizontal_split;
    }
}


impl iced_winit::runtime::Program for DataViewer {
    type Theme = WinitTheme;
    type Message = Message;
    type Renderer = Renderer;

    fn update(&mut self, message: Message) -> iced_winit::runtime::Task<Message> {
        // Check for any file paths received from the background thread
        let mut cli_tasks: Vec<Task<Message>> = Vec::new();
        while let Ok(path) = self.file_receiver.try_recv() {
            println!("Processing file path in main thread: {}", path);
            // Reset state and initialize the directory path
            self.reset_state(-1);
            println!("State reset complete, initializing directory path");
            let init_task = self.initialize_dir_path(&PathBuf::from(path), 0);
            cli_tasks.push(init_task);
            println!("Directory path initialization task queued");
        }

        let _update_start = Instant::now();

        // Route message to handler
        let task = message_handlers::handle_message(self, message);

        // Handle replay mode logic
        if let Some(replay_action) = self.update_replay_mode() {
            if let Some(replay_task) = self.process_replay_action(replay_action) {
                return replay_task;
            }
        }

        // Check if we have a keep-alive task to return (for replay mode timing)
        let keep_alive_task = self.replay_keep_alive_task.take();

        // Return the task if it's not skate mode
        // Skate mode overrides normal task handling for continuous navigation
        if self.skate_right {
            self.update_counter = 0;
            let nav_task = move_right_all(
                &self.device,
                &self.queue,
                self.cache_strategy,
                self.compression_strategy,
                &mut self.panes,
                &mut self.loading_status,
                &mut self.slider_value,
                &self.pane_layout,
                self.is_slider_dual,
                self.last_opened_pane as usize
            );
            // Batch with keep-alive task if present (for replay mode timing)
            if let Some(keep_alive) = keep_alive_task {
                self.replay_keep_alive_pending = true;
                Task::batch([nav_task, keep_alive])
            } else {
                nav_task
            }
        } else if self.skate_left {
            self.update_counter = 0;
            debug!("move_left_all from self.skate_left block");
            let nav_task = move_left_all(
                &self.device,
                &self.queue,
                self.cache_strategy,
                self.compression_strategy,
                &mut self.panes,
                &mut self.loading_status,
                &mut self.slider_value,
                &self.pane_layout,
                self.is_slider_dual,
                self.last_opened_pane as usize
            );
            // Batch with keep-alive task if present (for replay mode timing)
            if let Some(keep_alive) = keep_alive_task {
                self.replay_keep_alive_pending = true;
                Task::batch([nav_task, keep_alive])
            } else {
                nav_task
            }
        } else if keep_alive_task.is_some() {
            // Batch keep-alive task if present (for replay mode timing)
            let mut batch_tasks = cli_tasks;
            if let Some(keep_alive) = keep_alive_task {
                self.replay_keep_alive_pending = true;
                batch_tasks.push(keep_alive);
            }
            if batch_tasks.is_empty() {
                task
            } else {
                batch_tasks.push(task);
                Task::batch(batch_tasks)
            }
        } else {
            // No skate mode, return the task from message handler
            if self.update_counter == 0 {
                debug!("No skate mode detected, update_counter: {}", self.update_counter);
                self.update_counter += 1;
            }
            if !cli_tasks.is_empty() {
                cli_tasks.push(task);
                Task::batch(cli_tasks)
            } else {
                task
            }
        }
    }


    fn view(&self) -> Element<'_, Message, WinitTheme, Renderer> {
        let content = ui::build_ui(self);

        if self.show_success_save_modal {
            let modal_content = Self::save_result_modal("File saved", None, Message::HideSuccessSaveModal);
            modal::modal(content, modal_content, Message::HideSuccessSaveModal)
        } else if let Some(ref error_message) = self.show_failure_save_modal {
            let modal_content = Self::save_result_modal("Error saving file", Some(format!("Message: {error_message}")), Message::HideFailureSaveModal);
            modal::modal(content, modal_content, Message::HideFailureSaveModal)
        } else if self.settings.is_visible() {
            let options_content = crate::settings_modal::view_settings_modal(self);
            widgets::modal::modal(content, options_content, Message::HideOptions)
        } else if self.show_about {
            // Build the info column dynamically to avoid empty text widgets
            let mut info_column = column![
                text(format!("Version {}", BuildInfo::display_version())).size(15),
                text(format!("Build: {} ({})", BuildInfo::build_string(), BuildInfo::build_profile())).size(12)
                .style(|theme: &WinitTheme| {
                    iced_widget::text::Style {
                        color: Some(theme.extended_palette().background.weak.color)
                    }
                }),
                text(format!("Commit: {}", BuildInfo::git_hash_short())).size(12)
                .style(|theme: &WinitTheme| {
                    iced_widget::text::Style {
                        color: Some(theme.extended_palette().background.weak.color)
                    }
                }),
                text(format!("Platform: {}", BuildInfo::target_platform())).size(12)
                .style(|theme: &WinitTheme| {
                    iced_widget::text::Style {
                        color: Some(theme.extended_palette().background.weak.color),
                    }
                }),
                text(format!("Features: {}", BuildInfo::enabled_features())).size(12)
                .style(|theme: &WinitTheme| {
                    iced_widget::text::Style {
                        color: Some(theme.extended_palette().background.weak.color),
                    }
                }),
            ];

            // Add bundle version only on macOS to avoid empty widgets
            let bundle_info = BuildInfo::bundle_version_display();
            if !bundle_info.is_empty() {
                info_column = info_column.push(
                    text(format!("Bundle: {}", bundle_info)).size(12)
                    .style(|theme: &WinitTheme| {
                        iced_widget::text::Style {
                            color: Some(theme.extended_palette().background.weak.color),
                        }
                    })
                );
            }

            info_column = info_column.push(row![
                text("Author:  ").size(15),
                text("Gota Gando").size(15)
                .style(|theme: &WinitTheme| {
                    iced_widget::text::Style {
                        color: Some(theme.extended_palette().primary.strong.color),
                    }
                })
            ]);

            info_column = info_column.push(text("Learn more at:").size(15));

            info_column = info_column.push(button(
                text("https://github.com/ggand0/viewskater")
                    .size(18)
            )
            .style(|theme: &WinitTheme, _status| {
                iced_widget::button::Style {
                    background: Some(iced_winit::core::Color::TRANSPARENT.into()),
                    text_color: theme.extended_palette().primary.strong.color,
                    border: iced_winit::core::Border {
                        color: iced_winit::core::Color::TRANSPARENT,
                        width: 1.0,
                        radius: iced_winit::core::border::Radius::new(0.0),
                    },
                    ..Default::default()
                }
            })
            .on_press(Message::OpenWebLink(
                "https://github.com/ggand0/viewskater".to_string(),
            )));

            info_column = info_column.spacing(4);

            let about_content = container(
                column![
                    text("ViewSkater").size(25)
                    .font(Font {
                        family: iced_winit::core::font::Family::Name("Roboto"),
                        weight: iced_winit::core::font::Weight::Bold,
                        stretch: iced_winit::core::font::Stretch::Normal,
                        style: iced_winit::core::font::Style::Normal,
                    }),
                    info_column
                ]
                .spacing(15)
                .align_x(iced_winit::core::alignment::Horizontal::Center),

            )
            .padding(20)
            .style(|theme: &WinitTheme| {
                iced_widget::container::Style {
                    background: Some(theme.extended_palette().background.base.color.into()),
                    text_color: Some(theme.extended_palette().primary.weak.text),
                    border: iced_winit::core::Border {
                        color: theme.extended_palette().background.strong.color,
                        width: 1.0,
                        radius: iced_winit::core::border::Radius::from(8.0),
                    },
                    ..Default::default()
                }
            });

            widgets::modal::modal(content, about_content, Message::HideAbout)
        } else {
            content.into()
        }
    }
}


================================================
FILE: src/archive_cache.rs
================================================
use std::path::PathBuf;
use std::sync::Arc;
use std::io::Read;
use std::collections::HashMap;

#[allow(unused_imports)]
use log::{debug, error, warn};

#[derive(Debug, Clone)]
pub enum ArchiveType {
    Zip,
    Rar,
    SevenZ,
}

/// Archive cache that stores reusable archive instances per pane
pub struct ArchiveCache {
    /// Current compressed file being accessed
    current_archive: Option<(PathBuf, ArchiveType)>,
    
    /// Cached ZIP archive instance to avoid reopening the file
    zip_archive: Option<Arc<std::sync::Mutex<zip::ZipArchive<std::io::BufReader<std::fs::File>>>>>,
    
    /// Cached 7z archive instance 
    sevenz_archive: Option<Arc<std::sync::Mutex<sevenz_rust2::ArchiveReader<std::fs::File>>>>,
    
    /// Preloaded file data for small solid archives (filename -> bytes)
    preloaded_data: HashMap<String, Vec<u8>>,
}

impl ArchiveCache {
    pub fn new() -> Self {
        Self {
            current_archive: None,
            zip_archive: None,
            sevenz_archive: None,
            preloaded_data: HashMap::new(),
        }
    }
    
    /// Set the current archive that this cache is working with
    /// Clears existing cache if switching to a different archive file
    pub fn set_current_archive(&mut self, path: PathBuf, archive_type: ArchiveType) {
        // Clear cache if switching to a different archive
        if let Some((current_path, _)) = &self.current_archive {
            if *current_path != path {
                debug!("Switching archives, clearing cache: {:?} -> {:?}", current_path, path);
                self.clear_cache();
            }
        }
        
        self.current_archive = Some((path, archive_type));
    }
    
    /// Clear all cached archive instances
    pub fn clear_cache(&mut self) {
        self.zip_archive = None;
        self.sevenz_archive = None;
        self.preloaded_data.clear();
        debug!("Archive cache cleared");
    }
    
    /// Add preloaded data for a file (used for solid 7z preloading)
    pub fn add_preloaded_data(&mut self, filename: String, data: Vec<u8>) {
        self.preloaded_data.insert(filename, data);
    }
    
    /// Get preloaded data for a file if available
    pub fn get_preloaded_data(&self, filename: &str) -> Option<&[u8]> {
        self.preloaded_data.get(filename).map(|v| v.as_slice())
    }
    
    /// Clear all preloaded data
    pub fn clear_preloaded_data(&mut self) {
        self.preloaded_data.clear();
    }

    /// Read a file from the current compressed archive
    /// This is the main entry point for archive-only operations
    pub fn read_from_archive(&mut self, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let (path, archive_type) = match self.current_archive.as_ref() {
            Some((p, t)) => (p.clone(), t.clone()),
            None => return Err("No current archive set".into()),
        };
            
        match archive_type {
            ArchiveType::Zip => self.read_zip_file(&path, filename),
            ArchiveType::Rar => self.read_rar_file(&path, filename),
            ArchiveType::SevenZ => self.read_7z_file(&path, filename),
        }
    }
    
    /// Read a file from ZIP archive using cached ZipArchive instance
    fn read_zip_file(&mut self, path: &PathBuf, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        // Get or create cached ZIP archive
        if self.zip_archive.is_none() {
            debug!("Creating new ZIP archive instance for {:?}", path);
            let file = std::io::BufReader::new(std::fs::File::open(path)?);
            let zip_archive = zip::ZipArchive::new(file)?;
            self.zip_archive = Some(Arc::new(std::sync::Mutex::new(zip_archive)));
        }
        
        // Read from cached archive
        let zip_arc = self.zip_archive.as_ref().unwrap();
        let mut zip = zip_arc.lock().unwrap();
        let mut buffer = Vec::new();
        zip.by_name(filename)?.read_to_end(&mut buffer)?;
        debug!("Read {} bytes from ZIP file: {}", buffer.len(), filename);
        Ok(buffer)
    }
    
    /// Read a file from RAR archive using simple filename comparison
    /// Uses the contributor's straightforward approach - simple and intuitive
    fn read_rar_file(&mut self, path: &PathBuf, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let mut archive = unrar::Archive::new(path).open_for_processing()?;
        let buffer = Vec::new();
        
        while let Some(header) = archive.read_header()? {
            let entry_filename = header.entry().filename.as_os_str();

            // NOTE: Printing this in the while loop is very slow
            //debug!("reading rar {} ?= {:?}", filename, entry_filename);
            
            archive = if filename == entry_filename {
                let (data, rest) = header.read()?;
                drop(rest);
                debug!("Read {} bytes from RAR file: {}", data.len(), filename);
                return Ok(data);
            } else {
                header.skip()?
            };
        }
        
        Ok(buffer)
    }

    /// Read a file from 7z archive using cached ArchiveReader instance
    fn read_7z_file(&mut self, path: &PathBuf, filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        // Get or create cached 7z archive
        if self.sevenz_archive.is_none() {
            debug!("Creating new 7z archive instance for {:?}", path);
            let reader = sevenz_rust2::ArchiveReader::open(path, sevenz_rust2::Password::empty())?;
            self.sevenz_archive = Some(Arc::new(std::sync::Mutex::new(reader)));
        }
        
        // Read from cached archive
        let sevenz_arc = self.sevenz_archive.as_ref()
            .ok_or("7z archive not initialized")?;
        let data = match sevenz_arc.lock() {
            Ok(mut sevenz) => sevenz.read_file(filename)?,
            Err(e) => {
                error!("Failed to lock 7z archive: {}", e);
                return Err("Failed to lock 7z archive".into());
            }
        };
        
        debug!("Read {} bytes from 7z file: {}", data.len(), filename);
        Ok(data)
    }
    
}

impl Default for ArchiveCache {
    fn default() -> Self {
        Self::new()
    }
}


================================================
FILE: src/build_info.rs
================================================
/// Build information captured at compile time
pub struct BuildInfo;

impl BuildInfo {
    /// Get the package version from Cargo.toml
    pub fn version() -> &'static str {
        env!("CARGO_PKG_VERSION")
    }
    
    /// Get the build timestamp in YYYYMMDD.HHMMSS format
    pub fn build_timestamp() -> &'static str {
        env!("BUILD_TIMESTAMP")
    }
    
    /// Get the full git commit hash
    #[allow(dead_code)]
    pub fn git_hash() -> &'static str {
        env!("GIT_HASH")
    }
    
    /// Get the short git commit hash (first 7 characters)
    pub fn git_hash_short() -> &'static str {
        env!("GIT_HASH_SHORT")
    }
    
    /// Get the target platform (arch-os)
    pub fn target_platform() -> &'static str {
        env!("TARGET_PLATFORM")
    }
    
    /// Get the build profile (debug/release)
    pub fn build_profile() -> &'static str {
        env!("BUILD_PROFILE")
    }
    
    /// Get the combined build string (version.timestamp)
    pub fn build_string() -> &'static str {
        env!("BUILD_STRING")
    }
    
    /// Get the bundle version (macOS specific)
    #[cfg(target_os = "macos")]
    pub fn bundle_version() -> &'static str {
        env!("BUNDLE_VERSION")
    }
    
    /// Get a formatted version string for display
    pub fn display_version() -> String {
        format!("{} ({})", Self::version(), Self::build_timestamp())
    }
    
    /// Get detailed build information for about dialogs
    #[allow(dead_code, unused_mut)]
    pub fn detailed_info() -> String {
        let mut info = format!(
            "Version: {}\nBuild: {}\nCommit: {}\nPlatform: {}\nProfile: {}",
            Self::version(),
            Self::build_timestamp(),
            Self::git_hash_short(),
            Self::target_platform(),
            Self::build_profile()
        );
        
        #[cfg(target_os = "macos")]
        {
            info.push_str(&format!("\nBundle: {}", Self::bundle_version()));
        }
        
        info
    }
    
    /// Get bundle version information for display (returns empty string on non-macOS)
    pub fn bundle_version_display() -> &'static str {
        #[cfg(target_os = "macos")]
        {
            Self::bundle_version()
        }
        #[cfg(not(target_os = "macos"))]
        {
            ""
        }
    }

    /// Get enabled feature flags for display
    #[allow(unused_mut)]
    pub fn enabled_features() -> String {
        let mut features: Vec<&str> = Vec::new();

        #[cfg(feature = "selection")]
        features.push("selection");

        #[cfg(feature = "coco")]
        features.push("coco");

        #[cfg(feature = "jp2")]
        features.push("jp2");

        if features.is_empty() {
            "none".to_string()
        } else {
            features.join(", ")
        }
    }
} 

================================================
FILE: src/cache/cache_utils.rs
================================================
use std::io;
#[allow(unused_imports)]
use image::GenericImageView;
use image::DynamicImage;
use std::sync::Arc;
use wgpu::{Device, Queue};
use iced_wgpu::wgpu;
use iced_wgpu::engine::CompressionStrategy;
use crate::cache::{compression::{compress_image_bc1, CompressionAlgorithm}};
use texpresso::{Format, Params, Algorithm, COLOUR_WEIGHTS_PERCEPTUAL};

#[allow(unused_imports)]
use log::{debug, info, warn, error};

// Maximum texture size supported - matches the 8192x8192 limit mentioned in README
const MAX_TEXTURE_SIZE: u32 = 8192;

/// Checks if image exceeds MAX_TEXTURE_SIZE and resizes if needed while preserving aspect ratio
pub fn check_and_resize_if_oversized(img: DynamicImage) -> DynamicImage {
    let (width, height) = img.dimensions();

    if width > MAX_TEXTURE_SIZE || height > MAX_TEXTURE_SIZE {
        // Calculate scaling factor to fit within MAX_TEXTURE_SIZE while preserving aspect ratio
        let scale_factor = (MAX_TEXTURE_SIZE as f32 / width.max(height) as f32).min(1.0);
        let new_width = (width as f32 * scale_factor) as u32;
        let new_height = (height as f32 * scale_factor) as u32;

        warn!("Image {}x{} exceeds maximum texture size {}x{}. Resizing to {}x{} to prevent crashes.",
              width, height, MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, new_width, new_height);

        img.resize(new_width, new_height, image::imageops::FilterType::Lanczos3)
    } else {
        debug!("Image {}x{} is within size limits, no resizing needed", width, height);
        img
    }
}

/// Loads an image with safety resizing for oversized images (>8192px)
pub fn load_original_image(path_source: &crate::cache::img_cache::PathSource, archive_cache: Option<&mut crate::archive_cache::ArchiveCache>) -> Result<DynamicImage, io::Error> {
    let img = {
        // Use PathSource-aware unified function
        let bytes = crate::file_io::read_image_bytes(path_source, archive_cache)?;
        crate::file_io::decode_image_from_bytes(&bytes)
            .map_err(|e| io::Error::new(e, format!("Failed to read image from PathSource: {}", path_source.file_name())))?
    };
    Ok(check_and_resize_if_oversized(img))
}

/// Loads and resizes an image to target dimensions, then applies safety size check
pub fn load_and_resize_image(path_source: &crate::cache::img_cache::PathSource, target_width: u32, target_height: u32, archive_cache: Option<&mut crate::archive_cache::ArchiveCache>) -> Result<DynamicImage, io::Error> {
    let img = {
        // Use PathSource-aware unified function
        let bytes = crate::file_io::read_image_bytes(path_source, archive_cache)?;
        crate::file_io::decode_image_from_bytes(&bytes)
            .map_err(|e| io::Error::new(e, format!("Failed to read image from PathSource: {}", path_source.file_name())))?
    };
    let (original_width, original_height) = img.dimensions();
    info!("Resizing image: {}x{} -> {}x{}", original_width, original_height, target_width, target_height);

    // First resize to target dimensions
    let resized_img = img.resize_exact(target_width, target_height, image::imageops::FilterType::Triangle);

    // Then apply safety check for oversized images
    Ok(check_and_resize_if_oversized(resized_img))
}
fn convert_image_to_rgba(img: &DynamicImage) -> (Vec<u8>, u32, u32) {
    let rgba_image = img.to_rgba8();
    let (width, height) = rgba_image.dimensions();
    let rgba_bytes = rgba_image.into_raw();

    (rgba_bytes, width, height)
}

/// Checks if BC1 compression should be used based on dimensions and strategy
pub fn should_use_compression(width: u32, height: u32, strategy: CompressionStrategy) -> bool {
    match strategy {
        CompressionStrategy::Bc1 => {
            // BC1 compression requires dimensions to be multiples of 4
            if width.is_multiple_of(4) && height.is_multiple_of(4) {
                debug!("Using BC1 compression for image ({} x {})", width, height);
                true
            } else {
                debug!("Image dimensions ({} x {}) not compatible with BC1. Using uncompressed format.", width, height);
                false
            }
        },
        CompressionStrategy::None => false,
    }
}

/// Creates a texture with the appropriate format based on compression settings
pub fn create_gpu_texture(
    device: &wgpu::Device,
    width: u32,
    height: u32,
    compression_strategy: CompressionStrategy,
) -> wgpu::Texture {
    let use_compression = should_use_compression(width, height, compression_strategy);

    device.create_texture(&wgpu::TextureDescriptor {
        label: Some(if use_compression { "CompressedTexture" } else { "LoadedTexture" }),
        size: wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: if use_compression {
            wgpu::TextureFormat::Bc1RgbaUnormSrgb
        } else {
            wgpu::TextureFormat::Rgba8UnormSrgb
        },
        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC,
        view_formats: &[],
    })
}

/// Compresses image data using BC1 algorithm
/// TODO: Remove this after confirming that the texpresso compression is stable
#[allow(dead_code)]
pub fn compress_image_data(
    rgba_data: &[u8],
    width: u32,
    height: u32,
) -> (Vec<u8>, u32) {
    // Compress the image data
    let compressed_blocks = compress_image_bc1(
        rgba_data,
        width as usize,
        height as usize,
        CompressionAlgorithm::RangeFit
    );

    // Calculate compressed data layout
    let blocks_x = width.div_ceil(4);
    let bytes_per_block = 8; // BC1 uses 8 bytes per 4x4 block
    let row_bytes = blocks_x * bytes_per_block;

    // Flatten the blocks into a single buffer
    let compressed_data: Vec<u8> = compressed_blocks.iter()
        .flat_map(|block| block.iter().copied())
        .collect();

    (compressed_data, row_bytes)
}

/// Uploads uncompressed image data to a texture
pub fn upload_uncompressed_texture(
    queue: &wgpu::Queue,
    texture: &wgpu::Texture,
    image_bytes: &[u8],
    width: u32,
    height: u32,
) {
    let bytes_per_row = width * 4;

    queue.write_texture(
        wgpu::ImageCopyTexture {
            texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        image_bytes,
        wgpu::ImageDataLayout {
            offset: 0,
            bytes_per_row: Some(bytes_per_row),
            rows_per_image: None,
        },
        wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
    );
}

/// Uploads compressed image data to a texture
pub fn upload_compressed_texture(
    queue: &wgpu::Queue,
    texture: &wgpu::Texture,
    compressed_data: &[u8],
    width: u32,
    height: u32,
    row_bytes: u32,
) {
    queue.write_texture(
        wgpu::ImageCopyTexture {
            texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        compressed_data,
        wgpu::ImageDataLayout {
            offset: 0,
            bytes_per_row: Some(row_bytes),
            rows_per_image: None,
        },
        wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
    );
}

/// Compresses an image using the texpresso library (BC1/DXT1 format)
pub fn compress_image_data_texpresso(image_data: &[u8], width: u32, height: u32) -> (Vec<u8>, u32) {
    // Create 4x4 blocks of RGBA data from the image
    let width_usize = width as usize;
    let height_usize = height as usize;

    // Calculate the output size
    let blocks_wide = width_usize.div_ceil(4);
    let blocks_tall = height_usize.div_ceil(4);
    let block_size = Format::Bc1.block_size();
    let output_size = blocks_wide * blocks_tall * block_size;

    // Create output buffer
    let mut compressed_data = vec![0u8; output_size];

    // Set up compression parameters
    let params = Params {
        //algorithm: Algorithm::ClusterFit, // Higher quality but still fast
        algorithm: Algorithm::RangeFit,
        weights: COLOUR_WEIGHTS_PERCEPTUAL,
        weigh_colour_by_alpha: true, // Better for images with transparency
    };

    // Compress the image
    Format::Bc1.compress(
        image_data,
        width_usize,
        height_usize,
        params,
        &mut compressed_data
    );

    // Calculate bytes per row
    let bytes_per_row = blocks_wide * block_size;

    (compressed_data, bytes_per_row as u32)
}

/// Creates and uploads a texture with the appropriate format and data
pub fn create_and_upload_texture(
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    image_data: &[u8],
    width: u32,
    height: u32,
    compression_strategy: CompressionStrategy,
) -> wgpu::Texture {
    let use_compression = should_use_compression(width, height, compression_strategy);

    let texture = create_gpu_texture(device, width, height, compression_strategy);

    if use_compression {
        // Use texpresso for compression when BC1 is selected
        match compression_strategy {
            CompressionStrategy::Bc1 => {
                let (compressed_data, bytes_per_row) = compress_image_data_texpresso(image_data, width, height);
                upload_compressed_texture(queue, &texture, &compressed_data, width, height, bytes_per_row);
            },
            _ => {
                // Raise an error if an unsupported compression strategy is used
                panic!("Unsupported compression strategy: {:?}", compression_strategy);
            }
        }
    } else {
        upload_uncompressed_texture(queue, &texture, image_data, width, height);
    }

    texture
}

pub fn load_image_resized_sync(
    img_path: &crate::cache::img_cache::PathSource,
    is_slider_move: bool,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    existing_texture: &mut Arc<wgpu::Texture>,
    compression_strategy: CompressionStrategy,
    archive_cache: Option<&mut crate::archive_cache::ArchiveCache>
) -> Result<(), io::Error> {
    let img = if is_slider_move {
        load_and_resize_image(img_path, 1280, 720, archive_cache)?
    } else {
        load_original_image(img_path, archive_cache)?
    };
    let (image_bytes, width, height) = convert_image_to_rgba(&img);

    // Use our new utility function to create and upload the texture
    let texture = Arc::new(
        create_and_upload_texture(device, queue, &image_bytes, width, height, compression_strategy)
    );

    // Replace the old texture
    *existing_texture = texture;

    Ok(())
}

/// Loads an image and resizes it to 720p if needed, then uploads it to GPU.
pub async fn _load_image_resized(
    img_path: &crate::cache::img_cache::PathSource,
    is_slider_move: bool,
    device: &Device,
    queue: &Queue,
    existing_texture: &Arc<wgpu::Texture>,
) -> Result<(), io::Error> {
    // Use the appropriate loading function based on whether it's for slider or full-res
    let img = if is_slider_move {
        load_and_resize_image(img_path, 1280, 720, None)
    } else {
        load_original_image(img_path, None)
    }.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("Failed to open image: {}", e)))?;

    let rgba_image = img.to_rgba8();
    let (width, height) = rgba_image.dimensions();

    let rgba_bytes = rgba_image.as_raw();

    // 🔹 Align `bytes_per_row` to 256 bytes
    let unaligned_bytes_per_row = width * 4;
    let aligned_bytes_per_row = (unaligned_bytes_per_row + 255) & !255;

    // 🔹 Staging buffer
    let buffer_size = (aligned_bytes_per_row * height) as wgpu::BufferAddress;
    let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
        label: Some("Staging Buffer"),
        size: buffer_size,
        usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::MAP_WRITE,
        mapped_at_creation: true,
    });

    {
        let mut mapping = staging_buffer.slice(..).get_mapped_range_mut();
        for row in 0..height {
            let src_start = (row * width * 4) as usize;
            let dst_start = (row * aligned_bytes_per_row) as usize;
            mapping[dst_start..dst_start + (width * 4) as usize]
                .copy_from_slice(&rgba_bytes[src_start..src_start + (width * 4) as usize]);
        }
    }
    staging_buffer.unmap();

    let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Image Upload Encoder") });

    encoder.copy_buffer_to_texture(
        wgpu::ImageCopyBuffer {
            buffer: &staging_buffer,
            layout: wgpu::ImageDataLayout {
                offset: 0,
                bytes_per_row: Some(aligned_bytes_per_row),
                rows_per_image: Some(height),
            },
        },
        wgpu::ImageCopyTexture {
            texture: existing_texture,
            mip_level: 0,
            origin: wgpu::Origin3d::ZERO,
            aspect: wgpu::TextureAspect::All,
        },
        wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
    );

    queue.submit(Some(encoder.finish()));

    Ok(())
}


================================================
FILE: src/cache/compression.rs
================================================
//! A minimal BC1 compression library in pure Rust.

// Remove `#![no_std]` since we need standard library features like Vec.

/// Represents the compressed BC1 block (8 bytes).
pub type Bc1Block = [u8; 8];

/// Represents an uncompressed 4x4 block of RGBA pixels.
pub type RgbaBlock = [[u8; 4]; 16];

/// Defines available compression algorithms
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
#[derive(Default)]
pub enum CompressionAlgorithm {
    /// RangeFit algorithm - faster with good quality
    #[default]
    RangeFit,
}

/// Converts RGB values to the RGB565 format.
fn rgb_to_rgb565(r: u8, g: u8, b: u8) -> u16 {
    ((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3)
}

/// Computes the Euclidean distance between two colors.
fn color_distance(c1: (u8, u8, u8), c2: (u8, u8, u8)) -> f32 {
    let dr = c1.0 as f32 - c2.0 as f32;
    let dg = c1.1 as f32 - c2.1 as f32;
    let db = c1.2 as f32 - c2.2 as f32;
    (dr * dr + dg * dg + db * db).sqrt()
}

/// Computes the principal axis using covariance matrix
fn compute_principal_axis(colors: &[(u8, u8, u8)]) -> (f32, f32, f32) {
    let mut sum_r = 0.0f32;
    let mut sum_g = 0.0f32;
    let mut sum_b = 0.0f32;
    let mut count = 0.0f32;

    // Calculate mean color
    for &(r, g, b) in colors {
        sum_r += r as f32;
        sum_g += g as f32;
        sum_b += b as f32;
        count += 1.0;
    }

    let mean_r = sum_r / count;
    let mean_g = sum_g / count;
    let mean_b = sum_b / count;

    // Calculate covariance matrix
    let mut cov = [[0.0f32; 3]; 3];
    for &(r, g, b) in colors {
        let dr = (r as f32) - mean_r;
        let dg = (g as f32) - mean_g;
        let db = (b as f32) - mean_b;

        cov[0][0] += dr * dr;
        cov[0][1] += dr * dg;
        cov[0][2] += dr * db;
        cov[1][1] += dg * dg;
        cov[1][2] += dg * db;
        cov[2][2] += db * db;
    }

    cov[1][0] = cov[0][1];
    cov[2][0] = cov[0][2];
    cov[2][1] = cov[1][2];

    // Simple power iteration to find principal eigenvector
    let mut axis = (1.0f32, 1.0f32, 1.0f32);
    for _ in 0..4 {
        let x = cov[0][0] * axis.0 + cov[0][1] * axis.1 + cov[0][2] * axis.2;
        let y = cov[1][0] * axis.0 + cov[1][1] * axis.1 + cov[1][2] * axis.2;
        let z = cov[2][0] * axis.0 + cov[2][1] * axis.1 + cov[2][2] * axis.2;

        let length = (x * x + y * y + z * z).sqrt();
        if length > 0.0 {
            axis = (x / length, y / length, z / length);
        }
    }

    axis
}

/// Compresses a 4x4 block using the RangeFit algorithm
fn compress_bc1_block_rangefit(block: &RgbaBlock) -> Bc1Block {
    let mut colors: Vec<(u8, u8, u8)> = Vec::new();

    // Collect non-transparent pixels
    for &pixel in block.iter() {
        if pixel[3] > 128 {
            colors.push((pixel[0], pixel[1], pixel[2]));
        }
    }

    if colors.is_empty() {
        return [0u8; 8];
    }

    // Get principal axis
    let axis = compute_principal_axis(&colors);

    // Project colors onto principal axis
    let mut min_proj = f32::MAX;
    let mut max_proj = f32::MIN;
    let mut min_color = (0, 0, 0);
    let mut max_color = (0, 0, 0);

    for &(r, g, b) in &colors {
        let proj = (r as f32) * axis.0 + (g as f32) * axis.1 + (b as f32) * axis.2;

        if proj < min_proj {
            min_proj = proj;
            min_color = (r, g, b);
        }
        if proj > max_proj {
            max_proj = proj;
            max_color = (r, g, b);
        }
    }

    // Convert endpoints to RGB565
    let color0 = rgb_to_rgb565(min_color.0, min_color.1, min_color.2);
    let color1 = rgb_to_rgb565(max_color.0, max_color.1, max_color.2);

    // Create color palette
    let mut palette = [min_color, max_color, (0, 0, 0), (0, 0, 0)];

    if color0 > color1 {
        palette[2] = (
            ((2 * (min_color.0 as u16) + max_color.0 as u16) / 3) as u8,
            ((2 * (min_color.1 as u16) + max_color.1 as u16) / 3) as u8,
            ((2 * (min_color.2 as u16) + max_color.2 as u16) / 3) as u8,
        );
        palette[3] = (
            ((min_color.0 as u16 + 2 * (max_color.0 as u16)) / 3) as u8,
            ((min_color.1 as u16 + 2 * (max_color.1 as u16)) / 3) as u8,
            ((min_color.2 as u16 + 2 * (max_color.2 as u16)) / 3) as u8,
        );
    } else {
        palette[2] = (
            ((min_color.0 as u16 + max_color.0 as u16) / 2) as u8,
            ((min_color.1 as u16 + max_color.1 as u16) / 2) as u8,
            ((min_color.2 as u16 + max_color.2 as u16) / 2) as u8,
        );
        palette[3] = (0, 0, 0);
    }

    // Build indices
    let mut indices = 0u32;
    for (i, &pixel) in block.iter().enumerate() {
        let mut best_index = if pixel[3] <= 128 { 3 } else { 0 };
        let mut best_distance = f32::MAX;

        if pixel[3] > 128 {
            for (j, &palette_color) in palette.iter().enumerate() {
                let distance = color_distance((pixel[0], pixel[1], pixel[2]), palette_color);
                if distance < best_distance {
                    best_distance = distance;
                    best_index = j;
                }
            }
        }

        indices |= (best_index as u32) << (2 * i);
    }

    let mut block_data = [0u8; 8];
    block_data[0..2].copy_from_slice(&color0.to_le_bytes());
    block_data[2..4].copy_from_slice(&color1.to_le_bytes());
    block_data[4..8].copy_from_slice(&indices.to_le_bytes());

    block_data
}

/// Compresses a 4x4 block of RGBA pixels using the specified algorithm
pub fn compress_bc1_block(block: &RgbaBlock, algorithm: CompressionAlgorithm) -> Bc1Block {
    match algorithm {
        CompressionAlgorithm::RangeFit => compress_bc1_block_rangefit(block),
    }
}

/// Compresses an entire image of RGBA pixels
pub fn compress_image_bc1(
    image: &[u8],
    width: usize,
    height: usize,
    algorithm: CompressionAlgorithm
) -> Vec<Bc1Block> {
    use rayon::prelude::*;

    // Calculate the positions of all blocks to process
    let blocks_x = width.div_ceil(4);
    let blocks_y = height.div_ceil(4);
    let total_blocks = blocks_x * blocks_y;

    // Create a parallel iterator for all block positions
    (0..total_blocks)
        .into_par_iter()
        .map(|block_idx| {
            let block_x = (block_idx % blocks_x) * 4;
            let block_y = (block_idx / blocks_x) * 4;

            // Build the 4x4 block
            let mut block = [[0u8; 4]; 16];
            for by in 0..4 {
                for bx in 0..4 {
                    let px = block_x + bx;
                    let py = block_y + by;
                    let idx = 4 * (py * width + px);
                    if px < width && py < height {
                        block[by * 4 + bx] = [
                            image[idx],
                            image[idx + 1],
                            image[idx + 2],
                            image[idx + 3],
                        ];
                    }
                }
            }

            // Compress the block and return it
            compress_bc1_block(&block, algorithm)
        })
        .collect()
}


================================================
FILE: src/cache/cpu_img_cache.rs
================================================
#[allow(unused_imports)]
use log::{debug, info, warn, error};

use std::io;
use crate::cache::img_cache::{CachedData, ImageCacheBackend, ImageMetadata};
use iced_wgpu::engine::CompressionStrategy;


pub struct CpuImageCache;

impl CpuImageCache {
    pub fn new() -> Self {
        CpuImageCache
    }
}

impl ImageCacheBackend for CpuImageCache {
    fn load_image(
        &self,
        index: usize,
        image_paths: &[crate::cache::img_cache::PathSource],
        #[allow(unused_variables)] compression_strategy: CompressionStrategy,
        archive_cache: Option<&mut crate::archive_cache::ArchiveCache>
    ) -> Result<CachedData, io::Error> {
        if let Some(path_source) = image_paths.get(index) {
            debug!("CpuCache: Loading image from {:?}", path_source.file_name());
            Ok(CachedData::Cpu(crate::file_io::read_image_bytes(path_source, archive_cache)?))
        } else {
            Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid image index"))
        }
    }

    #[allow(clippy::needless_option_as_deref)]
    fn load_single_image(
        &mut self,
        image_paths: &[crate::cache::img_cache::PathSource],
        cache_count: usize,
        current_index: usize,
        cached_data: &mut Vec<Option<CachedData>>,
        cached_metadata: &mut Vec<Option<ImageMetadata>>,
        cached_image_indices: &mut Vec<isize>,
        current_offset: &mut isize,
        #[allow(unused_variables)] compression_strategy: CompressionStrategy,
        mut archive_cache: Option<&mut crate::archive_cache::ArchiveCache>,
    ) -> Result<(), io::Error> {
        // Calculate which cache slot to use for the current_index
        let cache_slot: usize;
        if current_index <= cache_count {
            cache_slot = current_index;
            *current_offset = -(cache_count as isize - current_index as isize);
        } else if current_index > (image_paths.len() - 1) - cache_count {
            cache_slot = cache_count + (cache_count as isize -
                          ((image_paths.len()-1) as isize - current_index as isize)) as usize;
            *current_offset = cache_count as isize -
                             ((image_paths.len()-1) as isize - current_index as isize);
        } else {
            cache_slot = cache_count;
            *current_offset = 0;
        }

        // Load only the single image at current_index
        if let Some(path_source) = image_paths.get(current_index) {
            match crate::file_io::read_image_bytes_with_size(p
Download .txt
gitextract_szegwn3j/

├── .gitignore
├── Cargo.toml
├── README.md
├── assets/
│   └── ViewSkater.icns
├── build.rs
├── docs/
│   ├── bundling.md
│   └── replay.md
├── resources/
│   ├── linux/
│   │   ├── appimage.desktop
│   │   └── viewskater.desktop
│   └── macos/
│       ├── Info.plist
│       ├── entitlements.plist
│       └── viewskater_wrapper.sh
└── src/
    ├── app/
    │   ├── keyboard_handlers.rs
    │   ├── message.rs
    │   ├── message_handlers.rs
    │   ├── replay_handlers.rs
    │   └── settings_widget.rs
    ├── app.rs
    ├── archive_cache.rs
    ├── build_info.rs
    ├── cache/
    │   ├── cache_utils.rs
    │   ├── compression.rs
    │   ├── cpu_img_cache.rs
    │   ├── gpu_img_cache.rs
    │   ├── img_cache.rs
    │   ├── mod.rs
    │   └── texture_cache.rs
    ├── coco/
    │   ├── annotation_manager.rs
    │   ├── mod.rs
    │   ├── overlay/
    │   │   ├── bbox_overlay.rs
    │   │   ├── bbox_shader.rs
    │   │   ├── bbox_shader.wgsl
    │   │   ├── mask_shader.rs
    │   │   ├── mask_shader.wgsl
    │   │   ├── mod.rs
    │   │   ├── polygon_shader.rs
    │   │   └── polygon_shader.wgsl
    │   ├── parser.rs
    │   ├── rle_decoder.rs
    │   └── widget.rs
    ├── config.rs
    ├── exif_utils.rs
    ├── file_io.rs
    ├── loading_handler.rs
    ├── loading_status.rs
    ├── logging.rs
    ├── macos_file_access.rs
    ├── main.rs
    ├── menu.rs
    ├── navigation_keyboard.rs
    ├── navigation_slider.rs
    ├── pane.rs
    ├── replay.rs
    ├── selection_manager.rs
    ├── settings.rs
    ├── settings_modal.rs
    ├── ui.rs
    ├── utils/
    │   ├── mem.rs
    │   ├── mod.rs
    │   ├── save.rs
    │   └── timing.rs
    ├── widgets/
    │   ├── circular.rs
    │   ├── dualslider.rs
    │   ├── easing.rs
    │   ├── mod.rs
    │   ├── modal.rs
    │   ├── selection_widget.rs
    │   ├── shader/
    │   │   ├── atlas_texture.wgsl
    │   │   ├── cpu_scene.rs
    │   │   ├── image_shader.rs
    │   │   ├── mod.rs
    │   │   ├── scene.rs
    │   │   ├── texture.wgsl
    │   │   ├── texture_pipeline.rs
    │   │   └── texture_scene.rs
    │   ├── split.rs
    │   ├── synced_image_split.rs
    │   ├── toggler.rs
    │   └── viewer.rs
    └── window_state.rs
Download .txt
SYMBOL INDEX (991 symbols across 58 files)

FILE: build.rs
  function main (line 11) | fn main() -> io::Result<()> {
  function capture_build_info (line 29) | fn capture_build_info() {
  function get_git_hash (line 77) | fn get_git_hash() -> Option<String> {
  function update_info_plist (line 91) | fn update_info_plist(build_timestamp: &str) {

FILE: src/app.rs
  type DataViewer (line 78) | pub struct DataViewer {
    type Target (line 143) | type Target = RuntimeSettings;
    method deref (line 145) | fn deref(&self) -> &Self::Target {
    method deref_mut (line 152) | fn deref_mut(&mut self) -> &mut Self::Target {
    method new (line 158) | pub fn new(
    method clear_primitive_storage (line 249) | pub fn clear_primitive_storage(&self) {
    method ensure_pane_exists (line 256) | fn ensure_pane_exists(&mut self, pane_index: usize) {
    method clear_slider_images (line 271) | fn clear_slider_images(&mut self) {
    method start_neighbor_loading (line 281) | fn start_neighbor_loading(&mut self, pane_index: usize) -> Task<Messag...
    method reset_state (line 314) | pub fn reset_state(&mut self, pane_index: isize) {
    method initialize_dir_path (line 349) | pub(crate) fn initialize_dir_path(&mut self, path: &PathBuf, pane_inde...
    method initialize_dir_path_sync (line 376) | fn initialize_dir_path_sync(&mut self, path: &PathBuf, pane_index: usi...
    method complete_dir_initialization (line 416) | pub(crate) fn complete_dir_initialization(
    method set_ctrl_pressed (line 453) | fn set_ctrl_pressed(&mut self, enabled: bool) {
    method toggle_success_save_modal (line 460) | pub(crate) fn toggle_success_save_modal(&mut self) {
    method set_failure_save_modal (line 464) | pub(crate) fn set_failure_save_modal(&mut self, error_message: Option<...
    method save_result_modal (line 468) | fn save_result_modal(
    method toggle_slider_type (line 511) | pub(crate) fn toggle_slider_type(&mut self) {
    method toggle_pane_layout (line 533) | pub(crate) fn toggle_pane_layout(&mut self, pane_layout: PaneLayout) {
    method toggle_footer (line 555) | pub(crate) fn toggle_footer(&mut self) {
    method title (line 559) | pub fn title(&self) -> String {
    method is_any_pane_loading (line 598) | pub fn is_any_pane_loading(&self) -> bool {
    method update_cache_strategy (line 602) | pub(crate) fn update_cache_strategy(&mut self, strategy: CacheStrategy) {
    method update_compression_strategy (line 644) | pub(crate) fn update_compression_strategy(&mut self, strategy: Compres...
    method toggle_split_orientation (line 698) | pub(crate) fn toggle_split_orientation(&mut self) {
    type Theme (line 705) | type Theme = WinitTheme;
    type Message (line 706) | type Message = Message;
    type Renderer (line 707) | type Renderer = Renderer;
    method update (line 709) | fn update(&mut self, message: Message) -> iced_winit::runtime::Task<Me...
    method view (line 811) | fn view(&self) -> Element<'_, Message, WinitTheme, Renderer> {

FILE: src/app/keyboard_handlers.rs
  function is_platform_modifier (line 11) | fn is_platform_modifier(modifiers: &keyboard::Modifiers) -> bool {
  method handle_key_pressed_event (line 20) | pub(crate) fn handle_key_pressed_event(&mut self, key: &keyboard::Key, m...
  method handle_key_released_event (line 383) | pub(crate) fn handle_key_released_event(&mut self, key_code: &keyboard::...

FILE: src/app/message.rs
  type DirectoryEnumResult (line 15) | pub struct DirectoryEnumResult {
  type DirectoryEnumError (line 23) | pub enum DirectoryEnumError {
  type SliderImageWidgetResult (line 30) | pub type SliderImageWidgetResult = Result<(usize, usize, Handle, (u32, u...
  type ImagesLoadedResult (line 33) | pub type ImagesLoadedResult = Result<(Vec<Option<CachedData>>, Vec<Optio...
  type Message (line 36) | pub enum Message {

FILE: src/app/message_handlers.rs
  function handle_message (line 31) | pub fn handle_message(app: &mut DataViewer, message: Message) -> Task<Me...
  function handle_ui_messages (line 146) | pub fn handle_ui_messages(app: &mut DataViewer, message: Message) -> Tas...
  function handle_settings_messages (line 207) | pub fn handle_settings_messages(app: &mut DataViewer, message: Message) ...
  function handle_file_messages (line 232) | pub fn handle_file_messages(app: &mut DataViewer, message: Message) -> T...
  function handle_image_loading_messages (line 368) | pub fn handle_image_loading_messages(app: &mut DataViewer, message: Mess...
  function handle_slider_messages (line 509) | pub fn handle_slider_messages(app: &mut DataViewer, message: Message) ->...
  function handle_toggle_messages (line 628) | pub fn handle_toggle_messages(app: &mut DataViewer, message: Message) ->...
  function handle_event_messages (line 838) | pub fn handle_event_messages(app: &mut DataViewer, event: Event) -> Task...
  function handle_window_file_drop (line 935) | fn handle_window_file_drop(app: &mut DataViewer, path: &std::path::Path)...
  function handle_file_dropped (line 966) | fn handle_file_dropped(app: &mut DataViewer, pane_index: isize, dropped_...
  function handle_save_settings (line 1001) | fn handle_save_settings(app: &mut DataViewer) -> Task<Message> {
  function handle_save_window_state (line 1251) | fn handle_save_window_state(app: &mut DataViewer) -> Task<Message> {
  function handle_reset_advanced_settings (line 1273) | fn handle_reset_advanced_settings(app: &mut DataViewer) {
  function handle_export_all_logs (line 1296) | fn handle_export_all_logs() {
  function handle_save_image (line 1327) | pub fn handle_save_image(app: &mut DataViewer, message: Message) -> Task...

FILE: src/app/replay_handlers.rs
  function reset_fps_trackers (line 12) | fn reset_fps_trackers() {
  method update_replay_mode (line 22) | pub(crate) fn update_replay_mode(&mut self) -> Option<crate::replay::Rep...
  method process_replay_action (line 149) | pub(crate) fn process_replay_action(&mut self, action: crate::replay::Re...

FILE: src/app/settings_widget.rs
  type RuntimeSettings (line 9) | pub struct RuntimeSettings {
    method from_user_settings (line 22) | pub fn from_user_settings(settings: &UserSettings) -> Self {
  type SettingsWidget (line 38) | pub struct SettingsWidget {
    method new (line 47) | pub fn new(settings: &UserSettings) -> Self {
    method show (line 69) | pub fn show(&mut self) {
    method hide (line 73) | pub fn hide(&mut self) {
    method set_save_status (line 77) | pub fn set_save_status(&mut self, status: Option<String>) {
    method clear_save_status (line 81) | pub fn clear_save_status(&mut self) {
    method set_active_tab (line 85) | pub fn set_active_tab(&mut self, tab: usize) {
    method set_advanced_input (line 89) | pub fn set_advanced_input(&mut self, key: String, value: String) {
    method is_visible (line 93) | pub fn is_visible(&self) -> bool {

FILE: src/archive_cache.rs
  type ArchiveType (line 10) | pub enum ArchiveType {
  type ArchiveCache (line 17) | pub struct ArchiveCache {
    method new (line 32) | pub fn new() -> Self {
    method set_current_archive (line 43) | pub fn set_current_archive(&mut self, path: PathBuf, archive_type: Arc...
    method clear_cache (line 56) | pub fn clear_cache(&mut self) {
    method add_preloaded_data (line 64) | pub fn add_preloaded_data(&mut self, filename: String, data: Vec<u8>) {
    method get_preloaded_data (line 69) | pub fn get_preloaded_data(&self, filename: &str) -> Option<&[u8]> {
    method clear_preloaded_data (line 74) | pub fn clear_preloaded_data(&mut self) {
    method read_from_archive (line 80) | pub fn read_from_archive(&mut self, filename: &str) -> Result<Vec<u8>,...
    method read_zip_file (line 94) | fn read_zip_file(&mut self, path: &PathBuf, filename: &str) -> Result<...
    method read_rar_file (line 114) | fn read_rar_file(&mut self, path: &PathBuf, filename: &str) -> Result<...
    method read_7z_file (line 138) | fn read_7z_file(&mut self, path: &PathBuf, filename: &str) -> Result<V...
  method default (line 164) | fn default() -> Self {

FILE: src/build_info.rs
  type BuildInfo (line 2) | pub struct BuildInfo;
    method version (line 6) | pub fn version() -> &'static str {
    method build_timestamp (line 11) | pub fn build_timestamp() -> &'static str {
    method git_hash (line 17) | pub fn git_hash() -> &'static str {
    method git_hash_short (line 22) | pub fn git_hash_short() -> &'static str {
    method target_platform (line 27) | pub fn target_platform() -> &'static str {
    method build_profile (line 32) | pub fn build_profile() -> &'static str {
    method build_string (line 37) | pub fn build_string() -> &'static str {
    method bundle_version (line 43) | pub fn bundle_version() -> &'static str {
    method display_version (line 48) | pub fn display_version() -> String {
    method detailed_info (line 54) | pub fn detailed_info() -> String {
    method bundle_version_display (line 73) | pub fn bundle_version_display() -> &'static str {
    method enabled_features (line 86) | pub fn enabled_features() -> String {

FILE: src/cache/cache_utils.rs
  constant MAX_TEXTURE_SIZE (line 16) | const MAX_TEXTURE_SIZE: u32 = 8192;
  function check_and_resize_if_oversized (line 19) | pub fn check_and_resize_if_oversized(img: DynamicImage) -> DynamicImage {
  function load_original_image (line 39) | pub fn load_original_image(path_source: &crate::cache::img_cache::PathSo...
  function load_and_resize_image (line 50) | pub fn load_and_resize_image(path_source: &crate::cache::img_cache::Path...
  function convert_image_to_rgba (line 66) | fn convert_image_to_rgba(img: &DynamicImage) -> (Vec<u8>, u32, u32) {
  function should_use_compression (line 75) | pub fn should_use_compression(width: u32, height: u32, strategy: Compres...
  function create_gpu_texture (line 92) | pub fn create_gpu_texture(
  function compress_image_data (line 123) | pub fn compress_image_data(
  function upload_uncompressed_texture (line 150) | pub fn upload_uncompressed_texture(
  function upload_compressed_texture (line 181) | pub fn upload_compressed_texture(
  function compress_image_data_texpresso (line 211) | pub fn compress_image_data_texpresso(image_data: &[u8], width: u32, heig...
  function create_and_upload_texture (line 249) | pub fn create_and_upload_texture(
  function load_image_resized_sync (line 280) | pub fn load_image_resized_sync(
  function _load_image_resized (line 308) | pub async fn _load_image_resized(

FILE: src/cache/compression.rs
  type Bc1Block (line 6) | pub type Bc1Block = [u8; 8];
  type RgbaBlock (line 9) | pub type RgbaBlock = [[u8; 4]; 16];
  type CompressionAlgorithm (line 15) | pub enum CompressionAlgorithm {
  function rgb_to_rgb565 (line 22) | fn rgb_to_rgb565(r: u8, g: u8, b: u8) -> u16 {
  function color_distance (line 27) | fn color_distance(c1: (u8, u8, u8), c2: (u8, u8, u8)) -> f32 {
  function compute_principal_axis (line 35) | fn compute_principal_axis(colors: &[(u8, u8, u8)]) -> (f32, f32, f32) {
  function compress_bc1_block_rangefit (line 89) | fn compress_bc1_block_rangefit(block: &RgbaBlock) -> Bc1Block {
  function compress_bc1_block (line 180) | pub fn compress_bc1_block(block: &RgbaBlock, algorithm: CompressionAlgor...
  function compress_image_bc1 (line 187) | pub fn compress_image_bc1(

FILE: src/cache/cpu_img_cache.rs
  type CpuImageCache (line 9) | pub struct CpuImageCache;
    method new (line 12) | pub fn new() -> Self {
  method load_image (line 18) | fn load_image(
  method load_single_image (line 34) | fn load_single_image(
  method load_initial_images (line 94) | fn load_initial_images(
  method load_pos (line 157) | fn load_pos(

FILE: src/cache/gpu_img_cache.rs
  type GpuImageCache (line 13) | pub struct GpuImageCache {
    method new (line 19) | pub fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>) -> Self {
  method load_image (line 25) | fn load_image(
  method load_single_image (line 78) | fn load_single_image(
  method load_initial_images (line 142) | fn load_initial_images(
  method load_pos (line 210) | fn load_pos(

FILE: src/cache/img_cache.rs
  type LoadOperation (line 26) | pub enum LoadOperation {
    method operation_type (line 44) | pub fn operation_type(&self) -> LoadOperationType {
  type LoadOperationType (line 35) | pub enum LoadOperationType {
  type CacheStrategy (line 57) | pub enum CacheStrategy {
    method is_gpu_based (line 63) | pub fn is_gpu_based(&self) -> bool {
  type ImageMetadata (line 73) | pub struct ImageMetadata {
    method new (line 80) | pub fn new(width: u32, height: u32, file_size: u64) -> Self {
    method resolution_string (line 85) | pub fn resolution_string(&self) -> String {
    method file_size_string (line 92) | pub fn file_size_string(&self, use_binary: bool) -> String {
  type CachedData (line 110) | pub enum CachedData {
    method take (line 117) | pub fn take(self) -> Self {
    method width (line 121) | pub fn width(&self) -> u32 {
    method height (line 125) | pub fn height(&self) -> u32 {
    method dimensions (line 131) | pub fn dimensions(&self) -> (u32, u32) {
    method handle (line 142) | pub fn handle(&self) -> Option<iced_core::image::Handle> {
    method len (line 154) | pub fn len(&self) -> usize {
    method as_vec (line 177) | pub fn as_vec(&self) -> Result<Vec<u8>, io::Error> {
    method is_compressed (line 188) | pub fn is_compressed(&self) -> bool {
    method compression_format (line 192) | pub fn compression_format(&self) -> Option<&'static str> {
  type PathSource (line 201) | pub enum PathSource {
    method path (line 212) | pub fn path(&self) -> &PathBuf {
    method file_name (line 220) | pub fn file_name(&self) -> std::borrow::Cow<'_, str> {
  type ImageCacheBackend (line 235) | pub trait ImageCacheBackend {
    method load_image (line 236) | fn load_image(
    method load_single_image (line 245) | fn load_single_image(
    method load_initial_images (line 260) | fn load_initial_images(
    method load_pos (line 275) | fn load_pos(
  type ImageCache (line 289) | pub struct ImageCache {
    method new (line 331) | pub fn new(
    method _get_cached_data (line 392) | pub fn _get_cached_data(&self, index: usize) -> Option<&CachedData> {
    method set_cached_data (line 396) | pub fn set_cached_data(&mut self, index: usize, data: CachedData) {
    method set_cached_metadata (line 402) | pub fn set_cached_metadata(&mut self, index: usize, metadata: ImageMet...
    method load_image (line 408) | pub fn load_image(&self, index: usize, archive_cache: Option<&mut crat...
    method _load_pos (line 412) | pub fn _load_pos(
    method load_single_image (line 433) | pub fn load_single_image(&mut self, archive_cache: Option<&mut crate::...
    method load_initial_images (line 448) | pub fn load_initial_images(&mut self, archive_cache: Option<&mut crate...
    method shift_cache_left (line 462) | pub fn shift_cache_left(&mut self, new_item: Option<CachedData>, new_m...
    method init_cache (line 483) | pub fn init_cache(
    method _set_compression_strategy (line 506) | pub fn _set_compression_strategy(&mut self, strategy: CompressionStrat...
    method print_cache (line 514) | pub fn print_cache(&self) {
    method print_cache_index (line 530) | pub fn print_cache_index(&self) {
    method clear_cache (line 538) | pub fn clear_cache(&mut self) {
    method move_next (line 569) | pub fn move_next(&mut self, new_image: Option<CachedData>, new_metadat...
    method move_prev (line 580) | pub fn move_prev(&mut self, new_image: Option<CachedData>, new_metadat...
    method move_next_edge (line 591) | pub fn move_next_edge(&self, _new_image: Option<CachedData>, _image_in...
    method move_prev_edge (line 599) | pub fn move_prev_edge(&self, _new_image: Option<CachedData>, _image_in...
    method shift_cache_right (line 607) | pub fn shift_cache_right(
    method get_initial_image (line 627) | pub fn get_initial_image(&self) -> Result<&CachedData, io::Error> {
    method get_initial_image_as_cpu (line 643) | pub fn get_initial_image_as_cpu(&self, archive_cache: Option<&mut crat...
    method get_current_image (line 672) | pub fn get_current_image(&self) -> Result<&CachedData, io::Error> {
    method get_image_by_index (line 701) | pub fn get_image_by_index(&self, index: usize) -> Result<&CachedData, ...
    method get_initial_metadata (line 715) | pub fn get_initial_metadata(&self) -> Option<&ImageMetadata> {
    method get_next_cache_index (line 720) | pub fn get_next_cache_index(&self) -> isize {
    method get_next_image_to_load (line 725) | pub fn get_next_image_to_load(&self) -> usize {
    method get_prev_image_to_load (line 730) | pub fn get_prev_image_to_load(&self) -> usize {
    method is_some_at_index (line 735) | pub fn is_some_at_index(&self, index: usize) -> bool {
    method is_cache_index_within_bounds (line 744) | pub fn is_cache_index_within_bounds(&self, index: usize) -> bool {
    method is_next_cache_index_within_bounds (line 752) | pub fn is_next_cache_index_within_bounds(&self) -> bool {
    method is_prev_cache_index_within_bounds (line 760) | pub fn is_prev_cache_index_within_bounds(&self) -> bool {
    method is_image_index_within_bounds (line 770) | pub fn is_image_index_within_bounds(&self, index: isize) -> bool {
    method _is_operation_in_queues (line 776) | pub fn _is_operation_in_queues(&self, operation: LoadOperationType) ->...
    method is_operation_blocking (line 783) | pub fn is_operation_blocking(&self, operation: LoadOperationType) -> b...
    method is_blocking_loading_ops_in_queue (line 803) | pub fn is_blocking_loading_ops_in_queue(
  method default (line 308) | fn default() -> Self {
  function load_images_by_operation_slider (line 856) | pub fn load_images_by_operation_slider(
  function load_images_by_indices (line 932) | pub fn load_images_by_indices(
  function load_images_by_operation (line 995) | pub fn load_images_by_operation(
  function load_all_images_in_queue (line 1051) | pub fn load_all_images_in_queue(

FILE: src/cache/texture_cache.rs
  type TextureCache (line 14) | pub struct TextureCache {
    method new (line 24) | pub fn new() -> Self {
    method get_or_create_texture (line 34) | pub fn get_or_create_texture(
    method hash_image (line 145) | fn hash_image(&self, bytes: &[u8]) -> u64 {
    method maybe_cleanup (line 178) | fn maybe_cleanup(&mut self) {
    method _stats (line 190) | pub fn _stats(&self) -> (usize, usize) {
  method default (line 196) | fn default() -> Self {

FILE: src/coco/annotation_manager.rs
  type AnnotationManager (line 12) | pub struct AnnotationManager {
    method new (line 38) | pub fn new() -> Self {
    method load_coco_file (line 51) | pub fn load_coco_file(&mut self, json_path: PathBuf) -> Result<bool, S...
    method set_image_directory (line 88) | pub fn set_image_directory(
    method find_image_directory (line 128) | fn find_image_directory(
    method verify_images_in_directory (line 177) | fn verify_images_in_directory(
    method get_annotations (line 199) | pub fn get_annotations(&self, filename: &str) -> Option<&Vec<ImageAnno...
    method has_annotations (line 205) | pub fn has_annotations(&self) -> bool {
    method get_image_directory (line 211) | pub fn get_image_directory(&self) -> Option<&PathBuf> {
    method get_json_path (line 218) | pub fn get_json_path(&self) -> Option<&PathBuf> {
    method get_stats (line 224) | pub fn get_stats(&self) -> Option<DatasetStats> {
    method has_invalid_annotations (line 233) | pub fn has_invalid_annotations(&self, filename: &str) -> bool {
    method clear (line 244) | pub fn clear(&mut self) {
  type LoadedDataset (line 21) | struct LoadedDataset {
  method default (line 252) | fn default() -> Self {
  type DatasetStats (line 260) | pub struct DatasetStats {
  function test_annotation_manager_creation (line 271) | fn test_annotation_manager_creation() {

FILE: src/coco/overlay/bbox_overlay.rs
  function get_category_color (line 18) | fn get_category_color(category_id: u64) -> Color {
  function render_bbox_overlay (line 58) | pub fn render_bbox_overlay<'a>(
  type BBoxLabels (line 168) | struct BBoxLabels {
    method into_element (line 176) | fn into_element(annotations: Vec<ImageAnnotation>, image_size: (u32, u...
    method size (line 191) | fn size(&self) -> iced_core::Size<Length> {
    method layout (line 198) | fn layout(
    method draw (line 207) | fn draw(
  function from (line 307) | fn from(widget: BBoxLabels) -> Self {
  type SegmentationMasks (line 313) | struct SegmentationMasks {
    method new (line 322) | fn new(annotations: Vec<ImageAnnotation>, image_size: (u32, u32), zoom...
    method size (line 337) | fn size(&self) -> iced_core::Size<Length> {
    method layout (line 344) | fn layout(
    method draw (line 353) | fn draw(
    method draw_polygon (line 417) | fn draw_polygon<R: iced_core::Renderer>(
    method draw_triangle (line 461) | fn draw_triangle<R: iced_core::Renderer>(
  function from (line 496) | fn from(widget: SegmentationMasks) -> Self {
  function test_category_colors (line 506) | fn test_category_colors() {

FILE: src/coco/overlay/bbox_shader.rs
  type BBoxShader (line 17) | pub struct BBoxShader<Message> {
  function new (line 28) | pub fn new(annotations: Vec<ImageAnnotation>, image_size: (u32, u32), zo...
  function width (line 40) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 45) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  function calculate_scale (line 52) | fn calculate_scale(&self, bounds: Rectangle) -> (f32, f32, f32, f32) {
  type BBoxPrimitive (line 76) | pub struct BBoxPrimitive {
    method prepare (line 90) | fn prepare(
    method render (line 208) | fn render(
  type BBoxBufferCache (line 85) | struct BBoxBufferCache {
  type BBoxVertex (line 257) | struct BBoxVertex {
    constant ATTRIBS (line 263) | const ATTRIBS: [wgpu::VertexAttribute; 2] =
    method desc (line 266) | fn desc() -> wgpu::VertexBufferLayout<'static> {
  type BBoxPipeline (line 277) | struct BBoxPipeline {
    method new (line 282) | fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
  function get_category_color (line 331) | fn get_category_color(category_id: u64) -> Color {
  function size (line 373) | fn size(&self) -> Size<Length> {
  function layout (line 380) | fn layout(
  function draw (line 389) | fn draw(
  function from (line 420) | fn from(shader: BBoxShader<Message>) -> Self {

FILE: src/coco/overlay/mask_shader.rs
  constant MAX_TEXTURE_CACHE_SIZE (line 19) | const MAX_TEXTURE_CACHE_SIZE: usize = 200;
  type MaskShader (line 22) | pub struct MaskShader<Message> {
  function new (line 33) | pub fn new(annotations: Vec<ImageAnnotation>, image_size: (u32, u32), zo...
  function width (line 45) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 50) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  type MaskPrimitive (line 58) | pub struct MaskPrimitive {
    method prepare (line 111) | fn prepare(
    method render (line 380) | fn render(
  type MaskBufferCache (line 67) | struct MaskBufferCache {
  type CachedRenderState (line 75) | struct CachedRenderState {
  type QuadRenderData (line 84) | struct QuadRenderData {
  type MaskTextureCache (line 91) | type MaskTextureCache = HashMap<u64, CachedTexture>;
  type CachedTexture (line 93) | struct CachedTexture {
  function get_annotation_cache_id (line 103) | fn get_annotation_cache_id(ann: &ImageAnnotation) -> u64 {
  function create_quad_vertices (line 432) | fn create_quad_vertices(
  function evict_lru_textures (line 476) | fn evict_lru_textures(cache: &mut MaskTextureCache, count: usize) {
  type MaskVertex (line 491) | struct MaskVertex {
    constant ATTRIBS (line 497) | const ATTRIBS: [wgpu::VertexAttribute; 2] =
    method desc (line 500) | fn desc() -> wgpu::VertexBufferLayout<'static> {
  type MaskUniforms (line 512) | struct MaskUniforms {
  type MaskPipeline (line 518) | struct MaskPipeline {
    method new (line 525) | fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
  function get_category_color (line 621) | fn get_category_color(category_id: u64) -> Color {
  function size (line 662) | fn size(&self) -> Size<Length> {
  function layout (line 669) | fn layout(
  function draw (line 678) | fn draw(
  function from (line 707) | fn from(shader: MaskShader<Message>) -> Self {

FILE: src/coco/overlay/polygon_shader.rs
  type PolygonShader (line 19) | pub struct PolygonShader<Message> {
  function new (line 31) | pub fn new(annotations: Vec<ImageAnnotation>, image_size: (u32, u32), zo...
  function width (line 44) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 49) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  type PolygonPrimitive (line 57) | pub struct PolygonPrimitive {
    method prepare (line 88) | fn prepare(
    method render (line 393) | fn render(
    method to_ndc (line 436) | fn to_ndc(&self, point: (f32, f32), viewport_size: iced_core::Size<u32...
  type PolygonBufferCache (line 67) | struct PolygonBufferCache {
  type RlePolygonCache (line 72) | type RlePolygonCache = HashMap<u64, Vec<Vec<(f32, f32)>>>;
  type SimplificationSetting (line 75) | struct SimplificationSetting {
  function get_annotation_cache_id (line 80) | fn get_annotation_cache_id(ann: &ImageAnnotation) -> u64 {
  type PolygonVertex (line 447) | struct PolygonVertex {
    constant ATTRIBS (line 453) | const ATTRIBS: [wgpu::VertexAttribute; 2] =
    method desc (line 456) | fn desc() -> wgpu::VertexBufferLayout<'static> {
  type PolygonPipeline (line 467) | struct PolygonPipeline {
    method new (line 472) | fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
  function get_category_color (line 520) | fn get_category_color(category_id: u64) -> Color {
  function size (line 561) | fn size(&self) -> Size<Length> {
  function layout (line 568) | fn layout(
  function draw (line 577) | fn draw(
  function from (line 607) | fn from(shader: PolygonShader<Message>) -> Self {

FILE: src/coco/parser.rs
  type CocoDataset (line 10) | pub struct CocoDataset {
    method from_file (line 63) | pub fn from_file(path: &PathBuf) -> Result<Self, String> {
    method from_str (line 71) | pub fn from_str(content: &str) -> Result<Self, String> {
    method validate_and_clean (line 78) | pub fn validate_and_clean(&mut self) -> (usize, Vec<String>, std::coll...
    method is_coco_format (line 132) | pub fn is_coco_format(content: &str) -> bool {
    method build_image_annotation_map (line 146) | pub fn build_image_annotation_map(&self) -> HashMap<String, Vec<ImageA...
    method get_image_filenames (line 188) | pub fn get_image_filenames(&self) -> Vec<String> {
  type CocoImage (line 17) | pub struct CocoImage {
  type CocoAnnotation (line 27) | pub struct CocoAnnotation {
  type CocoSegmentation (line 42) | pub enum CocoSegmentation {
  type CocoRLE (line 48) | pub struct CocoRLE {
  type CocoCategory (line 54) | pub struct CocoCategory {
  type ImageAnnotation (line 195) | pub struct ImageAnnotation {
  type BoundingBox (line 204) | pub struct BoundingBox {
    method to_corners (line 213) | pub fn to_corners(self) -> (f32, f32, f32, f32) {
  function test_coco_detection (line 223) | fn test_coco_detection() {
  function test_coco_parsing (line 236) | fn test_coco_parsing() {

FILE: src/coco/rle_decoder.rs
  function decode_rle (line 11) | pub fn decode_rle(rle: &CocoRLE) -> Vec<u8> {
  function mask_to_polygons (line 60) | pub fn mask_to_polygons(mask: &[u8], width: usize, height: usize, simpli...
  function trace_contour (line 93) | fn trace_contour(
  function is_boundary_pixel (line 181) | fn is_boundary_pixel(mask: &[u8], width: usize, height: usize, x: usize,...
  function should_add_point (line 204) | fn should_add_point(contour: &[(f32, f32)], x: f32, y: f32) -> bool {
  function simplify_polygon (line 227) | fn simplify_polygon(points: &[(f32, f32)], epsilon: f32) -> Vec<(f32, f3...
  function douglas_peucker_recursive (line 237) | fn douglas_peucker_recursive(points: &[(f32, f32)], epsilon: f32, result...
  function perpendicular_distance (line 274) | fn perpendicular_distance(point: (f32, f32), line_start: (f32, f32), lin...
  function test_rle_decode (line 296) | fn test_rle_decode() {
  function test_perpendicular_distance (line 311) | fn test_perpendicular_distance() {
  function test_simplify_polygon (line 321) | fn test_simplify_polygon() {

FILE: src/coco/widget.rs
  type CocoLoadResult (line 23) | type CocoLoadResult = Result<(CocoDataset, PathBuf, usize, Vec<String>, ...
  type CocoMessage (line 27) | pub enum CocoMessage {
  method from (line 58) | fn from(coco_msg: CocoMessage) -> Self {
  function coco_badge (line 65) | pub fn coco_badge(has_annotations: bool, num_annotations: usize) -> Elem...
  function empty_badge (line 94) | pub fn empty_badge() -> Element<'static, Message, WinitTheme, Renderer> {
  function handle_coco_message (line 102) | pub fn handle_coco_message(
  function handle_keyboard_event (line 387) | pub fn handle_keyboard_event(

FILE: src/config.rs
  constant DEFAULT_CACHE_SIZE (line 6) | pub const DEFAULT_CACHE_SIZE: usize = 5;
  constant DEFAULT_MAX_LOADING_QUEUE_SIZE (line 7) | pub const DEFAULT_MAX_LOADING_QUEUE_SIZE: usize = 3;
  constant DEFAULT_MAX_BEING_LOADED_QUEUE_SIZE (line 8) | pub const DEFAULT_MAX_BEING_LOADED_QUEUE_SIZE: usize = 3;
  constant DEFAULT_WINDOW_WIDTH (line 9) | pub const DEFAULT_WINDOW_WIDTH: u32 = 1200;
  constant DEFAULT_WINDOW_HEIGHT (line 10) | pub const DEFAULT_WINDOW_HEIGHT: u32 = 800;
  constant DEFAULT_ATLAS_SIZE (line 11) | pub const DEFAULT_ATLAS_SIZE: u32 = 2048;
  constant DEFAULT_DOUBLE_CLICK_THRESHOLD_MS (line 12) | pub const DEFAULT_DOUBLE_CLICK_THRESHOLD_MS: u16 = 250;
  constant DEFAULT_ARCHIVE_CACHE_SIZE (line 13) | pub const DEFAULT_ARCHIVE_CACHE_SIZE: u64 = 200;
  constant DEFAULT_ARCHIVE_WARNING_THRESHOLD_MB (line 14) | pub const DEFAULT_ARCHIVE_WARNING_THRESHOLD_MB: u64 = 500;
  type Config (line 16) | pub struct Config {

FILE: src/exif_utils.rs
  function decode_with_exif_orientation (line 22) | pub fn decode_with_exif_orientation(bytes: &[u8]) -> Result<DynamicImage...
  function get_orientation_aware_dimensions (line 79) | pub fn get_orientation_aware_dimensions(bytes: &[u8]) -> (u32, u32) {

FILE: src/file_io.rs
  constant ALLOWED_EXTENSIONS (line 26) | const ALLOWED_EXTENSIONS: [&str; 15] = ["jpg", "jpeg", "png", "gif", "bm...
  function is_jp2_format (line 31) | fn is_jp2_format(bytes: &[u8]) -> bool {
  function decode_jp2 (line 54) | fn decode_jp2(bytes: &[u8]) -> Result<DynamicImage, std::io::ErrorKind> {
  function decode_image_from_bytes (line 73) | pub fn decode_image_from_bytes(bytes: &[u8]) -> Result<DynamicImage, std...
  function is_supported_extension (line 86) | fn is_supported_extension(ext: &str) -> bool {
  constant ALLOWED_EXTENSIONS_JP2 (line 101) | const ALLOWED_EXTENSIONS_JP2: [&str; 3] = ["jp2", "j2k", "j2c"];
  constant ALLOWED_COMPRESSED_FILES (line 102) | pub const ALLOWED_COMPRESSED_FILES: [&str; 3] = ["zip", "rar", "7z"];
  function supported_image (line 104) | pub fn supported_image(name: &str) -> bool {
  type Error (line 134) | pub enum Error {
  function get_filename (line 141) | pub fn get_filename(path: &str) -> Option<String> {
  function read_image_bytes (line 162) | pub fn read_image_bytes(path_source: &crate::cache::img_cache::PathSourc...
  function read_image_bytes_with_size (line 234) | pub fn read_image_bytes_with_size(path_source: &crate::cache::img_cache:...
  function get_file_size (line 311) | pub fn get_file_size(path_source: &crate::cache::img_cache::PathSource, ...
  function async_load_image (line 345) | pub async fn async_load_image(path: impl AsRef<Path>, operation: LoadOpe...
  function load_image_cpu_async (line 363) | async fn load_image_cpu_async(path_source: Option<crate::cache::img_cach...
  function load_image_gpu_async (line 428) | async fn load_image_gpu_async(
  function load_images_async (line 552) | pub async fn load_images_async(
  function pick_folder (line 608) | pub async fn pick_folder() -> Result<String, Error> {
  function pick_save_file (line 628) | pub async fn pick_save_file() -> Result<PathBuf, Error> {
  function pick_file (line 656) | pub async fn pick_file() -> Result<String, Error> {
  function show_memory_warning_sync (line 687) | pub fn show_memory_warning_sync(archive_size_mb: u64, available_gb: f64,...
  function empty_async_block (line 736) | pub async fn empty_async_block(operation: LoadOperation) -> Result<(Opti...
  function empty_async_block_vec (line 740) | pub async fn empty_async_block_vec(operation: LoadOperation, count: usiz...
  function _literal_empty_async_block (line 744) | pub async fn _literal_empty_async_block() -> Result<(), std::io::ErrorKi...
  function is_file (line 749) | pub fn is_file(path: &Path) -> bool {
  function is_directory (line 753) | pub fn is_directory(path: &Path) -> bool {
  function get_file_index (line 757) | pub fn get_file_index(files: &[PathBuf], file: &Path) -> Option<usize> {
  type ImageError (line 765) | pub enum ImageError {
    method fmt (line 772) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  function handle_fallback_for_single_file (line 785) | fn handle_fallback_for_single_file(
  function request_directory_access_and_retry (line 860) | fn request_directory_access_and_retry(
  function process_directory_entries (line 964) | fn process_directory_entries(
  function get_image_paths (line 991) | pub fn get_image_paths(directory_path: &Path) -> Result<Vec<PathBuf>, Im...
  function get_image_paths_standard (line 1006) | fn get_image_paths_standard(directory_path: &Path) -> Result<Vec<PathBuf...
  function get_image_paths_macos (line 1018) | fn get_image_paths_macos(directory_path: &Path) -> Result<Vec<PathBuf>, ...
  function handle_macos_sandbox_access (line 1042) | fn handle_macos_sandbox_access(
  function convert_file_paths_to_image_paths (line 1089) | fn convert_file_paths_to_image_paths(
  function enumerate_directory_async (line 1124) | pub async fn enumerate_directory_async(path: PathBuf) -> Result<Director...

FILE: src/loading_handler.rs
  function handle_load_operation_all (line 10) | pub fn handle_load_operation_all(
  function handle_load_pos_operation (line 148) | pub fn handle_load_pos_operation(

FILE: src/loading_status.rs
  type LoadingStatus (line 8) | pub struct LoadingStatus {
    method new (line 24) | pub fn new() -> Self {
    method print_queue (line 34) | pub fn print_queue(&self) {
    method enqueue_image_load (line 39) | pub fn enqueue_image_load(&mut self, operation: LoadOperation) {
    method reset_image_load_queue (line 43) | pub fn reset_image_load_queue(&mut self) {
    method enqueue_image_being_loaded (line 47) | pub fn enqueue_image_being_loaded(&mut self, operation: LoadOperation) {
    method reset_image_being_loaded_queue (line 51) | pub fn reset_image_being_loaded_queue(&mut self) {
    method reset_load_next_queue_items (line 55) | pub fn reset_load_next_queue_items(&mut self) {
    method reset_load_previous_queue_items (line 60) | pub fn reset_load_previous_queue_items(&mut self) {
    method is_next_image_index_in_queue (line 68) | pub fn is_next_image_index_in_queue(&self, _cache_index: usize, next_i...
    method are_next_image_indices_in_queue (line 97) | pub fn are_next_image_indices_in_queue(&self, next_image_indices: &[Op...
    method is_blocking_loading_ops_in_queue (line 132) | pub fn is_blocking_loading_ops_in_queue(&self, panes: &mut Vec<&mut Pa...
    method is_operation_in_queues (line 142) | pub fn is_operation_in_queues(&self, operation: LoadOperationType) -> ...
  method default (line 17) | fn default() -> Self {

FILE: src/logging.rs
  constant MAX_LOG_LINES (line 97) | const MAX_LOG_LINES: usize = 1000;
  type BufferLogger (line 109) | struct BufferLogger {
    method new (line 115) | fn new() -> Self {
    method log_to_buffer (line 121) | fn log_to_buffer(&self, message: &str, target: &str, line: Option<u32>...
    method dump_logs (line 141) | fn dump_logs(&self) -> Vec<String> {
    method get_shared_buffer (line 147) | fn get_shared_buffer(&self) -> Arc<Mutex<VecDeque<String>>> {
    method enabled (line 153) | fn enabled(&self, metadata: &Metadata) -> bool {
    method log (line 157) | fn log(&self, record: &Record) {
    method flush (line 169) | fn flush(&self) {}
  type CompositeLogger (line 173) | struct CompositeLogger {
    method enabled (line 179) | fn enabled(&self, metadata: &Metadata) -> bool {
    method log (line 183) | fn log(&self, record: &Record) {
    method flush (line 192) | fn flush(&self) {
  function setup_logger (line 200) | pub fn setup_logger(_app_name: &str) -> Arc<Mutex<VecDeque<String>>> {
  function get_log_directory (line 287) | pub fn get_log_directory(app_name: &str) -> PathBuf {
  function export_debug_logs (line 308) | pub fn export_debug_logs(app_name: &str, log_buffer: Arc<Mutex<VecDeque<...
  function export_and_open_debug_logs (line 409) | pub fn export_and_open_debug_logs(app_name: &str, log_buffer: Arc<Mutex<...
  function setup_panic_hook (line 438) | pub fn setup_panic_hook(app_name: &str, log_buffer: Arc<Mutex<VecDeque<S...
  function open_in_file_explorer (line 506) | pub fn open_in_file_explorer(path: &str) {
  function setup_stdout_capture (line 544) | pub fn setup_stdout_capture() -> Arc<Mutex<VecDeque<String>>> {
  function setup_stdout_capture (line 653) | pub fn setup_stdout_capture() -> Arc<Mutex<VecDeque<String>>> {
  function export_stdout_logs (line 677) | pub fn export_stdout_logs(app_name: &str, stdout_buffer: Arc<Mutex<VecDe...
  function export_and_open_all_logs (line 739) | pub fn export_and_open_all_logs(app_name: &str, log_buffer: Arc<Mutex<Ve...
  function write_crash_debug_log (line 807) | pub fn write_crash_debug_log(message: &str) {
  function write_immediate_crash_log (line 836) | pub fn write_immediate_crash_log(message: &str) {
  function get_crash_debug_logs_from_userdefaults (line 918) | pub fn get_crash_debug_logs_from_userdefaults() -> Vec<String> {
  function setup_signal_crash_handler (line 952) | pub fn setup_signal_crash_handler() {
  function setup_signal_crash_handler (line 1001) | pub fn setup_signal_crash_handler() {

FILE: src/macos_file_access.rs
  type SecurityScopedURLInfo (line 154) | struct SecurityScopedURLInfo {
  constant NSURL_BOOKMARK_CREATION_WITH_SECURITY_SCOPE (line 168) | const NSURL_BOOKMARK_CREATION_WITH_SECURITY_SCOPE: u64 = 1 << 11;
  constant NSURL_BOOKMARK_RESOLUTION_WITH_SECURITY_SCOPE (line 169) | const NSURL_BOOKMARK_RESOLUTION_WITH_SECURITY_SCOPE: u64 = 1 << 10;
  constant DISABLE_BOOKMARK_RESTORATION (line 172) | const DISABLE_BOOKMARK_RESTORATION: bool = false;
  constant DISABLE_BOOKMARK_CREATION (line 175) | const DISABLE_BOOKMARK_CREATION: bool = false;
  function write_security_bookmark_debug_log (line 181) | fn write_security_bookmark_debug_log(message: &str) {
  function make_bookmark_keys (line 209) | fn make_bookmark_keys(directory_path: &str) -> (
  function set_file_channel (line 231) | pub fn set_file_channel(sender: Sender<String>) {
  function store_security_scoped_url (line 240) | fn store_security_scoped_url(path: &str, url: Retained<NSURL>) {
  function store_security_scoped_url_with_info (line 245) | fn store_security_scoped_url_with_info(path: &str, url: Retained<NSURL>,...
  function get_security_scoped_path (line 263) | pub fn get_security_scoped_path(original_path: &str) -> Option<String> {
  function has_security_scoped_access (line 293) | pub fn has_security_scoped_access(path: &str) -> bool {
  function get_accessible_paths (line 306) | pub fn get_accessible_paths() -> Vec<String> {
  function cleanup_all_security_scoped_access (line 316) | pub fn cleanup_all_security_scoped_access() {
  function create_and_store_security_scoped_bookmark (line 346) | fn create_and_store_security_scoped_bookmark(url: &Retained<NSURL>, dire...
  function restore_directory_access_for_path (line 475) | pub fn restore_directory_access_for_path(directory_path: &str) -> bool {
  function request_directory_access_with_optimized_dialog (line 494) | fn request_directory_access_with_optimized_dialog(requested_path: &str) ...
  function request_parent_directory_permission_dialog (line 632) | pub fn request_parent_directory_permission_dialog(file_path: &str) -> bo...
  function restore_full_disk_access (line 645) | pub fn restore_full_disk_access() -> bool {
  function has_full_disk_access (line 651) | pub fn has_full_disk_access() -> bool {
  function handle_opened_file (line 672) | unsafe extern "C" fn handle_opened_file(
  function handle_opened_file_single (line 828) | unsafe extern "C" fn handle_opened_file_single(
  function handle_will_finish_launching (line 862) | unsafe extern "C" fn handle_will_finish_launching(
  function register_file_handler (line 880) | pub fn register_file_handler() {
  function get_resolved_security_scoped_url (line 955) | pub fn get_resolved_security_scoped_url(directory_path: &str) -> Option<...
  function read_directory_with_security_scoped_url (line 1233) | pub fn read_directory_with_security_scoped_url(directory_path: &str) -> ...
  function test_crash_logging_methods (line 1287) | pub fn test_crash_logging_methods() {

FILE: src/main.rs
  constant MAX_TEXTURE_SIZE (line 84) | const MAX_TEXTURE_SIZE: u32 = 8192;
  constant QUEUE_LOG_THRESHOLD (line 120) | const QUEUE_LOG_THRESHOLD: usize = 20;
  constant QUEUE_RESET_THRESHOLD (line 121) | const QUEUE_RESET_THRESHOLD: usize = 50;
  constant FULLSCREEN_TOP_ZONE_HEIGHT (line 125) | const FULLSCREEN_TOP_ZONE_HEIGHT: f64 = 200.0;
  constant FULLSCREEN_TOP_ZONE_HEIGHT (line 128) | const FULLSCREEN_TOP_ZONE_HEIGHT: f64 = 50.0;
  constant FULLSCREEN_BOTTOM_ZONE_HEIGHT (line 130) | const FULLSCREEN_BOTTOM_ZONE_HEIGHT: f64 = 100.0;
  function get_shared_log_buffer (line 144) | pub fn get_shared_log_buffer() -> Option<Arc<Mutex<VecDeque<String>>>> {
  function set_shared_log_buffer (line 148) | pub fn set_shared_log_buffer(buffer: Arc<Mutex<VecDeque<String>>>) {
  function get_shared_stdout_buffer (line 152) | pub fn get_shared_stdout_buffer() -> Option<Arc<Mutex<VecDeque<String>>>> {
  function set_shared_stdout_buffer (line 156) | pub fn set_shared_stdout_buffer(buffer: Arc<Mutex<VecDeque<String>>>) {
  function load_icon (line 160) | fn load_icon() -> Option<winit::window::Icon> {
  type Args (line 173) | struct Args {
  function register_font_manually (line 234) | fn register_font_manually(font_data: &'static [u8]) {
  type Control (line 248) | enum Control {
  type Event (line 261) | enum Event<T: 'static> {
  function monitor_message_queue (line 272) | fn monitor_message_queue(state: &mut program::State<DataViewer>) {
  type RendererRequest (line 292) | enum RendererRequest {
  function update_memory_usage (line 298) | fn update_memory_usage() {
  function main (line 305) | pub fn main() -> Result<(), winit::error::EventLoopError> {
  function track_render_cycle (line 1471) | fn track_render_cycle() {
  function track_async_delivery (line 1513) | fn track_async_delivery() {

FILE: src/menu.rs
  type PaneLayout (line 40) | pub enum PaneLayout {
  constant MENU_FONT_SIZE (line 45) | const MENU_FONT_SIZE : u16 = 16;
  constant MENU_ITEM_FONT_SIZE (line 46) | const MENU_ITEM_FONT_SIZE : u16 = 14;
  constant _CARET_PATH (line 47) | const _CARET_PATH : &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/...
  constant MENU_PADDING_VERTICAL (line 50) | const MENU_PADDING_VERTICAL: u16 = 4;
  constant MENU_PADDING_HORIZONTAL (line 51) | const MENU_PADDING_HORIZONTAL: u16 = 8;
  constant MENU_BAR_HEIGHT (line 54) | pub const MENU_BAR_HEIGHT: f32 = (MENU_FONT_SIZE + MENU_PADDING_VERTICAL...
  function button_style (line 56) | pub fn button_style(theme: &WinitTheme, status: button::Status, style_ty...
  function _transparent_style (line 115) | fn _transparent_style(theme: &WinitTheme, status: button::Status) -> Sty...
  function labeled_style (line 119) | fn labeled_style(theme: &WinitTheme, status: button::Status) -> Style {
  function default_style (line 123) | fn default_style(_theme: &WinitTheme, _status: button::Status) -> Style {
  function base_button (line 127) | fn base_button<'a>(
  function labeled_button (line 136) | fn labeled_button<'a>(
  function labeled_button_maybe (line 152) | fn labeled_button_maybe<'a>(
  function nothing_button (line 168) | fn nothing_button<'a>(label: &'a str, text_size: u16) -> button::Button<...
  function submenu_button (line 177) | fn submenu_button(label: &str, text_size: u16) -> button::Button<'_, Mes...
  function menu_3 (line 196) | pub fn menu_3<'a>(app: &DataViewer) -> Menu<'a, Message, WinitTheme, Ren...
  function menu_1 (line 339) | pub fn menu_1<'a>(app: &DataViewer) -> Menu<'a, Message, WinitTheme, Ren...
  function menu_help (line 423) | pub fn menu_help<'a>(_app: &DataViewer) -> Menu<'a, Message, WinitTheme,...
  function build_menu (line 437) | pub fn build_menu(app: &DataViewer) -> MenuBar<'_, Message, WinitTheme, ...

FILE: src/navigation_keyboard.rs
  function init_is_next_image_loaded (line 36) | fn init_is_next_image_loaded(panes: &mut Vec<&mut Pane>, _pane_layout: &...
  function init_is_prev_image_loaded (line 42) | fn init_is_prev_image_loaded(panes: &mut Vec<&mut Pane>, _pane_layout: &...
  function are_all_next_images_loaded (line 52) | fn are_all_next_images_loaded(panes: &Vec<&mut Pane>, is_slider_dual: bo...
  function are_all_prev_images_loaded (line 63) | fn are_all_prev_images_loaded(panes: &Vec<&mut Pane>, is_slider_dual: bo...
  function are_panes_cached_next (line 74) | pub fn are_panes_cached_next(panes: &Vec<&mut Pane>, _pane_layout: &Pane...
  function are_panes_cached_prev (line 80) | pub fn are_panes_cached_prev(panes: &Vec<&mut Pane>, _pane_layout: &Pane...
  function render_next_image_all (line 88) | pub fn render_next_image_all(panes: &mut Vec<&mut Pane>, _pane_layout: &...
  function render_prev_image_all (line 138) | pub fn render_prev_image_all(panes: &mut Vec<&mut Pane>, _pane_layout: &...
  function load_next_images_all (line 199) | pub fn load_next_images_all(
  function calculate_loading_conditions_for_next (line 260) | fn calculate_loading_conditions_for_next(
  function get_target_indices_for_next (line 303) | fn get_target_indices_for_next(panes: &mut Vec<&mut Pane>) -> Vec<Option...
  function load_prev_images_all (line 317) | pub fn load_prev_images_all(
  function calculate_loading_conditions_for_previous (line 367) | fn calculate_loading_conditions_for_previous(
  function should_enqueue_loading (line 410) | fn should_enqueue_loading(
  function get_target_indices_for_previous (line 423) | fn get_target_indices_for_previous(panes: &mut Vec<&mut Pane>) -> Vec<Op...
  function move_right_all (line 443) | pub fn move_right_all(
  function move_left_all (line 581) | pub fn move_left_all(

FILE: src/navigation_slider.rs
  constant _THROTTLE_INTERVAL_MS (line 51) | const _THROTTLE_INTERVAL_MS: u64 = 100;
  function load_full_res_image (line 53) | fn load_full_res_image(
  function get_loading_tasks_slider (line 222) | fn get_loading_tasks_slider(
  function load_remaining_images (line 306) | pub fn load_remaining_images(
  function load_initial_neighbors (line 386) | pub fn load_initial_neighbors(
  function create_async_image_widget_task (line 421) | pub async fn create_async_image_widget_task(
  function update_pos (line 501) | pub fn update_pos(
  function load_current_slider_image (line 643) | fn load_current_slider_image(pane: &mut pane::Pane, pos: usize) -> Resul...
  function load_current_slider_image_widget (line 728) | fn load_current_slider_image_widget(pane: &mut pane::Pane, pos: usize ) ...

FILE: src/pane.rs
  type Pane (line 45) | pub struct Pane {
    method new (line 130) | pub fn new(
    method print_state (line 182) | pub fn print_state(&self) {
    method reset_state (line 189) | pub fn reset_state(&mut self) {
    method setup_scene_for_image (line 228) | fn setup_scene_for_image(&mut self, cached_data: &CachedData) {
    method resize_panes (line 258) | pub fn resize_panes(panes: &mut Vec<Pane>, new_size: usize) {
    method is_pane_cached_next (line 292) | pub fn is_pane_cached_next(&self) -> bool {
    method is_pane_cached_prev (line 301) | pub fn is_pane_cached_prev(&self) -> bool {
    method render_next_image (line 309) | pub fn render_next_image(&mut self, pane_layout: &PaneLayout, is_slide...
    method render_prev_image (line 361) | pub fn render_prev_image(&mut self, pane_layout: &PaneLayout, is_slide...
    method initialize_dir_path (line 421) | pub fn initialize_dir_path(
    method initialize_with_paths (line 679) | pub fn initialize_with_paths(
    method build_ui_container (line 772) | pub fn build_ui_container(&self, use_slider_image_for_render: bool, is...
  method default (line 86) | fn default() -> Self {
  function get_pane_with_largest_dir_size (line 847) | pub fn get_pane_with_largest_dir_size(panes: &Vec<&mut Pane>) -> isize {
  function get_master_slider_value (line 859) | pub fn get_master_slider_value(panes: &[&mut Pane],
  function read_zip_path (line 874) | fn read_zip_path(path: &PathBuf, file_paths: &mut Vec<PathSource>, archi...
  function read_rar_path (line 916) | fn read_rar_path(path: &PathBuf, file_paths: &mut Vec<PathSource>, archi...
  function read_7z_path (line 967) | fn read_7z_path(path: &PathBuf, file_paths: &mut Vec<PathSource>, archiv...

FILE: src/replay.rs
  type OutputFormat (line 6) | pub enum OutputFormat {
  type NavigationMode (line 14) | pub enum NavigationMode {
  type ReplayConfig (line 21) | pub struct ReplayConfig {
  type ReplayDirection (line 40) | pub enum ReplayDirection {
  type ReplayState (line 47) | pub enum ReplayState {
  type ReplayMetrics (line 57) | pub struct ReplayMetrics {
    method new (line 79) | pub fn new(directory_path: PathBuf, direction: ReplayDirection) -> Self {
    method add_sample (line 103) | pub fn add_sample(&mut self, ui_fps: f32, image_fps: f32, memory_mb: f...
    method finalize (line 127) | pub fn finalize(&mut self) {
    method duration (line 150) | pub fn duration(&self) -> Duration {
    method print_summary (line 154) | pub fn print_summary(&self) {
  type ReplayController (line 179) | pub struct ReplayController {
    method new (line 199) | pub fn new(config: ReplayConfig) -> Self {
    method start (line 215) | pub fn start(&mut self) {
    method is_active (line 231) | pub fn is_active(&self) -> bool {
    method is_completed (line 235) | pub fn is_completed(&self) -> bool {
    method get_current_directory (line 239) | pub fn get_current_directory(&self) -> Option<&PathBuf> {
    method on_navigation_performed (line 251) | pub fn on_navigation_performed(&mut self) {
    method set_at_boundary (line 258) | pub fn set_at_boundary(&mut self, at_boundary: bool) {
    method on_directory_loaded (line 269) | pub fn on_directory_loaded(&mut self, directory_index: usize) {
    method on_ready_to_navigate (line 281) | pub fn on_ready_to_navigate(&mut self) {
    method set_image_count (line 316) | pub fn set_image_count(&mut self, count: usize) {
    method update_metrics (line 329) | pub fn update_metrics(&mut self, ui_fps: f32, image_fps: f32, memory_m...
    method finalize_current_metrics (line 345) | fn finalize_current_metrics(&mut self) {
    method should_transition (line 357) | fn should_transition(&self, elapsed: Duration) -> bool {
    method update (line 363) | pub fn update(&mut self) -> Option<ReplayAction> {
    method navigate_slider_right (line 438) | fn navigate_slider_right(&mut self) -> Option<ReplayAction> {
    method navigate_slider_left (line 456) | fn navigate_slider_left(&mut self) -> Option<ReplayAction> {
    method advance_to_next_directory (line 472) | fn advance_to_next_directory(&mut self, current_directory_index: usize...
    method print_final_summary (line 502) | pub fn print_final_summary(&self) {
    method export_metrics_to_file (line 530) | fn export_metrics_to_file(&self, output_file: &PathBuf) -> Result<(), ...
    method export_text (line 542) | fn export_text(&self, file: &mut std::fs::File) -> Result<(), std::io:...
    method export_json (line 571) | fn export_json(&self, file: &mut std::fs::File) -> Result<(), std::io:...
    method export_markdown (line 613) | fn export_markdown(&self, file: &mut std::fs::File) -> Result<(), std:...
  type ReplayAction (line 665) | pub enum ReplayAction {

FILE: src/selection_manager.rs
  type ImageMark (line 12) | pub enum ImageMark {
  type SelectionState (line 21) | pub struct SelectionState {
    method new (line 31) | pub fn new(directory_path: String) -> Self {
    method mark_image (line 41) | pub fn mark_image(&mut self, filename: &str, mark: ImageMark) {
    method get_mark (line 49) | pub fn get_mark(&self, filename: &str) -> ImageMark {
    method toggle_selected (line 54) | pub fn toggle_selected(&mut self, filename: &str) {
    method toggle_excluded (line 65) | pub fn toggle_excluded(&mut self, filename: &str) {
    method clear_mark (line 76) | pub fn clear_mark(&mut self, filename: &str) {
    method selected_count (line 86) | pub fn selected_count(&self) -> usize {
    method excluded_count (line 92) | pub fn excluded_count(&self) -> usize {
    method marked_count (line 98) | pub fn marked_count(&self) -> usize {
  type SelectionManager (line 104) | pub struct SelectionManager {
    method new (line 110) | pub fn new() -> Self {
    method get_selections_dir (line 127) | fn get_selections_dir() -> PathBuf {
    method get_selection_file_path (line 134) | fn get_selection_file_path(&self, dir_path: &str) -> PathBuf {
    method load_for_directory (line 142) | pub fn load_for_directory(&mut self, dir_path: &str) -> Result<(), std...
    method save (line 177) | pub fn save(&mut self) -> Result<(), std::io::Error> {
    method export_to_file (line 215) | pub fn export_to_file(&self, export_path: &Path) -> Result<(), std::io...
    method current_state_mut (line 233) | pub fn current_state_mut(&mut self) -> Option<&mut SelectionState> {
    method current_state (line 239) | pub fn current_state(&self) -> Option<&SelectionState> {
    method mark_image (line 245) | pub fn mark_image(&mut self, filename: &str, mark: ImageMark) {
    method toggle_selected (line 252) | pub fn toggle_selected(&mut self, filename: &str) {
    method toggle_excluded (line 259) | pub fn toggle_excluded(&mut self, filename: &str) {
    method clear_mark (line 266) | pub fn clear_mark(&mut self, filename: &str) {
    method get_mark (line 273) | pub fn get_mark(&self, filename: &str) -> ImageMark {
  method default (line 282) | fn default() -> Self {
  function test_selection_state (line 292) | fn test_selection_state() {

FILE: src/settings.rs
  type UserSettings (line 11) | pub struct UserSettings {
    method settings_path (line 260) | pub fn settings_path() -> PathBuf {
    method load (line 270) | pub fn load(custom_path: Option<&str>) -> Self {
    method save (line 310) | pub fn save(&self) -> Result<(), String> {
    method update_yaml_values (line 348) | fn update_yaml_values(&self, yaml_content: &str) -> String {
    method get_comment_for_key (line 430) | fn get_comment_for_key(key: &str) -> String {
    method replace_yaml_value_or_track (line 451) | fn replace_yaml_value_or_track(yaml: &str, key: &str, new_value: &str,...
    method to_yaml_with_comments (line 475) | fn to_yaml_with_comments(&self) -> String {
    method get_cache_strategy (line 607) | pub fn get_cache_strategy(&self) -> CacheStrategy {
    method get_compression_strategy (line 619) | pub fn get_compression_strategy(&self) -> CompressionStrategy {
  type CocoMaskRenderMode (line 121) | pub enum CocoMaskRenderMode {
  method default (line 129) | fn default() -> Self {
  type SpinnerLocation (line 136) | pub enum SpinnerLocation {
  method default (line 146) | fn default() -> Self {
  type WindowState (line 152) | pub enum WindowState {
  function default_show_footer (line 160) | fn default_show_footer() -> bool {
  function default_synced_zoom (line 164) | fn default_synced_zoom() -> bool {
  function default_cache_strategy (line 168) | fn default_cache_strategy() -> String {
  function default_compression_strategy (line 172) | fn default_compression_strategy() -> String {
  function default_show_copy_buttons (line 176) | fn default_show_copy_buttons() -> bool {
  function default_show_metadata (line 180) | fn default_show_metadata() -> bool {
  function default_cache_size (line 185) | fn default_cache_size() -> usize {
  function default_max_loading_queue_size (line 189) | fn default_max_loading_queue_size() -> usize {
  function default_max_being_loaded_queue_size (line 193) | fn default_max_being_loaded_queue_size() -> usize {
  function default_window_width (line 197) | fn default_window_width() -> u32 {
  function default_window_height (line 201) | fn default_window_height() -> u32 {
  function default_atlas_size (line 205) | fn default_atlas_size() -> u32 {
  function default_double_click_threshold_ms (line 209) | fn default_double_click_threshold_ms() -> u16 {
  function default_archive_cache_size (line 213) | fn default_archive_cache_size() -> u64 {
  function default_archive_warning_threshold_mb (line 217) | fn default_archive_warning_threshold_mb() -> u64 {
  method default (line 222) | fn default() -> Self {

FILE: src/settings_modal.rs
  function view_settings_modal (line 16) | pub fn view_settings_modal<'a>(viewer: &'a DataViewer) -> Element<'a, Me...
  function view_general_tab (line 157) | fn view_general_tab<'a>(viewer: &'a DataViewer) -> Element<'a, Message, ...
  function labeled_text_input_row (line 373) | fn labeled_text_input_row<'a>(
  function view_advanced_tab (line 393) | fn view_advanced_tab<'a>(viewer: &'a DataViewer) -> Element<'a, Message,...
  function view_coco_tab (line 475) | fn view_coco_tab<'a>(viewer: &'a DataViewer) -> Element<'a, Message, Win...

FILE: src/ui.rs
  function icon (line 50) | fn icon<'a, Message>(codepoint: char) -> Element<'a, Message, WinitTheme...
  function file_copy_icon (line 59) | fn file_copy_icon<'a, Message>() -> Element<'a, Message, WinitTheme, Ren...
  function folder_copy_icon (line 63) | fn folder_copy_icon<'a, Message>() -> Element<'a, Message, WinitTheme, R...
  function image_copy_icon (line 67) | fn image_copy_icon<'a, Message>() -> Element<'a, Message, WinitTheme, Re...
  type FooterOptions (line 73) | pub struct FooterOptions {
    method new (line 79) | pub fn new() -> Self {
    method with_mark (line 87) | pub fn with_mark(mut self, mark: crate::selection_manager::ImageMark) ...
    method with_coco (line 94) | pub fn with_coco(mut self, has_annotations: bool, num_annotations: usi...
    method get_mark_badge (line 100) | pub fn get_mark_badge(self) -> Element<'static, Message, WinitTheme, R...
    method get_coco_badge (line 114) | pub fn get_coco_badge(self) -> Element<'static, Message, WinitTheme, R...
  type ResponsiveFooterState (line 129) | struct ResponsiveFooterState {
  function measure_text_width (line 138) | fn measure_text_width(text: &str) -> f32 {
  function get_responsive_footer_state (line 174) | fn get_responsive_footer_state(
  function get_footer (line 312) | pub fn get_footer(
  function build_ui (line 497) | pub fn build_ui(app: &DataViewer) -> Container<'_, Message, WinitTheme, ...
  function build_ui_dual_pane_slider1 (line 1009) | pub fn build_ui_dual_pane_slider1(
  function build_ui_dual_pane_slider2 (line 1046) | pub fn build_ui_dual_pane_slider2<'a>(
  function get_fps_container (line 1196) | fn get_fps_container(app: &DataViewer) -> Container<'_, Message, WinitTh...

FILE: src/utils/mem.rs
  function update_memory_usage (line 11) | pub fn update_memory_usage() -> u64 {
  function try_get_memory_usage (line 84) | fn try_get_memory_usage() -> Option<u64> {
  function log_memory (line 94) | pub fn log_memory(label: &str) {
  function check_memory_for_archive (line 107) | pub fn check_memory_for_archive(archive_size_mb: u64) -> (f64, bool) {

FILE: src/utils/save.rs
  function extract_gpu_image (line 7) | pub(crate) fn extract_gpu_image(app: &mut DataViewer, texture: &Arc<Text...

FILE: src/utils/timing.rs
  type TimingStats (line 6) | pub struct TimingStats {
    method new (line 13) | pub fn new(name: &str) -> Self {
    method add_measurement (line 21) | pub fn add_measurement(&mut self, duration: Duration) {
    method average_ms (line 34) | pub fn average_ms(&self) -> f64 {
  type ScopedTimer (line 43) | pub struct ScopedTimer<'a> {
  function new (line 50) | pub fn new(stats: &'a mut TimingStats) -> Self {
  method drop (line 59) | fn drop(&mut self) {

FILE: src/widgets/circular.rs
  constant MIN_ANGLE (line 23) | const MIN_ANGLE: Radians = Radians(PI / 8.0);
  constant WRAP_ANGLE (line 24) | const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0);
  type State (line 27) | struct State {
  method default (line 32) | fn default() -> Self {
  type Circular (line 40) | pub struct Circular<'a> {
  function new (line 51) | pub fn new() -> Self {
  function size (line 64) | pub fn size(mut self, size: f32) -> Self {
  method default (line 73) | fn default() -> Self {
  function tag (line 82) | fn tag(&self) -> tree::Tag {
  function state (line 86) | fn state(&self) -> tree::State {
  function size (line 90) | fn size(&self) -> Size<Length> {
  function layout (line 97) | fn layout(
  function on_event (line 106) | fn on_event(
  function draw (line 121) | fn draw(
  function from (line 211) | fn from(circular: Circular<'a>) -> Self {
  function mini_circular (line 217) | pub fn mini_circular<'a, Message: Clone + 'a>() -> Element<'a, Message, ...

FILE: src/widgets/dualslider.rs
  type DualSlider (line 86) | pub struct DualSlider<'a, T, Message, Theme = crate::Theme>
  constant DEFAULT_HEIGHT (line 111) | pub const DEFAULT_HEIGHT: f32 = 22.0;
  function new (line 121) | pub fn new<F, G>(range: RangeInclusive<T>, value: T, pane_index: isize, ...
  function default (line 156) | pub fn default(mut self, default: impl Into<T>) -> Self {
  function on_release (line 167) | pub fn on_release<F>(mut self, on_release: F) -> Self
  function width (line 176) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 182) | pub fn height(mut self, height: impl Into<Pixels>) -> Self {
  function step (line 188) | pub fn step(mut self, step: impl Into<T>) -> Self {
  function shift_step (line 196) | pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
  function style (line 203) | pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> ...
  function class (line 213) | pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
  function tag (line 227) | fn tag(&self) -> tree::Tag {
  function state (line 231) | fn state(&self) -> tree::State {
  function size (line 235) | fn size(&self) -> Size<Length> {
  function layout (line 242) | fn layout(
  function on_event (line 251) | fn on_event(
  function draw (line 419) | fn draw(
  function mouse_interaction (line 521) | fn mouse_interaction(
  function from (line 552) | fn from(
  type Status (line 562) | pub enum Status {
  type State (line 573) | pub struct State {
    method new (line 580) | pub fn new() -> State {
  type Style (line 588) | pub struct Style {
    method with_circular_handle (line 598) | pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Se...
  type Rail (line 608) | pub struct Rail {
  type Handle (line 619) | pub struct Handle {
  type HandleShape (line 632) | pub enum HandleShape {
  type Catalog (line 648) | pub trait Catalog: Sized {
    method default (line 653) | fn default<'a>() -> Self::Class<'a>;
    method style (line 656) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
    type Class (line 663) | type Class<'a> = StyleFn<'a, Self>;
    method default (line 665) | fn default<'a>() -> Self::Class<'a> {
    method style (line 669) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
  type StyleFn (line 660) | pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
  function default (line 675) | pub fn default(theme: &Theme, status: Status) -> Style {

FILE: src/widgets/easing.rs
  type Easing (line 14) | pub struct Easing {
    method builder (line 20) | pub fn builder() -> Builder {
    method y_at_x (line 24) | pub fn y_at_x(&self, x: f32) -> f32 {
  type Builder (line 35) | pub struct Builder(NoAttributes<BuilderImpl>);
    method new (line 38) | pub fn new() -> Self {
    method cubic_bezier_to (line 46) | pub fn cubic_bezier_to(
    method build (line 61) | pub fn build(mut self) -> Easing {
    method point (line 71) | fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> {
  method default (line 78) | fn default() -> Self {

FILE: src/widgets/modal.rs
  function modal (line 8) | pub fn modal<'a, Message>(

FILE: src/widgets/selection_widget.rs
  type SelectionMessage (line 22) | pub enum SelectionMessage {
  method from (line 32) | fn from(msg: SelectionMessage) -> Self {
  function mark_badge (line 38) | pub fn mark_badge(mark: ImageMark) -> Element<'static, Message, WinitThe...
  function empty_badge (line 84) | pub fn empty_badge() -> Element<'static, Message, WinitTheme, Renderer> {
  function handle_selection_message (line 92) | pub fn handle_selection_message(
  function handle_keyboard_event (line 185) | pub fn handle_keyboard_event(

FILE: src/widgets/shader/cpu_scene.rs
  type CpuPipelineRegistry (line 28) | pub struct CpuPipelineRegistry {
  type CpuScene (line 33) | pub struct CpuScene {
    method new (line 42) | pub fn new(image_bytes: Vec<u8>, use_cached_texture: bool) -> Self {
    method update_image (line 71) | pub fn update_image(&mut self, new_image_bytes: Vec<u8>) {
    method ensure_texture (line 85) | pub fn ensure_texture(&mut self, device: &Arc<wgpu::Device>, queue: &A...
    type State (line 353) | type State = ();
    type Primitive (line 354) | type Primitive = CpuPrimitive;
    method draw (line 356) | fn draw(
  type CpuPrimitive (line 210) | pub struct CpuPrimitive {
    method new (line 219) | pub fn new(
    method prepare (line 237) | fn prepare(
    method render (line 319) | fn render(

FILE: src/widgets/shader/image_shader.rs
  type ImageShader (line 22) | pub struct ImageShader<Message> {
  function new (line 49) | pub fn new(scene: Option<&Scene>) -> Self {
  function with_zoom_state (line 96) | pub fn with_zoom_state(mut self, scale: f32, offset: Vector) -> Self {
  function width (line 103) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 109) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  function content_fit (line 115) | pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
  function update_scene (line 121) | pub fn update_scene(&mut self, new_scene: Scene) {
  function max_scale (line 129) | pub fn max_scale(mut self, max_scale: f32) -> Self {
  function min_scale (line 137) | pub fn min_scale(mut self, min_scale: f32) -> Self {
  function scale_step (line 145) | pub fn scale_step(mut self, scale_step: f32) -> Self {
  function double_click_threshold_ms (line 153) | pub fn double_click_threshold_ms(mut self, threshold_ms: u16) -> Self {
  function _calculate_layout (line 159) | fn _calculate_layout(&self, bounds: Rectangle) -> Rectangle {
  function horizontal_split (line 226) | pub fn horizontal_split(mut self, is_horizontal: bool) -> Self {
  type ImageShaderState (line 234) | pub struct ImageShaderState {
    method new (line 245) | pub fn new() -> Self {
    method is_cursor_grabbed (line 257) | pub fn is_cursor_grabbed(&self) -> bool {
    method offset (line 262) | fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
  type ImagePrimitive (line 276) | pub struct ImagePrimitive {
    method prepare (line 287) | fn prepare(
    method render (line 376) | fn render(
  type PipelineRegistry (line 442) | pub struct PipelineRegistry {
    method insert (line 460) | pub fn insert(&mut self, key: String, pipeline: TexturePipeline) {
    method _get (line 482) | pub fn _get(&mut self, key: &str) -> Option<&TexturePipeline> {
    method get_mut (line 496) | pub fn get_mut(&mut self, key: &str) -> Option<&mut TexturePipeline> {
    method contains_key (line 510) | pub fn contains_key(&self, key: &str) -> bool {
    method get_ref (line 515) | pub fn get_ref(&self, key: &str) -> Option<&TexturePipeline> {
  method default (line 449) | fn default() -> Self {
  function tag (line 526) | fn tag(&self) -> tree::Tag {
  function state (line 530) | fn state(&self) -> tree::State {
  function diff (line 541) | fn diff(&self, tree: &mut Tree) {
  function size (line 552) | fn size(&self) -> Size<Length> {
  function layout (line 559) | fn layout(
  function on_event (line 568) | fn on_event(
  function mouse_interaction (line 803) | fn mouse_interaction(
  function draw (line 828) | fn draw(
  function from (line 886) | fn from(shader: ImageShader<Message>) -> Self {
  function calculate_scaled_size (line 893) | fn calculate_scaled_size(&self, bounds_size: Size, scale: f32) -> Size {
  function calculate_content_bounds (line 935) | fn calculate_content_bounds(&self, bounds: Rectangle, scaled_size: Size,...
  function with_interaction_state (line 953) | pub fn with_interaction_state(mut self, mouse_wheel_zoom: bool, ctrl_pre...
  function pane_index (line 961) | pub fn pane_index(mut self, pane_index: usize) -> Self {
  function on_zoom_change (line 968) | pub fn on_zoom_change<F>(mut self, callback: F) -> Self
  function image_index (line 977) | pub fn image_index(mut self, image_index: usize) -> Self {
  function use_nearest_filter (line 983) | pub fn use_nearest_filter(mut self, use_nearest: bool) -> Self {

FILE: src/widgets/shader/scene.rs
  type Scene (line 20) | pub enum Scene {
    method new (line 32) | pub fn new(initial_image: Option<&CachedData>) -> Self {
    method get_texture (line 49) | pub fn get_texture(&self) -> Option<&Arc<wgpu::Texture>> {
    method update_texture (line 56) | pub fn update_texture(&mut self, texture: Arc<wgpu::Texture>) {
    method update_cpu_image (line 65) | pub fn update_cpu_image(&mut self, image_bytes: Vec<u8>) {
    method has_valid_dimensions (line 71) | pub fn has_valid_dimensions(&self) -> bool {
    method ensure_texture (line 78) | pub fn ensure_texture(&mut self, device: &Arc<wgpu::Device>, queue: &A...
    type State (line 91) | type State = ();
    type Primitive (line 92) | type Primitive = ScenePrimitive;
    method draw (line 94) | fn draw(
  type ScenePrimitive (line 26) | pub enum ScenePrimitive {
    method prepare (line 114) | fn prepare(
    method render (line 133) | fn render(
  type Primitive (line 153) | pub struct Primitive {
    method new (line 161) | pub fn new(
    method prepare (line 175) | fn prepare(
    method render (line 216) | fn render(

FILE: src/widgets/shader/texture_pipeline.rs
  type TexturePipeline (line 16) | pub struct TexturePipeline {
    method new (line 26) | pub fn new(
    method update_texture (line 191) | pub fn update_texture(
    method render (line 237) | pub fn render(
    method update_vertices (line 272) | pub fn update_vertices(
    method update_screen_uniforms (line 281) | pub fn update_screen_uniforms(

FILE: src/widgets/shader/texture_scene.rs
  type TextureScene (line 17) | pub struct TextureScene {
    method new (line 26) | pub fn new(initial_image: Option<&CachedData>) -> Self {
    method width (line 47) | pub fn width(mut self, width: impl Into<Length>) -> Self {
    method height (line 52) | pub fn height(mut self, height: impl Into<Length>) -> Self {
    method content_fit (line 57) | pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
    method update_texture (line 62) | pub fn update_texture(&mut self, new_texture: Arc<wgpu::Texture>) {
    type State (line 213) | type State = ();
    type Primitive (line 214) | type Primitive = TexturePrimitive;
    method draw (line 216) | fn draw(
  type TexturePrimitive (line 74) | pub struct TexturePrimitive {
    method new (line 82) | pub fn new(
    method placeholder (line 96) | pub fn placeholder(_bounds: Rectangle) -> Self {
    method prepare (line 110) | fn prepare(
    method render (line 183) | fn render(
  type PipelineRegistry (line 105) | pub struct PipelineRegistry {

FILE: src/widgets/split.rs
  constant DIVIDER_HITBOX_EXPANSION (line 73) | pub const DIVIDER_HITBOX_EXPANSION: f32 = 10.0;
  type Split (line 94) | pub struct Split<'a, Message, Theme, Renderer>
  function new (line 161) | pub fn new<A, B, F, G, H, I>(
  function double_click_threshold_ms (line 218) | pub fn double_click_threshold_ms(mut self, threshold_ms: u16) -> Self {
  function padding (line 225) | pub fn padding(mut self, padding: f32) -> Self {
  function spacing (line 233) | pub fn spacing(mut self, spacing: f32) -> Self {
  function width (line 240) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 247) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  function min_size_first (line 254) | pub fn min_size_first(mut self, size: u16) -> Self {
  function min_size_second (line 261) | pub fn min_size_second(mut self, size: u16) -> Self {
  function style (line 268) | pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> ...
  function class (line 279) | pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
  function tag (line 292) | fn tag(&self) -> tree::Tag {
  function state (line 296) | fn state(&self) -> tree::State {
  function children (line 300) | fn children(&self) -> Vec<Tree> {
  function diff (line 304) | fn diff(&self, tree: &mut Tree) {
  function size (line 308) | fn size(&self) -> Size<Length> {
  function layout (line 312) | fn layout(
  function on_event (line 340) | fn on_event(
  function mouse_interaction (line 534) | fn mouse_interaction(
  function draw (line 585) | fn draw(
  function operate (line 808) | fn operate<'b>(
  function overlay (line 831) | fn overlay<'b>(
  function is_cursor_within_bounds (line 869) | fn is_cursor_within_bounds(
  type State (line 890) | pub struct State {
    method new (line 904) | pub const fn new() -> Self {
  function horizontal_split (line 915) | pub fn horizontal_split<'a, Message, Theme, Renderer>(
  function vertical_split (line 1016) | pub fn vertical_split<'a, Message, Theme, Renderer>(
  function from (line 1155) | fn from(split_pane: Split<'a, Message, Theme, Renderer>) -> Self {
  type Axis (line 1162) | pub enum Axis {
  method default (line 1170) | fn default() -> Self {
  type Status (line 1177) | pub enum Status {
  type Style (line 1190) | pub struct Style {
    method with_background (line 1213) | pub fn with_background(self, background: impl Into<Background>) -> Self {
  method default (line 1222) | fn default() -> Self {
  type Catalog (line 1243) | pub trait Catalog {
    method default (line 1248) | fn default<'a>() -> Self::Class<'a>;
    method style (line 1251) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
    type Class (line 1258) | type Class<'a> = StyleFn<'a, Self>;
    method default (line 1260) | fn default<'a>() -> Self::Class<'a> {
    method style (line 1264) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
  type StyleFn (line 1255) | pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
  function default (line 1269) | pub fn default(theme: &Theme, status: Status) -> Style {
  function base (line 1281) | fn base(palette: palette::Extended) -> Style {
  function disabled (line 1294) | fn disabled(style: Style) -> Style {
  type SplitLayoutConfig (line 1303) | pub struct SplitLayoutConfig<'a, Message, Theme, Renderer> {

FILE: src/widgets/synced_image_split.rs
  constant DEBUG_LOGS_ENABLED (line 76) | const DEBUG_LOGS_ENABLED: bool = false;
  type SyncedImageSplit (line 106) | pub struct SyncedImageSplit<'a, Message, Theme, Renderer>
  function new (line 178) | pub fn new<A, B, F, G, H, I>(
  function double_click_threshold_ms (line 241) | pub fn double_click_threshold_ms(mut self, threshold_ms: u16) -> Self {
  function padding (line 248) | pub fn padding(mut self, padding: f32) -> Self {
  function spacing (line 256) | pub fn spacing(mut self, spacing: f32) -> Self {
  function width (line 263) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 270) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  function min_size_first (line 277) | pub fn min_size_first(mut self, size: u16) -> Self {
  function min_size_second (line 284) | pub fn min_size_second(mut self, size: u16) -> Self {
  function style (line 291) | pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> ...
  function class (line 302) | pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
  function min_scale (line 309) | pub fn min_scale(mut self, min_scale: f32) -> Self {
  function max_scale (line 315) | pub fn max_scale(mut self, max_scale: f32) -> Self {
  function scale_step (line 321) | pub fn scale_step(mut self, scale_step: f32) -> Self {
  function synced_zoom (line 327) | pub fn synced_zoom(mut self, synced_zoom: bool) -> Self {
  function tag (line 340) | fn tag(&self) -> tree::Tag {
  function state (line 344) | fn state(&self) -> tree::State {
  function children (line 348) | fn children(&self) -> Vec<Tree> {
  function diff (line 352) | fn diff(&self, tree: &mut Tree) {
  function size (line 356) | fn size(&self) -> Size<Length> {
  function layout (line 360) | fn layout(
  function on_event (line 388) | fn on_event(
  function mouse_interaction (line 987) | fn mouse_interaction(
  function draw (line 1038) | fn draw(
  function operate (line 1261) | fn operate<'b>(
  function overlay (line 1329) | fn overlay<'b>(
  function is_cursor_within_bounds (line 1367) | fn is_cursor_within_bounds(
  type State (line 1388) | pub struct State {
    method new (line 1405) | pub fn new() -> Self {
  function from (line 1430) | fn from(split_pane: SyncedImageSplit<'a, Message, Theme, Renderer>) -> S...
  type ZoomStateOperation (line 1438) | pub struct ZoomStateOperation {
    method new_query (line 1448) | pub fn new_query() -> Self {
    method new_apply (line 1456) | pub fn new_apply(scale: f32, offset: Vector) -> Self {
    method container (line 1467) | fn container(
    method operate (line 1480) | pub fn operate<T>(

FILE: src/widgets/toggler.rs
  type Toggler (line 100) | pub struct Toggler<
  constant DEFAULT_SIZE (line 131) | pub const DEFAULT_SIZE: f32 = 16.0;
  function new (line 142) | pub fn new<F>(
  function label (line 171) | pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
  function on_toggle (line 180) | pub fn on_toggle(
  function on_toggle_maybe (line 192) | pub fn on_toggle_maybe(
  function size (line 201) | pub fn size(mut self, size: impl Into<Pixels>) -> Self {
  function width (line 207) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function text_size (line 213) | pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
  function text_line_height (line 219) | pub fn text_line_height(
  function text_alignment (line 228) | pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
  function text_shaping (line 234) | pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
  function text_wrapping (line 240) | pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
  function spacing (line 246) | pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
  function padding (line 252) | pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
  function font (line 260) | pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
  function style (line 267) | pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> ...
  function class (line 277) | pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
  function tag (line 289) | fn tag(&self) -> tree::Tag {
  function state (line 293) | fn state(&self) -> tree::State {
  function size (line 297) | fn size(&self) -> Size<Length> {
  function layout (line 304) | fn layout(
  function on_event (line 354) | fn on_event(
  function mouse_interaction (line 386) | fn mouse_interaction(
  function draw (line 405) | fn draw(
  function from (line 567) | fn from(
  constant DEFAULT_PADDING (line 575) | pub(crate) const DEFAULT_PADDING: Padding = Padding {
  type Status (line 584) | pub enum Status {
  type Style (line 601) | pub struct Style {
  type Catalog (line 619) | pub trait Catalog: Sized {
    method default (line 624) | fn default<'a>() -> Self::Class<'a>;
    method style (line 627) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
    type Class (line 636) | type Class<'a> = StyleFn<'a, Self>;
    method default (line 638) | fn default<'a>() -> Self::Class<'a> {
    method style (line 642) | fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
  type StyleFn (line 633) | pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
  function default (line 648) | pub fn default(theme: &Theme, status: Status) -> Style {

FILE: src/widgets/viewer.rs
  type Viewer (line 36) | pub struct Viewer<Handle, Message = ()> {
  function new (line 57) | pub fn new<T: Into<Handle>>(handle: T) -> Self {
  function filter_method (line 79) | pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Se...
  function content_fit (line 85) | pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
  function padding (line 91) | pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
  function width (line 97) | pub fn width(mut self, width: impl Into<Length>) -> Self {
  function height (line 103) | pub fn height(mut self, height: impl Into<Length>) -> Self {
  function max_scale (line 111) | pub fn max_scale(mut self, max_scale: f32) -> Self {
  function min_scale (line 119) | pub fn min_scale(mut self, min_scale: f32) -> Self {
  function scale_step (line 128) | pub fn scale_step(mut self, scale_step: f32) -> Self {
  function with_zoom_state (line 135) | pub fn with_zoom_state(mut self, scale: f32, offset: Vector) -> Self {
  function pane_index (line 143) | pub fn pane_index(mut self, pane_index: usize) -> Self {
  function on_zoom_change (line 150) | pub fn on_zoom_change<F>(mut self, f: F) -> Self
  function tag (line 165) | fn tag(&self) -> tree::Tag {
  function state (line 169) | fn state(&self) -> tree::State {
  function diff (line 180) | fn diff(&self, tree: &mut Tree) {
  function size (line 191) | fn size(&self) -> Size<Length> {
  function layout (line 198) | fn layout(
  function on_event (line 230) | fn on_event(
  function mouse_interaction (line 404) | fn mouse_interaction(
  function draw (line 425) | fn draw(
  type State (line 518) | pub struct State {
    method new (line 540) | pub fn new() -> Self {
    method offset (line 546) | fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
    method is_cursor_grabbed (line 560) | pub fn is_cursor_grabbed(&self) -> bool {
  method default (line 527) | fn default() -> Self {
  function from (line 572) | fn from(viewer: Viewer<Handle, Message>) -> Element<'a, Message, Theme, ...
  function scaled_image_size (line 580) | pub fn scaled_image_size<Renderer>(

FILE: src/window_state.rs
  constant VISIBLE_SIZE (line 10) | const VISIBLE_SIZE: i32 = 30;
  function get_window_visible (line 15) | pub fn get_window_visible(
  function query_is_zoomed (line 44) | pub fn query_is_zoomed(window: &winit::window::Window) -> bool {
  function save_window_state_to_disk (line 61) | pub fn save_window_state_to_disk(app: &DataViewer, window: &winit::windo...
  function setup_macos_window (line 109) | pub fn setup_macos_window(window: &winit::window::Window) {
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,224K chars).
[
  {
    "path": ".gitignore",
    "chars": 507,
    "preview": "CLAUDE.md\n.DS_Store\nicon.png\nassets_dev/\nbenchmarks/\ndevlogs/\ndocs/plans/\nlogs/\ntmp/\n\n# Generated by Cargo\n# will have c"
  },
  {
    "path": "Cargo.toml",
    "chars": 5718,
    "preview": "[package]\nname = \"viewskater\"\nversion = \"0.3.1\"\nedition = \"2021\"\ndescription = \"A fast image viewer for browsing large c"
  },
  {
    "path": "README.md",
    "chars": 6293,
    "preview": "<img src=\"https://github.com/user-attachments/assets/4c410f1b-1103-4b84-87f1-7278aa3a46f9\" alt=\"Alt text\" width=\"600\"/>\n"
  },
  {
    "path": "build.rs",
    "chars": 4694,
    "preview": "use {\n    std::{\n        env,\n        io,\n        process::Command,\n        fs,\n    },\n    winres::WindowsResource,\n};\n\n"
  },
  {
    "path": "docs/bundling.md",
    "chars": 1081,
    "preview": "# Building and Packaging ViewSkater\n\nThis guide explains how to build and package ViewSkater for different operating sys"
  },
  {
    "path": "docs/replay.md",
    "chars": 4368,
    "preview": "# Replay Mode\n\nReplay mode automates image navigation for performance benchmarking. It loads test directories, navigates"
  },
  {
    "path": "resources/linux/appimage.desktop",
    "chars": 129,
    "preview": "[Desktop Entry]\nName=ViewSkater\nExec=viewskater\nIcon=icon\nType=Application\nCategories=Graphics;Viewer;\nStartupWMClass=vi"
  },
  {
    "path": "resources/linux/viewskater.desktop",
    "chars": 210,
    "preview": "[Desktop Entry]\nName=ViewSkater\nExec=/path/to/viewskater %f\nIcon=viewskater\nType=Application\nCategories=Graphics;Viewer;"
  },
  {
    "path": "resources/macos/Info.plist",
    "chars": 3061,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.c"
  },
  {
    "path": "resources/macos/entitlements.plist",
    "chars": 1080,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "resources/macos/viewskater_wrapper.sh",
    "chars": 1603,
    "preview": "#!/bin/bash\n#\n# viewskater_wrapper.sh\n#\n# Purpose:\n# This wrapper script is intended for debugging macOS file associatio"
  },
  {
    "path": "src/app/keyboard_handlers.rs",
    "chars": 17479,
    "preview": "use log::debug;\nuse iced_core::keyboard::{self, Key, key::Named};\nuse iced_winit::runtime::Task;\n\nuse crate::app::{DataV"
  },
  {
    "path": "src/app/message.rs",
    "chars": 3778,
    "preview": "use std::path::PathBuf;\n\nuse iced_core::Event;\nuse iced_core::image::Handle;\nuse iced_core::Color;\nuse iced_winit::winit"
  },
  {
    "path": "src/app/message_handlers.rs",
    "chars": 61068,
    "preview": "// Comprehensive message handler module that routes different message categories\n// This significantly reduces the size "
  },
  {
    "path": "src/app/replay_handlers.rs",
    "chars": 11051,
    "preview": "//! Replay mode integration for DataViewer\n//!\n//! Handles replay controller updates and action processing for automated"
  },
  {
    "path": "src/app/settings_widget.rs",
    "chars": 4441,
    "preview": "//! Settings widget module\n//! Manages all settings-related state and UI for the application\n\nuse std::collections::Hash"
  },
  {
    "path": "src/app.rs",
    "chars": 38593,
    "preview": "// Submodules\nmod message;\nmod message_handlers;\nmod keyboard_handlers;\nmod replay_handlers;\nmod settings_widget;\n\nuse i"
  },
  {
    "path": "src/archive_cache.rs",
    "chars": 6262,
    "preview": "use std::path::PathBuf;\nuse std::sync::Arc;\nuse std::io::Read;\nuse std::collections::HashMap;\n\n#[allow(unused_imports)]\n"
  },
  {
    "path": "src/build_info.rs",
    "chars": 2810,
    "preview": "/// Build information captured at compile time\npub struct BuildInfo;\n\nimpl BuildInfo {\n    /// Get the package version f"
  },
  {
    "path": "src/cache/cache_utils.rs",
    "chars": 13313,
    "preview": "use std::io;\n#[allow(unused_imports)]\nuse image::GenericImageView;\nuse image::DynamicImage;\nuse std::sync::Arc;\nuse wgpu"
  },
  {
    "path": "src/cache/compression.rs",
    "chars": 7116,
    "preview": "//! A minimal BC1 compression library in pure Rust.\n\n// Remove `#![no_std]` since we need standard library features like"
  },
  {
    "path": "src/cache/cpu_img_cache.rs",
    "chars": 7642,
    "preview": "#[allow(unused_imports)]\nuse log::{debug, info, warn, error};\n\nuse std::io;\nuse crate::cache::img_cache::{CachedData, Im"
  },
  {
    "path": "src/cache/gpu_img_cache.rs",
    "chars": 10189,
    "preview": "#[allow(unused_imports)]\nuse log::{debug, info, warn, error};\n\n\nuse std::io;\nuse std::sync::Arc;\nuse image::GenericImage"
  },
  {
    "path": "src/cache/img_cache.rs",
    "chars": 39571,
    "preview": "#[allow(unused_imports)]\nuse std::time::Instant;\n\n#[allow(unused_imports)]\nuse log::{debug, info, warn, error};\n\nuse std"
  },
  {
    "path": "src/cache/mod.rs",
    "chars": 129,
    "preview": "pub mod img_cache;\npub mod cpu_img_cache;\npub mod gpu_img_cache;\npub mod cache_utils;\npub mod texture_cache;\npub mod com"
  },
  {
    "path": "src/cache/texture_cache.rs",
    "chars": 6917,
    "preview": "use iced_wgpu::wgpu;\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::time::Instant;\nuse std::hash::{Hash, Ha"
  },
  {
    "path": "src/coco/annotation_manager.rs",
    "chars": 8615,
    "preview": "/// Annotation manager for COCO datasets\n///\n/// Manages loading, caching, and accessing COCO annotations.\n/// Associate"
  },
  {
    "path": "src/coco/mod.rs",
    "chars": 276,
    "preview": "/// COCO dataset visualization module\n///\n/// This module handles COCO format dataset loading, annotation management,\n//"
  },
  {
    "path": "src/coco/overlay/bbox_overlay.rs",
    "chars": 19079,
    "preview": "/// Bounding box overlay rendering for COCO annotations\n///\n/// This module renders bounding boxes using a custom WGPU s"
  },
  {
    "path": "src/coco/overlay/bbox_shader.rs",
    "chars": 15989,
    "preview": "/// BBox shader widget for rendering COCO bounding boxes\n///\n/// Uses WGPU to draw colored rectangles with labels over i"
  },
  {
    "path": "src/coco/overlay/bbox_shader.wgsl",
    "chars": 592,
    "preview": "// BBox rectangle shader using vertex attributes\n\nstruct VertexInput {\n    @location(0) position: vec2<f32>,\n    @locati"
  },
  {
    "path": "src/coco/overlay/mask_shader.rs",
    "chars": 26866,
    "preview": "/// Pixel-perfect mask shader widget for rendering COCO RLE segmentation masks\n///\n/// Uses WGPU texture-based rendering"
  },
  {
    "path": "src/coco/overlay/mask_shader.wgsl",
    "chars": 1128,
    "preview": "// Pixel-level mask shader using textures\n\nstruct VertexInput {\n    @location(0) position: vec2<f32>,\n    @location(1) t"
  },
  {
    "path": "src/coco/overlay/mod.rs",
    "chars": 344,
    "preview": "/// Overlay rendering for COCO annotations\n///\n/// This module provides GPU-accelerated rendering of bounding boxes\n/// "
  },
  {
    "path": "src/coco/overlay/polygon_shader.rs",
    "chars": 26558,
    "preview": "/// Polygon mask shader widget for rendering COCO segmentation masks\n///\n/// Uses WGPU to draw filled polygons with prop"
  },
  {
    "path": "src/coco/overlay/polygon_shader.wgsl",
    "chars": 584,
    "preview": "// Polygon mask shader for filled shapes\n\nstruct VertexInput {\n    @location(0) position: vec2<f32>,\n    @location(1) co"
  },
  {
    "path": "src/coco/parser.rs",
    "chars": 8616,
    "preview": "/// COCO dataset JSON parser\n///\n/// This module parses COCO format annotation files.\n/// Format specification: https://"
  },
  {
    "path": "src/coco/rle_decoder.rs",
    "chars": 9977,
    "preview": "/// RLE (Run-Length Encoding) decoder for COCO segmentation masks\n///\n/// This module decodes COCO RLE format masks and "
  },
  {
    "path": "src/coco/widget.rs",
    "chars": 16673,
    "preview": "/// COCO dataset visualization widget and message handling\n///\n/// This module is only compiled when the \"coco\" feature "
  },
  {
    "path": "src/config.rs",
    "chars": 2118,
    "preview": "use once_cell::sync::Lazy;\nuse crate::settings::{UserSettings, WindowState};\n\n// Default values for configuration\n// The"
  },
  {
    "path": "src/exif_utils.rs",
    "chars": 4171,
    "preview": "//! EXIF orientation utilities for automatic image rotation correction.\n//!\n//! This module provides EXIF-aware image de"
  },
  {
    "path": "src/file_io.rs",
    "chars": 48164,
    "preview": "use std::fs;\nuse std::path::Path;\nuse std::path::PathBuf;\nuse tokio::io::AsyncReadExt;\nuse futures::future::join_all;\nus"
  },
  {
    "path": "src/loading_handler.rs",
    "chars": 12057,
    "preview": "#[allow(unused_imports)]\nuse log::{debug, error, warn, info};\nuse crate::Arc;\nuse crate::pane;\nuse crate::loading_status"
  },
  {
    "path": "src/loading_status.rs",
    "chars": 6400,
    "preview": "use crate::cache::img_cache::{LoadOperation, LoadOperationType};\nuse std::collections::VecDeque;\nuse crate::pane::Pane;\n"
  },
  {
    "path": "src/logging.rs",
    "chars": 39056,
    "preview": "/*\n================================================================================\n                            ViewSkat"
  },
  {
    "path": "src/macos_file_access.rs",
    "chars": 70753,
    "preview": "/*\n================================================================================\n                        macOS File A"
  },
  {
    "path": "src/main.rs",
    "chars": 71087,
    "preview": "#![windows_subsystem = \"windows\"]\n\nmod cache;\nmod navigation_keyboard;\nmod navigation_slider;\nmod file_io;\nmod menu;\nmod"
  },
  {
    "path": "src/menu.rs",
    "chars": 17110,
    "preview": "#[cfg(target_os = \"linux\")]\nmod other_os {\n    pub use iced_custom as iced;\n    pub use iced_aw as iced_aw; // TODO: Cha"
  },
  {
    "path": "src/navigation_keyboard.rs",
    "chars": 27673,
    "preview": "#[warn(unused_imports)]\n#[cfg(target_os = \"linux\")]\nmod other_os {\n    pub use iced_custom as iced;\n}\n\n#[cfg(not(target_"
  },
  {
    "path": "src/navigation_slider.rs",
    "chars": 36001,
    "preview": "#[warn(unused_imports)]\n#[cfg(target_os = \"linux\")]\nmod other_os {\n    pub use iced_custom as iced;\n}\n\n#[cfg(not(target_"
  },
  {
    "path": "src/pane.rs",
    "chars": 45277,
    "preview": "use std::error::Error;\nuse std::path::Path;\nuse std::path::PathBuf;\nuse std::sync::{Arc, Mutex};\nuse std::time::Instant;"
  },
  {
    "path": "src/replay.rs",
    "chars": 28029,
    "preview": "use std::time::{Duration, Instant};\nuse std::path::PathBuf;\nuse log::{debug, info, warn};\n\n#[derive(Debug, Clone, Partia"
  },
  {
    "path": "src/selection_manager.rs",
    "chars": 10351,
    "preview": "use std::collections::{HashMap, hash_map::DefaultHasher};\nuse std::hash::{Hash, Hasher};\nuse std::path::{Path, PathBuf};"
  },
  {
    "path": "src/settings.rs",
    "chars": 24513,
    "preview": "use serde::{Deserialize, Serialize};\nuse std::fs;\nuse std::path::PathBuf;\nuse log::{debug, info, warn, error};\nuse iced_"
  },
  {
    "path": "src/settings_modal.rs",
    "chars": 21627,
    "preview": "use iced_winit::core::{Element, Length, Alignment, Color};\nuse iced_winit::core::font::Font;\nuse iced_widget::{row, colu"
  },
  {
    "path": "src/ui.rs",
    "chars": 50048,
    "preview": "#[cfg(target_os = \"linux\")]\nmod other_os {\n    //pub use iced;\n    pub use iced_custom as iced;\n}\n\n#[cfg(not(target_os ="
  },
  {
    "path": "src/utils/mem.rs",
    "chars": 4106,
    "preview": "use crate::CURRENT_MEMORY_USAGE;\nuse crate::LAST_MEMORY_UPDATE;\nuse std::time::Instant;\n\n#[allow(unused_imports)]\nuse lo"
  },
  {
    "path": "src/utils/mod.rs",
    "chars": 43,
    "preview": "pub mod mem;\npub mod save;\npub mod timing;\n"
  },
  {
    "path": "src/utils/save.rs",
    "chars": 1768,
    "preview": "use std::sync::Arc;\n\nuse iced_wgpu::wgpu::{self, util::align_to, Texture};\n\nuse crate::app::DataViewer;\n\npub(crate) fn e"
  },
  {
    "path": "src/utils/timing.rs",
    "chars": 1373,
    "preview": "use std::time::{Duration, Instant};\n\n#[allow(unused_imports)]\nuse log::{debug, info};\n\npub struct TimingStats {\n    pub "
  },
  {
    "path": "src/widgets/circular.rs",
    "chars": 6398,
    "preview": "//! Circular loading spinner widget that animates automatically.\n//!\n//! Implements Widget trait directly (not canvas::P"
  },
  {
    "path": "src/widgets/dualslider.rs",
    "chars": 21105,
    "preview": "//! Sliders let users set a value by moving an indicator.\n//!\n//! # Example\n//! ```no_run\n//! # mod iced { pub mod widge"
  },
  {
    "path": "src/widgets/easing.rs",
    "chars": 2018,
    "preview": "use iced_winit::core::Point;\n\nuse lyon_algorithms::measure::PathMeasurements;\nuse lyon_algorithms::path::{builder::NoAtt"
  },
  {
    "path": "src/widgets/mod.rs",
    "chars": 216,
    "preview": "pub mod split;\npub mod dualslider;\npub mod toggler;\npub mod viewer;\npub mod modal;\npub mod shader;\npub mod synced_image_"
  },
  {
    "path": "src/widgets/modal.rs",
    "chars": 966,
    "preview": "use iced_winit::core::{\n    Color, Element\n};\nuse iced_widget::{container, stack, mouse_area, center, opaque};\nuse iced_"
  },
  {
    "path": "src/widgets/selection_widget.rs",
    "chars": 8509,
    "preview": "/// Image selection and curation widget for dataset preparation\n///\n/// This module is only compiled when the \"selection"
  },
  {
    "path": "src/widgets/shader/atlas_texture.wgsl",
    "chars": 1227,
    "preview": "struct VertexOutput {\n    @builtin(position) position: vec4<f32>,\n    @location(0) tex_coords: vec2<f32>,\n};\n\nstruct Uni"
  },
  {
    "path": "src/widgets/shader/cpu_scene.rs",
    "chars": 14279,
    "preview": "#[allow(unused_imports)]\nuse log::{debug, info, warn, error};\n\nuse std::sync::Arc;\nuse std::time::Instant;\nuse std::coll"
  },
  {
    "path": "src/widgets/shader/image_shader.rs",
    "chars": 38256,
    "preview": "#[allow(unused_imports)]\nuse log::{Level, debug, info, warn, error};\n\nuse std::marker::PhantomData;\nuse std::sync::Arc;\n"
  },
  {
    "path": "src/widgets/shader/mod.rs",
    "chars": 104,
    "preview": "pub mod scene;\npub mod texture_pipeline;\npub mod texture_scene;\npub mod cpu_scene;\npub mod image_shader;"
  },
  {
    "path": "src/widgets/shader/scene.rs",
    "chars": 6915,
    "preview": "use std::sync::Arc;\nuse std::sync::Mutex;\nuse once_cell::sync::Lazy;\nuse iced_widget::shader::{self, Viewport};\nuse iced"
  },
  {
    "path": "src/widgets/shader/texture.wgsl",
    "chars": 901,
    "preview": "@group(0) @binding(0)\nvar my_texture: texture_2d<f32>;\n\n@group(0) @binding(1)\nvar my_sampler: sampler;\n\n@group(0) @bindi"
  },
  {
    "path": "src/widgets/shader/texture_pipeline.rs",
    "chars": 10457,
    "preview": "use std::sync::Arc;\nuse std::sync::Mutex;\nuse once_cell::sync::Lazy;\nuse iced_core::Rectangle;\nuse iced_wgpu::wgpu::{sel"
  },
  {
    "path": "src/widgets/shader/texture_scene.rs",
    "chars": 8948,
    "preview": "use std::sync::Arc;\nuse std::sync::Mutex;\nuse once_cell::sync::Lazy;\nuse iced_core::{Length, Size, Point, ContentFit};\nu"
  },
  {
    "path": "src/widgets/split.rs",
    "chars": 44036,
    "preview": "// Further modified from https://gist.github.com/airstrike/1169980e58ccb20a88e21af23dcf2650\n// ---\n// Modified from iced"
  },
  {
    "path": "src/widgets/synced_image_split.rs",
    "chars": 55293,
    "preview": "// Further modified from https://gist.github.com/airstrike/1169980e58ccb20a88e21af23dcf2650\n// ---\n// Modified from iced"
  },
  {
    "path": "src/widgets/toggler.rs",
    "chars": 20922,
    "preview": "//! Togglers let users make binary choices by toggling a switch.\n//!\n//! # Example\n//! ```no_run\n//! # mod iced { pub mo"
  },
  {
    "path": "src/widgets/viewer.rs",
    "chars": 19867,
    "preview": "//! Zoom and pan on an image.\n#[cfg(target_os = \"linux\")]\nmod other_os {\n    //pub use iced;\n    pub use iced_custom as "
  },
  {
    "path": "src/window_state.rs",
    "chars": 7258,
    "preview": "use iced_winit::winit;\nuse winit::dpi::{PhysicalPosition, PhysicalSize};\nuse winit::monitor::MonitorHandle;\nuse log::err"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the ggand0/viewskater GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (1.1 MB), approximately 251.8k tokens, and a symbol index with 991 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!