rust_widgets 2.8.4

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
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
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Forwards a control's own signal emissions to a name-addressed
//! [`CustomSignalHub`].
//!
//! # The gap this closes
//!
//! [`WidgetFactory::connect_event`](crate::widget::capability::WidgetFactory::connect_event)
//! lets a caller subscribe to an event name a capability publishes, and its tests prove
//! the subscription is validated and delivers. What they do **not** prove — because it is
//! not true of `connect_event` alone — is that the *control* reaches that subscriber.
//! `connect_event` registers a slot on a hub; the control emits its own typed signals
//! (`Button`'s `clicked`, `Slider`'s `value_changed`). Nothing joined the two, so a
//! subscriber validated against a published name would still never be called by a real
//! control.
//!
//! # Why this is a binder and not automatic
//!
//! A control's signals are **typed**: `clicked` is `GenericSignal`, `value_changed` is
//! `Signal1<i32>`, `toggled` is `Signal1<bool>`. A hub name is untyped
//! (`CustomSignalHub::emit(name)` carries no value). Bridging them therefore requires a
//! per-event decision about what happens to the payload, and that decision belongs to the
//! caller: one that does not use the values does not pay for them, and one that does
//! supplies the mapping rather than receiving a lossy default.
//!
//! This type is the reusable half: it owns the subscriptions and removes them together,
//! so a dropped binder leaves nothing behind.
//!
//! # What a host has to do
//!
//! One call per control, whatever names its capability publishes:
//!
//! ```ignore
//! let mut binder = EventSignalBinder::new(hub);
//! binder.forward_all(&control); // wires every published name in one call
//! ```
//!
//! [`Self::forward_all`] walks the control's published events and subscribes to each through
//! [`Widget::event_signal_dyn`](crate::widget::Widget::event_signal_dyn), so a converted control is
//! covered by a single line and a name the panel offers **is** wired. The older per-name entries
//! (`forward_unit`, `forward_mapped`) remain for a hand-written wire that wants to observe a value
//! on the way past, but they are no longer the shape a host should reach for: asking a host to write
//! one line per event is exactly the work rule #98 rules out, because a designer learns its wires at
//! run time and cannot pre-generate them.
//!
//! # Wiring status (read this before assuming an event fires)
//!
//! **No control in this library wires itself to a hub.** Signals and the hub are two
//! separate worlds by design: a control owns its typed signals and knows nothing
//! about an application-level name space, and a hub knows nothing about controls. The
//! join is the *host's* job, performed at mount time.
//!
//! `EventSignalBinder` is the mechanism for that join, and
//! `tests/event_signal_bridge_test.rs` is its proof — but that test performs the
//! wiring itself. It proves the binder **works**, not that this crate already did the
//! wiring for any control. Two consequences a caller must know:
//!
//! * `WidgetFactory::connect_event` returns `Ok` for any published name whether or not
//!   anything is wired to it, because "is this name valid?" and "is something
//!   emitting it?" are different questions. Subscribing therefore cannot fail just
//!   because a host skipped the wiring step — an event that is valid but unwired is
//!   indistinguishable from an event that never occurs.
//! * [`Self::detached`] exists for a control built without a hub; its forward calls are
//!   documented no-ops, so a host that uses it has opted out explicitly rather than silently.
//!
//! The single host entry point is [`Self::forward_all`]: one call per control, wiring every name
//! the control's capability publishes by asking the control for each one's signal through
//! [`Widget::event_signal_dyn`](crate::widget::Widget::event_signal_dyn). This covers payload-free
//! names (`clicked`, `dismissed`) and payload-carrying names (`value_changed` is `Signal1<i32>`,
//! `toggled` is `Signal1<bool>`) alike, because the erasure happens inside the control where the
//! concrete payload type is still known. The count it returns is the number of names it wired, so a
//! caller that receives `0` can see the control contributed nothing instead of assuming it did.
//!
//! [`Self::forward_widget_events`] is the earlier, narrower helper: it wires the one name every
//! `Widget` is guaranteed to have — the base `clicked` signal — and nothing else. It remains so an
//! existing caller keeps working, but a new caller choosing between the two wants `forward_all`.
//!
//! Exhaustiveness of the *subscribable* side is stated by
//! `tests/event_signal_bridge_test.rs`'s `every_published_event_accepts_a_forwarding_call`,
//! which requires every name a capability publishes to be accepted by `forward_unit`/`forward_mapped`
//! (i.e. to have a hub destination). That is a real guarantee, but it is narrower than it sounds:
//! it proves a forwarding call **can** be made for each published name, not that this crate
//! **does** make one. Nothing here wires a control's non-`clicked` events automatically —
//! see "Wiring status" above — so a control whose events are never forwarded by its host still
//! has published names that are valid and inert.

use super::CustomSignalHub;
use crate::signal::{ConnectionHandle, GenericSignal, Signal1};
use alloc::boxed::Box;
use alloc::sync::Arc;

