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
//! Theme ladder L1's Apple half: mirrors
//! `crate::android::theme`'s L1 (that module's own doc — "why a wrapped
//! Context" — is the reference account of the *problem* L1 solves; this
//! module is the same fix through UIKit's own, genuinely different
//! mechanism).
//!
//! # Why there is no `createConfigurationContext` analogue here
//!
//! Android's L1 wraps the `Context` a control is *constructed against*,
//! because a framework `Button`/`Switch`/`SeekBar` resolves its
//! platform-owned chrome (ripple colour, thumb/track resting colour) from
//! that Context's `Configuration` at construction time — there is no other
//! hook. UIKit has no equivalent "construct against a themed context" step:
//! every control is built with `UIButton::buttonWithType`/`UILabel::new`/etc,
//! taking only a [`MainThreadMarker`] (`crate::apple::ctx`'s own doc: "the
//! ObjC runtime is globally reachable"). Instead, UIKit resolves a *dynamic*
//! `UIColor`'s and every semantic-colour drawable's on-screen value from the
//! view's own `traitCollection` at DRAW time, and
//! `UIView.overrideUserInterfaceStyle` is the per-view knob that pins what
//! that trait collection reports — set once, after construction, rather than
//! supplied to a constructor.
//!
//! So this arm's L1 is: read the same [`crate::controls::DARK`] wire bit
//! Android's [`brightness_is_dark`] reads (this module's own copy, since
//! there is no cross-target module either arm could share without pulling
//! `crate::controls` into a `#[cfg]`-neutral position it does not need to be
//! in — mirroring `crate::android::fonts` vs `crate::apple::fonts`'s
//! identical per-platform-module duplication), then set
//! [`apply_user_interface_style`] on the freshly created control's own
//! top-level `UIView` — `crate::apple::factory::create_control` is the one
//! generic call site every control's `createView` passes through, the exact
//! same single-choke-point shape `crate::android::create_control` uses for
//! its own context-wrapping (`crate::android::create_control`'s own doc).
//!
//! `overrideUserInterfaceStyle` **is available since iOS 13** (Apple's
//! documentation, retrieved 2026-07-27) and applies to the whole view
//! subtree it's set on — every descendant (a `UIButton`'s internal
//! `titleLabel`, a `UISwitch`'s thumb/track drawables) resolves its own
//! dynamic colours against the override without needing the property set
//! again.
//!
//! # LIVE on this arm — iOS does not share Android's L1 limitation
//!
//! Android's L1 genuinely must bake: `createConfigurationContext` yields a
//! `Context` consumed *at View construction*, so a live brightness toggle
//! cannot re-resolve a control's platform chrome without recreating it
//! (`crate::android::theme`'s "Baked at construction, not live").
//!
//! **`overrideUserInterfaceStyle` has no such constraint** — it is a plain
//! mutable `UIView` property, settable at any time, and setting it
//! re-resolves the whole subtree. So [`apply_user_interface_style`] is called
//! from **both** `create_control` and `update_control`.
//!
//! This module was originally written the Android way, mirroring that
//! contract rather than the platform, and **an on-device gate run found the
//! bug that caused**: a control culled off-screen and later RECREATED adopts
//! whatever brightness is current at recreate time, while its never-culled
//! siblings keep the one they were born under — so the two diverge. On the
//! reported repro (launch dark, switch to light, run the 50-slot scroll
//! stress, scroll back) the recreated `UISwitch` returned Light-pinned among
//! Dark-pinned peers; toggling back to dark then drew its default thumb
//! accent-on-accent, i.e. invisible.
//!
//! Re-pinning on update fixes that by making "which brightness is this
//! control pinned to" a function of the CURRENT theme rather than of when the
//! control happened to be constructed. It also makes an in-place brightness
//! toggle re-theme platform chrome live on iOS — strictly better than the
//! Android arm, whose baked-at-construction approximation has no iOS
//! counterpart. **Do not "restore symmetry" by reverting this**;
//! the asymmetry belongs to the platforms, not to a defect.
use ;
use crateDARK;
use crateParams;
/// Whether a slot's create params requested the dark half of the active
/// theme — read straight off the raw wire ([`DARK`]) before any per-control
/// typed `Props` decode runs, exactly like
/// `crate::android::theme::brightness_is_dark`. Absent (an older api layer,
/// or params from a control kind this task didn't fold theme tokens into)
/// degrades to `false` (light) rather than an error — night-qualification is
/// a cosmetic improvement, never a reason to fail control creation.
pub
/// L1: pin `view`'s (and its whole subtree's) resolved brightness via
/// `overrideUserInterfaceStyle` — see the module doc for why this is a
/// per-view property set after construction rather than a `Context` supplied
/// to one, and why it is called from **both** `create_control` and
/// `update_control` on this arm (the recreate-divergence fix).
pub