Skip to main content

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}