/// A set of hub subscriptions that forwards control signals, removed together.
///
/// The subscription list is kept so dropping the binder unsubscribes everything it added.
/// Without it, a rebuilt control would gain a subscriber per rebuild — the leak a
/// "connect and forget" binding produces.
///
/// # Why there is no separate wiring ledger
///
/// An earlier revision kept a process-wide count of how many events each control *kind* had
/// published and wired. That count was historical (it never decreased on unbind), shared across
/// instances of a kind, and folded with `max`/`sum`, so it answered "what did some instance of this
/// kind once resolve" rather than "is *this* control's wire live". The live answer is derivable
/// from the subscriptions the binder actually holds — see [`Self::event_is_wired`] and
/// [`Self::unwired_events_for`] — so the ledger was removed rather than kept alongside a second
/// source of truth (rule #101).
pub struct EventSignalBinder {
    /// `Arc`, not `CustomSignalHub`, because the hub's state is a `Mutex` and it is
    /// therefore not `Clone`. A slot must own a handle to emit through, so the shared
    /// handle is an `Arc` — the same shape every other signal in this module uses.
    hub: Option<Arc<CustomSignalHub>>,
    forwards: alloc::vec::Vec<Forwarded>,
    /// Whether this binder has ever wired anything.
    ///
    /// `unwired_events_for` answers `None` ("not wired yet") until the first `forward_*` call, and a
    /// live `Some(count)` afterwards — even across `unbind_all`. That is what keeps "never wired"
    /// distinct from "wired and then unbound": both have an empty `forwards`, but only the second
    /// has a history, and the query must report the second as fully unwired rather than as no answer
    /// (BLUE-issue E-07).
    wired_once: bool,
}

impl Default for EventSignalBinder {
    fn default() -> Self {
        Self::detached()
    }
}

impl EventSignalBinder {
    /// Creates an empty binder that forwards into `hub`.
    pub fn new(hub: Arc<CustomSignalHub>) -> Self {
        Self { hub: Some(hub), forwards: alloc::vec::Vec::new(), wired_once: false }
    }

    /// Creates a binder with nowhere to forward, so the forwarding calls are no-ops.
    ///
    /// Used by a control constructed without an application hub. The alternative —
    /// forcing every caller to supply one — would make the hub a mandatory part of every
    /// constructor, which is the global state this design avoids.
    pub fn detached() -> Self {
        Self { hub: None, forwards: alloc::vec::Vec::new(), wired_once: false }
    }

    /// Reports whether this binder forwards into a hub.
    pub fn is_attached(&self) -> bool {
        self.hub.is_some()
    }

    /// Number of subscriptions this binder owns.
    pub fn len(&self) -> usize {
        self.forwards.len()
    }

    /// Reports whether no subscription has been registered.
    pub fn is_empty(&self) -> bool {
        self.forwards.is_empty()
    }

    /// Forwards a payload-free signal to the hub under `event_name`.
    ///
    /// The typical case: `clicked`, `dismissed` — the event carries the fact that it
    /// happened, and the name carries the rest.
    pub fn forward_unit(&mut self, event_name: &str, signal: &GenericSignal) {
        let Some(hub) = self.hub.clone() else {
            return;
        };
        // The slot outlives this call, so it must own the name rather than borrow it.
        let name = alloc::string::String::from(event_name);
        let handle = signal.connect(move || hub.emit(&name));
        let probe = signal.clone();
        self.forwards.push(Forwarded {
            source: ForwardSource::Unit(signal.clone()),
            handle,
            event_name: alloc::string::String::from(event_name),
            signal_identity: signal.identity(),
            is_connected: alloc::boxed::Box::new(move || probe.is_connected(handle)),
        });
        // Every successful forward records the history, not just `wire_one`. Without
        // this, a binder used only through the public manual entry points
        // (`forward_unit`, and `forward_widget_events` which calls it) left
        // `wired_once == false`, so `unwired_events_for` answered `None` — reporting a
        // real wire as "not wired yet" — and `event_is_wired` could disagree with the
        // See `unwired_events_for`'s contract (BLUE-issue E-07).
        self.wired_once = true;
    }

    /// Wires **every** event a mounted control publishes, in one call.
    ///
    /// # Why this is the entry point a designer needs
    ///
    /// A designer's wires are decided at run time: the user drew a line from `value_changed`, and
    /// the program learns which name that was when the project is loaded. So the host cannot be
    /// asked to write `forward_unit("clicked", ..)` / `forward_mapped("value_changed", ..)` per
    /// control — those lines would have to be generated for wires that do not exist yet, which is
    /// exactly the host work rule #98 rules out.
    ///
    /// This walks the control's *published* events instead, asking the control for each one's signal
    /// through [`Widget::event_signal_dyn`], and subscribes to all of them. A converted control
    /// therefore needs **one** line at its mount site, and every name its capability offers in the
    /// panel **is** wired.
    ///
    /// # What happens to the payload
    ///
    /// The hub carries names, not values, so the slot forwards the name and drops the value — the
    /// same loss [`Self::forward_unit`] makes, stated here rather than left implicit. The
    /// **name-addressed** channel is deliberately value-free; a consumer that needs the value takes
    /// the payload-bearing channel instead (`crate::json::bind_published_event`, whose handler
    /// receives the emitted [`CapabilityValue`](crate::widget::capability::CapabilityValue) in
    /// [`crate::json::EventHandlerContext::payload`]). Stating which channel carries the value — and
    /// which does not — is what stops a caller assuming a value it will never receive.
    ///
    /// # Returns
    ///
    /// The number of events wired. A control that has not been converted yet reports `0`, so a
    /// caller can see that it contributed nothing instead of assuming it did.
    ///
    /// # Relationship to [`Self::forward_widget_events`]
    ///
    /// That method is the earlier, weaker version: it wires `clicked` and nothing else, because when
    /// it was written a control had no way to name its other signals. This one supersedes it for a
    /// call that is choosing between the two; the narrower method remains so an existing caller
    /// keeps working.
    ///
    /// [`Widget::event_signal_dyn`]: crate::widget::Widget::event_signal_dyn
    pub fn forward_all<W>(&mut self, widget: &W) -> usize
    where
        W: crate::widget::Widget,
    {
        // A stripped profile compiles the capability table out, so there is no published name list
        // to walk. `0` is the honest answer: the method reports how many events it wired.
        #[cfg(full_widgets)]
        {
            let factory = crate::widget::capability::WidgetFactory::new_with_defaults();
            let Some(capability) = factory.capability_for_kind_instance(widget) else {
                return 0;
            };
            let mut wired = 0usize;
            for schema in capability.events {
                // A control that publishes a name it cannot resolve would otherwise be wired
                // silently short. Treating it as zero keeps the return value honest, and
                // `tools/check_event_signal_dyn.sh` fails the build-time counterpart of this case.
                if self.wire_one(widget, schema.name) {
                    wired += 1;
                }
            }
            wired
        }
        #[cfg(not(full_widgets))]
        {
            let _ = widget;
            0
        }
    }

