#![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, /// Time of the next popup spawn, if a batch is in progress. next_spawn_at: Option, 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) }