1#![cfg(target_os = "windows")]
36
37use std::thread;
38use windows_sys::Win32::Foundation::*;
39use windows_sys::Win32::Graphics::Gdi::*;
40use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
41use windows_sys::Win32::System::Threading::*;
42use windows_sys::Win32::UI::WindowsAndMessaging::*;
43
44include!(concat!(env!("OUT_DIR"), "/brain_dims.rs"));
48
49const SPLASH_PADDING: i32 = 12;
52const SPLASH_SIZE: i32 = BRAIN_W + SPLASH_PADDING * 2;
54const BRAIN_X: i32 = SPLASH_PADDING;
55const BRAIN_Y: i32 = SPLASH_PADDING;
56
57const FOOTER_COLOR: [u8; 3] = [0x8A, 0x8A, 0x93];
60const FOOTER_PAD_TOP: i32 = 12;
61const FOOTER_LINE_GAP: i32 = 3;
62const FOOTER_PAD_BOTTOM: i32 = 12;
63const FOOTER_H: i32 = FOOTER_PAD_TOP
64 + 2 * crate::splash_font::GLYPH_H as i32
65 + FOOTER_LINE_GAP
66 + FOOTER_PAD_BOTTOM;
67const SPLASH_W: i32 = SPLASH_SIZE;
69const SPLASH_H: i32 = SPLASH_SIZE + FOOTER_H;
70
71const BG_B: u8 = 0x1F;
75const BG_G: u8 = 0x1A;
76const BG_R: u8 = 0x1A;
77
78static BRAIN_BGRA: &[u8] =
81 include_bytes!(concat!(env!("OUT_DIR"), "/brain_bgra.bin"));
82
83struct SendHandle(HANDLE);
88unsafe impl Send for SendHandle {}
89impl SendHandle {
90 fn take(self) -> HANDLE { self.0 }
91}
92
93pub fn spawn_splash(dir_hash: &str) -> Option<String> {
97 let event_name = format!("AgentMuxSplash-{}", dir_hash);
98 let nul_name: Vec<u16> = format!("{}\0", event_name)
99 .encode_utf16()
100 .collect();
101
102 let ev = unsafe {
103 CreateEventW(
104 std::ptr::null(), 1, 0, nul_name.as_ptr(),
108 )
109 };
110 if ev.is_null() {
111 crate::log("splash: CreateEventW failed — skipping splash");
112 return None;
113 }
114
115 let class_name = format!("AgentMuxSplash-{}", dir_hash);
120 let handle = SendHandle(ev);
121 thread::spawn(move || unsafe { run_splash(handle.take(), class_name) });
122 Some(event_name)
123}
124
125unsafe fn run_splash(dismiss_ev: HANDLE, class_name: String) {
126 let class: Vec<u16> = format!("{}\0", class_name).encode_utf16().collect();
127 let hinst = GetModuleHandleW(std::ptr::null());
128
129 let wc = WNDCLASSEXW {
130 cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
131 style: 0,
132 lpfnWndProc: Some(DefWindowProcW),
133 cbClsExtra: 0,
134 cbWndExtra: 0,
135 hInstance: hinst,
136 hIcon: std::ptr::null_mut(),
137 hCursor: std::ptr::null_mut(),
138 hbrBackground: std::ptr::null_mut(),
139 lpszMenuName: std::ptr::null(),
140 lpszClassName: class.as_ptr(),
141 hIconSm: std::ptr::null_mut(),
142 };
143 RegisterClassExW(&wc);
145
146 let sw = GetSystemMetrics(SM_CXSCREEN);
147 let sh = GetSystemMetrics(SM_CYSCREEN);
148 let x = (sw - SPLASH_W) / 2;
149 let y = (sh - SPLASH_H) / 2;
150
151 let hwnd = CreateWindowExW(
152 WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
153 class.as_ptr(),
154 std::ptr::null(), WS_POPUP,
156 x, y, SPLASH_W, SPLASH_H,
157 std::ptr::null_mut(), std::ptr::null_mut(), hinst,
160 std::ptr::null(), );
162 if hwnd.is_null() {
163 CloseHandle(dismiss_ev);
164 return;
165 }
166
167 let screen_dc = GetDC(std::ptr::null_mut());
172 let mem_dc = CreateCompatibleDC(screen_dc);
173 let mut bmi: BITMAPINFO = std::mem::zeroed();
174 bmi.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
175 bmi.bmiHeader.biWidth = SPLASH_W;
176 bmi.bmiHeader.biHeight = -SPLASH_H;
178 bmi.bmiHeader.biPlanes = 1;
179 bmi.bmiHeader.biBitCount = 32;
180 bmi.bmiHeader.biCompression = BI_RGB as u32;
181
182 let mut dib_pixels_raw: *mut core::ffi::c_void = std::ptr::null_mut();
183 let dib = CreateDIBSection(
184 mem_dc,
185 &bmi,
186 DIB_RGB_COLORS,
187 &mut dib_pixels_raw,
188 std::ptr::null_mut(),
189 0,
190 );
191 if dib.is_null() || dib_pixels_raw.is_null() {
192 ReleaseDC(std::ptr::null_mut(), screen_dc);
193 DeleteDC(mem_dc);
194 DestroyWindow(hwnd);
195 CloseHandle(dismiss_ev);
196 return;
197 }
198 let old_obj = SelectObject(mem_dc, dib as _);
199
200 let dib_pixels = std::slice::from_raw_parts_mut(
201 dib_pixels_raw as *mut u8,
202 (SPLASH_W * SPLASH_H * 4) as usize,
203 );
204
205 ShowWindow(hwnd, SW_SHOWNOACTIVATE);
206
207 let info = crate::splash_info::SplashInfo::gather();
209 let max_chars = ((SPLASH_W - 24) / crate::splash_font::GLYPH_W as i32).max(8) as usize;
210 let footer = info.footer_lines(max_chars);
211
212 let start = std::time::Instant::now();
213
214 loop {
217 if WaitForSingleObject(dismiss_ev, 0) == WAIT_OBJECT_0 {
219 fade_out(hwnd, mem_dc, dib_pixels);
220 break;
221 }
222
223 let t = start.elapsed().as_secs_f32();
224 let brain_alpha: u8 = if t < 0.2 {
225 (t / 0.2 * 220.0) as u8
226 } else {
227 let pulse = (((t - 0.2) * std::f32::consts::TAU * 1.1).sin() + 1.0) * 0.5;
228 (160.0 + pulse * 60.0) as u8
229 };
230
231 composite(dib_pixels, brain_alpha, &footer);
232 push_layered(hwnd, mem_dc, 255);
233
234 std::thread::sleep(std::time::Duration::from_millis(16)); }
236
237 SelectObject(mem_dc, old_obj);
238 DeleteObject(dib as _);
239 DeleteDC(mem_dc);
240 ReleaseDC(std::ptr::null_mut(), screen_dc);
241 DestroyWindow(hwnd);
242 CloseHandle(dismiss_ev);
243}
244
245fn composite(dib: &mut [u8], brain_alpha: u8, footer: &[String]) {
252 for px in dib.chunks_exact_mut(4) {
254 px[0] = BG_B;
255 px[1] = BG_G;
256 px[2] = BG_R;
257 px[3] = 0xFF;
258 }
259
260 let ba = brain_alpha as u16;
264 for y in 0..BRAIN_H {
265 let dib_row = ((BRAIN_Y + y) * SPLASH_W * 4) as usize;
266 let src_row = (y * BRAIN_W * 4) as usize;
267 for x in 0..BRAIN_W {
268 let di = dib_row + ((BRAIN_X + x) * 4) as usize;
269 let si = src_row + (x * 4) as usize;
270 let sb = BRAIN_BGRA[si] as u16;
271 let sg = BRAIN_BGRA[si + 1] as u16;
272 let sr = BRAIN_BGRA[si + 2] as u16;
273 let sa = BRAIN_BGRA[si + 3] as u16;
274 if sa == 0 {
275 continue;
276 }
277 let mb = (sb * ba + 127) / 255;
279 let mg = (sg * ba + 127) / 255;
280 let mr = (sr * ba + 127) / 255;
281 let ma = (sa * ba + 127) / 255;
282 let inv = 255u16 - ma;
284 let bb = (dib[di] as u16) * inv / 255 + mb;
285 let bg = (dib[di + 1] as u16) * inv / 255 + mg;
286 let br = (dib[di + 2] as u16) * inv / 255 + mr;
287 dib[di] = bb.min(255) as u8;
288 dib[di + 1] = bg.min(255) as u8;
289 dib[di + 2] = br.min(255) as u8;
290 }
292 }
293
294 let footer_top = SPLASH_H - FOOTER_H + FOOTER_PAD_TOP;
298 for (i, line) in footer.iter().enumerate() {
299 let y = footer_top + i as i32 * (crate::splash_font::GLYPH_H as i32 + FOOTER_LINE_GAP);
300 crate::splash_text::draw_text_centered(dib, SPLASH_W, SPLASH_H, y, line, FOOTER_COLOR, 1.0, true);
301 }
302}
303
304unsafe fn push_layered(hwnd: HWND, mem_dc: HDC, source_alpha: u8) {
305 let mut sz = SIZE {
306 cx: SPLASH_W,
307 cy: SPLASH_H,
308 };
309 let mut src_pt = POINT { x: 0, y: 0 };
310 let blend = BLENDFUNCTION {
311 BlendOp: AC_SRC_OVER as u8,
312 BlendFlags: 0,
313 SourceConstantAlpha: source_alpha,
314 AlphaFormat: AC_SRC_ALPHA as u8,
315 };
316 UpdateLayeredWindow(
317 hwnd,
318 std::ptr::null_mut(),
319 std::ptr::null(),
320 &mut sz,
321 mem_dc,
322 &mut src_pt,
323 0,
324 &blend,
325 ULW_ALPHA,
326 );
327}
328
329unsafe fn fade_out(hwnd: HWND, mem_dc: HDC, _dib: &mut [u8]) {
333 let mut alpha: i32 = 255;
334 while alpha > 0 {
335 alpha -= 25;
336 if alpha < 0 {
337 alpha = 0;
338 }
339 push_layered(hwnd, mem_dc, alpha as u8);
340 std::thread::sleep(std::time::Duration::from_millis(16));
341 }
342}