    /// How many of `widget`'s published events have **no** live subscription from this binder.
    ///
    /// `None` when this binder holds no subscription at all, which is a different answer from
    /// `Some(0)` — "not wired yet" and "fully wired" must not look alike.
    ///
    /// This is the query BLUE19 #97 requires: `connect_event` returning `Ok` proves only that a
    /// name is valid, so without this a host cannot tell "this wire is live" from "this wire was
    /// accepted and will never fire".
    ///
    /// # Why this walks the capability instead of reading a recorded count
    ///
    /// The answer is derived from the subscriptions the binder actually holds *now*, one published
    /// name at a time, through [`Self::event_is_wired`]. That makes it instance-independent, deduped
    /// by event name, and truthful after [`Self::unbind_all`]: an event whose wire was removed
    /// counts as unwired again, and a brand-new instance of a kind never inherits a sibling's
    /// history. A recorded ledger could not do any of those — it was process-wide, never decreased
    /// on unbind, and conflated repeated connections of one name with other names.
    #[cfg(full_widgets)]
    pub fn unwired_events_for<W>(&self, widget: &W) -> Option<usize>
    where
        W: crate::widget::Widget,
    {
        // "Never wired" and "not a control with events" both mean there is no answer. A **detached**
        // binder has nowhere to wire, so it has nothing live to report either.
        if !self.wired_once || self.hub.is_none() {
            return None;
        }
        let factory = crate::widget::capability::WidgetFactory::new_with_defaults();
        let capability = factory.capability_for_kind_instance(widget)?;
        let unwired = capability
            .events
            .iter()
            .filter(|schema| !self.event_is_wired(widget, schema.name))
            .count();
        Some(unwired)
    }

    /// The stripped-profile form, which has no capability table to have wired against.
    #[cfg(not(full_widgets))]
    pub fn unwired_events_for<W>(&self, _widget: &W) -> Option<usize>
    where
        W: crate::widget::Widget,
    {
        None
    }

    /// Wires one published event of `widget`, reporting whether it was wired.
    ///
    /// The single-event form of [`Self::forward_all`], for a caller that wants to skip a name (a
    /// designer with one hand-made exception) or to check a name without wiring the rest.
    pub fn forward_one<W>(&mut self, widget: &W, event_name: &str) -> bool
    where
        W: crate::widget::Widget,
    {
        self.wire_one(widget, event_name)
    }

    /// The shared body of [`Self::forward_all`] and [`Self::forward_one`].
    ///
    /// Keeping the two entry points on one implementation is what stops the single-event form from
    /// drifting from the all-events form: a fix to how a name is resolved is a fix to both.
    fn wire_one<W>(&mut self, widget: &W, event_name: &str) -> bool
    where
        W: crate::widget::Widget,
    {
        let Some(reference) = widget.event_signal_dyn(event_name) else {
            return false;
        };
        // The reference must *declare* the name it was asked for.
        //
        // Without this, the declared name was write-only: `event_signal_dyn` could resolve
        // `"clicked"` to a reference built over `self.value_changed` and `wire_one` would subscribe
        // to that signal anyway, reporting success. The hub name came from the argument rather than
        // the reference, so the mismatch was invisible — a subscriber to `"clicked"` would be
        // attached to an event the control never fires under that name, which is precisely the
        // silent "valid but inert" failure rule #97 exists to rule out. `EventSignalRef::name`
        // existed solely for this check and, before it, had no reader anywhere in the crate.
        if reference.name() != event_name {
            return false;
        }
        let Some(hub) = self.hub.clone() else {
            // A detached binder registers nothing; it is documented as a no-op, and reporting
            // `true` here would claim a wire that no slot backs.
            return false;
        };
        // From here on this binder has wired, so `unwired_events_for` switches from "no answer" to a
        // live count that survives a later `unbind_all` (BLUE-issue E-07).
        // The slot owns the name because it outlives this call. It is the reference's own name, so
        // the name a caller subscribes to and the name the slot emits cannot drift apart.
        let name = alloc::string::String::from(reference.name());
        let handle = reference.subscribe(alloc::boxed::Box::new(move |_value| hub.emit(&name)));

        // Resolve the signal once more to capture a *live-ness* probe for exactly this handle.
        //
        // `EventSignalRef` is not `Clone` (its closures are not) and its `is_connected` is per-call,
        // so the probe resolves the reference a second time — cheap, and the same shape the
        // disconnect closure below already uses.
        let probe = widget.event_signal_dyn(event_name);
        let is_connected: alloc::boxed::Box<dyn Fn() -> bool + Send + Sync> =
            alloc::boxed::Box::new(move || {
                probe.as_ref().is_some_and(|reference| reference.is_connected(handle))
            });

        // `EventSignalRef` is not `Clone` (its closures are not), so the disconnect closure resolves
        // the signal a second time from the widget. That lookup is cheap and — unlike making the
        // type cloneable — cannot leave two copies of a slot count that disagree.
        let source = widget.event_signal_dyn(event_name);
        self.forwards.push(Forwarded {
            source: ForwardSource::Erased(alloc::boxed::Box::new(move |handle| {
                // The boolean is discarded because `ForwardSource::disconnect` reports nothing; it
                // is here so the closure's return type matches the shared alias.
                let _ = source.as_ref().is_some_and(|reference| reference.disconnect(handle));
            })),
            handle,
            // `reference.name()` is `&'static str` (the capability table is a `static`), so the
            // stored name is the canonical spelling rather than the caller's.
            event_name: alloc::string::String::from(reference.name()),
            signal_identity: reference.identity(),
            is_connected,
        });
        self.wired_once = true;
        true
    }

