peter-griffin-prank / src/main.rs

raw file 7181 bytes
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

//! Rust remake of Peter.exe (the "Peter Griffin Virus"):
//! a 397x319 borderless window showing Peter's face with a "Close" button that
//! spawns 50 windows (800x450) at random screen positions, 100 ms apart.

use std::time::Duration;

use eframe::egui;

const FACE_JPG: &[u8] = include_bytes!("../peter_face.jpg");
const BODY_JPG: &[u8] = include_bytes!("../peter_body.jpg");

const MAIN_W: f32 = 397.0;
const MAIN_H: f32 = 319.0;

const POPUP_W: f32 = 800.0;
const POPUP_H: f32 = 450.0;
const POPUP_COUNT: u32 = 50;
/// Seconds between popups (original: Thread.Sleep(100)).
const POPUP_INTERVAL: f64 = 0.1;

fn main() -> eframe::Result {
    force_x11_unless_opted_out();

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("Peter")
            .with_inner_size([MAIN_W, MAIN_H])
            .with_resizable(false)
            .with_decorations(false),
        ..Default::default()
    };

    eframe::run_native(
        "Peter",
        options,
        Box::new(|cc| Ok(Box::new(PeterApp::new(cc)))),
    )
}

struct Popup {
    id: egui::ViewportId,
    pos: egui::Pos2,
}

/// Wayland compositors ignore client-requested window positions, so popups end up
/// wherever the compositor feels like (usually the same spot). winit prefers
/// Wayland whenever `WAYLAND_DISPLAY`/`WAYLAND_SOCKET` is set, so hide those to
/// make it use X11/XWayland, where `ViewportBuilder::with_position` works.
///
/// Set `PETER_KEEP_WAYLAND=1` to keep the native Wayland backend (positions will
/// then be chosen by the compositor).
fn force_x11_unless_opted_out() {
    if std::env::var_os("PETER_KEEP_WAYLAND").is_some() {
        return;
    }
    if std::env::var_os("DISPLAY").is_some() {
        // SAFETY: called before any threads are spawned by the event loop.
        unsafe {
            std::env::remove_var("WAYLAND_DISPLAY");
            std::env::remove_var("WAYLAND_SOCKET");
        }
    }
}

struct PeterApp {
    face: egui::TextureHandle,
    body: egui::TextureHandle,
    popups: Vec<Popup>,
    /// Time of the next popup spawn, if a batch is in progress.
    next_spawn_at: Option<f64>,
    spawn_remaining: u32,
    centered: bool,
}

impl PeterApp {
    fn new(cc: &eframe::CreationContext<'_>) -> Self {
        let ctx = &cc.egui_ctx;
        // Light theme so the button looks like the classic WinForms one.
        ctx.set_visuals(egui::Visuals::light());

        let face = load_texture(ctx, "peter_face", FACE_JPG);
        let body = load_texture(ctx, "peter_body", BODY_JPG);

        let app = Self {
            face,
            body,
            popups: Vec::new(),
            next_spawn_at: None,
            spawn_remaining: 0,
            centered: false,
        };

        app
    }

    fn start_batch(&mut self, now: f64) {
        self.spawn_remaining = POPUP_COUNT;
        self.next_spawn_at = Some(now + POPUP_INTERVAL);
    }

    fn spawn_popup(&mut self, ctx: &egui::Context) {
        let monitor = ctx
            .input(|i| i.viewport().monitor_size)
            .unwrap_or(egui::vec2(1920.0, 1080.0));

        let max_x = (monitor.x - POPUP_W).max(0.0);
        let max_y = (monitor.y - POPUP_H).max(0.0);

        let pos = egui::pos2(
            rand::random_range(0.0..=max_x),
            rand::random_range(0.0..=max_y),
        );

        let id = egui::ViewportId::from_hash_of(format!("peter-popup-{}", self.popups.len()));
        self.popups.push(Popup { id, pos });
    }
}

impl eframe::App for PeterApp {
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        let ctx = ui.ctx().clone();

        // Center the main window on the monitor once we know its size.
        if !self.centered {
            if let Some(monitor) = ctx.input(|i| i.viewport().monitor_size) {
                if monitor.x > MAIN_W && monitor.y > MAIN_H {
                    ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(
                        (monitor.x - MAIN_W) * 0.5,
                        (monitor.y - MAIN_H) * 0.5,
                    )));
                    self.centered = true;
                }
            }
        }

        let rect = ui.max_rect();
        let origin = rect.min;

        // Window background (the .resx canvas was plain white).
        ui.painter()
            .rect_filled(rect, 0.0, egui::Color32::WHITE);

        // pictureBox1: Location (12,43), Size 373x171, SizeMode = StretchImage
        let image_rect = egui::Rect::from_min_size(
            origin + egui::vec2(12.0, 43.0),
            egui::vec2(373.0, 171.0),
        );
        ui.painter().image(
            self.face.id(),
            image_rect,
            egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
            egui::Color32::WHITE,
        );

        // button1: Location (149,273), Size 75x23, Text "Close"
        let button_rect = egui::Rect::from_min_size(
            origin + egui::vec2(149.0, 273.0),
            egui::vec2(75.0, 23.0),
        );
        if ui.put(button_rect, egui::Button::new("Close")).clicked()
            && self.spawn_remaining == 0
        {
            self.start_batch(ctx.input(|i| i.time));
        }

        // Spawn one popup per interval until the batch is done.
        if let Some(spawn_at) = self.next_spawn_at {
            let now = ctx.input(|i| i.time);
            if now >= spawn_at {
                self.spawn_popup(&ctx);
                self.spawn_remaining -= 1;
                self.next_spawn_at = if self.spawn_remaining == 0 {
                    None
                } else {
                    Some(spawn_at + POPUP_INTERVAL)
                };
            }
            ctx.request_repaint_after(Duration::from_millis(10));
        }

        // Deferred viewports must be declared every frame to stay alive.
        let body_texture = self.body.id();
        let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
        for popup in &self.popups {
            ctx.show_viewport_deferred(
                popup.id,
                egui::ViewportBuilder::default()
                    .with_title("Form2")
                    .with_inner_size([POPUP_W, POPUP_H])
                    .with_resizable(false)
                    .with_position(popup.pos),
                move |ui, _class| {
                    let rect = ui.max_rect();
                    ui.painter()
                        .rect_filled(rect, 0.0, egui::Color32::WHITE);
                    ui.painter().image(body_texture, rect, uv, egui::Color32::WHITE);
                },
            );
        }
    }
}

fn load_texture(ctx: &egui::Context, name: &str, bytes: &[u8]) -> egui::TextureHandle {
    let image = image::load_from_memory(bytes)
        .unwrap_or_else(|err| panic!("failed to decode {name}: {err}"))
        .to_rgba8();
    let size = [image.width() as usize, image.height() as usize];
    let color_image = egui::ColorImage::from_rgba_unmultiplied(size, image.as_raw());
    ctx.load_texture(name, color_image, egui::TextureOptions::LINEAR)
}