use day_spec::Event;
use day_spec::ffi_guard;
use day_spec::props::{TextAreaPatch as TextPatch, TextAreaProps as TextProps};
use day_spec::sidetable::SideTable;
use crate::AppKit;
use day_spec::{NodeId, Proposal, Size};
use objc2::rc::Retained;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send};
use objc2_app_kit::{
NSAutoresizingMaskOptions, NSColor, NSFont, NSScrollView, NSTextDelegate, NSTextField,
NSTextView, NSTextViewDelegate, NSView,
};
use objc2_foundation::{NSNotification, NSObject, NSPoint, NSRect, NSSize, NSString};
const FONT_SIZE: f64 = 13.0;
const INSET: f64 = 6.0;
struct TAIvars {
node: NodeId,
placeholder: Retained<NSTextField>,
submit_on_enter: bool,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DayTextAreaTarget"]
#[ivars = TAIvars]
struct TATarget;
unsafe impl NSObjectProtocol for TATarget {}
unsafe impl NSTextViewDelegate for TATarget {}
unsafe impl NSTextDelegate for TATarget {
#[unsafe(method(textDidChange:))]
fn text_did_change(&self, notification: &NSNotification) {
ffi_guard::contain((), || {
let node = self.ivars().node;
if let Some(obj) = notification.object()
&& let Ok(tv) = obj.downcast::<NSTextView>()
{
let s = tv.string().to_string();
self.ivars().placeholder.setHidden(!s.is_empty());
crate::emit(node, Event::TextChanged(s));
}
})
}
}
impl TATarget {
#[unsafe(method(textView:doCommandBySelector:))]
fn do_command(&self, _tv: &NSTextView, sel: objc2::runtime::Sel) -> objc2::runtime::Bool {
ffi_guard::contain(objc2::runtime::Bool::NO, || {
if !self.ivars().submit_on_enter || sel != objc2::sel!(insertNewline:) {
return objc2::runtime::Bool::NO;
}
let shift = objc2_app_kit::NSApplication::sharedApplication(self.mtm())
.currentEvent()
.map(|e| {
e.modifierFlags()
.contains(objc2_app_kit::NSEventModifierFlags::Shift)
})
.unwrap_or(false);
if shift {
return objc2::runtime::Bool::NO;
}
crate::emit(self.ivars().node, Event::Submitted);
objc2::runtime::Bool::YES
})
}
}
);
impl TATarget {
fn new(
mtm: MainThreadMarker,
node: NodeId,
placeholder: Retained<NSTextField>,
submit_on_enter: bool,
) -> Retained<Self> {
let this = Self::alloc(mtm).set_ivars(TAIvars {
node,
placeholder,
submit_on_enter,
});
unsafe { msg_send![super(this), init] }
}
}
struct TAState {
tv: Retained<NSTextView>,
placeholder: Retained<NSTextField>,
#[allow(dead_code)]
target: Retained<TATarget>,
line_h: f64,
min_lines: u32,
max_lines: u32,
}
day_core::tls_group! {
static STATE: SideTable<TAState> = SideTable::with_teardown(|st: TAState| {
st.tv.setDelegate(None);
});
}
fn key(v: &Retained<NSView>) -> usize {
Retained::as_ptr(v) as usize
}
fn apply_attrs(tv: &NSTextView, editable: bool, selectable: bool, spell: bool) {
tv.setEditable(editable);
tv.setSelectable(selectable);
unsafe {
let _: () = msg_send![tv, setContinuousSpellCheckingEnabled: spell];
let _: () = msg_send![tv, setAutomaticSpellingCorrectionEnabled: spell];
let _: () = msg_send![tv, setGrammarCheckingEnabled: spell];
}
}
fn make(backend: &mut AppKit, p: &TextProps, id: NodeId) -> Retained<NSView> {
let mtm = backend.mtm();
let font = NSFont::systemFontOfSize(FONT_SIZE);
let scroll = NSScrollView::new(mtm);
scroll.setDrawsBackground(false);
scroll.setHasVerticalScroller(true);
scroll.setScrollerStyle(objc2_app_kit::NSScrollerStyle::Overlay);
scroll.setHasHorizontalScroller(false);
let tv = NSTextView::new(mtm);
tv.setFont(Some(&font));
tv.setRichText(false);
apply_attrs(&tv, p.editable, p.selectable, p.spellcheck);
tv.setUsesFontPanel(false);
tv.setTextContainerInset(NSSize::new(INSET, INSET));
tv.setVerticallyResizable(true);
tv.setHorizontallyResizable(false);
tv.setAutoresizingMask(NSAutoresizingMaskOptions::ViewWidthSizable);
tv.setMinSize(NSSize::new(0.0, 0.0));
tv.setMaxSize(NSSize::new(1.0e7, 1.0e7));
if let Some(tc) = unsafe { tv.textContainer() } {
tc.setWidthTracksTextView(true);
}
if !p.text.is_empty() {
tv.setString(&NSString::from_str(&p.text));
}
let line_h = unsafe { tv.layoutManager() }
.map(|lm| lm.defaultLineHeightForFont(&font))
.unwrap_or(FONT_SIZE * 1.3);
let ph = NSTextField::labelWithString(&NSString::from_str(&p.placeholder), mtm);
ph.setTextColor(Some(&NSColor::placeholderTextColor()));
ph.setFont(Some(&font));
ph.setFrame(NSRect::new(
NSPoint::new(INSET + 5.0, INSET),
NSSize::new(320.0, line_h.ceil()),
));
ph.setHidden(!p.text.is_empty());
tv.addSubview(<NSTextField as AsRef<NSView>>::as_ref(&ph));
let target = TATarget::new(mtm, id, ph.clone(), p.submit_on_enter);
tv.setDelegate(Some(ProtocolObject::from_ref(&*target)));
scroll.setDocumentView(Some(&tv));
let ns: Retained<NSView> = Retained::from(<NSScrollView as AsRef<NSView>>::as_ref(&scroll));
STATE.with(|t| {
t.insert(
key(&ns),
TAState {
tv,
placeholder: ph,
target,
line_h,
min_lines: p.min_lines,
max_lines: p.max_lines,
},
)
});
ns
}
fn update(_backend: &mut AppKit, h: &Retained<NSView>, patch: &TextPatch) {
STATE.with(|t| {
t.with(key(h), |st| match patch {
TextPatch::SetText(t) => {
if st.tv.string().to_string() != *t {
st.tv.setString(&NSString::from_str(t));
st.placeholder.setHidden(!t.is_empty());
}
}
TextPatch::SetEditable(v) => st.tv.setEditable(*v),
TextPatch::SetSelectable(v) => st.tv.setSelectable(*v),
TextPatch::SetSpellCheck(v) => unsafe {
let _: () = msg_send![&st.tv, setContinuousSpellCheckingEnabled: *v];
let _: () = msg_send![&st.tv, setAutomaticSpellingCorrectionEnabled: *v];
let _: () = msg_send![&st.tv, setGrammarCheckingEnabled: *v];
},
});
});
}
fn measure(_backend: &mut AppKit, h: &Retained<NSView>, p: Proposal) -> Size {
let avail_w = p.width.unwrap_or(300.0).max(120.0);
STATE.with(|t| {
t.with(key(h), |st| {
let pad = 2.0 * INSET;
let min_h = (st.min_lines as f64) * st.line_h + pad;
let max_h = if st.max_lines > 0 {
(st.max_lines as f64) * st.line_h + pad
} else {
f64::MAX
};
let (Some(tc), Some(lm)) = (unsafe { st.tv.textContainer() }, unsafe {
st.tv.layoutManager()
}) else {
return Size::new(avail_w, min_h.ceil());
};
let lfp = tc.lineFragmentPadding();
let inner_w = (avail_w - 2.0 * INSET - 2.0 * lfp).max(1.0);
tc.setWidthTracksTextView(false);
tc.setContainerSize(NSSize::new(inner_w, 1.0e7));
let _ = lm.glyphRangeForTextContainer(&tc);
let used = lm.usedRectForTextContainer(&tc);
tc.setWidthTracksTextView(true);
let content_h = used.size.height + pad;
let hgt = content_h.clamp(min_h, max_h);
Size::new(avail_w, hgt.ceil())
})
.unwrap_or(Size::new(avail_w, 44.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::<TextProps>(day_spec::kinds::TEXT_AREA, "appkit", props) {
Some(p) => make(b, p, id),
None => crate::placeholder_view(b.mtm(), day_spec::kinds::TEXT_AREA),
}
}
pub(crate) fn update_any(b: &mut AppKit, h: &crate::Handle, patch: &dyn std::any::Any) {
if let Some(p) = patch.downcast_ref::<TextPatch>() {
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)
}