    /// Reports whether any subscriber reaches `widget`'s `event_name`.
    ///
    /// # Why this is the query rule #97 requires
    ///
    /// `WidgetFactory::connect_event` returns `Ok` for a published name whether or not anything was
    /// ever wired to it: "is this name valid?" and "is something emitting it?" are different
    /// questions. Until this method existed, the difference was **unaskable**, which made
    /// "subscribed successfully but never called" a silent failure with no way to detect it.
    ///
    /// # Why it asks about *this binder's* handle, not the signal's slot count
    ///
    /// `slot_count() > 0` only proves *someone* observes the signal. A direct observer attached
    /// elsewhere — or a subscription pointing at a **different** hub — drives the count above zero
    /// while no wire from *this* binder reaches the hub the caller cares about. Because the binder
    /// keeps the handle of every slot it registered, the honest query is "is *my* subscription to
    /// this widget still live?", which is what this now answers by checking the stored handles.
    /// A name this binder never wired (or whose wire was later removed) reports `false` even when
    /// the signal has other observers.
    ///
    /// # Why the signal identity is compared too
    ///
    /// A forward is stored together with the identity of the signal it subscribed to
    /// ([`EventSignalRef::identity`]). This method resolves `widget`'s own signal for `event_name`
    /// and requires that identity to match before reporting wired. Comparing the published name
    /// alone made two sibling instances of one kind indistinguishable: wiring `A` left a forward
    /// named `clicked`, so a never-wired `B` answered `true` because *a* `clicked` forward existed
    /// (BLUE-issue E-15). The identity makes this a per-instance answer, so a real broadcast can no
    /// longer make an unrelated instance claim its own wire.
    ///
    /// [`EventSignalRef::identity`]: crate::signal::EventSignalRef::identity
    pub fn event_is_wired<W>(&self, widget: &W, event_name: &str) -> bool
    where
        W: crate::widget::Widget,
    {
        // A reference that answers to another name is not a wire to *this* name — the same
        // declared-name check `wire_one` makes, for the same reason.
        let Some(reference) = widget.event_signal_dyn(event_name) else {
            return false;
        };
        if reference.name() != event_name {
            return false;
        }
        // Any slot this binder registered on the *same signal* of *this* control, still connected,
        // is a live wire. The stored handle is what makes this "this binder's wire" rather than
        // "some observer"; the identity is what makes it about *this* control's signal rather than
        // a same-named event on a sibling.
        let identity = reference.identity();
        self.forwards.iter().any(|forwarded| {
            forwarded.event_name == event_name
                && forwarded.signal_identity == identity
                && (forwarded.is_connected)()
        })
    }

    /// Wires every published event a mounted widget can actually emit.
    ///
    /// # Why this exists
    ///
    /// [`WidgetFactory::connect_event`](crate::widget::capability::WidgetFactory::connect_event)
    /// validates a name against the capability table and registers a slot. It cannot
    /// check that anything emits that name, because a control's typed signals and the
    /// hub's names are separate worlds until a binder joins them. A host that only
    /// called `connect_event` therefore got a subscriber that was never invoked.
    ///
    /// This is that join, in one call: it connects the widget's own `clicked` signal
    /// to the hub under the names the widget's capability publishes, and owns the
    /// subscriptions so they are released with the binder.
    ///
    /// # Which names it wires
    ///
    /// Only the names backed by a signal **every** `Widget` has — the base
    /// `clicked` signal. A control's other events (`value_changed`, `toggled`, …) are
    /// typed `Signal1<T>` and live on the concrete type, so bridging them needs a
    /// payload decision this helper cannot make generically; use [`Self::forward_mapped`]
    /// at the control's own construction site for those. Returning the number wired
    /// lets a caller see that a widget contributed nothing rather than assuming it did.
    ///
    /// # Usage
    ///
    /// Call once per mounted widget, right after it is registered:
    ///
    /// ```ignore
    /// let mut binder = EventSignalBinder::new(hub);
    /// binder.forward_widget_events(widget.as_ref());
    /// ```
    pub fn forward_widget_events<W>(&mut self, widget: &W) -> usize
    where
        W: crate::widget::Widget,
    {
        // The capability registry is compiled out of a stripped profile, so there is no
        // published name list to consult there. Returning `0` is the honest answer: the
        // helper is documented as reporting how many events it wired.
        #[cfg(full_widgets)]
        {
            let factory = crate::widget::capability::WidgetFactory::new_with_defaults();
            let Some(capability) = factory.capability_for_kind_instance(widget) else {
                return 0;
            };
            if !capability.events.iter().any(|schema| schema.name == "clicked") {
                return 0;
            }
            // `clicked_signal` is a `Widget` trait method with a default body that reads
            // the base signal, so it is available on every control without naming the
            // concrete type.
            self.forward_unit("clicked", widget.clicked_signal());
            1
        }
        #[cfg(not(full_widgets))]
        {
            let _ = widget;
            0
        }
    }

