use super::CustomSignalHub;
use crate::signal::{ConnectionHandle, GenericSignal, Signal1};
use alloc::boxed::Box;
use alloc::sync::Arc;
pub struct EventSignalBinder {
hub: Option<Arc<CustomSignalHub>>,
forwards: alloc::vec::Vec<Forwarded>,
wired_once: bool,
}
impl Default for EventSignalBinder {
fn default() -> Self {
Self::detached()
}
}
impl EventSignalBinder {
pub fn new(hub: Arc<CustomSignalHub>) -> Self {
Self { hub: Some(hub), forwards: alloc::vec::Vec::new(), wired_once: false }
}
pub fn detached() -> Self {
Self { hub: None, forwards: alloc::vec::Vec::new(), wired_once: false }
}
pub fn is_attached(&self) -> bool {
self.hub.is_some()
}
pub fn len(&self) -> usize {
self.forwards.len()
}
pub fn is_empty(&self) -> bool {
self.forwards.is_empty()
}
pub fn forward_unit(&mut self, event_name: &str, signal: &GenericSignal) {
let Some(hub) = self.hub.clone() else {
return;
};
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)),
});
self.wired_once = true;
}
pub fn forward_all<W>(&mut self, widget: &W) -> usize
where
W: crate::widget::Widget,
{
#[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 {
if self.wire_one(widget, schema.name) {
wired += 1;
}
}
wired
}
#[cfg(not(full_widgets))]
{
let _ = widget;
0
}
}
#[cfg(full_widgets)]
pub fn unwired_events_for<W>(&self, widget: &W) -> Option<usize>
where
W: crate::widget::Widget,
{
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)
}
#[cfg(not(full_widgets))]
pub fn unwired_events_for<W>(&self, _widget: &W) -> Option<usize>
where
W: crate::widget::Widget,
{
None
}
pub fn forward_one<W>(&mut self, widget: &W, event_name: &str) -> bool
where
W: crate::widget::Widget,
{
self.wire_one(widget, event_name)
}
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;
};
if reference.name() != event_name {
return false;
}
let Some(hub) = self.hub.clone() else {
return false;
};
let name = alloc::string::String::from(reference.name());
let handle = reference.subscribe(alloc::boxed::Box::new(move |_value| hub.emit(&name)));
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))
});
let source = widget.event_signal_dyn(event_name);
self.forwards.push(Forwarded {
source: ForwardSource::Erased(alloc::boxed::Box::new(move |handle| {
let _ = source.as_ref().is_some_and(|reference| reference.disconnect(handle));
})),
handle,
event_name: alloc::string::String::from(reference.name()),
signal_identity: reference.identity(),
is_connected,
});
self.wired_once = true;
true
}
pub fn event_is_wired<W>(&self, widget: &W, event_name: &str) -> bool
where
W: crate::widget::Widget,
{
let Some(reference) = widget.event_signal_dyn(event_name) else {
return false;
};
if reference.name() != event_name {
return false;
}
let identity = reference.identity();
self.forwards.iter().any(|forwarded| {
forwarded.event_name == event_name
&& forwarded.signal_identity == identity
&& (forwarded.is_connected)()
})
}
pub fn forward_widget_events<W>(&mut self, widget: &W) -> usize
where
W: crate::widget::Widget,
{
#[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;
}
self.forward_unit("clicked", widget.clicked_signal());
1
}
#[cfg(not(full_widgets))]
{
let _ = widget;
0
}
}
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);
});
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)),
});
self.wired_once = true;
}
pub fn unbind_all(&mut self) {
for entry in core::mem::take(&mut self.forwards) {
entry.source.disconnect(entry.handle);
}
}
}
enum ForwardSource {
Unit(GenericSignal),
Typed(Box<dyn Fn(ConnectionHandle) + Send>),
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),
}
}
}
struct Forwarded {
source: ForwardSource,
handle: ConnectionHandle,
event_name: alloc::string::String,
signal_identity: usize,
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 {
f.debug_struct("EventSignalBinder")
.field("attached", &self.is_attached())
.field("subscriptions", &self.forwards.len())
.finish()
}
}
impl Drop for EventSignalBinder {
fn drop(&mut self) {
self.unbind_all();
}
}
#[cfg(all(test, full_widgets))]
mod tests {
use super::EventSignalBinder;
use crate::core::Rect;
#[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));
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));
check("cupertino_switch", &crate::widget::cupertino::core::CupertinoSwitch::new(r));
}
#[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`"
);
assert_eq!(binder.unwired_events_for(&view), Some(0));
}
#[test]
fn a_reference_that_declares_another_name_does_not_wire() {
use crate::widget::Widget;
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"
);
}
#[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);
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"
);
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"
);
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"
);
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, |_| {});
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"
);
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");
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"
);
}
}