bevy_window_manager 0.20.2

Bevy plugin for primary window restoration and multi-monitor support
Documentation
# macOS: Panic on exit from exclusive fullscreen mode

## Summary

On macOS, exiting an app while in exclusive fullscreen mode causes a panic due to Thread Local Storage (TLS) being accessed during its destruction.

## Reproduction

Minimal example:

```rust
use bevy::prelude::*;
use bevy::window::{MonitorSelection, VideoModeSelection, WindowMode};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Exclusive Fullscreen Test".into(),
                mode: WindowMode::Fullscreen(
                    MonitorSelection::Primary,
                    VideoModeSelection::Current,
                ),
                ..default()
            }),
            ..default()
        }))
        .run();
}
```

1. Run the app (enters exclusive fullscreen)
2. Press Cmd+Q to quit
3. Panic occurs

## Environment

- macOS 26.1 (Sequoia)
- Bevy 0.17
- winit 0.30.12

## Error

```
thread 'main' panicked at .../std/src/thread/local.rs:281:25:
cannot access a Thread Local Storage value during or after destruction: AccessError
```

## Root Cause Analysis

The panic occurs because `WINIT_WINDOWS` is stored in TLS, and windows are dropped during TLS destruction rather than during the event loop's `exiting` callback.

### Sequence of events:

1. `fn exiting` runs → `world.clear_all()` clears ECS resources
2. `winit_runner` returns from `event_loop.run_app()`
3. TLS destructors run as the thread cleans up
4. `WINIT_WINDOWS` TLS is destroyed, dropping all windows
5. winit's `Window::drop` calls `set_fullscreen(None)` for exclusive fullscreen
6. macOS sends a frame change callback
7. The callback tries to access TLS → **panic**

### Stack trace with annotations:

```
# TLS destructors running (thread cleanup phase)
56: std::sys::thread_local::destructors::list::run
55: std::sys::thread_local::native::eager::destroy

# WINIT_WINDOWS HashMap being dropped during TLS destruction
54: <hashbrown::raw::RawTable as Drop>::drop
53: hashbrown::raw::RawTableInner::drop_inner_table
52: hashbrown::raw::RawTableInner::drop_elements
51: hashbrown::raw::Bucket<T>::drop

# WindowWrapper<WinitWindow> being dropped
48: drop_in_place<bevy_window::raw_handle::WindowWrapper<winit::window::Window>>

# winit Window::drop calling set_fullscreen(None)
43: drop_in_place<winit::window::Window>
38-39: <winit::window::Window as Drop>::drop::{{closure}}
38: WindowDelegate::set_fullscreen

# macOS callback fires, tries to access TLS that's being destroyed
20: WinitView::frame_did_change

# PANIC: TLS already in destruction
```

### Evidence from winit source (v0.30.12)

winit's `Window::drop` explicitly exits fullscreen:

```rust
// src/window.rs
impl Drop for Window {
    fn drop(&mut self) {
        self.window.maybe_wait_on_main(|w| {
            // If the window is in exclusive fullscreen, we must restore the desktop
            // video mode (generally this would be done on application exit, but
            // closing the window doesn't necessarily always mean application exit,
            // such as when there are multiple windows)
            if let Some(Fullscreen::Exclusive(_)) = w.fullscreen().map(|f| f.into()) {
                w.set_fullscreen(None);
            }
        })
    }
}
```

## Suggested Fix

Drop windows from TLS in `fn exiting` before the event loop returns, while the event loop is still active:

```rust
// bevy_winit/src/state.rs
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
    // Drop windows while event loop is still active, before TLS destruction
    WINIT_WINDOWS.with(|ww| ww.borrow_mut().windows.clear());
    self.world_mut().clear_all();
}
```

This ensures windows are dropped in a controlled context where:
- The event loop is still running
- TLS is not being destroyed
- winit's `set_fullscreen(None)` can complete successfully

## Current Workaround

Create a resource that exits fullscreen in its `Drop` impl (runs during `world.clear_all()`):

```rust
use std::ops::Deref;
use bevy::prelude::*;
use bevy::winit::WINIT_WINDOWS;

#[derive(Resource)]
struct FullscreenExitGuard;

impl Drop for FullscreenExitGuard {
    fn drop(&mut self) {
        WINIT_WINDOWS.with(|ww| {
            for (_, window) in ww.borrow().windows.iter() {
                window.deref().set_fullscreen(None);
            }
        });
    }
}

// Insert in Startup system:
commands.insert_resource(FullscreenExitGuard);
```

This works because after exiting fullscreen, winit's `Window::drop` check (`if let Some(Fullscreen::Exclusive(_))`) fails, so it never calls `set_fullscreen(None)` during TLS destruction.