    /// Forwards a signal whose payload the hub cannot carry, running `observe` first.
    ///
    /// # Why the payload needs an explicit destination
    ///
    /// `CustomSignalHub::emit(name)` is untyped, so a `Signal1<T>` payload has nowhere to
    /// go. Silently dropping it would make the bridge lossy in a way the caller cannot
    /// see; requiring `observe` makes the loss explicit and hands over the value in the
    /// same call. A caller with no use for it passes `|_| {}` and has *said* so, rather
    /// than having it decided for them.
    pub fn forward_mapped<T, F>(&mut self, event_name: &str, signal: &Signal1<T>, mut observe: F)
    where
        T: Clone + Send + 'static,
        F: FnMut(&T) + Send + Sync + 'static,
    {
        let Some(hub) = self.hub.clone() else {
            return;
        };
        let name = alloc::string::String::from(event_name);
        let handle = signal.connect(move |value: Arc<T>| {
            observe(&value);
            hub.emit(&name);
        });
        // `Signal<T>` is generic, so its disconnect cannot be stored type-erased the way
        // `GenericSignal`'s can — the closure is built here, while `T` is still known.
        let source = signal.clone();
        let probe = signal.clone();
        self.forwards.push(Forwarded {
            source: ForwardSource::Typed(Box::new(move |handle| {
                source.disconnect(handle);
            })),
            handle,
            event_name: alloc::string::String::from(event_name),
            signal_identity: signal.identity(),
            is_connected: alloc::boxed::Box::new(move || probe.is_connected(handle)),
        });
        // Every successful forward records the history (see `forward_unit`): without it a
        // binder used only through `forward_mapped` reported `None` from
        // `unwired_events_for`, i.e. "not wired", for a real wire (BLUE-issue N-S-31).
        self.wired_once = true;
    }

    /// Removes every subscription this binder registered, leaving the binder reusable.
    ///
    /// Safe to call more than once. The hub is retained, so a later `forward_*` attaches
    /// again — which is what a control that rebuilds its internals needs.
    ///
    /// # Why the *source* signal is remembered, not the hub
    ///
    /// The handle `forward_*` returns belongs to the **control's** signal. An earlier
    /// revision called `hub.disconnect(name, handle)` and therefore disconnected nothing:
    /// the handle is not a key in the hub, the call returned `false`, and every forwarded
    /// slot leaked — a rebuilt control would accumulate one live subscriber per rebuild.
    /// `dropping_the_binder_unsubscribes` caught it, and the fix is to hold the signal the
    /// handle came from.
    pub fn unbind_all(&mut self) {
        for entry in core::mem::take(&mut self.forwards) {
            entry.source.disconnect(entry.handle);
        }
    }
}

/// How to remove a forwarded slot, captured beside the handle it removes.
///
/// Two variants because `GenericSignal` is a concrete type while `Signal1<T>` is generic:
/// the typed case stores a disconnect closure rather than the signal, erasing `T` at the
/// one point where it is still known.
enum ForwardSource {
    /// A payload-free signal, kept directly.
    Unit(GenericSignal),
    /// A payload-carrying signal, erased to a disconnect closure.
    Typed(Box<dyn Fn(ConnectionHandle) + Send>),
    /// A signal resolved by name, erased by the control itself.
    ///
    /// Separate from `Typed` because the handle does not come from a signal this module can name:
    /// it came from `EventSignalRef`, whose whole purpose is to hide which concrete signal it is.
    Erased(Box<dyn Fn(ConnectionHandle) + Send>),
}

impl ForwardSource {
    fn disconnect(&self, handle: ConnectionHandle) {
        match self {
            Self::Unit(signal) => {
                signal.disconnect(handle);
            }
            Self::Typed(disconnect) => disconnect(handle),
            Self::Erased(disconnect) => disconnect(handle),
        }
    }
}

/// One forwarded event: the signal it came from and the handle that removes it.
struct Forwarded {
    source: ForwardSource,
    handle: ConnectionHandle,
    /// The published event name this subscription was made for.
    ///
    /// Kept so [`EventSignalBinder::event_is_wired`] can answer about the *named* event: without it
    /// a binder that wired only `clicked` would report every other published name as wired too,
    /// because it could only see "some handle of mine is connected".
    event_name: alloc::string::String,
    /// Identity of the signal this forward was subscribed to.
    ///
    /// Kept so a query can require that the wired signal **is** the one the queried control
    /// resolves, rather than only that a forward exists under the same name. Without it, wiring
    /// `A` made a never-wired sibling `B` of the same kind report itself wired, because the query
    /// compared the name alone (BLUE-issue E-15). See [`crate::signal::Signal::identity`].
    signal_identity: usize,
    /// Whether this specific subscription is still live on the widget's own signal.
    ///
    /// Kept beside the handle so [`EventSignalBinder::event_is_wired`] can ask about **this**
    /// binder's wire rather than about whether *anyone* observes the signal. Erased because the
    /// concrete signal type is not nameable here.
    is_connected: Box<dyn Fn() -> bool + Send + Sync>,
}

