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
//! The typed native-widget event vocabulary plus the primitive `detail`
//! codec every interactive control's
//! Android listener packs into — the layer above [`crate::runtime`]'s raw
//! `(kind, detail)` transport.
//!
//! # Native-widget events bypass `RenderRoot::event` — again, in this module
//!
//! Every [`EventPayload`] decoded here is a **platform interaction
//! surfacing as a Rust callback**, delivered on the platform main thread
//! straight from `dev.frust.nativewidgets.FrustNativeListener` through
//! `crate::android`'s JNI export and
//! [`crate::runtime::NativeRuntime::on_event`] — never
//! through `frust-core`'s `EventCtx`. No pointer capture, no focus, none of
//! frust's fire-on-up-inside press semantics apply (see the crate doc's
//! *Native-widget events bypass `RenderRoot::event`*, and
//! `docs/CODE_STANDARDS.md`'s Interaction Semantics, which this vocabulary
//! is deliberately exempt from — a native control's interaction is entirely
//! platform-owned). The app-facing api wraps a decoded
//! [`EventPayload`] straight into a signal write, which is what wakes
//! exactly one frust frame.
//!
//! # One listener, eight kinds, no JSON
//!
//! Kinds 1-5 are emitted on Android; the sixth,
//! [`EVENT_KIND_SELECTION`], is appended for the Apple-arm-only segmented
//! control and is emitted only by the two Apple target classes today (its
//! Kotlin twin exists purely to keep the shared table whole; the iOS tab bar
//! reports its selections through it too). The seventh,
//! [`EVENT_KIND_DATE`], is appended after it for the date picker and is
//! emitted by all three arms. The eighth, [`EVENT_KIND_RESELECTED`], is the
//! iOS-only tab bar's tap on its showing item — emitted by the iOS target
//! class alone, its Kotlin twin again kept only for table parity.
//!
//! `FrustNativeListener` implements every listener interface
//! a v1 control needs — `View.OnClickListener`,
//! `CompoundButton.OnCheckedChangeListener`,
//! `SeekBar.OnSeekBarChangeListener`, `DatePicker.OnDateChangedListener` —
//! and funnels all of them into the one
//! native method `nativeOnEvent(slotId, kind, detail)`
//! (`crate::android::Java_dev_frust_nativewidgets_FrustNativeListener_nativeOnEvent`).
//! `detail` packs a primitive payload into a `jlong`; a value listener can
//! fire at drag rate, and allocating a JSON string per event on the main
//! thread is exactly what `docs/CODE_STANDARDS.md`'s no-JSON-on-the-hot-path
//! rule is about.
//!
//! **The `EVENT_KIND_*` constants below are LAW, shared verbatim with
//! `FrustNativeListener.kt`'s companion `KIND_*` constants — edit both
//! tables together.** This is mechanically enforced, not just a comment:
//! `plugins/native-widgets/tests/kotlin_conformance.rs` scans both files and
//! fails on any drift in the `KIND_*`/`EVENT_KIND_*` values, in
//! [`pack_value_changed`]/[`unpack_value_changed`]'s mask/shift contract, or
//! in [`pack_date`]'s field layout (Kotlin's `onDateChanged` packs the same
//! three fields by hand).
//!
//! # Slider values are platform-space until they leave this module
//!
//! `SeekBar` is always zero-based (`setMin` needs API 26; this plugin's
//! floor is 24 — `crate::controls::slider`'s module doc), so
//! `onProgressChanged` reports `progress - min` under the hood.
//! `crate::controls::slider::decode_event` (the only caller with a `min` to
//! add back) is what restores the app-space value the callback actually
//! sees; every other decode function in this module has no such conversion
//! because no other control maps its range.
use crateCivilDate;
use crateNativeEvent;
// --- the kind vocabulary -----------------------------------------------
/// `View.OnClickListener.onClick` — `detail` unused (`0`).
pub const EVENT_KIND_CLICK: i32 = 1;
/// `CompoundButton.OnCheckedChangeListener.onCheckedChanged` — `detail`
/// packs the checked state, see [`pack_bool`]/[`unpack_bool`].
pub const EVENT_KIND_TOGGLED: i32 = 2;
/// `SeekBar.OnSeekBarChangeListener.onProgressChanged` — `detail` packs the
/// platform-space progress plus `fromUser`, see
/// [`pack_value_changed`]/[`unpack_value_changed`].
pub const EVENT_KIND_VALUE_CHANGED: i32 = 3;
/// `SeekBar.OnSeekBarChangeListener.onStartTrackingTouch` — `detail` unused.
pub const EVENT_KIND_DRAG_START: i32 = 4;
/// `SeekBar.OnSeekBarChangeListener.onStopTrackingTouch` — `detail` unused.
pub const EVENT_KIND_DRAG_END: i32 = 5;
/// A segmented control's `ValueChanged` action (`UISegmentedControl` on iOS,
/// `NSSegmentedControl`'s action on macOS) — `detail` packs the reported
/// segment index, see [`pack_index`]/[`unpack_index`].
///
/// **Appended, Apple-arm-only in this build.** Kotlin's `KIND_SELECTION`
/// carries the same value so the two tables stay one table
/// (`tests/kotlin_conformance.rs`), but no Android listener ever emits it:
/// the segmented control has no Android arm yet (`crate::controls::segmented`'s
/// module doc).
pub const EVENT_KIND_SELECTION: i32 = 6;
/// A date picker's committed date change (`DatePicker.OnDateChangedListener`
/// on Android, `UIDatePicker`'s `ValueChanged` action on iOS,
/// `NSDatePicker`'s action on macOS) — `detail` packs the reported civil
/// date, see [`pack_date`]/[`unpack_date`].
///
/// **Appended** after [`EVENT_KIND_SELECTION`], never renumbered into the
/// table: kinds 1-6 are shipped wire values. Unlike `SELECTION`, all three
/// arms emit it (`crate::controls::date_picker` is a shared control).
pub const EVENT_KIND_DATE: i32 = 7;
/// A tab bar's tap on the item it was already showing
/// (`UITabBarDelegate.tabBar:didSelectItem:` for the showing item) — `detail`
/// packs the item index exactly as [`EVENT_KIND_SELECTION`] does
/// ([`pack_index`]); a tap on any other item reports `SELECTION` instead
/// (`crate::controls::tab_bar::tap_kind`).
///
/// **Appended** after [`EVENT_KIND_DATE`], **iOS-only in this build**: the tab
/// bar has no macOS or Android arm (`crate::controls::tab_bar`'s module doc).
/// Kotlin's `KIND_RESELECTED` carries the same value so the two tables stay
/// one table (`tests/kotlin_conformance.rs`), though no Android listener emits
/// it.
pub const EVENT_KIND_RESELECTED: i32 = 8;
// --- the typed vocabulary the app-facing api wraps into a signal -----------
/// One decoded native-widget event, past the raw `(kind, detail)` wire —
/// [`crate::runtime::NativeRuntime::on_event`] hands this to a slot's
/// registered `Arc<dyn Fn(EventPayload) + Send + Sync>` callback, invoked on
/// the platform main thread.
///
/// **Bypasses `RenderRoot::event` entirely** — see the module doc.
pub
// --- the detail codec -------------------------------------------------------
/// Pack a boolean into `detail` — [`EVENT_KIND_TOGGLED`]'s whole payload.
pub
/// [`pack_bool`]'s inverse: any non-zero `detail` is `true`.
pub
/// Pack a platform-space progress plus `fromUser` into `detail` —
/// [`EVENT_KIND_VALUE_CHANGED`]'s whole payload: the progress in the low 32
/// bits, the flag in bit 32. Progress is never negative
/// (`crate::controls::slider`'s `span`/`progress` are both clamped `>= 0`),
/// so the low 32 bits round-trip exactly for every value `SeekBar` can
/// report.
pub
/// [`pack_value_changed`]'s inverse.
pub
/// Pack a platform-reported segment index into `detail` —
/// [`EVENT_KIND_SELECTION`]'s whole payload. **Layout: the whole 64-bit
/// `detail` is the platform's own signed index, sign-extended** — no mask, no
/// flag bits (unlike [`pack_value_changed`]). Taking the signed `NSInteger`
/// verbatim keeps the platforms' "no segment selected" sentinel
/// (`UISegmentedControlNoSegment`, `-1` on both Apple arms) representable, so
/// [`unpack_index`] can refuse it rather than a caller having to guess.
pub
/// [`pack_index`]'s inverse: the reported segment, or `None` for a negative
/// (no-selection) report — which carries no requested segment to deliver.
pub
/// Pack a civil date into `detail` — [`EVENT_KIND_DATE`]'s whole payload.
///
/// **Layout** (the low 32 bits of the `i64`; bits 32-63 are always zero):
///
/// | bits | field | range |
/// |---|---|---|
/// | 16-31 | `year` | 1-9999 (fits the 16-bit field with room to spare) |
/// | 8-15 | `month` | **1-based**, 1-12 (Android's `monthOfYear` is 0-based — the Kotlin listener adds 1 before packing) |
/// | 0-7 | `day` | 1-31 |
///
/// Unlike [`pack_value_changed`]'s `progress | fromUser << 32`, no flag bit
/// rides along: a date report carries no `fromUser`, because no arm can
/// tell (and the runtime's re-entrancy drop already swallows the only
/// programmatic echo — `crate::controls::date_picker`'s module doc).
/// `FrustNativeListener.onDateChanged` duplicates this arithmetic by hand;
/// `tests/kotlin_conformance.rs` pins the masks, the shifts and the month
/// offset against this body.
pub
/// [`pack_date`]'s inverse — validated: `None` for any `detail` that is not
/// exactly a packed, real calendar date (stray high bits, a zero or
/// out-of-range field, a 31st of a 30-day month, a 29 February outside a
/// leap year), so a corrupted report can never reach an app callback as a
/// date the calendar does not have.
pub
/// Decode an [`EVENT_KIND_CLICK`] firing — `Button`'s whole event surface,
/// no echo guard (a click is never caused by `update`'s own setters, unlike
/// `Switch`/`Slider` — see `crate::controls`'s controlled-component note).
///
/// `None` when `event.kind` is not the click kind — defensive, since a
/// `Button`'s listener is only ever attached as an `OnClickListener` and so
/// can only ever report this one kind.
pub