use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
use kui_core::{Core, Key};
use objc2::encode::Encode;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, Bool, Imp, Method, Sel};
use objc2::{ffi, msg_send, sel};
use objc2_app_kit::NSView;
use objc2_foundation::{NSAttributedString, NSObject, NSRange, NSString};
use winit::window::Window;
use crate::Waker;
type CountedFrom = (Key, u64, usize, Option<(usize, usize)>);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Facts {
editable: bool,
caret: usize,
selection: usize,
from: Option<CountedFrom>,
}
#[derive(Default)]
struct State {
key_depth: u32,
facts: HashMap<usize, Facts>,
commits: Vec<(usize, String)>,
waker: Option<Waker>,
}
thread_local! {
static STATE: RefCell<State> = RefCell::new(State::default());
}
struct Originals {
key_down: Imp,
insert_text: Imp,
selected_range: Imp,
}
static ORIGINALS: OnceLock<Option<Originals>> = OnceLock::new();
fn view_of(window: &Window) -> Option<Retained<NSView>> {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let RawWindowHandle::AppKit(h) = window.window_handle().ok()?.as_raw() else {
return None;
};
unsafe { Retained::retain(h.ns_view.as_ptr().cast::<NSView>()) }
}
pub fn view_ptr(window: &Window) -> Option<usize> {
view_of(window).map(|v| Retained::as_ptr(&v) as usize)
}
pub fn set_waker(waker: Waker) {
STATE.with(|s| s.borrow_mut().waker = Some(waker));
}
pub fn attach(window: &Window) -> bool {
let Some(view) = view_of(window) else {
return false;
};
let cls = view.class();
let installed = ORIGINALS.get_or_init(|| install(cls)).is_some();
if installed {
STATE.with(|s| {
s.borrow_mut()
.facts
.insert(Retained::as_ptr(&view) as usize, Facts::default())
});
}
installed
}
pub fn detach(window: &Window) {
let Some(ptr) = view_ptr(window) else {
return;
};
STATE.with(|s| {
let mut s = s.borrow_mut();
s.facts.remove(&ptr);
s.commits.retain(|(v, _)| *v != ptr);
});
}
pub fn stamp(window: &Window, core: &Core) {
let Some(ptr) = view_ptr(window) else {
return;
};
STATE.with(|s| {
let mut s = s.borrow_mut();
let Some(facts) = s.facts.get_mut(&ptr) else {
return;
};
facts.editable = core.ime_rect().is_some();
let Some(key) = core.edit.focused() else {
facts.caret = 0;
facts.selection = 0;
facts.from = None;
return;
};
let Some((caret, selection)) = core.edit.caret_and_selection(key) else {
return;
};
let from = Some((key, core.edit.version(key), caret, selection));
if facts.from == from {
return;
}
facts.from = from;
let Some(text) = core.edit.text(key) else {
return;
};
let utf16 = |byte: usize| text.get(..byte).map_or(0, |t| t.encode_utf16().count());
match selection {
Some((a, z)) => {
facts.caret = utf16(a);
facts.selection = utf16(z) - facts.caret;
}
None => {
facts.caret = utf16(caret);
facts.selection = 0;
}
}
});
}
pub fn discard_marked_text(window: &Window) {
let Some(view) = view_of(window) else {
return;
};
unsafe {
let ctx: Option<Retained<AnyObject>> = msg_send![&*view, inputContext];
if let Some(ctx) = ctx {
let _: () = msg_send![&*ctx, discardMarkedText];
}
}
}
pub fn take_commits() -> Vec<(usize, String)> {
STATE.with(|s| std::mem::take(&mut s.borrow_mut().commits))
}
type KeyDownFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject);
type InsertTextFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, &NSObject, NSRange);
type SelectedRangeFn = unsafe extern "C-unwind" fn(&AnyObject, Sel) -> NSRange;
type IsEditableFn = unsafe extern "C-unwind" fn(&AnyObject, Sel) -> Bool;
fn imp<F: Copy>(f: F) -> Imp {
const { assert!(std::mem::size_of::<F>() == std::mem::size_of::<Imp>()) };
unsafe { std::mem::transmute_copy(&f) }
}
fn defining_class(cls: &AnyClass, sel: Sel) -> Option<(&AnyClass, &Method)> {
let mut at = Some(cls);
while let Some(c) = at {
if let Some(m) = c
.instance_methods()
.iter()
.find(|m| m.name() == sel)
.copied()
{
return Some((c, m));
}
at = c.superclass();
}
None
}
fn install(cls: &AnyClass) -> Option<Originals> {
let (_, key_down) = defining_class(cls, sel!(keyDown:))?;
let (owner, insert_text) = defining_class(cls, sel!(insertText:replacementRange:))?;
let (_, selected_range) = defining_class(cls, sel!(selectedRange))?;
let originals = Originals {
key_down: key_down.implementation(),
insert_text: insert_text.implementation(),
selected_range: selected_range.implementation(),
};
let imps: [(&Method, Imp); 3] = [
(key_down, imp(key_down_override as KeyDownFn)),
(insert_text, imp(insert_text_override as InsertTextFn)),
(
selected_range,
imp(selected_range_override as SelectedRangeFn),
),
];
for (m, imp) in imps {
unsafe { m.set_implementation(imp) };
}
let types = format!("{}@:\0", Bool::ENCODING);
let added = unsafe {
ffi::class_addMethod(
owner as *const AnyClass as *mut AnyClass,
sel!(isEditable),
imp(is_editable_override as IsEditableFn),
types.as_ptr().cast(),
)
};
added.as_bool().then_some(originals)
}
fn originals() -> &'static Originals {
ORIGINALS
.get()
.and_then(Option::as_ref)
.expect("an override runs only once installed")
}
fn facts_of(this: &AnyObject) -> Option<Facts> {
let ptr = this as *const AnyObject as usize;
STATE.with(|s| s.borrow().facts.get(&ptr).copied())
}
struct KeyGuard;
impl KeyGuard {
fn enter() -> Self {
STATE.with(|s| s.borrow_mut().key_depth += 1);
Self
}
}
impl Drop for KeyGuard {
fn drop(&mut self) {
STATE.with(|s| s.borrow_mut().key_depth -= 1);
}
}
unsafe extern "C-unwind" fn key_down_override(this: &AnyObject, sel: Sel, event: &AnyObject) {
let _inside = KeyGuard::enter();
let orig: KeyDownFn = unsafe { std::mem::transmute(originals().key_down) };
unsafe { orig(this, sel, event) }
}
unsafe extern "C-unwind" fn insert_text_override(
this: &AnyObject,
sel: Sel,
string: &NSObject,
range: NSRange,
) {
let had_marked: bool = unsafe { msg_send![this, hasMarkedText] };
let inside_key = STATE.with(|s| s.borrow().key_depth > 0);
let orig: InsertTextFn = unsafe { std::mem::transmute(originals().insert_text) };
unsafe { orig(this, sel, string, range) };
if had_marked || inside_key {
return;
}
let text = if let Some(s) = string.downcast_ref::<NSAttributedString>() {
s.string().to_string()
} else if let Some(s) = string.downcast_ref::<NSString>() {
s.to_string()
} else {
return;
};
if text.is_empty() || text.chars().next().is_some_and(char::is_control) {
return;
}
let ptr = this as *const AnyObject as usize;
STATE.with(|s| {
let mut s = s.borrow_mut();
if !s.facts.contains_key(&ptr) {
return;
}
s.commits.push((ptr, text));
if let Some(w) = &s.waker {
w.wake();
}
});
}
unsafe extern "C-unwind" fn selected_range_override(this: &AnyObject, sel: Sel) -> NSRange {
match facts_of(this) {
Some(f) if f.editable => NSRange::new(f.caret, f.selection),
_ => {
let orig: SelectedRangeFn = unsafe { std::mem::transmute(originals().selected_range) };
unsafe { orig(this, sel) }
}
}
}
unsafe extern "C-unwind" fn is_editable_override(this: &AnyObject, _sel: Sel) -> Bool {
Bool::new(facts_of(this).is_none_or(|f| f.editable))
}