impl core::fmt::Debug for EventSignalBinder {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        // The handles are opaque and the hub is shared state; reporting the shape is what
        // a caller reading a log line can act on.
        f.debug_struct("EventSignalBinder")
            .field("attached", &self.is_attached())
            .field("subscriptions", &self.forwards.len())
            .finish()
    }
}

impl Drop for EventSignalBinder {
    fn drop(&mut self) {
        // A binder that outlives its control must not leave slots pointing into it.
        self.unbind_all();
    }
}

#[cfg(all(test, full_widgets))]
mod tests {
    use super::EventSignalBinder;
    use crate::core::Rect;

    /// Every control whose `event_signal_dyn` is implemented must wire **all** its published names.
    ///
    /// # What this proves that the source-level gate cannot
    ///
    /// `tools/check_event_signal_dyn.sh` reads the `match` arms and compares their literals to the
    /// capability table. That catches a missing or misspelled arm, but it is a text check: it
    /// cannot see that the signal behind an arm is the one the control actually **emits**. A
    /// control that resolved `clicked` to a signal it never fires would satisfy the gate and still
    /// deliver nothing.
    ///
    /// This drives the real path — `forward_all` walks the capability's published names and asks
    /// the control to resolve each — and then asks `unwired_events_for` how many were left. Zero
    /// is the only acceptable answer for a converted control, and the shortfall is what the
    /// binder records when a name resolves to nothing.
    ///
    /// # Why the list is explicit
    ///
    /// It is the same commitment `check_event_signal_dyn.py`'s `CONVERTED` table makes, and it has
    /// to be stated for the same reason: a control that has *not* been converted is
    /// indistinguishable, from the outside, from one that has and is missing an arm — both wire
    /// zero. Naming the converted set is what turns "the gap is fine" into a checked claim.
    #[test]
    fn converted_controls_wire_every_published_event() {
        fn check<W: crate::widget::Widget>(name: &str, widget: &W) {
            let hub = std::sync::Arc::new(crate::signal::hub::CustomSignalHub::new());
            let mut binder = EventSignalBinder::new(hub);
            let wired = binder.forward_all(widget);
            assert!(
                wired > 0,
                "{name}: `forward_all` wired nothing, so either the control is not converted or \
                 its capability publishes no events"
            );
            assert_eq!(
                binder.unwired_events_for(widget),
                Some(0),
                "{name}: some published event did not resolve to a signal the control emits"
            );
        }

        let r = Rect::new(0, 0, 100, 40);
        check("button", &crate::widget::base_widgets::button::Button::new("b".to_string(), r));
        check("check_box", &crate::widget::base_widgets::checkbox::CheckBox::new(r));
        check(
            "color_well",
            &crate::widget::display_widgets::color_well::ColorWell::new(
                crate::core::Color::WHITE,
                r,
            ),
        );
        check(
            "dropdown",
            &crate::widget::input_widgets::dropdown::Dropdown::new(vec!["one".to_string()], r),
        );
        check("empty_state", &crate::widget::display_widgets::empty_state::EmptyState::new(r));
        check("progress_dialog", &crate::widget::dialog::progress_dialog::ProgressDialog::new(r));
        check(
            "refresh_control",
            &crate::widget::overlay_widgets::refresh_control::RefreshControl::new(r),
        );
        check(
            "text_area",
            &crate::widget::input_widgets::textarea::TextArea::new(String::new(), r),
        );
        check("window", &crate::widget::window::Window::new("w".to_string(), r));

        // The second batch: the controls converted after the first nine, so the set the runtime
        // check covers keeps pace with the `CONVERTED` table rather than trailing it. A control is
        // added here only when a call reaches every name its capability publishes without a
        // `_ = ` discard, which is what makes the count meaningful.
        check("app_bar", &crate::widget::nav_widgets::app_bar::AppBar::new("t", r));
        check("bottom_sheet", &crate::widget::dialog::bottom_sheet::BottomSheet::new(r));
        check("dialog", &crate::widget::dialog::dialog_widget::Dialog::new(r));
        check(
            "modal_bottom_sheet",
            &crate::widget::dialog::modal_bottom_sheet::ModalBottomSheet::new(r),
        );
        check("popup_window", &crate::widget::dialog::popup_window::PopupWindow::new(r));
        check(
            "hero_animation",
            &crate::widget::media_widgets::hero_animation::HeroAnimation::new(r),
        );
        check("lottie_widget", &crate::widget::media_widgets::lottie_widget::LottieWidget::new(r));
        check("rive_widget", &crate::widget::media_widgets::rive_widget::RiveWidget::new(r));
        check("mini_canvas", &crate::widget::display_widgets::mini_canvas::MiniCanvas::new(r));
        check(
            "mobile_date_picker",
            &crate::widget::misc_widgets::mobile_date_picker::MobileDatePicker::new(r),
        );
        check("status_bar", &crate::widget::menu_toolbar::status_bar::StatusBar::new(r));
        check("tool_button", &crate::widget::menu_toolbar::tool_button::ToolButton::new("t", r));
        check(
            "terminal_view",
            &crate::widget::special_widgets::terminal_view::TerminalView::new(r),
        );
        check(
            "timeline_widget",
            &crate::widget::special_widgets::timeline_widget::TimelineWidget::new(r),
        );
        check("progress_bar", &crate::widget::display_widgets::progressbar::ProgressBar::new(r));
        check("rating", &crate::widget::display_widgets::rating::Rating::new(r));
        check("switch", &crate::widget::display_widgets::switch::Switch::new(r));
        check("scroll_bar", &crate::widget::display_widgets::scrollbar::ScrollBar::new(r));
        check("stepper", &crate::widget::container_widgets::stepper::Stepper::new(r));
        check(
            "animated_image",
            &crate::widget::media_widgets::animated_image::AnimatedImage::new(r),
        );
        check("chip", &crate::widget::special_widgets::chip::Chip::new(r));
        check("group_box", &crate::widget::container_widgets::groupbox::GroupBox::new(r));

        // A delegating newtype must forward the *resolution* too, not just the behaviour.
        // `CupertinoSwitch` publishes `toggled` and delegates everything to its inner `Switch`;
        // before the forward it accepted a subscription and never fired it.
        check("cupertino_switch", &crate::widget::cupertino::core::CupertinoSwitch::new(r));
    }

