use std::{cell::RefCell, ops::Range};
use gpui::{Bounds, Pixels, PlatformInputHandler, TextInputConfiguration, point, px};
use objc2::{
ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, Message, define_class, msg_send,
rc::Retained,
runtime::{NSObjectProtocol, ProtocolObject},
};
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::{NSCopying, NSObject, NSZone};
use objc2_ui_kit::{UITextInputDelegate, UITextPosition, UITextRange};
use super::{
InputView, VIEW,
text::{Change, TextEdit, TextState, change, utf16_len},
};
thread_local! {
static STATE: RefCell<Option<TextState>> = const { RefCell::new(None) };
static CONFIGURATION: RefCell<TextInputConfiguration> =
RefCell::new(TextInputConfiguration::default());
static RELOAD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) struct Busy;
fn gpui<R>(f: impl FnOnce(&mut PlatformInputHandler) -> R) -> Result<Option<R>, Busy> {
crate::ffi::callback("a UIKit text input callback", None, || {
crate::runtime::with(|runtime| Ok(runtime.ios_input_handler(f)))
.ok()
.flatten()
})
.ok_or(Busy)
}
fn read_state(handler: &mut PlatformInputHandler) -> Option<TextState> {
let selection = handler.selected_text_range(false)?.range;
let marked = handler.marked_text_range();
let len = handler.text_length_utf16().unwrap_or_else(|| {
let mut adjusted = None;
let text = handler.text_for_range(0..usize::MAX, &mut adjusted);
adjusted
.map(|range| range.end)
.or(text.map(|text| utf16_len(&text)))
.unwrap_or(selection.end)
});
let document = handler.text_input_editable_range().unwrap_or(0..len);
Some(TextState {
document,
selection,
marked,
})
}
pub(super) fn state() -> Option<TextState> {
match gpui(read_state) {
Ok(state) => {
let state = state.flatten();
STATE.with(|slot| *slot.borrow_mut() = state.clone());
state
}
Err(Busy) => STATE.with(|slot| slot.borrow().clone()),
}
}
pub(super) fn composing() -> bool {
STATE.with(|slot| slot.borrow().as_ref().is_some_and(|s| s.marked.is_some()))
}
pub(super) fn edit(edit: TextEdit) {
STATE.with(|slot| {
if let Some(state) = slot.borrow_mut().as_mut() {
state.apply(&edit);
}
});
super::push(super::ViewEvent::Text(edit));
state();
}
pub(super) fn query<R>(f: impl FnOnce(&mut PlatformInputHandler) -> Option<R>) -> Option<R> {
gpui(f).ok().flatten().flatten()
}
pub(super) fn text_in(range: Range<usize>) -> Option<String> {
query(|handler| handler.text_for_range(range, &mut None))
}
pub(super) fn bounds_for(range: Range<usize>) -> Option<Bounds<Pixels>> {
query(|handler| handler.bounds_for_range(range))
}
pub(super) fn index_at(at: CGPoint) -> Option<usize> {
query(|handler| handler.character_index_for_point(point(px(at.x as f32), px(at.y as f32))))
}
pub(super) fn cg_rect(bounds: Bounds<Pixels>) -> CGRect {
CGRect::new(
CGPoint::new(
f32::from(bounds.origin.x).into(),
f32::from(bounds.origin.y).into(),
),
CGSize::new(
f32::from(bounds.size.width).into(),
f32::from(bounds.size.height).into(),
),
)
}
pub(super) fn configuration() -> TextInputConfiguration {
CONFIGURATION.with(|c| c.borrow().clone())
}
pub(crate) fn set_text_input_configuration(configuration: TextInputConfiguration) {
CONFIGURATION.with(|c| *c.borrow_mut() = configuration);
RELOAD.with(|reload| reload.set(true));
}
pub(super) fn reset() {
STATE.with(|slot| *slot.borrow_mut() = None);
}
pub(crate) fn sync() {
let Some(view) = VIEW.with(|slot| slot.borrow().clone()) else {
return;
};
if !view.isFirstResponder() {
reset();
return;
}
if RELOAD.with(|reload| reload.replace(false)) {
view.reloadInputViews();
}
let Ok(new) = gpui(read_state) else { return };
let new = new.flatten();
let old = STATE.with(|slot| slot.replace(new.clone()));
let Some(delegate) = view.input_delegate() else {
return;
};
let input = Some(ProtocolObject::from_ref(&*view));
match change(old.as_ref(), new.as_ref()) {
Change::None => {}
Change::Selection => {
delegate.selectionWillChange(input);
delegate.selectionDidChange(input);
}
Change::Text => {
delegate.textWillChange(input);
delegate.textDidChange(input);
}
}
}
define_class!(
#[unsafe(super(UITextPosition, NSObject))]
#[name = "TauriGpuiTextPosition"]
#[thread_kind = MainThreadOnly]
#[ivars = usize]
pub(super) struct TextPosition;
unsafe impl NSObjectProtocol for TextPosition {}
unsafe impl NSCopying for TextPosition {
#[unsafe(method_id(copyWithZone:))]
fn copy_with_zone(&self, _zone: *mut NSZone) -> Retained<Self> {
self.retain()
}
}
);
define_class!(
#[unsafe(super(UITextRange, NSObject))]
#[name = "TauriGpuiTextRange"]
#[thread_kind = MainThreadOnly]
#[ivars = Range<usize>]
pub(super) struct TextRange;
impl TextRange {
#[unsafe(method_id(start))]
fn start(&self) -> Retained<UITextPosition> {
position(self.mtm(), self.ivars().start)
}
#[unsafe(method_id(end))]
fn end(&self) -> Retained<UITextPosition> {
position(self.mtm(), self.ivars().end)
}
#[unsafe(method(isEmpty))]
fn is_empty(&self) -> bool {
self.ivars().is_empty()
}
}
unsafe impl NSObjectProtocol for TextRange {}
unsafe impl NSCopying for TextRange {
#[unsafe(method_id(copyWithZone:))]
fn copy_with_zone(&self, _zone: *mut NSZone) -> Retained<Self> {
self.retain()
}
}
);
pub(super) fn position(mtm: MainThreadMarker, offset: usize) -> Retained<UITextPosition> {
let this = TextPosition::alloc(mtm).set_ivars(offset);
let this: Retained<TextPosition> = unsafe { msg_send![super(this), init] };
Retained::into_super(this)
}
pub(super) fn range(mtm: MainThreadMarker, range: Range<usize>) -> Retained<UITextRange> {
let this = TextRange::alloc(mtm).set_ivars(range);
let this: Retained<TextRange> = unsafe { msg_send![super(this), init] };
Retained::into_super(this)
}
pub(super) fn offset(position: &UITextPosition) -> Option<usize> {
position
.isKindOfClass(TextPosition::class())
.then(|| *unsafe { &*(position as *const UITextPosition as *const TextPosition) }.ivars())
}
pub(super) fn offsets(range: &UITextRange) -> Option<Range<usize>> {
if range.isKindOfClass(TextRange::class()) {
Some(
unsafe { &*(range as *const UITextRange as *const TextRange) }
.ivars()
.clone(),
)
} else {
Some(offset(&range.start())?..offset(&range.end())?)
}
}
impl InputView {
pub(super) fn input_delegate(
&self,
) -> Option<Retained<ProtocolObject<dyn UITextInputDelegate>>> {
self.ivars()
.delegate
.borrow()
.as_ref()
.and_then(|delegate| delegate.load())
}
}