1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! `AppState` construction and high-level lifecycle handlers.
//!
//! - `new`: constructor; windows map starts empty — callers insert
//! `WindowState` entries immediately after (one per platform window).
//! - `handle_wake`: drain the foreground executor on background-task wake.
//! - `handle_window_destroyed`: per-window cleanup; last-window-aware quit.
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use slate_platform::{
DefaultPlatform, DefaultWindow, Window, WindowId, WindowImeDelegate, WindowRenderDelegate,
wake_run_loop,
};
use crate::app::AppContext;
use crate::executor::{Executor, RedrawRequester};
use crate::paint_cache::{TextShapingCache, TextShapingCacheObserver};
use crate::reactive_state::StateRegistry;
use super::state::{AppState, ErasedViewFactory, PendingWindowCreate};
use super::types::AppSignal;
#[cfg(any(test, feature = "test-hooks"))]
use super::types::RecoveryState;
use super::window_state::WindowState;
impl AppState {
/// Create a new `AppState`.
///
/// The `windows` map starts empty. The caller must insert a `WindowState`
/// for the initial window immediately after construction. The reactive
/// runtime's redraw bridge is wired here: it captures the
/// `Arc<Mutex<Vec<RedrawRequester>>>` so it satisfies `Send + Sync` even
/// though the `windows` `HashMap` itself is `!Sync`.
///
/// Wake-all policy (v1): every registered window is woken synchronously in
/// insertion order on each reactive signal change. Per-window subscription
/// tagging is post-v1.
pub fn new(
executor: Executor,
_redraw_requester: RedrawRequester,
runtime: Arc<slate_reactive::Runtime>,
) -> Self {
let redraw_requesters: Arc<Mutex<Vec<(WindowId, RedrawRequester)>>> =
Arc::new(Mutex::new(Vec::new()));
// Wire the reactive runtime's redraw bridge. The closure captures only
// Arc<Mutex<…>> which is Send+Sync, satisfying install_redraw's bound.
//
// Per-window RedrawRequesters all wrap the same `wake_run_loop`
// function (on Win32: PostMessage(WM_APP_WAKE)). Calling them in a loop
// posts N identical wake messages per signal change. On multi-window
// apps that combines with the wake handler (which re-posts once per
// window) into an exponential WM_APP_WAKE flood that starves WM_PAINT.
// Fire wake exactly once per signal change: the wake handler will
// then InvalidateRect every live window.
runtime.install_redraw({
let requesters = Arc::clone(&redraw_requesters);
Arc::new(move || {
let guard = requesters.lock().unwrap();
if let Some((_, req)) = guard.first() {
req.request();
}
})
});
let state_registry = StateRegistry::new(runtime.clone());
// Shared, atlas-independent caches.
let text_system = Rc::new(RefCell::new(None));
let text_shaping_cache = Rc::new(RefCell::new(TextShapingCache::new()));
let text_shaping_cache_observer = Rc::new(TextShapingCacheObserver::new(Rc::downgrade(
&text_shaping_cache,
)));
// GlyphCache + ImageCache live per-window (in WindowState) because
// they store atlas-scoped AllocIds and each window owns its own atlas.
Self {
windows: RefCell::new(HashMap::new()),
runtime,
executor,
text_system,
text_shaping_cache,
text_shaping_cache_observer,
state_registry: RefCell::new(state_registry),
redraw_requesters,
pending_quit: std::cell::Cell::new(false),
platform: RefCell::new(None),
pending_window_creates: RefCell::new(Vec::new()),
on_key_down: RefCell::new(Vec::new()),
on_key_up: RefCell::new(Vec::new()),
on_text_input: RefCell::new(Vec::new()),
on_ime_preedit: RefCell::new(Vec::new()),
on_ime_commit: RefCell::new(Vec::new()),
on_ime_enabled: RefCell::new(Vec::new()),
on_ime_disabled: RefCell::new(Vec::new()),
}
}
/// Register a new window's redraw requester so the reactive wake-all bridge
/// can include it. Called after inserting the `WindowState` into `windows`.
pub(crate) fn register_redraw_requester(&self, window: WindowId, req: RedrawRequester) {
self.redraw_requesters.lock().unwrap().push((window, req));
}
/// Install the platform handle so `AppContext::create_window` can allocate
/// new platform windows mid-dispatch. Called once by `App::run` immediately
/// before entering the event loop.
pub(crate) fn install_platform(&self, platform: Rc<DefaultPlatform>) {
*self.platform.borrow_mut() = Some(platform);
}
/// Wire a freshly-allocated platform window into `AppState`:
/// 1. Construct + insert `WindowState`.
/// 2. Register a `RedrawRequester` so the reactive wake-all bridge fans
/// out to this window.
/// 3. Install the shared render + IME delegates as `Weak<dyn …>` (same
/// Rc-clone-then-coerce dance as the first window in `App::run`).
///
/// Used by both the pre-`run` `App::create_window` path and the mid-loop
/// `AppContext::create_window` drain path. The renderer is **not** built
/// here — `init_surfaces` does that, called by the caller after the
/// `Event::Resumed` rendezvous (pre-run) or immediately at drain time
/// (dynamic).
pub(crate) fn install_window(self: &Rc<Self>, window: Arc<DefaultWindow>) {
let id = window.id();
// 1. Insert WindowState.
let win_state = WindowState::new(window.clone(), self.runtime.clone());
self.windows.borrow_mut().insert(id, win_state);
// 2. Register a redraw requester for the reactive wake-all bridge.
let redraw_requester = RedrawRequester::new(wake_run_loop);
self.register_redraw_requester(id, redraw_requester);
// 3. Install delegates. CANNOT use `as Weak<dyn …>` — unsizing
// coercions don't trigger on `as`. Must Rc::clone-then-coerce
// via let-binding, then downgrade. Identical SAFETY argument as
// the first-window path in `App::run`.
let dyn_strong: Rc<dyn WindowRenderDelegate> = self.clone();
let dyn_weak = Rc::downgrade(&dyn_strong);
window.set_render_delegate(dyn_weak);
drop(dyn_strong);
let ime_strong: Rc<dyn WindowImeDelegate> = self.clone();
let ime_weak = Rc::downgrade(&ime_strong);
window.set_ime_delegate(ime_weak);
drop(ime_strong);
}
/// Drain `pending_window_creates` and finish wiring each window:
/// 1. `install_window` (state insert + delegates).
/// 2. `init_surfaces` (renderer + text system + view).
///
/// Called by `App::run` after every event dispatch. Idempotent: an empty
/// queue is a no-op. The drain takes ownership before iterating so a
/// reactive effect fired by `init_surfaces` cannot re-push and deadlock.
pub(crate) fn drain_pending_window_creates(
self: &Rc<Self>,
cx: &AppContext,
platform: &DefaultPlatform,
) {
let drained: Vec<PendingWindowCreate> =
self.pending_window_creates.borrow_mut().drain(..).collect();
for mut pending in drained {
let id = pending.window.id();
self.install_window(pending.window);
if let Err(e) = self.init_surfaces(id, &mut pending.view_factory, cx, platform) {
log::error!(
"drain_pending_window_creates: init_surfaces failed for {:?}: {}",
id,
e
);
}
}
}
/// Push a window onto the pending-create queue. Returns the `WindowId`
/// of the (already-allocated) platform window so a caller can store it
/// before the drain step finishes wiring it.
pub(crate) fn push_pending_window_create(
&self,
window: Arc<DefaultWindow>,
view_factory: ErasedViewFactory,
) -> WindowId {
let id = window.id();
self.pending_window_creates
.borrow_mut()
.push(PendingWindowCreate {
window,
view_factory,
});
id
}
/// Unregister a window's redraw requester. Called before removing the
/// `WindowState` from `windows`.
pub(crate) fn unregister_redraw_requester(&self, window: WindowId) {
self.redraw_requesters
.lock()
.unwrap()
.retain(|(id, _)| *id != window);
}
/// Handle background task completion (`Event::Wake`).
///
/// Polls the foreground executor and requests a redraw on every registered
/// window (the executor may have unblocked a reactive effect).
pub fn handle_wake(&self) -> AppSignal {
self.executor.foreground.poll();
// Invalidate every live window directly (InvalidateRect / setNeedsDisplay).
// Earlier versions called `req.request()` here, which on Win32 posts a
// fresh WM_APP_WAKE per window — that recursively re-enters this handler
// and on N>1 windows turns into a runaway WM_APP_WAKE flood that
// starves WM_PAINT. Direct invalidation requests a paint tick without
// re-posting the wake message.
let guard = self.windows.borrow();
for win in guard.values() {
win.window.request_redraw();
}
AppSignal::None
}
/// Handle window close by platform (`Event::WindowDestroyed`).
///
/// Removes the window's entry from `windows` and clears its redraw
/// requester. Returns `RequestQuit` only when the last window was closed
/// (Win32 platform-default: quit on last-window-close) or `None` on macOS
/// (AppKit stays alive — Cmd+Q wired in the example).
pub fn handle_window_destroyed(&self, id: WindowId) -> AppSignal {
log::debug!("WindowDestroyed for {:?}", id);
// Drop the WindowState — renderer, view, and all per-window resources
// are freed here. This is the only correct place to do it: the borrow
// on `windows` is NOT held when user handlers could fire (dispatch
// discipline), so removing here is safe.
self.unregister_redraw_requester(id);
self.windows.borrow_mut().remove(&id);
let is_last = self.windows.borrow().is_empty();
if is_last {
#[cfg(target_os = "macos")]
return AppSignal::None; // AppKit stays alive; example wires Cmd+Q
#[cfg(not(target_os = "macos"))]
return AppSignal::RequestQuit; // Win32 platform-default: quit on last-window-close
}
AppSignal::None
}
/// Request a redraw on the window with the given id.
///
/// No-op (with a debug log) if the window is not found — this can happen
/// in a race between a reactive signal change and a window destroy.
pub(crate) fn request_redraw_for(&self, window: WindowId) {
let guard = self.windows.borrow();
if let Some(win) = guard.get(&window) {
win.window.request_redraw();
} else {
log::debug!(
"request_redraw_for: unknown WindowId {:?} — window may have been destroyed",
window
);
}
}
/// Snapshot the current recovery state for a specific window. Test-only use.
#[cfg(any(test, feature = "test-hooks"))]
pub fn current_recovery_state_for(&self, window: WindowId) -> Option<RecoveryState> {
self.windows
.borrow()
.get(&window)
.map(|w| w.recovery_state.borrow().clone())
}
}