    /// A control sharing a `WidgetKind` with a larger control must not inherit its shortfall.
    ///
    /// # The defect this pins
    ///
    /// `WidgetKind` is not one-to-one with capability: `WidgetKind::WebEngineView` backs both
    /// `media_player` (4 published events) and `web_engine_view` (11). The wiring-outcome table was
    /// keyed by kind alone and folded the two counts with `max` **independently**, so a fully-wired
    /// `MediaPlayer` inherited `published = 11` from its larger sibling while keeping `wired = 4` of
    /// its own — `unwired_events_for` then reported **7 unwired events that do not exist**, which is
    /// precisely the false shortfall this query exists to make impossible.
    ///
    /// `WidgetKind::Table` is the extreme case: five capabilities (2, 2, 1, 1, 1 events) share one
    /// kind.
    ///
    /// The two controls must share **one binder** for the defect to appear: the table is per-binder,
    /// so a fresh binder for each control would hide the cross-contamination entirely. That is also
    /// what a real host looks like — one binder wires every control in a window.
    ///
    /// The order is the one that exposes it: the **larger** control goes first, so a smaller sibling
    /// wired afterwards is answered from an entry that already holds the larger `published`. To make
    /// the fold bite, the smaller control is left with a genuine shortfall — one published name
    /// skipped — so a kind-only entry would compute `11 - 3 = 8` (the larger sibling's published
    /// minus the smaller control's wired) instead of the true `4 - 3 = 1`.
    ///
    /// The shortfall is produced with [`EventSignalBinder::forward_one`] rather than a wrapper type:
    /// the capability tie-break downcasts to the concrete control, so a wrapper would resolve no
    /// capability at all and `forward_all` would return `0` for a reason unrelated to the keying.
    /// Skipping one name is also exactly what a host with a hand-made exception does — the case the
    /// single-event form exists for, and the case the count has to stay honest about.
    #[test]
    fn a_control_on_a_shared_kind_reports_its_own_shortfall() {
        let r = Rect::new(0, 0, 100, 40);
        let hub = std::sync::Arc::new(crate::signal::hub::CustomSignalHub::new());
        let mut binder = EventSignalBinder::new(hub);

        let view = crate::widget::web_widgets::web_engine::WebEngineView::new(r);
        assert_eq!(binder.forward_all(&view), 11, "`web_engine_view` publishes eleven events");

        let player = crate::widget::special_widgets::media_player::MediaPlayer::new(r);
        for name in ["playback_changed", "position_changed", "volume_changed"] {
            assert!(binder.forward_one(&player, name), "`{name}` resolves on `media_player`");
        }

        assert_eq!(
            binder.unwired_events_for(&player),
            Some(1),
            "the shortfall is this control's own (4 published - 3 wired), not `WebEngineView`'s \
             four extra names folded in through a shared `WidgetKind`"
        );
        // The larger sibling is still fully wired; neither answer may be read off the other.
        assert_eq!(binder.unwired_events_for(&view), Some(0));
    }

