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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
//! Surface lifecycle management for Guido applications.
//!
//! This module provides types for managing the lifecycle of surfaces,
//! including GPU initialization and widget layout.
use rustc_hash::FxHashMap;
use crate::layout::Constraints;
use crate::reactive::owner::{OwnerId, dispose_owner_now};
use crate::renderer::{
CommandLayer, FlattenScratch, FlattenedCommand, GpuContext, RenderNode, RenderTarget,
};
use crate::surface::{SurfaceConfig, SurfaceId};
use crate::tree::{Tree, WidgetId};
use crate::widgets::{Rect, Widget};
use crate::{Platform, Surface};
/// A surface with unified GPU lifecycle management.
///
/// This combines the widget tree, GPU surface state, and configuration
/// into a single struct that manages the surface's entire lifecycle.
pub struct ManagedSurface {
/// The unique identifier for this surface
pub id: SurfaceId,
/// Configuration for the surface
pub config: SurfaceConfig,
/// The root widget ID (widget is stored in the tree)
pub widget_id: WidgetId,
/// Owner for reactive primitives created in the widget factory
owner_id: OwnerId,
/// Where this surface's frames land (None until GPU init).
pub wgpu_surface: Option<RenderTarget>,
/// Previous scale factor for detecting changes
pub previous_scale_factor: f32,
/// Root render node (reused across frames to avoid allocation)
pub root_node: RenderNode,
/// Flattened commands buffer (reused across frames to avoid allocation)
pub flattened_commands: Vec<FlattenedCommand>,
/// Draw groups over `flattened_commands`, in draw order.
pub command_layers: Vec<CommandLayer>,
/// Where flatten builds those two before it fills them (reused across
/// frames to avoid allocation)
pub flatten_scratch: FlattenScratch,
/// Where input reaches this surface before any container has spoken:
/// what the config declared, and what a handle has said since.
///
/// One home for it, so a runtime `set_input_region` and a declaration in
/// the tree compose instead of erasing each other.
pub input_base: Option<Vec<Rect>>,
/// The region this surface last asked the compositor for, or `None` while
/// no container has ever declared one.
///
/// Kept here rather than in a platform so that both of them publish on the
/// same rule: once, when it changes, and never for a surface whose tree has
/// said nothing about input.
pub input_region: Option<crate::region::InputRegionRequest>,
/// The logical size this surface last declared its buffer stands for, or
/// `None` while it has declared none.
///
/// Here beside the reservation and the input region because it is the same
/// kind of thing: a double-buffered request the compositor already holds,
/// which a frame with nothing new to say must not send again.
pub viewport_destination: Option<(u32, u32)>,
/// The reservation this surface last asked the compositor for, starting
/// with the one a layer surface is created with, so a number the compositor
/// already has is not sent again. Popups and lock surfaces carry one too and
/// never send it.
pub exclusive_zone: Option<i32>,
}
impl ManagedSurface {
/// Create a new managed surface (wgpu_surface is None until GPU init).
/// The root widget and its children are registered in the tree.
pub fn new(
id: SurfaceId,
config: SurfaceConfig,
widget: Box<dyn Widget>,
owner_id: OwnerId,
tree: &mut Tree,
) -> Self {
let config_input_region = config.input_region.clone();
let exclusive_zone = Some(crate::surface::initial_declaration(&config).exclusive_zone);
// Register root widget - tree assigns the ID
let widget_id = tree.register(widget);
// Register children recursively with the assigned widget ID
tree.with_widget_mut(widget_id, |widget, id, tree| {
widget.register_children(tree, id);
});
Self {
id,
config,
widget_id,
owner_id,
wgpu_surface: None,
previous_scale_factor: 1.0,
root_node: RenderNode::new(widget_id.as_u64()),
flattened_commands: Vec::new(),
command_layers: Vec::new(),
flatten_scratch: FlattenScratch::default(),
input_base: config_input_region,
input_region: None,
viewport_destination: None,
exclusive_zone,
}
}
/// Initialize GPU surface. Returns true if successful.
///
/// `at` is the moment the first layout runs at — see
/// [`Self::layout_widget`] for why it is the caller's to name.
pub fn init_gpu<P: Platform>(
&mut self,
gpu_context: &GpuContext,
platform: &P,
(width, height): (u32, u32),
scale_factor: f32,
tree: &mut Tree,
at: std::time::Instant,
) -> bool {
if self.wgpu_surface.is_some() {
return true; // Already initialized
}
let (physical_width, physical_height) =
crate::surface::buffer_size((width, height), scale_factor);
log::info!(
"Creating render target for {:?}: logical {}x{}, physical {}x{}, scale {}",
self.id,
width,
height,
physical_width,
physical_height,
scale_factor
);
let Some(target) =
platform.create_render_target(self.id, gpu_context, (physical_width, physical_height))
else {
return false;
};
self.wgpu_surface = Some(target);
self.previous_scale_factor = scale_factor;
// Perform initial layout
self.layout_widget(tree, width as f32, height as f32, at);
true
}
/// Check if GPU is initialized.
pub fn is_gpu_ready(&self) -> bool {
self.wgpu_surface.is_some()
}
/// Perform widget layout with the given dimensions, at the moment `at`.
///
/// This is a surface's *first* layout, and a pass like any other: an enter
/// animation is seeded here and advanced by the frame that follows, so the
/// two have to be reading one clock. Undeclared, this one took the wall
/// clock while the frame took the loop's `frame_at`, which on a bar is
/// microseconds of nothing and to a driver that names its own instant is
/// an enter already over before its first frame.
pub fn layout_widget(&self, tree: &mut Tree, width: f32, height: f32, at: std::time::Instant) {
let constraints = Constraints::new(0.0, 0.0, width, height);
tree.set_frame_instant(Some(at));
tree.layout_widget(self.widget_id, constraints);
// Set root widget origin after layout
tree.set_origin(self.widget_id, 0.0, 0.0);
// The pass is over, and so is its instant — the rule `render_surface`
// keeps for the same reason: a stale clock fails silently.
tree.set_frame_instant(None);
}
}
impl Drop for ManagedSurface {
fn drop(&mut self) {
dispose_owner_now(self.owner_id);
}
}
/// Tear down a closed surface's widget tree synchronously.
///
/// Removing a `ManagedSurface` only disposes its reactive owner; without
/// this, the widget subtree stays registered in the [`Tree`] forever (a
/// leak that grows with every closed popup) and its signal subscribers
/// stay live — the next write to a subscribed signal reconciles a dead
/// dynamic child whose closure reads owner-disposed state and panics.
///
/// Widgets are unregistered children-first, so each Drop's deferred
/// Unregister requests target already-removed ids and no-op; any stale
/// queued job no-ops too because the ids fail the tree's generation check.
///
/// Call this BEFORE dropping the `ManagedSurface`: subscribers must be
/// gone before the owner (and the signals it holds) is disposed.
pub(crate) fn teardown_widget_subtree(tree: &mut Tree, root: WidgetId) {
crate::jobs::teardown_widget_subtree(tree, root);
}
/// Manages all surfaces in the application.
pub struct SurfaceManager {
surfaces: FxHashMap<SurfaceId, ManagedSurface>,
}
impl SurfaceManager {
/// Create a new empty surface manager.
pub fn new() -> Self {
Self {
surfaces: FxHashMap::default(),
}
}
/// Add a surface.
pub fn add(&mut self, surface: ManagedSurface) {
self.surfaces.insert(surface.id, surface);
}
/// Remove a surface by ID.
pub fn remove(&mut self, id: SurfaceId) -> Option<ManagedSurface> {
self.surfaces.remove(&id)
}
/// Get a surface by ID.
pub fn get(&self, id: SurfaceId) -> Option<&ManagedSurface> {
self.surfaces.get(&id)
}
/// Get a mutable surface by ID.
pub fn get_mut(&mut self, id: SurfaceId) -> Option<&mut ManagedSurface> {
self.surfaces.get_mut(&id)
}
/// Iterate over all surface IDs.
pub fn ids(&self) -> impl Iterator<Item = SurfaceId> + '_ {
self.surfaces.keys().copied()
}
/// Check if empty.
pub fn is_empty(&self) -> bool {
self.surfaces.is_empty()
}
/// First initialized wgpu surface, if any.
///
/// Used to create the shared `Renderer` lazily: apps may start with zero
/// surfaces and spawn them dynamically (e.g. one bar per output).
pub fn first_gpu_surface(&self) -> Option<&RenderTarget> {
self.surfaces.values().find_map(|s| s.wgpu_surface.as_ref())
}
/// Initialize GPU for surfaces that need it, laying each out at `at`.
///
/// This iterates over all surfaces and initializes GPU for any
/// that are configured in Wayland but don't yet have a wgpu surface.
pub fn init_pending_gpu<P: Platform>(
&mut self,
gpu_context: &GpuContext,
wayland_state: &mut P,
tree: &mut Tree,
at: std::time::Instant,
) {
for (id, surface) in self.surfaces.iter_mut() {
if surface.is_gpu_ready() {
continue;
}
// A surface with no confirmed size has nothing to build a target
// at, and one with no confirmed scale would build it at the wrong
// one.
let facts = wayland_state
.surface(*id)
.and_then(|s| Some((s.configured_size()?, s.scale_factor()?)));
let Some(((width, height), scale)) = facts else {
continue;
};
surface.init_gpu(gpu_context, wayland_state, (width, height), scale, tree, at);
}
}
}
impl Default for SurfaceManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use std::rc::Rc;
use super::*;
use crate::layout::{Constraints, Size};
use crate::reactive::owner::{dispose_owner_now as dispose, with_owner};
use crate::reactive::{create_memo, create_signal};
use crate::tree::LayoutCtx;
use crate::widgets::Widget;
use crate::widgets::children::ChildrenSource;
use crate::widgets::into_child::{DynamicChild, IntoChild};
struct TestWidget;
impl Widget for TestWidget {
fn layout(&mut self, _: &mut LayoutCtx, _: Constraints) -> Size {
Size::zero()
}
fn paint(&self, _: &mut crate::renderer::PaintContext) {}
}
/// The surface-close crash scenario: a dynamic child reads a memo owned
/// by the surface factory's owner scope; the surface closes (owner
/// disposed); a later write to the tracked signal must NOT re-run the
/// dead closure (which would read the disposed memo and panic) — and the
/// subtree must actually leave the tree (the popup leak).
#[test]
fn teardown_clears_subscribers_and_empties_tree() {
let mut tree = Tree::new();
let parent = tree.register(Box::new(TestWidget));
let mut source = ChildrenSource::default();
source.set_container_id(parent);
let sig = create_signal(0u64);
let builds = Rc::new(Cell::new(0usize));
// Mimic the surface factory: reactive state owned by a scope that
// dies with the surface
let (memo, owner_id) = with_owner(|| create_memo(move || sig.get() + 1));
let counter = builds.clone();
let closure = move || {
let _ = memo.get();
counter.set(counter.get() + 1);
TestWidget
};
IntoChild::<DynamicChild>::add_to_container(closure, &mut source);
source.reconcile_and_get(&mut tree);
assert_eq!(builds.get(), 1);
assert!(tree.widget_count() > 1);
// Surface close: subtree teardown, THEN owner disposal
teardown_widget_subtree(&mut tree, parent);
dispose(owner_id);
// The write that used to reconcile the dead child into a panic
sig.set(1);
source.reconcile_with_tracking(&mut tree);
assert_eq!(builds.get(), 1, "dead closure must not re-run");
assert_eq!(tree.widget_count(), 0, "subtree must leave the tree");
}
}