#![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)
}