    /// A reference that declares a different name than it was asked for must not wire.
    ///
    /// # The defect this pins
    ///
    /// `EventSignalRef::name` was **write-only**: `forward_one`/`forward_all` took the hub name
    /// from their own argument and never compared it to the name the reference declared. A control
    /// whose `event_signal_dyn` resolved `"clicked"` to a signal built under another name therefore
    /// wired "successfully" while the control emitted the real `"clicked"` into nothing. Both
    /// queries a host has — the `wired` count and [`EventSignalBinder::event_is_wired`] — reported
    /// success.
    ///
    /// This test builds one control whose resolver lies about the name and asserts the binder
    /// refuses it, so the check cannot be dropped without a failure. It is deliberately not written
    /// against a real control: no shipping control lies, so only a synthetic one can reach the
    /// branch, and the branch is what the test is about.
    #[test]
    fn a_reference_that_declares_another_name_does_not_wire() {
        use crate::widget::Widget;

        /// A `ColorWell` that answers `"clicked"` with a signal declaring a different name — the
        /// shape a copy-paste mistake in a real `event_signal_dyn` produces.
        ///
        /// Only `handle_event` and `base`/`base_mut` are delegated: `kind` has a default body that
        /// reads the base, so the wrapper reports the same kind as the control it wraps — which is
        /// what lets the capability lookup find the real event list.
        struct Misnamed(crate::widget::display_widgets::color_well::ColorWell);

        impl crate::event::EventHandler for Misnamed {
            fn handle_event(&mut self, event: &crate::event::Event) {
                self.0.handle_event(event);
            }
        }

        impl Widget for Misnamed {
            fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
                match name {
                    "clicked" => {
                        Some(crate::signal::EventSignalRef::unit("not_clicked", &self.0.clicked))
                    }
                    _ => None,
                }
            }
            fn base(&self) -> &crate::widget::BaseWidget {
                Widget::base(&self.0)
            }
            fn base_mut(&mut self) -> &mut crate::widget::BaseWidget {
                Widget::base_mut(&mut self.0)
            }
        }

        let widget = Misnamed(crate::widget::display_widgets::color_well::ColorWell::new(
            crate::core::Color::WHITE,
            Rect::new(0, 0, 100, 40),
        ));
        let hub = std::sync::Arc::new(crate::signal::hub::CustomSignalHub::new());
        let mut binder = EventSignalBinder::new(hub);

        assert!(
            !binder.forward_one(&widget, "clicked"),
            "a reference declaring `not_clicked` must not satisfy a request for `clicked`"
        );
        assert!(
            !binder.event_is_wired(&widget, "clicked"),
            "`event_is_wired` must agree with `forward_one`: neither may report a wire whose hub \
             name and signal name disagree"
        );
    }

    /// Every successful forward must record the "has wired" history, not only `wire_one`.
    ///
    /// # The defect this pins (BLUE-issue N-S-31)
    ///
    /// `wired_once` gated `unwired_events_for`: it returned `None` ("not wired yet") until the
    /// first forward. Only `wire_one` set it, so a host that used the public manual entry points
    /// — `forward_unit`, `forward_widget_events` (which calls `forward_unit`), or `forward_mapped`
    /// — wired real slots while the count query kept answering `None`, i.e. reporting a live wire
    /// as never wired. This test drives each successful entry point and requires the query to
    /// answer `Some(...)` — and requires a **detached** binder (which wires nothing) to keep
    /// answering `None`, so the fix cannot be "always report an answer".
    #[test]
    fn every_successful_forward_entry_records_the_wired_history() {
        use crate::widget::Widget;
        use std::sync::Arc;

        let r = Rect::new(0, 0, 100, 40);

        // A fresh binder has no history: the query must have no answer yet.
        let button = crate::widget::base_widgets::button::Button::new("b".to_string(), r);
        let hub = Arc::new(crate::signal::hub::CustomSignalHub::new());
        let mut binder = EventSignalBinder::new(hub);
        assert_eq!(
            binder.unwired_events_for(&button),
            None,
            "a binder that has never forwarded must report no answer, not a count"
        );

        // `forward_unit` alone must switch the query to a live answer.
        binder.forward_unit("clicked", button.clicked_signal());
        assert!(
            binder.unwired_events_for(&button).is_some(),
            "`forward_unit` wires a real slot, so the query must report a count rather than None"
        );

        // A fresh binder driven only through `forward_widget_events` (which uses
        // `forward_unit`) must also report a count.
        let hub = Arc::new(crate::signal::hub::CustomSignalHub::new());
        let mut binder = EventSignalBinder::new(hub);
        assert_eq!(binder.forward_widget_events(&button), 1, "`clicked` is wired");
        assert!(
            binder.unwired_events_for(&button).is_some(),
            "`forward_widget_events` goes through `forward_unit`, so it must record the history"
        );

        // A fresh binder driven only through `forward_mapped` must also report a count.
        let hub = Arc::new(crate::signal::hub::CustomSignalHub::new());
        let mut binder = EventSignalBinder::new(hub);
        let slider: crate::signal::Signal1<i32> = crate::signal::Signal1::new();
        binder.forward_mapped("value_changed", &slider, |_| {});
        // `unwired_events_for` walks the *control's* capability, so use a real control
        // that publishes `value_changed`; the binder history is what is under test.
        let area = crate::widget::input_widgets::textarea::TextArea::new(String::new(), r);
        assert!(
            binder.unwired_events_for(&area).is_some(),
            "`forward_mapped` wires a real slot, so the query must report a count rather than None"
        );

        // `unbind_all` removes the slots but must retain the history, so the query
        // reports the honest "everything is unwired again" count rather than flipping
        // back to the no-answer state.
        binder.unbind_all();
        assert!(
            binder.unwired_events_for(&area).is_some(),
            "after `unbind_all` the binder has still wired once, so it must report a count"
        );
        assert!(!binder.event_is_wired(&area, "value_changed"), "the wire was removed");

        // A detached binder wires nothing, so it must keep reporting `None`.
        let mut detached = EventSignalBinder::detached();
        detached.forward_unit("clicked", button.clicked_signal());
        assert_eq!(
            detached.unwired_events_for(&button),
            None,
            "a detached binder has nowhere to wire, so it must not claim a history"
        );
    }
}