use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObject, NSObjectProtocol, ProtocolObject, Sel};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
#[cfg(feature = "frust-api")]
use objc2_ui_kit::UIView;
use objc2_ui_kit::{
UIButton, UIControl, UIControlEvents, UIDatePicker, UISegmentedControl, UISlider, UIStepper,
UISwitch, UITabBar, UITabBarDelegate, UITabBarItem,
};
use crate::controls::date_picker::foundation::civil_date;
use crate::controls::tab_bar::tap_kind;
use crate::events::{
EVENT_KIND_CLICK, EVENT_KIND_DATE, EVENT_KIND_DRAG_END, EVENT_KIND_DRAG_START,
EVENT_KIND_SELECTION, EVENT_KIND_TOGGLED, EVENT_KIND_VALUE_CHANGED, pack_bool, pack_date,
pack_index, pack_value_changed,
};
use crate::registry::SlotId;
use crate::runtime::{self, NativeEvent};
pub(crate) struct TargetIvars {
slot: SlotId,
showing_tab: Cell<Option<usize>>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[ivars = TargetIvars]
pub(crate) struct FrustNativeControlTarget;
unsafe impl NSObjectProtocol for FrustNativeControlTarget {}
unsafe impl UITabBarDelegate for FrustNativeControlTarget {
#[unsafe(method(tabBar:didSelectItem:))]
fn tab_bar_did_select_item(&self, _tab_bar: &UITabBar, item: &UITabBarItem) {
let tag = item.tag();
let Ok(index) = usize::try_from(tag) else {
log::debug!(
"frust-native-widgets: slot {} tab item has a negative tag — dropped",
self.slot()
);
return;
};
let kind = tap_kind(self.ivars().showing_tab.get(), index);
self.dispatch(kind, pack_index(tag), "tabBar:didSelectItem:");
}
}
impl FrustNativeControlTarget {
#[unsafe(method(handleClick:))]
fn handle_click(&self, _sender: &UIControl) {
self.dispatch(EVENT_KIND_CLICK, 0, "handleClick:");
}
#[unsafe(method(handleSwitchValueChanged:))]
fn handle_switch_value_changed(&self, sender: &UISwitch) {
let detail = pack_bool(sender.isOn());
self.dispatch(EVENT_KIND_TOGGLED, detail, "handleSwitchValueChanged:");
}
#[unsafe(method(handleSliderValueChanged:))]
fn handle_slider_value_changed(&self, sender: &UISlider) {
let detail = pack_value_changed(sender.value().round() as i32, true);
self.dispatch(
EVENT_KIND_VALUE_CHANGED,
detail,
"handleSliderValueChanged:",
);
}
#[unsafe(method(handleSliderDragStart:))]
fn handle_slider_drag_start(&self, _sender: &UISlider) {
self.dispatch(EVENT_KIND_DRAG_START, 0, "handleSliderDragStart:");
}
#[unsafe(method(handleSliderDragEnd:))]
fn handle_slider_drag_end(&self, _sender: &UISlider) {
self.dispatch(EVENT_KIND_DRAG_END, 0, "handleSliderDragEnd:");
}
#[unsafe(method(handleSegmentValueChanged:))]
fn handle_segment_value_changed(&self, sender: &UISegmentedControl) {
let detail = pack_index(sender.selectedSegmentIndex());
self.dispatch(EVENT_KIND_SELECTION, detail, "handleSegmentValueChanged:");
}
#[unsafe(method(handleStepperValueChanged:))]
fn handle_stepper_value_changed(&self, sender: &UIStepper) {
let detail = pack_value_changed(sender.value().round() as i32, true);
self.dispatch(
EVENT_KIND_VALUE_CHANGED,
detail,
"handleStepperValueChanged:",
);
}
#[unsafe(method(handleDateValueChanged:))]
fn handle_date_value_changed(&self, sender: &UIDatePicker) {
let Some(date) = civil_date(&sender.date()) else {
log::debug!(
"frust-native-widgets: slot {} picked a date outside 0001-9999 — dropped",
self.slot()
);
return;
};
self.dispatch(EVENT_KIND_DATE, pack_date(date), "handleDateValueChanged:");
}
}
);
impl FrustNativeControlTarget {
fn new(mtm: MainThreadMarker, slot: SlotId) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(TargetIvars {
slot,
showing_tab: Cell::new(None),
});
unsafe { msg_send![super(this), init] }
}
fn slot(&self) -> SlotId {
self.ivars().slot
}
fn dispatch(&self, kind: i32, detail: i64, which: &'static str) {
let slot = self.slot();
let event = NativeEvent { kind, detail };
let outcome = catch_unwind(AssertUnwindSafe(|| {
runtime::with_runtime(|runtime| runtime.on_event(slot, event))
}));
match outcome {
Ok(delivered) => {
if delivered.is_none() {
log::debug!("frust-native-widgets: event for slot {slot} dropped (re-entrant)");
}
}
Err(_) => {
log::warn!(
"frust-native-widgets: panic caught in {which} for slot {slot} — event dropped"
);
}
}
}
fn add(&self, control: &UIControl, action: Sel, events: UIControlEvents) {
let target: &AnyObject = self;
unsafe { control.addTarget_action_forControlEvents(Some(target), action, events) };
}
fn remove(&self, control: &UIControl, action: Sel, events: UIControlEvents) {
let target: &AnyObject = self;
unsafe { control.removeTarget_action_forControlEvents(Some(target), Some(action), events) };
}
pub(crate) fn attach_button(
mtm: MainThreadMarker,
slot: SlotId,
view: &UIButton,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(view, sel!(handleClick:), UIControlEvents::TouchUpInside);
target
}
pub(crate) fn detach_button(&self, view: &UIButton) {
self.remove(view, sel!(handleClick:), UIControlEvents::TouchUpInside);
}
pub(crate) fn attach_switch(
mtm: MainThreadMarker,
slot: SlotId,
view: &UISwitch,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(
view,
sel!(handleSwitchValueChanged:),
UIControlEvents::ValueChanged,
);
target
}
pub(crate) fn detach_switch(&self, view: &UISwitch) {
self.remove(
view,
sel!(handleSwitchValueChanged:),
UIControlEvents::ValueChanged,
);
}
pub(crate) fn attach_slider(
mtm: MainThreadMarker,
slot: SlotId,
view: &UISlider,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(
view,
sel!(handleSliderValueChanged:),
UIControlEvents::ValueChanged,
);
target.add(
view,
sel!(handleSliderDragStart:),
UIControlEvents::TouchDown,
);
target.add(
view,
sel!(handleSliderDragEnd:),
UIControlEvents::TouchUpInside | UIControlEvents::TouchUpOutside,
);
target
}
pub(crate) fn detach_slider(&self, view: &UISlider) {
self.remove(
view,
sel!(handleSliderValueChanged:),
UIControlEvents::ValueChanged,
);
self.remove(
view,
sel!(handleSliderDragStart:),
UIControlEvents::TouchDown,
);
self.remove(
view,
sel!(handleSliderDragEnd:),
UIControlEvents::TouchUpInside | UIControlEvents::TouchUpOutside,
);
}
pub(crate) fn attach_segmented(
mtm: MainThreadMarker,
slot: SlotId,
view: &UISegmentedControl,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(
view,
sel!(handleSegmentValueChanged:),
UIControlEvents::ValueChanged,
);
target
}
pub(crate) fn detach_segmented(&self, view: &UISegmentedControl) {
self.remove(
view,
sel!(handleSegmentValueChanged:),
UIControlEvents::ValueChanged,
);
}
pub(crate) fn attach_stepper(
mtm: MainThreadMarker,
slot: SlotId,
view: &UIStepper,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(
view,
sel!(handleStepperValueChanged:),
UIControlEvents::ValueChanged,
);
target
}
pub(crate) fn detach_stepper(&self, view: &UIStepper) {
self.remove(
view,
sel!(handleStepperValueChanged:),
UIControlEvents::ValueChanged,
);
}
pub(crate) fn attach_date_picker(
mtm: MainThreadMarker,
slot: SlotId,
view: &UIDatePicker,
) -> Retained<Self> {
let target = Self::new(mtm, slot);
target.add(
view,
sel!(handleDateValueChanged:),
UIControlEvents::ValueChanged,
);
target
}
pub(crate) fn detach_date_picker(&self, view: &UIDatePicker) {
self.remove(
view,
sel!(handleDateValueChanged:),
UIControlEvents::ValueChanged,
);
}
pub(crate) fn tab_bar_target(mtm: MainThreadMarker, slot: SlotId) -> Retained<Self> {
Self::new(mtm, slot)
}
pub(crate) fn note_tab_showing(&self, index: Option<usize>) {
self.ivars().showing_tab.set(index);
}
pub(crate) fn attach_tab_bar(&self, view: &UITabBar) {
view.setDelegate(Some(ProtocolObject::from_ref(self)));
}
pub(crate) fn detach_tab_bar(&self, view: &UITabBar) {
view.setDelegate(None);
}
#[cfg(feature = "frust-api")]
pub(crate) fn attach_component(
mtm: MainThreadMarker,
slot: SlotId,
view: &UIView,
click: bool,
toggled: bool,
value_changed: bool,
) -> Result<(Retained<Self>, Retained<UIControl>), String> {
use objc2::Message as _;
let any: &AnyObject = view;
let control = any.downcast_ref::<UIControl>().ok_or_else(|| {
"iOS attach_listener: the view is not a UIControl, so no target-action can be \
attached to it"
.to_string()
})?;
if toggled && any.downcast_ref::<UISwitch>().is_none() {
return Err(
"iOS attach_listener: TOGGLED reads a UISwitch's isOn, and this control is no \
UISwitch"
.into(),
);
}
if value_changed && any.downcast_ref::<UISlider>().is_none() {
return Err(
"iOS attach_listener: VALUE_CHANGED reads a UISlider's value, and this control \
is no UISlider"
.into(),
);
}
let target = Self::new(mtm, slot);
target.wire(control, click, toggled, value_changed, true);
Ok((target, control.retain()))
}
#[cfg(feature = "frust-api")]
pub(crate) fn detach_component(
&self,
control: &UIControl,
click: bool,
toggled: bool,
value_changed: bool,
) {
self.wire(control, click, toggled, value_changed, false);
}
#[cfg(feature = "frust-api")]
fn wire(
&self,
control: &UIControl,
click: bool,
toggled: bool,
value_changed: bool,
attach: bool,
) {
let mut pairs: Vec<(Sel, UIControlEvents)> = Vec::with_capacity(5);
if click {
pairs.push((sel!(handleClick:), UIControlEvents::TouchUpInside));
}
if toggled {
pairs.push((
sel!(handleSwitchValueChanged:),
UIControlEvents::ValueChanged,
));
}
if value_changed {
pairs.push((
sel!(handleSliderValueChanged:),
UIControlEvents::ValueChanged,
));
pairs.push((sel!(handleSliderDragStart:), UIControlEvents::TouchDown));
pairs.push((
sel!(handleSliderDragEnd:),
UIControlEvents::TouchUpInside | UIControlEvents::TouchUpOutside,
));
}
for (action, events) in pairs {
if attach {
self.add(control, action, events);
} else {
self.remove(control, action, events);
}
}
}
}