use day_spec::Event;
use day_spec::ffi_guard;
use day_spec::props::{PickerPatch, PickerProps, PickerStyle};
use day_spec::sidetable::SideTable;
use crate::AppKit;
use day_spec::{NodeId, Proposal, Size};
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObjectProtocol};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSButton, NSControlStateValueOn, NSPopUpButton, NSSegmentSwitchTracking, NSSegmentedControl,
NSStackView, NSUserInterfaceLayoutOrientation, NSView,
};
use objc2_foundation::{NSObject, NSPoint, NSRect, NSSize, NSString};
struct PickerIvars {
node: NodeId,
separators_before: Vec<usize>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayPickerTarget"]
#[ivars = PickerIvars]
struct PickerTarget;
unsafe impl NSObjectProtocol for PickerTarget {}
impl PickerTarget {
#[unsafe(method(fire:))]
fn fire(&self, sender: &AnyObject) {
ffi_guard::contain((), || {
let idx = if let Some(p) = sender.downcast_ref::<NSPopUpButton>() {
p.selectedItem().map(|item| item.tag()).unwrap_or(-1)
} else if let Some(s) = sender.downcast_ref::<NSSegmentedControl>() {
s.selectedSegment()
} else if let Some(b) = sender.downcast_ref::<NSButton>() {
b.tag()
} else {
-1
};
if idx >= 0 {
crate::emit(self.ivars().node, Event::SelectionChanged(idx as i64));
}
})
}
}
);
impl PickerTarget {
fn new(mtm: MainThreadMarker, node: NodeId, separators_before: Vec<usize>) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(PickerIvars {
node,
separators_before,
});
unsafe { msg_send![super(this), init] }
}
}
day_core::tls_group! {
static TARGETS: SideTable<Retained<PickerTarget>> = SideTable::new();
}
fn zero_rect() -> NSRect {
NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(0.0, 0.0))
}
fn make_menu(mtm: MainThreadMarker, p: &PickerProps, target: &PickerTarget) -> Retained<NSView> {
let popup =
NSPopUpButton::initWithFrame_pullsDown(NSPopUpButton::alloc(mtm), zero_rect(), false);
fill_menu(&popup, &p.options, &p.separators_before, p.selected);
unsafe {
popup.setTarget(Some(target));
popup.setAction(Some(sel!(fire:)));
}
Retained::from(<NSPopUpButton as AsRef<NSView>>::as_ref(&popup))
}
fn fill_menu(popup: &NSPopUpButton, options: &[String], separators: &[usize], selected: usize) {
let mtm = popup.mtm();
let menu = objc2_app_kit::NSMenu::new(mtm);
for (index, title) in options.iter().enumerate() {
if index > 0 && separators.contains(&index) {
menu.addItem(&objc2_app_kit::NSMenuItem::separatorItem(mtm));
}
let item = unsafe {
objc2_app_kit::NSMenuItem::initWithTitle_action_keyEquivalent(
objc2_app_kit::NSMenuItem::alloc(mtm),
&NSString::from_str(title),
None,
&NSString::from_str(""),
)
};
item.setTag(index as isize);
menu.addItem(&item);
}
popup.setMenu(Some(&menu));
if selected < options.len() {
popup.selectItemWithTag(selected as isize);
}
}
fn make_segmented(
mtm: MainThreadMarker,
p: &PickerProps,
target: &PickerTarget,
) -> Retained<NSView> {
let seg = NSSegmentedControl::new(mtm);
seg.setSegmentCount(p.options.len() as isize);
seg.setTrackingMode(NSSegmentSwitchTracking::SelectOne);
for (i, opt) in p.options.iter().enumerate() {
seg.setLabel_forSegment(&NSString::from_str(opt), i as isize);
}
if p.selected < p.options.len() {
seg.setSelectedSegment(p.selected as isize);
}
unsafe {
seg.setTarget(Some(target));
seg.setAction(Some(sel!(fire:)));
}
Retained::from(<NSSegmentedControl as AsRef<NSView>>::as_ref(&seg))
}
fn make_inline(mtm: MainThreadMarker, p: &PickerProps, target: &PickerTarget) -> Retained<NSView> {
let stack = NSStackView::new(mtm);
stack.setOrientation(NSUserInterfaceLayoutOrientation::Vertical);
stack.setSpacing(4.0);
stack.setAlignment(objc2_app_kit::NSLayoutAttribute::Leading);
for (i, opt) in p.options.iter().enumerate() {
let radio = unsafe {
NSButton::radioButtonWithTitle_target_action(
&NSString::from_str(opt),
Some(target),
Some(sel!(fire:)),
mtm,
)
};
radio.setTag(i as isize);
if i == p.selected {
radio.setState(NSControlStateValueOn);
}
stack.addArrangedSubview(<NSButton as AsRef<NSView>>::as_ref(&radio));
}
Retained::from(<NSStackView as AsRef<NSView>>::as_ref(&stack))
}
fn make(backend: &mut AppKit, p: &PickerProps, id: NodeId) -> Retained<NSView> {
let mtm = backend.mtm();
let target = PickerTarget::new(mtm, id, p.separators_before.clone());
let view = match p.style {
PickerStyle::Menu => make_menu(mtm, p, &target),
PickerStyle::Segmented => make_segmented(mtm, p, &target),
PickerStyle::Inline => make_inline(mtm, p, &target),
};
TARGETS.with(|t| t.insert((view.as_ref() as *const NSView) as usize, target));
if !p.enabled {
set_enabled(&view, false);
}
view
}
fn radios(stack: &NSStackView) -> Vec<Retained<NSButton>> {
stack
.arrangedSubviews()
.iter()
.filter_map(|v| v.downcast_ref::<NSButton>().map(Retained::from))
.collect()
}
fn set_enabled(h: &NSView, on: bool) {
if let Some(c) = h.downcast_ref::<objc2_app_kit::NSControl>() {
c.setEnabled(on);
} else if let Some(stack) = h.downcast_ref::<NSStackView>() {
for b in radios(stack) {
b.setEnabled(on);
}
}
}
pub(crate) fn read_native(h: &NSView) -> Option<(Option<String>, Option<bool>)> {
if !TARGETS.with(|t| t.contains((h as *const NSView) as usize)) {
return None;
}
if let Some(popup) = h.downcast_ref::<NSPopUpButton>() {
let text = popup.selectedItem().map(|item| item.title().to_string());
return Some((text, Some(popup.isEnabled())));
}
if let Some(seg) = h.downcast_ref::<NSSegmentedControl>() {
let i = seg.selectedSegment();
let text = (i >= 0 && i < seg.segmentCount())
.then(|| seg.labelForSegment(i))
.flatten()
.map(|s| s.to_string());
return Some((text, Some(seg.isEnabled())));
}
let stack = h.downcast_ref::<NSStackView>()?;
let radios = radios(stack);
let text = radios
.iter()
.find(|b| b.state() == NSControlStateValueOn)
.map(|b| b.title().to_string());
let enabled = match radios.first() {
Some(first) => {
let on = first.isEnabled();
radios.iter().all(|b| b.isEnabled() == on).then_some(on)
}
None => None,
};
Some((text, enabled))
}
fn update(_backend: &mut AppKit, h: &Retained<NSView>, patch: &PickerPatch) {
let i = match patch {
PickerPatch::Selected(i) => *i,
PickerPatch::Options(opts) => return set_options(h, opts),
PickerPatch::Enabled(on) => return set_enabled(h, *on),
};
if let Some(popup) = h.downcast_ref::<NSPopUpButton>() {
popup.selectItemWithTag(i as isize);
} else if let Some(seg) = h.downcast_ref::<NSSegmentedControl>() {
if (i as isize) < seg.segmentCount() && seg.selectedSegment() != i as isize {
seg.setSelectedSegment(i as isize);
}
} else if let Some(stack) = h.downcast_ref::<NSStackView>() {
let subs = stack.arrangedSubviews();
if let Some(v) = subs.iter().nth(i)
&& let Some(b) = v.downcast_ref::<NSButton>()
{
b.setState(NSControlStateValueOn);
}
}
}
fn set_options(h: &Retained<NSView>, opts: &[String]) {
if let Some(popup) = h.downcast_ref::<NSPopUpButton>() {
let want = popup
.selectedItem()
.map(|item| item.tag())
.unwrap_or(0)
.max(0) as usize;
let separators = TARGETS
.with(|t| t.get((h.as_ref() as *const NSView) as usize))
.map(|target| target.ivars().separators_before.clone())
.unwrap_or_default();
fill_menu(
popup,
opts,
&separators,
want.min(opts.len().saturating_sub(1)),
);
} else if let Some(seg) = h.downcast_ref::<NSSegmentedControl>() {
let want = seg.selectedSegment().max(0) as usize;
seg.setSegmentCount(opts.len() as isize);
for (i, o) in opts.iter().enumerate() {
seg.setLabel_forSegment(&NSString::from_str(o), i as isize);
}
if !opts.is_empty() {
seg.setSelectedSegment(want.min(opts.len() - 1) as isize);
}
} else if let Some(stack) = h.downcast_ref::<NSStackView>() {
let subs = stack.arrangedSubviews();
let existing: Vec<Retained<NSButton>> = subs
.iter()
.filter_map(|v| v.downcast_ref::<NSButton>().map(Retained::from))
.collect();
for (i, o) in opts.iter().enumerate() {
match existing.get(i) {
Some(b) => b.setTitle(&NSString::from_str(o)),
None => {
let Some(first) = existing.first() else { break };
let radio = unsafe {
NSButton::radioButtonWithTitle_target_action(
&NSString::from_str(o),
first.target().as_deref(),
first.action(),
stack.mtm(),
)
};
radio.setTag(i as isize);
radio.setEnabled(first.isEnabled());
stack.addArrangedSubview(<NSButton as AsRef<NSView>>::as_ref(&radio));
}
}
}
for b in existing.iter().skip(opts.len()) {
stack.removeArrangedSubview(<NSButton as AsRef<NSView>>::as_ref(b));
b.removeFromSuperview();
}
}
}
fn measure(_backend: &mut AppKit, h: &Retained<NSView>, _p: Proposal) -> Size {
let s = h.fittingSize();
Size::new(s.width.ceil().max(60.0), s.height.ceil().max(22.0))
}
pub(crate) fn realize_any(
b: &mut AppKit,
props: &dyn std::any::Any,
id: day_spec::NodeId,
) -> crate::Handle {
match day_spec::props_of::<PickerProps>(day_spec::kinds::PICKER, "appkit", props) {
Some(p) => make(b, p, id),
None => crate::placeholder_view(b.mtm(), day_spec::kinds::PICKER),
}
}
pub(crate) fn update_any(b: &mut AppKit, h: &crate::Handle, patch: &dyn std::any::Any) {
if let Some(p) = patch.downcast_ref::<PickerPatch>() {
update(b, h, p);
}
}
pub(crate) fn measure_any(
b: &mut AppKit,
h: &crate::Handle,
p: day_spec::Proposal,
) -> day_spec::Size {
measure(b, h, p)
}