fastpack-gui 0.21.0

Native desktop GUI for FastPack (primary interface)
Documentation
use eframe::egui;

use crate::state::AppState;

pub fn show(ctx: &egui::Context, state: &mut AppState, atlas_textures: &[egui::TextureHandle]) {
    if !state.anim_preview.open {
        return;
    }

    let n = state.selected_frames.len();
    if n == 0 {
        state.anim_preview.open = false;
        return;
    }

    if state.anim_preview.playing {
        let dt = ctx.input(|i| i.unstable_dt) as f64;
        state.anim_preview.elapsed_secs += dt;
        let frame_dur = 1.0 / state.anim_preview.fps as f64;
        while state.anim_preview.elapsed_secs >= frame_dur {
            state.anim_preview.elapsed_secs -= frame_dur;
            let next = state.anim_preview.current_frame + 1;
            if next >= n {
                if state.anim_preview.looping {
                    state.anim_preview.current_frame = 0;
                } else {
                    state.anim_preview.current_frame = n - 1;
                    state.anim_preview.playing = false;
                }
            } else {
                state.anim_preview.current_frame = next;
            }
        }
        ctx.request_repaint();
    }

    if state.anim_preview.current_frame >= n {
        state.anim_preview.current_frame = n - 1;
    }

    let mut open = state.anim_preview.open;
    egui::Window::new("Animation Preview")
        .open(&mut open)
        .resizable(true)
        .default_size([320.0, 400.0])
        .min_size([240.0, 200.0])
        .show(ctx, |ui| {
            // Controls toolbar
            ui.horizontal(|ui| {
                if ui.button("").clicked() {
                    state.anim_preview.playing = false;
                    state.anim_preview.current_frame =
                        state.anim_preview.current_frame.saturating_sub(1);
                    state.anim_preview.elapsed_secs = 0.0;
                }

                let play_label = if state.anim_preview.playing {
                    ""
                } else {
                    ""
                };
                if ui.button(play_label).clicked() {
                    state.anim_preview.playing = !state.anim_preview.playing;
                    state.anim_preview.elapsed_secs = 0.0;
                }

                if ui.button("▶|").clicked() {
                    state.anim_preview.playing = false;
                    let next = state.anim_preview.current_frame + 1;
                    state.anim_preview.current_frame = if next >= n { 0 } else { next };
                    state.anim_preview.elapsed_secs = 0.0;
                }

                ui.separator();

                ui.label("FPS");
                ui.add(egui::Slider::new(&mut state.anim_preview.fps, 1.0..=60.0).step_by(1.0));

                ui.separator();

                ui.label(format!("{} / {}", state.anim_preview.current_frame + 1, n));

                ui.separator();

                ui.checkbox(&mut state.anim_preview.looping, "Loop");
            });

            ui.separator();

            // Fixed canvas — all remaining space
            let available = ui.available_size();
            let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag());

            if response.dragged() {
                let d = response.drag_delta();
                state.anim_preview.pan[0] += d.x;
                state.anim_preview.pan[1] += d.y;
            }

            let scroll_y = ui.input(|i| i.smooth_scroll_delta.y);
            if response.hovered() && scroll_y != 0.0 {
                let factor: f32 = if scroll_y > 0.0 { 1.1 } else { 1.0 / 1.1 };
                state.anim_preview.zoom = (state.anim_preview.zoom * factor).clamp(0.05, 64.0);
            }

            if response.double_clicked() {
                state.anim_preview.pan = [0.0, 0.0];
                state.anim_preview.zoom = 1.0;
            }

            let canvas = response.rect;
            let zoom = state.anim_preview.zoom;
            let pan = egui::vec2(state.anim_preview.pan[0], state.anim_preview.pan[1]);

            painter.rect_filled(canvas, 0.0, egui::Color32::from_rgb(35, 35, 35));
            draw_checker(&painter, canvas);

            let frame_idx = state.selected_frames[state.anim_preview.current_frame];
            if let Some(frame) = state.frames.get(frame_idx) {
                let si = frame.sheet_idx;
                if si < atlas_textures.len() && si < state.sheets.len() {
                    let sheet = &state.sheets[si];
                    let aw = sheet.width as f32;
                    let ah = sheet.height as f32;
                    let uv = egui::Rect::from_min_max(
                        egui::pos2(frame.x as f32 / aw, frame.y as f32 / ah),
                        egui::pos2(
                            (frame.x + frame.w) as f32 / aw,
                            (frame.y + frame.h) as f32 / ah,
                        ),
                    );

                    let sw = frame.w as f32 * zoom;
                    let sh = frame.h as f32 * zoom;
                    let origin = canvas.center() + pan - egui::vec2(sw * 0.5, sh * 0.5);
                    let img_rect = egui::Rect::from_min_size(origin, egui::vec2(sw, sh));

                    painter.image(atlas_textures[si].id(), img_rect, uv, egui::Color32::WHITE);

                    // Frame ID label at bottom-left of canvas
                    painter.text(
                        canvas.left_bottom() + egui::vec2(6.0, -6.0),
                        egui::Align2::LEFT_BOTTOM,
                        &frame.id,
                        egui::FontId::proportional(11.0),
                        egui::Color32::WHITE,
                    );
                }
            }
        });

    state.anim_preview.open = open;
}

fn draw_checker(painter: &egui::Painter, rect: egui::Rect) {
    let tile = 8.0_f32;
    let c1 = egui::Color32::from_rgb(50, 50, 50);
    let c2 = egui::Color32::from_rgb(60, 60, 60);
    let mut x = (rect.min.x / tile).floor() * tile;
    while x < rect.max.x {
        let mut y = (rect.min.y / tile).floor() * tile;
        while y < rect.max.y {
            let chess = ((x / tile) as i32 + (y / tile) as i32) % 2 == 0;
            let color = if chess { c1 } else { c2 };
            let tile_rect = egui::Rect::from_min_max(
                egui::pos2(x.max(rect.min.x), y.max(rect.min.y)),
                egui::pos2((x + tile).min(rect.max.x), (y + tile).min(rect.max.y)),
            );
            painter.rect_filled(tile_rect, 0.0, color);
            y += tile;
        }
        x += tile;
    }
}