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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
//! HOST-declared translucent-surface slot:
//! [`declare_host_translucent_surface`]/[`SurfaceModeWatcher::current`], plus
//! its outward-facing sibling — the RESOLVED slot
//! ([`publish_resolved_surface_mode`]/[`resolved_surface_mode`]) an app or
//! plugin reads to learn what the platform actually gave us, including a
//! refusal (`translucencyRefused`). See `docs/SHELLS_ARCHITECTURE.md`'s
//! surface-mode resolution flow.
//!
//! # The gap this closes
//!
//! Platform-view compositing (Mode B) needs the GPU surface itself to be
//! created with an alpha channel (Android's `EGLConfig`, iOS's
//! `CAMetalLayer.isOpaque`) so a native sibling view placed *behind* it can
//! show through wherever frust paints nothing. That surface-format choice
//! happens once, at surface-creation time, well before any app code runs — so
//! there is no "widget asks for translucency" moment the way there is for,
//! say, `set_app_theme`.
//!
//! # Callers: host glue only
//!
//! [`declare_host_translucent_surface`] is called **only** by
//! `frust-shell-android::jni_glue`'s `native_set_surface_mode` and
//! `frust-shell-ios::ffi_glue`'s `set_surface_mode` — the fixed JNI/C-ABI
//! exports the generated host template's Kotlin/Swift calls during startup,
//! from the same branch that sets `SurfaceHolder`'s `PixelFormat.TRANSLUCENT` /
//! `CAMetalLayer.isOpaque = false` on the native window itself and arranges the
//! native-sibling z-order; each shell's surface-creation path then reads
//! [`SurfaceModeWatcher::current`] **before** configuring the surface. Setting
//! the latch is a claim that the window is *already* configured translucent, so
//! **calling it from anywhere else, or without that configuration in place, is
//! a host-template bug**: an app opting in from Rust alone with no matching
//! host window config is a reachable black-rectangle vector, which is why the
//! call is deliberately not re-exported past `frust-shell-common`.
//!
//! # This latch is a HOST DECLARATION, not the outcome
//!
//! Declaring it doesn't decide the outcome. Each shell translates this latch
//! into a `frust_render::SurfaceAlphaRequest`, and `frust-render` resolves
//! *that* against the platform's advertised `CompositeAlphaMode`s at configure
//! time — falling back to an opaque swapchain (with a `log::warn!`) when the
//! platform advertises no translucent mode, and the GPU-tier blit fallback can
//! further refuse a premultiply-expecting mode it can't reproduce. **The
//! resolved truth lives at a different seam**:
//! `frust_render::SurfaceRenderer::surface_resolved_translucent`, read by each
//! shell after every surface (re)install and threaded into
//! `RenderRoot::set_surface_translucent` — that seam governs paint.
//!
//! So: read this latch to decide what to *request*; never to decide whether to
//! paint the Mode B contract (a transparent base clear, a `platform_view` hole
//! punch). Keying paint off the declaration alone (ignoring the resolved seam)
//! means a fallback clears to `TRANSPARENT` and `DestOut`-punches every slot
//! rect on an OPAQUE swapchain — black rectangles instead of a graceful degrade
//! to Mode A.
//!
//! # Latch contract (one-way, v1)
//!
//! [`declare_host_translucent_surface`] only ever moves the slot from
//! [`SurfaceMode::Opaque`] to [`SurfaceMode::Translucent`] — there is no
//! "undo" call, and once observed as `Translucent` it never reverts. The
//! surface format is fixed at creation (the platform APIs above expose no
//! supported runtime toggle), so "reverting" would mean destroying and
//! recreating the whole surface — out of scope for v1, and no current use case
//! needs it (an app either wants platform-view compositing for the process's
//! lifetime, or it doesn't). A live flip would have to plumb a full
//! surface-recreation round-trip through each shell's `SurfacePhase` state
//! machine (`docs/ARCHITECTURE.md`'s frame pipelines) — not a slot-shape
//! change.
//!
//! One-way applies to the DECLARATION only. The *resolved* state below is not
//! one-way and is not fixed before the surface exists: every (re)install
//! re-resolves it, and a failed install clears it — which is exactly why the
//! shells re-read it per frame rather than caching it at construction.
//!
//! # The RESOLVED slot
//!
//! Everything above is the *inward* half: what the host declared, read by the
//! shells to decide what to request. [`publish_resolved_surface_mode`]/
//! [`resolved_surface_mode`] are the *outward* half — the resolved verdict
//! ([`ResolvedSurfaceMode`]) travelling back out to app/plugin code, whose
//! whole reason to exist is
//! [`ResolvedSurfaceMode::RefusedTranslucent`]: the host declared Mode B and
//! the platform resolved opaque anyway (its compositor offered no translucent
//! `CompositeAlphaMode`; see `docs/NATIVE_WIDGETS_ARCHITECTURE.md`'s Mode-B
//! paragraph). Without it that case is invisible *and* unsignalled — a
//! native sibling arranged behind a now-opaque frust surface simply vanishes,
//! with no way for app code to fall back deliberately.
//!
//! Publishing is again shell-glue-only (pinned by the same
//! `crates/frust/tests/surface_mode_conformance.rs` scan): each mobile shell
//! publishes whatever
//! `frust_render::SurfaceRenderer::surface_resolved_translucent` reports for
//! the live surface, on the same UI-thread beat it pushes
//! `RenderRoot::set_surface_translucent` — seeded at handle construction,
//! re-published on every later (re)install. Reading is open to anyone; the
//! `frust` facade re-exports [`resolved_surface_mode`] +
//! [`ResolvedSurfaceMode`] (never the publisher).
//!
//! **Polling contract, not a reactive one.** Nothing here wakes a frame: this
//! slot is a plain process-global read, exactly like
//! [`crate::theme_override::theme_override_active`]. App code reads it *during
//! a rebuild* (or during paint/an event handler) and branches on what it finds;
//! since both mobile shells run a continuous per-frame loop and re-publish
//! every frame, a downgrade is observed on the rebuild after the install that
//! caused it. A value read on a frame where nothing else is dirty does **not**
//! by itself schedule another frame — pair a fallback decision with an actual
//! state write (a signal set, a component-state change) if the UI must change
//! shape because of it.
//!
//! # Layering and thread contract
//!
//! Same shape as [`crate::theme_override`]/[`crate::system_ui`]: process-global
//! `Mutex` slots living in `frust-shell-common`, the crate every shell already
//! polls this kind of state from, each callable from any thread with no
//! ordering enforced. Unlike those two, neither slot is a per-frame
//! generation/poll pair, so [`SurfaceModeWatcher`] carries no per-instance
//! "last seen" cursor — `current` is an associated function, a plain peek.
//!
//! In practice [`declare_host_translucent_surface`] must be called before the
//! shell's surface-creation path reads [`SurfaceModeWatcher::current`]
//! (startup-time only — see the module docs above), and each shell publishes
//! the resolved slot from its UI thread (the one owning the `RenderRoot`) while
//! app code reads it from that same thread during a rebuild. Both orderings are
//! caller responsibilities, not something this module enforces; the read is a
//! plain lock-load either way, so an off-thread reader sees a consistent value,
//! just possibly one frame old.
use Mutex;
/// Whether a shell's GPU surface should be created with an alpha channel.
/// See the module docs' Latch contract — this only ever moves
/// `Opaque` → `Translucent`, never back.
/// The process-wide latch. No generation counter (unlike
/// [`crate::theme_override`]/[`crate::system_ui`]'s slots) — see the module
/// docs' Layering and thread contract for why a per-frame "changed since last
/// poll" concept doesn't apply here.
static SURFACE_MODE: = new;
/// Declare that the native host window has already been configured
/// translucent (`PixelFormat.TRANSLUCENT`/`isOpaque = false`) so this
/// shell's next GPU surface should be created with an alpha channel too.
///
/// **Called only by the generated host glue** (`jni_glue::native_set_surface_mode`
/// on Android, `ffi_glue::set_surface_mode` on iOS), from the same branch
/// that actually configured the window — see the module docs' Callers
/// section. Calling this without that window configuration in place is a
/// host-template bug, not a supported app-Rust opt-in.
///
/// Callable from any thread (see the module docs' Layering and thread
/// contract), and
/// idempotent — calling it more than once, or after the surface already
/// latched translucent, has no additional effect.
///
/// Must be called before the running shell's surface-creation path reads
/// [`SurfaceModeWatcher::current`] (see the module docs) — calling it after
/// the surface already exists has no effect on that surface.
///
/// Declaring translucency does not guarantee the resolved outcome: the
/// platform may refuse (see the module docs' *This latch is a HOST
/// DECLARATION, not the outcome*), in which case the app degrades to the
/// opaque Mode A contract.
/// Per-shell-instance reader over the process-wide latch. Kept as a type
/// (mirroring [`crate::theme_override::ThemeOverrideWatcher`]/
/// [`crate::system_ui::SystemUiWatcher`]'s shape) even though it carries no
/// state of its own — [`current`](Self::current) is a plain peek, not a
/// diffed poll, per the module docs' Layering and thread contract.
;
/// What the platform **actually gave us**, as opposed to what the host
/// declared ([`SurfaceMode`]) — see the module docs' *The RESOLVED slot*.
///
/// Read via [`resolved_surface_mode`] (re-exported by the `frust` facade);
/// published only by the two mobile shells, once per frame, from the same
/// value that drives `RenderRoot::set_surface_translucent`.
/// The process-wide RESOLVED slot, beside [`SURFACE_MODE`]'s declaration
/// latch. Unlike that latch this is **not** one-way: every surface (re)install
/// re-resolves, and a failed install downgrades — see the module docs.
static RESOLVED_SURFACE_MODE: = new;
/// Publish the live surface's **resolved** translucency, mapped against the
/// host declaration into a [`ResolvedSurfaceMode`] (see
/// [`ResolvedSurfaceMode::resolve`]).
///
/// **Called only by the two mobile shells' `app.rs`** — the UI-thread beat
/// that already reads the resolved flag and pushes
/// `RenderRoot::set_surface_translucent` — pinned by
/// `crates/frust/tests/surface_mode_conformance.rs` the same way
/// [`declare_host_translucent_surface`] is. `resolved_translucent` is what
/// `frust_render::SurfaceRenderer::surface_resolved_translucent` reports for
/// the surface that is live *now*; a failed install reports `false`, which is
/// the honest answer (nothing to punch a hole in).
///
/// Idempotent and re-callable in any direction: publishing the same value
/// every frame is the expected usage, and a later install may legitimately
/// move the slot back (`Translucent` → `RefusedTranslucent`, or the reverse
/// once a re-created surface comes up capable again).
/// Read the resolved surface mode — [`ResolvedSurfaceMode::Unknown`] until a
/// shell publishes one (no surface yet, or a shell with no Mode B seam, e.g.
/// the desktop preview).
///
/// A **poll**, not a subscription: reading this never wakes a frame, and a
/// change here never marks anything dirty on its own. Read it during a rebuild
/// (the same place `theme_override`-style process-global state is read) and
/// branch; see the module docs' *The RESOLVED slot* for the full contract.