kui_ffi/input.rs
1//! Input in, events out: the pointer, wheel, key, text and preedit entry
2//! points, the event poll, the cursor shape, and the editor text
3//! a host reads back or replaces.
4
5use super::*;
6
7pub(crate) fn push_input(ptr: *mut KuiCtx, ev: InputEvent) {
8 guard((), || {
9 if let Some(c) = unsafe { ctx(ptr) } {
10 let evs = c.core().handle_input(ev);
11 c.absorb(evs);
12 }
13 })
14}
15
16/// The pointer moved to (`x`, `y`) in logical pixels. Hover, drags and
17/// the cursor shape follow from it.
18#[unsafe(no_mangle)]
19pub extern "C" fn kui_input_cursor(ptr: *mut KuiCtx, x: f32, y: f32) {
20 push_input(ptr, InputEvent::CursorMoved(Vec2::new(x, y)));
21}
22
23/// The pointer left the window: nothing is hovered until it comes back.
24#[unsafe(no_mangle)]
25pub extern "C" fn kui_input_cursor_left(ptr: *mut KuiCtx) {
26 push_input(ptr, InputEvent::CursorLeft);
27}
28
29/// A primary-button press (`down` true) or release at the last cursor
30/// position; `clicks` is the click count (1, or 2 for a double-click).
31/// [`kui_input_mouse_button`] carries the other buttons.
32#[unsafe(no_mangle)]
33pub extern "C" fn kui_input_mouse(ptr: *mut KuiCtx, down: bool, clicks: u32) {
34 kui_input_mouse_button(ptr, down, MouseButton::Primary.code(), clicks);
35}
36
37/// [`kui_input_mouse`] for a named button (`KUI_MOUSE_*`, or `3 + n` for
38/// a further button `n`).
39#[unsafe(no_mangle)]
40pub extern "C" fn kui_input_mouse_button(ptr: *mut KuiCtx, down: bool, button: u32, clicks: u32) {
41 let button = MouseButton::from_code(button);
42 push_input(
43 ptr,
44 if down {
45 InputEvent::MouseDown {
46 button,
47 clicks: clicks.clamp(1, u8::MAX as u32) as u8,
48 }
49 } else {
50 InputEvent::MouseUp { button }
51 },
52 );
53}
54
55/// A wheel or trackpad delta in logical px (positive `dy` scrolls up), as
56/// a scroll gesture of its own: it goes to the innermost scroller under
57/// the pointer that can move that way.
58#[unsafe(no_mangle)]
59pub extern "C" fn kui_input_scroll(ptr: *mut KuiCtx, dx: f32, dy: f32) {
60 push_input(ptr, InputEvent::Scroll(Vec2::new(dx, dy)));
61}
62
63/// A wheel or trackpad delta that is part of a scroll gesture: `begins`
64/// on its first event, after which the rest go to the targets that one
65/// picked wherever the pointer or the content has gone. For a host whose
66/// event loop can tell one swipe from the next (`kui_run` begins a new
67/// gesture after a 200 ms pause).
68#[unsafe(no_mangle)]
69pub extern "C" fn kui_input_scroll_gesture(ptr: *mut KuiCtx, dx: f32, dy: f32, begins: bool) {
70 push_input(
71 ptr,
72 InputEvent::ScrollGesture {
73 delta: Vec2::new(dx, dy),
74 begins,
75 },
76 );
77}
78
79/// Committed text input (typing, paste); routed to the focused editor.
80#[unsafe(no_mangle)]
81pub extern "C" fn kui_input_text(ptr: *mut KuiCtx, text: KuiStr) {
82 guard((), || {
83 let text = kstr(text).into_owned();
84 push_input(ptr, InputEvent::Text(text));
85 });
86}
87
88/// The paths a file-drag door carries: `count` borrowed `KuiStr`s, read
89/// once and owned by the event.
90fn paths_of(paths: *const KuiStr, count: usize) -> Vec<String> {
91 if paths.is_null() || count == 0 {
92 return Vec::new();
93 }
94 // SAFETY: the host promises `count` strings at `paths` for the call.
95 unsafe { std::slice::from_raw_parts(paths, count) }
96 .iter()
97 .map(|s| kstr(*s).into_owned())
98 .collect()
99}
100
101/// Files dragged in from the OS are over the window at (`x`, `y`),
102/// entering and moving alike: the drop zone under the point hears
103/// `{kind:"drop", phase:"enter"|"move"}`, a zone it left hears `leave`.
104/// `paths` are `count` OS paths. The host's answer to the OS (copy over a
105/// zone, not-allowed elsewhere) is [`kui_drop_target`].
106#[unsafe(no_mangle)]
107pub extern "C" fn kui_input_drag_files(
108 ptr: *mut KuiCtx,
109 paths: *const KuiStr,
110 count: usize,
111 x: f32,
112 y: f32,
113) {
114 guard((), || {
115 let paths = paths_of(paths, count);
116 push_input(
117 ptr,
118 InputEvent::DragFiles {
119 paths,
120 at: Vec2::new(x, y),
121 },
122 );
123 });
124}
125
126/// The dragged files were released at (`x`, `y`): the zone there hears
127/// `{kind:"drop", phase:"drop", paths, x, y, tag}` and no `leave` after;
128/// with no zone there, nothing but the lit zone's `leave`.
129#[unsafe(no_mangle)]
130pub extern "C" fn kui_input_drop_files(
131 ptr: *mut KuiCtx,
132 paths: *const KuiStr,
133 count: usize,
134 x: f32,
135 y: f32,
136) {
137 guard((), || {
138 let paths = paths_of(paths, count);
139 push_input(
140 ptr,
141 InputEvent::DropFiles {
142 paths,
143 at: Vec2::new(x, y),
144 },
145 );
146 });
147}
148
149/// The dragged files left the window, or the OS ended the drag
150/// elsewhere: the lit zone hears its `leave`.
151#[unsafe(no_mangle)]
152pub extern "C" fn kui_input_drag_cancel(ptr: *mut KuiCtx) {
153 push_input(ptr, InputEvent::DragCancel);
154}
155
156/// The OS asked the app to open these `count` documents (backlog F124):
157/// the host hears `{kind:"open", paths}` on the root, asked for or not;
158/// none emits nothing. A host driving its own window on macOS sends it
159/// from its app delegate's `application:openURLs:`; under `kui_run` the
160/// runner does.
161#[unsafe(no_mangle)]
162pub extern "C" fn kui_input_open(ptr: *mut KuiCtx, paths: *const KuiStr, count: usize) {
163 guard((), || {
164 let paths = paths_of(paths, count);
165 push_input(ptr, InputEvent::Open(paths));
166 });
167}
168
169fn edit_key_of(key: u32) -> Option<EditKey> {
170 KUI_EDIT_KEYS.get(key as usize).map(|(_, k)| *k)
171}
172
173/// Physical modifier state changed (KUI_KMOD_* bits). The host polls a
174/// `{kind="modifiers", shift, ctrl, alt, super}` event when it differs from
175/// the last report.
176#[unsafe(no_mangle)]
177pub extern "C" fn kui_input_modifiers(ptr: *mut KuiCtx, mods: u32) {
178 push_input(
179 ptr,
180 InputEvent::Modifiers(kui_core::KeyMods::from_bits(mods)),
181 );
182}
183
184/// Editing key with modifier bits (1 = shift, 2 = word/alt, 4 = doc/primary).
185#[unsafe(no_mangle)]
186pub extern "C" fn kui_input_key(ptr: *mut KuiCtx, key: u32, mods: u32) {
187 if let Some(k) = edit_key_of(key) {
188 let mods = Mods {
189 shift: mods & KUI_MOD_SHIFT != 0,
190 word: mods & KUI_MOD_WORD != 0,
191 doc: mods & KUI_MOD_DOC != 0,
192 };
193 push_input(ptr, InputEvent::Key(k, mods));
194 }
195}
196
197fn key_press_of(
198 code: KuiStr,
199 physical: KuiStr,
200 kmods: u32,
201 text: KuiStr,
202) -> Option<kui_core::KeyPress> {
203 let layout = kui_core::KeyCode::from_name(&kstr(code))?;
204 let mods = kui_core::KeyMods::from_bits(kmods);
205 // A NULL `physical` means "the key I just named": a host that does not
206 // track positions says so by omission, and gets `code` through unchanged
207 // because the two agree (a letter's position is its lower-case letter,
208 // as a window reports it). A host that does track them hands both over and
209 // `from_layout` applies the same non-Latin fallback the winit driver
210 // does — the rule lives in the core so no host reimplements it.
211 let press = match physical.ptr.is_null() {
212 true => kui_core::KeyPress::new(layout, mods),
213 false => {
214 let phys = kui_core::KeyCode::from_name(&kstr(physical))?;
215 // The layout's script rides in the same word.
216 let script = kui_core::LayoutScript::from_bits(kmods);
217 kui_core::KeyPress::from_layout_in(layout, phys, mods, script)
218 }
219 };
220 // A NULL `text` means "whatever this key inserts": the plain
221 // character keys insert themselves, a chord inserts nothing. Asked of
222 // the key the layout named, not the US stand-in in `code`: shift on
223 // the key printed `;` on a Russian layout types `Ж`, not `:`.
224 let text = match text.ptr.is_null() {
225 false => Some(kstr(text).into_owned()),
226 true => layout.typed(mods),
227 };
228 // The same word's upper bits: the lock state and the key's location
229 // (`KUI_KLOCK_*`, `KUI_KLOC_*`), zero for what every press was
230 // before.
231 Some(kui_core::KeyPress {
232 text,
233 location: kui_core::KeyLocation::from_bits(kmods),
234 locks: kui_core::KeyLocks::from_bits(kmods),
235 ..press
236 })
237}
238
239/// A raw key press for `on_key` sinks; most hosts want [`kui_input_press`],
240/// which sends this and then what the key means.
241///
242/// `code` is a single character as the layout produced it ("W", "$") or a
243/// name ("left", "enter", "escape", "f5"); `physical` is the US-QWERTY key
244/// at that position, spelled the same way, or NULL when the host does not
245/// track positions (then it equals `code`); `kmods` is `KUI_KMOD_*` bits,
246/// with `KUI_KLOCK_*`, one `KUI_KLOC_*` and `KUI_KLAYOUT_NONLATIN` beside
247/// them; `text` is what the press inserts, or NULL to derive it from
248/// `code`; `repeat` marks an auto-repeat. The focused sink polls
249/// `{kind="key", phase="down", code, physical, ctrl, alt, shift, super,
250/// text, repeat, location, caps_lock, num_lock, tag}`. An unknown `code`
251/// or `physical` is ignored.
252///
253/// Passing both spellings makes a keymap portable: on a layout that
254/// produces non-ASCII characters (Cyrillic, Greek, Hebrew, Arabic) kui
255/// reports the position's US key as `code`, so a Latin keymap still
256/// matches, while a NULL `text` is still the layout's own character. A
257/// host whose layout is not Latin says so with `KUI_KLAYOUT_NONLATIN`,
258/// and the US key stands in for the layout's ASCII too.
259#[unsafe(no_mangle)]
260pub extern "C" fn kui_input_key_down(
261 ptr: *mut KuiCtx,
262 code: KuiStr,
263 physical: KuiStr,
264 kmods: u32,
265 text: KuiStr,
266 repeat: bool,
267) {
268 guard((), || {
269 if let Some(kp) = key_press_of(code, physical, kmods, text) {
270 push_input(
271 ptr,
272 InputEvent::KeyDown(kui_core::KeyPress { repeat, ..kp }),
273 );
274 }
275 });
276}
277
278/// The release of a key pressed with `kui_input_key_down`, spelled the same
279/// way (`physical` included, NULL for "same as `code`"); the sink polls
280/// `{kind="key", phase="up", ...}` with a null `text`.
281/// A release whose press the sink never got resolves nothing, and moving
282/// focus while a key is held delivers the "up" first, so a held-key binding
283/// (WASD, press-and-hold) cannot be left stuck down.
284#[unsafe(no_mangle)]
285pub extern "C" fn kui_input_key_up(ptr: *mut KuiCtx, code: KuiStr, physical: KuiStr, kmods: u32) {
286 guard((), || {
287 if let Some(kp) = key_press_of(
288 code,
289 physical,
290 kmods,
291 KuiStr {
292 ptr: std::ptr::null(),
293 len: 0,
294 },
295 ) {
296 push_input(ptr, InputEvent::KeyUp(kp.released()));
297 }
298 });
299}
300
301/// A whole key going down, the way a window sends it: the call a host
302/// driving kui from its own event loop wants. Spelled exactly as
303/// [`kui_input_key_down`]; it sends that raw press first, then what the
304/// key means (what [`kui_input_key`] carries on its own): Escape dismisses
305/// a modal, Tab walks the focus ring, an arrow nudges a focused slider,
306/// Space presses a focused control, a printable character reaches the
307/// focused editor.
308///
309/// The two older calls remain as the halves for a host that drives one
310/// channel and not the other; `kui_input_key_down(ctx, KUI_STR("escape"),
311/// ...)` alone leaves a modal open. An unknown `code` or `physical` is
312/// ignored.
313#[unsafe(no_mangle)]
314pub extern "C" fn kui_input_press(
315 ptr: *mut KuiCtx,
316 code: KuiStr,
317 physical: KuiStr,
318 kmods: u32,
319 text: KuiStr,
320 repeat: bool,
321) {
322 guard((), || {
323 let Some(kp) = key_press_of(code, physical, kmods, text) else {
324 return;
325 };
326 if let Some(c) = unsafe { ctx(ptr) } {
327 let evs = c.core().press(kui_core::KeyPress { repeat, ..kp });
328 c.absorb(evs);
329 }
330 });
331}
332
333/// The same key coming up, spelled the way `kui_input_press` spells it
334/// (`physical` included, {NULL, 0} for "same as `code`"). One channel,
335/// because only one has a second half: the editing keys act on the way
336/// down, so this is `kui_input_key_up` under the name that pairs with
337/// `kui_input_press`.
338#[unsafe(no_mangle)]
339pub extern "C" fn kui_input_release(ptr: *mut KuiCtx, code: KuiStr, physical: KuiStr, kmods: u32) {
340 guard((), || {
341 let Some(kp) = key_press_of(
342 code,
343 physical,
344 kmods,
345 KuiStr {
346 ptr: std::ptr::null(),
347 len: 0,
348 },
349 ) else {
350 return;
351 };
352 if let Some(c) = unsafe { ctx(ptr) } {
353 let evs = c.core().release(kp);
354 c.absorb(evs);
355 }
356 });
357}
358
359/// Lets go of every key the focused sink is holding, as if the user had
360/// released them. Hosts call it when the window loses the keyboard: the OS
361/// stops delivering key events to it, so the release of anything held over
362/// an app switch would never arrive. Focus moves do this by themselves.
363#[unsafe(no_mangle)]
364pub extern "C" fn kui_release_held_keys(ptr: *mut KuiCtx) {
365 guard((), || {
366 if let Some(c) = unsafe { ctx(ptr) } {
367 c.core().release_held_keys();
368 }
369 });
370}
371
372/// Pops the next pending event into `out` (start from `KUI_EVENT_INIT`);
373/// false when the queue is empty. Call it after each input and each frame
374/// until it returns false.
375///
376/// `out->payload` is borrowed until the next poll on this context or
377/// [`kui_ctx_free`]. Also false, leaving the event queued, when `out`'s
378/// `size` is below the layout this library knows.
379#[unsafe(no_mangle)]
380pub extern "C" fn kui_poll_event(ptr: *mut KuiCtx, out: *mut KuiEvent) -> bool {
381 guard(false, || {
382 let Some(c) = (unsafe { ctx(ptr) }) else {
383 return false;
384 };
385 // Drop the previously handed-out payload.
386 c.last_payload = None;
387 // Also whatever a call between frames left pending — the synthetic
388 // key releases `kui_focus` / `kui_release_held_keys` force.
389 let pending = c.core().take_pending_events();
390 c.absorb(pending);
391 // Refuse before popping: a reservation this library cannot write
392 // into must leave the queue where it was, not swallow an event.
393 if c.events.is_empty() || !out_accepts(out) {
394 return false;
395 }
396 let ev = c.events.remove(0);
397 let payload = Box::new(KuiValue(ev.payload));
398 let payload_ptr: *const KuiValue = &*payload;
399 c.last_payload = Some(payload);
400 write_out(
401 out,
402 KuiEvent {
403 origin: ev.origin.0,
404 key: ev.key.0,
405 payload: payload_ptr,
406 window: ev.window.0,
407 slot: ev.slot.map_or(0, |k| k.0),
408 ..Default::default()
409 },
410 )
411 })
412}
413
414/// The pointer shape for where the pointer is now (KUI_CURSOR_*, never 0):
415/// the `cursor` the topmost node under it declared, the I-beam over text,
416/// the arrow otherwise. A query, not a queue — read it after each input and
417/// each frame and apply it to the real window when it changes. Hosts
418/// without a pointer simply never call.
419#[unsafe(no_mangle)]
420pub extern "C" fn kui_cursor_shape(ptr: *mut KuiCtx) -> u32 {
421 guard(0, || {
422 let Some(c) = (unsafe { ctx(ptr) }) else {
423 return 0;
424 };
425 let shape = c.core().cursor_shape();
426 // KUI_CURSOR_* = schema index + 1, matching KuiSpec.cursor.
427 kui_core::schema::CURSORS
428 .iter()
429 .position(|n| *n == shape.name())
430 .map_or(0, |i| i as u32 + 1)
431 })
432}
433
434/// Text an IME committed at the end of a composition: a focused editor
435/// takes it as [`kui_input_text`] would; otherwise the focused `on_key`
436/// sink hears `{kind:"text", text, tag}`, the one committed text a `key`
437/// event never carries. Typing stays on `kui_input_text`.
438#[unsafe(no_mangle)]
439pub extern "C" fn kui_input_commit(ptr: *mut KuiCtx, text: KuiStr) {
440 guard((), || {
441 let text = kstr(text).into_owned();
442 push_input(ptr, InputEvent::Commit(text));
443 });
444}
445
446/// The clipboard's answer to a paste (`KUI_MENU_ACTION_PASTE`), with the
447/// pasteboard's markers as `KUI_PASTE_*` bits: routed as
448/// [`kui_input_commit`] is, and a focused `on_key` sink hears
449/// `{kind:"text", text, tag}` with `concealed: true` / `transient: true`
450/// for the bits that are set. A host that cannot read the markers
451/// answers with 0, or with `kui_input_commit`, which is the same answer.
452#[unsafe(no_mangle)]
453pub extern "C" fn kui_input_paste(ptr: *mut KuiCtx, text: KuiStr, marks: u32) {
454 guard((), || {
455 let text = kstr(text).into_owned();
456 let marks = kui_core::ClipboardMarks::from_bits(marks);
457 push_input(ptr, InputEvent::Paste { text, marks });
458 });
459}
460
461/// In-progress IME composition, shown at the focused editor's caret, or
462/// with no editor focused delivered to the focused `onKey` sink as
463/// `{kind:"preedit", text, cursor, tag}`. Empty text clears it; the commit
464/// arrives via `kui_input_commit`. `cursor_start`/`cursor_end` are byte
465/// offsets into `text`, or `UINT32_MAX` for none.
466#[unsafe(no_mangle)]
467pub extern "C" fn kui_input_preedit(
468 ptr: *mut KuiCtx,
469 text: KuiStr,
470 cursor_start: u32,
471 cursor_end: u32,
472) {
473 guard((), || {
474 let text = kstr(text).into_owned();
475 let cursor =
476 (cursor_start != u32::MAX).then_some((cursor_start as usize, cursor_end as usize));
477 push_input(ptr, InputEvent::Preedit(text, cursor));
478 });
479}
480
481/// The current text of the editor `key` ([`kui_text_edit`] or
482/// [`kui_text_input`]), borrowed until the next `kui_edit_text` on this
483/// context or [`kui_ctx_free`]. False when `key` is no editor.
484#[unsafe(no_mangle)]
485pub extern "C" fn kui_edit_text(ptr: *mut KuiCtx, key: u64, out: *mut KuiStr) -> bool {
486 guard(false, || {
487 let (Some(c), Some(out)) = (unsafe { ctx(ptr) }, unsafe { out.as_mut() }) else {
488 return false;
489 };
490 let Some(text) = c.core().edit_text(Key(key)) else {
491 return false;
492 };
493 c.last_edit_text = Some(text);
494 let s = c.last_edit_text.as_ref().unwrap();
495 *out = KuiStr {
496 ptr: s.as_ptr(),
497 len: s.len(),
498 };
499 true
500 })
501}
502
503/// Replaces the text of the editor `key`, as if the user had typed it;
504/// the caret moves to the end. [`kui_edit_set_text_label`] does the same
505/// for an editor the host only knows by label.
506#[unsafe(no_mangle)]
507pub extern "C" fn kui_edit_set_text(ptr: *mut KuiCtx, key: u64, text: KuiStr) {
508 guard((), || {
509 if let Some(c) = unsafe { ctx(ptr) } {
510 let text = kstr(text).into_owned();
511 c.core().set_edit_text(Key(key), &text);
512 }
513 });
514}
515
516/// [`kui_edit_set_text`] by the label the view declares the editor with,
517/// for a host that has no key yet (an editor opened for the first time
518/// has fired no event). A label some frame declared is applied at once;
519/// one nothing has declared is held for the next frame, seeding a new
520/// editor over its `initial`. Held for that one frame only: if no editor
521/// is declared under it, the text is dropped with an
522/// `edit-text-without-editor` warning.
523#[unsafe(no_mangle)]
524pub extern "C" fn kui_edit_set_text_label(ptr: *mut KuiCtx, label: KuiStr, text: KuiStr) {
525 guard((), || {
526 if let Some(c) = unsafe { ctx(ptr) } {
527 let label = kstr(label).into_owned();
528 let text = kstr(text).into_owned();
529 c.core().set_edit_text_by_label(&label, &text);
530 }
531 });
532}