kui_ffi/lib.rs
1//! C API for kui: a cdylib plus `include/kui.h`, and [`CExtension`] for loading a C plugin into a Rust host.
2//!
3//! kui splits a UI into a model that lays out and paints into a display
4//! list (`kui-core`), a renderer (`kui-wgpu`) and a windowed runner
5//! (`kui-native`). This crate puts the model, and optionally the runner,
6//! behind a flat C ABI: every `pub extern "C" fn` here is a `kui_*`
7//! symbol declared in `include/kui.h`, and the `Kui*` structs are the
8//! `repr(C)` mirrors the header declares field for field.
9//!
10//! Two kinds of program use it:
11//!
12//! - A **C, C++ or other-language host** links the cdylib (`libkui_ffi`)
13//! and includes `kui.h`. It either hands a window to `kui_run` and
14//! builds its view in a callback, or owns its own event loop and
15//! renderer: it feeds input with `kui_input_*`, builds a frame between
16//! [`kui_frame_begin`] and [`kui_frame_finish`], polls
17//! [`kui_poll_event`] and draws [`kui_draw_data`].
18//! - A **Rust host** running `kui-native` loads a C shared library as a
19//! guest extension through [`CExtension`]: the plugin draws into a slot
20//! the host declares and gets its own events back.
21//!
22//! The C programs under `examples/c/` in the repository are working
23//! references for every call below.
24//!
25//! # Conventions
26//!
27//! - **Strings** cross as [`KuiStr`], a `(ptr, len)` pair of UTF-8 bytes
28//! that is never NUL-terminated (`KUI_STR("literal")` in C). Invalid
29//! UTF-8 is replaced. A string the library hands back is borrowed until
30//! the next call of the same function on that context.
31//! - **Values.** A [`KuiValue`] from a `kui_value_*` constructor is yours
32//! until you pass it to a function documented as consuming it (an
33//! `on_click` payload, the value given to [`kui_value_map_set`]); free
34//! anything else with [`kui_value_free`]. The payload on a polled event
35//! is borrowed until the next [`kui_poll_event`].
36//! - **Panics never cross.** Every entry point catches panics and returns
37//! its failure value instead (`false`, `0` or NULL). A NULL context is
38//! answered the same way.
39//! - **ABI handshake.** Call [`kui_abi_version`] first and compare it for
40//! equality with the header's `KUI_ABI_VERSION` (see
41//! [`KUI_ABI_VERSION`]). Structs the library writes into your memory
42//! lead with a `size` you set from `sizeof` (the `KUI_*_INIT`
43//! initializers do), and the library writes no further than that.
44//! - **Coordinates** are logical pixels; draw data comes back in physical
45//! pixels at the frame's `scale`.
46//!
47//! # A host's frame loop in C
48//!
49//! The shape without `kui_run`: the calls a host makes around a window and
50//! renderer of its own (the same calls work headless, which is how the C
51//! examples test themselves).
52//!
53//! ```c
54//! #include "kui.h"
55//!
56//! if (kui_abi_version() != KUI_ABI_VERSION) return 1;
57//! KuiCtx *ctx = kui_ctx_new();
58//! long long count = 0;
59//!
60//! for (;;) {
61//! /* Input from the windowing library, in logical pixels. */
62//! kui_input_cursor(ctx, mouse_x, mouse_y);
63//! if (clicked) { kui_input_mouse(ctx, true, 1); kui_input_mouse(ctx, false, 1); }
64//!
65//! /* What the UI emitted; a payload is borrowed until the next poll. */
66//! KuiEvent ev = KUI_EVENT_INIT;
67//! while (kui_poll_event(ctx, &ev)) {
68//! const KuiValue *kind = kui_value_get(ev.payload, KUI_STR("kind"));
69//! KuiStr s;
70//! if (kind && kui_value_as_str(kind, &s) && kui_str_eq(s, "inc")) count++;
71//! }
72//!
73//! /* Build the frame from scratch. */
74//! kui_set_time(ctx, now_seconds());
75//! kui_frame_begin(ctx, width, height, scale);
76//! KuiTheme t = KUI_THEME_INIT;
77//! kui_theme(ctx, &t);
78//! KuiSpec root = {.width = {KUI_GROW, 1}, .height = {KUI_GROW, 1},
79//! .main_align = KUI_CENTER, .cross_align = KUI_CENTER, .bg = t.bg};
80//! kui_root(ctx, &root);
81//! char buf[32];
82//! snprintf(buf, sizeof buf, "%lld", count);
83//! KuiTextStyle big = {.size = 56};
84//! kui_text(ctx, KUI_STR(buf), &big);
85//! KuiValue *inc = kui_value_map();
86//! kui_value_map_set(inc, KUI_STR("kind"), kui_value_str(KUI_STR("inc")));
87//! kui_button(ctx, KUI_STR("+1"), inc); /* consumes inc */
88//! kui_frame_finish(ctx);
89//!
90//! /* Draw it: one quad list, physical pixels, plus the glyph atlas to mirror. */
91//! KuiDrawData dd = KUI_DRAW_DATA_INIT;
92//! if (kui_draw_data(ctx, &dd)) {
93//! if (dd.atlas_dirty) upload_atlas(dd.atlas_pixels, dd.atlas_size);
94//! draw_quads(dd.quads, dd.quad_count, dd.clips);
95//! }
96//! if (!kui_animating(ctx)) wait_for_input();
97//! }
98//! kui_ctx_free(ctx);
99//! ```
100//!
101//! With the `runner` feature, `kui_run(title, view, on_event, user)` does
102//! all of this around a window of its own and calls `view` once per frame
103//! with a context to build into; `examples/c/apps/counter.c` is that
104//! program.
105//!
106//! # A C extension in a Rust app
107//!
108//! A plugin is a shared library exporting `kui_ext_abi` and `kui_ext_view`
109//! (and optionally `kui_ext_name`, `kui_ext_init`, `kui_ext_slots`,
110//! `kui_ext_on_event`, `kui_ext_free`); `examples/c/features/slots/panel.c`
111//! is one. The host loads it and declares where it draws:
112//!
113//! ```rust,no_run
114//! use kui_ffi::CExtension;
115//! use kui_native::{App, NodeSpec, Ui};
116//!
117//! struct Host;
118//!
119//! impl App for Host {
120//! fn view(&mut self, ui: &mut Ui<'_>) {
121//! ui.open(NodeSpec::row().fill());
122//! // The plugin fills this position: `todos` is the namespace the
123//! // host loaded it under, `panel` a slot the plugin lists.
124//! ui.slot("todos/panel");
125//! ui.close();
126//! }
127//! }
128//!
129//! fn main() -> Result<(), Box<dyn std::error::Error>> {
130//! // SAFETY: the plugin's code runs in this process. Loading it is
131//! // trusting it as much as linking it would be.
132//! let ext = unsafe { CExtension::open("target/debug/panel.so")? };
133//! kui_native::app("host").extension_as("todos", ext).run(Host)
134//! }
135//! ```
136//!
137//! # Where to look
138//!
139//! Everything is exported flat at the crate root. By job:
140//!
141//! - Context: [`kui_ctx_new`], [`kui_ctx_free`], [`KuiCtx`].
142//! - Building a frame: [`kui_frame_begin`], [`kui_root`], [`kui_open`],
143//! [`kui_open_keyed`], [`kui_text`], [`kui_close`], [`kui_frame_finish`];
144//! [`KuiSpec`] and [`KuiTextStyle`] are what a node is built from.
145//! - Input and events: [`kui_input_cursor`], [`kui_input_mouse`],
146//! [`kui_input_press`], [`kui_input_text`], [`kui_poll_event`],
147//! [`KuiEvent`].
148//! - Values: [`kui_value_map`], [`kui_value_str`], [`kui_value_get`],
149//! [`kui_value_as_str`], [`KuiValue`].
150//! - Drawing: [`kui_draw_data`], [`KuiDrawData`], [`KuiQuad`],
151//! [`kui_set_subpixel_text`].
152//! - Widgets: [`kui_button`], [`kui_text_input`], [`kui_checkbox`],
153//! [`kui_slider`], [`kui_select`].
154//! - Theme and metrics: [`kui_env_set_system`], [`kui_theme`],
155//! [`KuiTheme`], [`kui_metrics`].
156//! - Windows: [`kui_window_declare`], [`kui_take_window_command`],
157//! [`KuiWindowCommand`].
158//! - Accessibility: [`kui_access_tree`], [`KuiAccessNode`],
159//! [`kui_input_access`].
160//! - Extensions: [`CExtension`], [`kui_ctx_add_extension`], [`kui_slot`],
161//! [`kui_reply`].
162//! - Diagnostics: [`kui_set_diagnostics`], [`kui_take_warnings`],
163//! [`kui_set_devtools`].
164//! - The windowed runner (feature `runner`): `kui_run`, `kui_run_with`,
165//! [`KuiRunConfig`].
166//!
167//! # Features
168//!
169//! - `runner` (on by default): `kui_run` and `kui_run_with`, the windowed
170//! runner from `kui-native`. A host that owns its own window and
171//! renderer builds with `--no-default-features` and ships less than
172//! half the library.
173//!
174//! The book: <https://kui-book.qxuken.dev>. Repository:
175//! <https://github.com/qxuken/kui> (design records live under `docs/adr`
176//! there).
177
178// Safe extern fns taking raw pointers is the point of this layer: every
179// entry point null-checks and catches panics instead of being `unsafe`.
180#![allow(clippy::not_unsafe_ptr_arg_deref)]
181
182// The other direction: a C shared library as a guest inside a host that
183// already owns the frame.
184mod ext;
185pub use ext::CExtension;
186
187#[macro_use]
188mod abi;
189mod access;
190mod convert;
191mod dialogs;
192mod focus;
193mod frame;
194mod input;
195mod menu;
196mod resources;
197#[cfg(feature = "runner")]
198mod run;
199mod scrolling;
200mod select;
201mod size;
202mod slots;
203mod types;
204mod value;
205mod widgets;
206mod windows;
207
208pub use abi::*;
209pub use access::*;
210pub use dialogs::*;
211pub use focus::*;
212pub use frame::*;
213pub use input::*;
214pub use menu::*;
215pub use resources::*;
216#[cfg(feature = "runner")]
217pub use run::*;
218pub use scrolling::*;
219pub use select::*;
220pub use size::*;
221pub use slots::*;
222pub use types::*;
223pub use value::*;
224pub use widgets::*;
225pub use windows::*;
226
227use convert::*;
228
229use std::collections::VecDeque;
230use std::ffi::c_void;
231use std::panic::{AssertUnwindSafe, catch_unwind};
232use std::rc::Rc;
233
234use kui_core::{
235 Align, Appearance, Assistive, AudioDevice, AudioEnv, Color, Core, DismissReason, Edges,
236 EditKey, EditOptions, Enter, FloatConfig, InputEvent, Key, Keyframe, Locale, Mods, MotionPref,
237 MouseButton, NodeSpec, OptionAsAlt, Rect, Size, Sizing, Span, SystemEnv, TextStyle, UiEvent,
238 Value, Vec2, WindowButton, WindowCommand, WindowConfig, WindowId, WindowKind,
239};
240
241// ---------------------------------------------------------------------------
242// Context lifecycle
243
244/// Creates a standalone context: a core of its own plus an event queue.
245///
246/// Free it with [`kui_ctx_free`]. Diagnostics start off; turn them on with
247/// [`kui_set_diagnostics`] in a development build. Returns NULL only if
248/// the core cannot be created.
249#[unsafe(no_mangle)]
250pub extern "C" fn kui_ctx_new() -> *mut KuiCtx {
251 guard(std::ptr::null_mut(), || {
252 let mut owned = Box::new(Core::new());
253 // A host driving its own frames opts into diagnostics explicitly,
254 // like everything else it drains; kui_run follows the runner's
255 // debug-build default.
256 owned.set_diagnostics(false);
257 let core: *mut Core = &mut *owned;
258 Box::into_raw(Box::new(KuiCtx {
259 core,
260 _owned: Some(owned),
261 events: Vec::new(),
262 last_payload: None,
263 last_edit_text: None,
264 fragment_source: String::new(),
265 fragment_draws: Vec::new(),
266 texture_draws: Vec::new(),
267 draws_current: false,
268 image_pixels: None,
269 last_warnings: Vec::new(),
270 pending_warnings: Vec::new(),
271 last_access: Default::default(),
272 last_runs: Vec::new(),
273 last_announcements: Vec::new(),
274 window_commands: VecDeque::new(),
275 menu_actions: VecDeque::new(),
276 menu_text: String::new(),
277 row_text: String::new(),
278 menu_accel: String::new(),
279 copy_text: String::new(),
280 menu_html: String::new(),
281 devtools_key: String::new(),
282 file_request: None,
283 file_filter_text: String::new(),
284 devtools_tab: String::new(),
285 selection_text: String::new(),
286 selection_html: String::new(),
287 font_families: Vec::new(),
288 system_fonts: Vec::new(),
289 nodes: None,
290 last_window_name: None,
291 slot_name: None,
292 slot_namespace: None,
293 slot_params: None,
294 extensions: Default::default(),
295 last_ext_error: String::new(),
296 host_ui: std::ptr::null_mut(),
297 }))
298 })
299}
300
301/// Frees a context from [`kui_ctx_new`], its queued events, every string
302/// it lent out and every extension it loaded. NULL is a no-op.
303#[unsafe(no_mangle)]
304pub extern "C" fn kui_ctx_free(ptr: *mut KuiCtx) {
305 if !ptr.is_null() {
306 drop(unsafe { Box::from_raw(ptr) });
307 }
308}
309
310// ---------------------------------------------------------------------------
311// Host environment
312
313/// Window facts for views to read: the display's refresh rate
314/// (`refresh_hz <= 0` is unknown) and whether the window has keyboard
315/// focus.
316///
317/// Sticky across frames; set it on change or every frame. A window that
318/// lost focus releases every key a sink was holding, and those releases
319/// are polled like any event.
320#[unsafe(no_mangle)]
321pub extern "C" fn kui_env_set(ptr: *mut KuiCtx, refresh_hz: f32, focused: bool) {
322 guard((), || {
323 if let Some(c) = unsafe { ctx(ptr) } {
324 c.core().env.refresh_hz = (refresh_hz > 0.0).then_some(refresh_hz);
325 c.core().set_focused(focused);
326 }
327 });
328}
329
330/// The OS's settings, for views and the theme to read: `appearance` is a
331/// `KUI_APPEARANCE_*`, `motion` a `KUI_MOTION_*` (0 is unknown for both),
332/// `accent` the accent colour as `0xRRGGBBAA` (0 is unknown) and `locale`
333/// a BCP-47 tag (empty is unknown; one over 31 bytes or not ASCII reads
334/// back as unknown).
335///
336/// Push them at startup and on the OS's change notification; they stick
337/// across frames. The palette [`kui_theme`] reports is re-derived at once.
338/// An out-of-range code is ignored. On a context handed to `kui_run_with`
339/// these become the window's pin over the OS's own reading; a zero field
340/// keeps following the OS.
341#[unsafe(no_mangle)]
342pub extern "C" fn kui_env_set_system(
343 ptr: *mut KuiCtx,
344 appearance: u32,
345 accent: u32,
346 motion: u32,
347 locale: KuiStr,
348) {
349 guard((), || {
350 if let Some(c) = unsafe { ctx(ptr) } {
351 // An out-of-range code is ignored rather than folded onto a
352 // real setting: a host built against a newer header says
353 // something this build has no name for.
354 let sys = SystemEnv {
355 appearance: Appearance::from_code(appearance).unwrap_or_default(),
356 motion: MotionPref::from_code(motion).unwrap_or_default(),
357 accent: (accent != 0).then(|| Color::hex(accent)),
358 locale: opt_str(locale).and_then(|tag| Locale::new(&tag)),
359 // Not this setter's: the four arguments are the settings,
360 // and the fifth reading has its own door below, so a host
361 // re-pushing the settings on an OS notification does not
362 // forget that a screen reader is attached.
363 assistive: c.core().env.system.assistive,
364 };
365 // `set_system` re-resolves the palette from what was just
366 // written, so a host that pushes the appearance and reads
367 // `kui_theme` back before its next frame sees the answer.
368 c.core().set_system(sys);
369 }
370 });
371}
372
373/// Whether assistive technology is listening, for views to read: a
374/// `KUI_ASSISTIVE_*` (0 is unknown, which is what a host with no
375/// accessibility bridge reports by never calling this).
376///
377/// A host bridging the platform's accessibility API pushes `LISTENING`
378/// when a client first asks for the tree and `NONE` if the platform says
379/// the client left. An out-of-range code is ignored.
380#[unsafe(no_mangle)]
381pub extern "C" fn kui_env_set_assistive(ptr: *mut KuiCtx, assistive: u32) {
382 guard((), || {
383 if let Some(c) = unsafe { ctx(ptr) } {
384 c.core().env.system.assistive = Assistive::from_code(assistive).unwrap_or_default();
385 }
386 });
387}
388
389/// What the host's audio output is doing, for views to read: `device` a
390/// `KUI_AUDIO_DEVICE_*` (0 is closed, which a host with no device reports
391/// by never calling this) and `live` the number of playbacks started or
392/// waiting on the open. A fact, not a command: nothing here closes the
393/// device. An out-of-range code is ignored.
394#[unsafe(no_mangle)]
395pub extern "C" fn kui_env_set_audio(ptr: *mut KuiCtx, device: u32, live: u32) {
396 guard((), || {
397 if let Some(c) = unsafe { ctx(ptr) } {
398 c.core().env.audio = AudioEnv {
399 device: AudioDevice::from_code(device).unwrap_or_default(),
400 live,
401 };
402 }
403 });
404}
405
406/// This window's palette as of the current or last frame: one
407/// `0xRRGGBBAA` per role, derived from what [`kui_env_set_system`] reported
408/// unless the host pinned something with [`kui_theme_set_accent`] or
409/// [`kui_theme_set`].
410///
411/// The stock widgets, the focus ring, the scrollbars and any text with a
412/// zero `color` already follow it. Read it for paint of your own:
413/// `KuiTheme t = KUI_THEME_INIT; kui_theme(ctx, &t);` then
414/// `spec.bg = t.surface`.
415///
416/// False for a bad context, a NULL `out`, or a `size` below the first
417/// ABI's layout.
418#[unsafe(no_mangle)]
419pub extern "C" fn kui_theme(ptr: *mut KuiCtx, out: *mut KuiTheme) -> bool {
420 guard(false, || {
421 let Some(c) = (unsafe { ctx(ptr) }) else {
422 return false;
423 };
424 let t = *c.core().theme();
425 write_out(
426 out,
427 KuiTheme {
428 appearance: t.appearance.code(),
429 disabled_opacity: t.disabled_opacity,
430 bg: t.bg.to_hex(),
431 surface: t.surface.to_hex(),
432 raised: t.raised.to_hex(),
433 sunken: t.sunken.to_hex(),
434 border: t.border.to_hex(),
435 border_strong: t.border_strong.to_hex(),
436 fg: t.fg.to_hex(),
437 muted: t.muted.to_hex(),
438 faint: t.faint.to_hex(),
439 accent: t.accent.to_hex(),
440 accent_hover: t.accent_hover.to_hex(),
441 accent_pressed: t.accent_pressed.to_hex(),
442 on_accent: t.on_accent.to_hex(),
443 accent_soft: t.accent_soft.to_hex(),
444 selection: t.selection.to_hex(),
445 focus_ring: t.focus_ring.to_hex(),
446 hover: t.hover.to_hex(),
447 pressed: t.pressed.to_hex(),
448 success: t.success.to_hex(),
449 warning: t.warning.to_hex(),
450 danger: t.danger.to_hex(),
451 scrollbar: t.scrollbar.to_hex(),
452 scrollbar_active: t.scrollbar_active.to_hex(),
453 ..Default::default()
454 },
455 )
456 })
457}
458
459/// Keeps following the OS's light or dark base but paints `accent`
460/// (`0xRRGGBBAA`) instead of the OS's accent; zero goes back to the OS's.
461///
462/// Everything derived from the accent moves with it: a button's hover and
463/// pressed shades, the label on it, the selection tint and the focus ring.
464#[unsafe(no_mangle)]
465pub extern "C" fn kui_theme_set_accent(ptr: *mut KuiCtx, accent: u32) {
466 guard((), || {
467 if let Some(c) = unsafe { ctx(ptr) } {
468 match accent {
469 0 => c.core().derive_theme(),
470 hex => c.core().set_accent(Color::hex(hex)),
471 }
472 }
473 })
474}
475
476/// Pins the whole palette to exactly these colours, following neither the
477/// OS's appearance nor its accent; NULL goes back to deriving both.
478///
479/// Every field is read (`size` is ignored), so start from [`kui_theme`]
480/// and change the roles you mean to change: a zeroed role is transparent,
481/// not "leave it".
482#[unsafe(no_mangle)]
483pub extern "C" fn kui_theme_set(ptr: *mut KuiCtx, theme: *const KuiTheme) {
484 guard((), || {
485 if let Some(c) = unsafe { ctx(ptr) } {
486 match unsafe { theme.as_ref() } {
487 None => c.core().derive_theme(),
488 Some(t) => c.core().set_theme(theme_of(t)),
489 }
490 }
491 })
492}
493
494/// Declares the named colours and lengths of the calling origin: the
495/// host's outside a plugin's view, the plugin's own inside `kui_ext_view`.
496///
497/// Replaces the table whole, so an app whose lengths follow a viewport
498/// tier declares again on a resize. A name a theme or metrics role owns is
499/// dropped with a `reserved-token` warning. Either array may be NULL with
500/// a zero count. A C spec carries plain numbers, so read a token back with
501/// [`kui_token_color`] or [`kui_token_length`] and write the value; the
502/// table gives the name to the devtools inspector, to guests reading the
503/// host's vocabulary, and to a Lua panel referencing `"$name"`.
504#[unsafe(no_mangle)]
505pub extern "C" fn kui_tokens_set(
506 ptr: *mut KuiCtx,
507 colors: *const KuiColorToken,
508 color_count: usize,
509 lengths: *const KuiLengthToken,
510 length_count: usize,
511) {
512 guard((), || {
513 let Some(c) = (unsafe { ctx(ptr) }) else {
514 return;
515 };
516 let mut t = kui_core::Tokens::new();
517 if !colors.is_null() {
518 for tok in unsafe { std::slice::from_raw_parts(colors, color_count) } {
519 t = t.color_themed(
520 kstr(tok.name).into_owned(),
521 Color::hex(tok.light),
522 Color::hex(tok.dark),
523 );
524 }
525 }
526 if !lengths.is_null() {
527 for tok in unsafe { std::slice::from_raw_parts(lengths, length_count) } {
528 t = t.length(kstr(tok.name).into_owned(), tok.value);
529 }
530 }
531 c.core().set_tokens(t);
532 })
533}
534
535/// Adds derived colour tokens to the calling origin's table, after
536/// [`kui_tokens_set`]: each a name, the colour token or theme role it
537/// derives from, and a chain of ops applied in order.
538///
539/// The ops: `KUI_OP_LIFT` and `KUI_OP_DARKEN` move toward white or black
540/// by `t`, `KUI_OP_RAISE` toward the front of the base in effect,
541/// `KUI_OP_ALPHA` sets the alpha, `KUI_OP_MIX` mixes toward the token
542/// `other` names, and `KUI_OP_READABLE` moves toward black or white until
543/// the contrast ratio `t` against `other` is met. A source that is neither
544/// a token declared before it nor a role drops that token with an
545/// `unknown-token` warning. Returns false, adding nothing, for a malformed
546/// op: one past `KUI_OP_READABLE`, or `other` given to a verb that takes
547/// none or missing from one that does.
548#[unsafe(no_mangle)]
549pub extern "C" fn kui_tokens_derive(
550 ptr: *mut KuiCtx,
551 derived: *const KuiDerivedToken,
552 count: usize,
553) -> bool {
554 guard(false, || {
555 let Some(c) = (unsafe { ctx(ptr) }) else {
556 return false;
557 };
558 if derived.is_null() || count == 0 {
559 return true;
560 }
561 let mut t = c.core().tokens().cloned().unwrap_or_default();
562 for d in unsafe { std::slice::from_raw_parts(derived, count) } {
563 let ops = if d.ops.is_null() {
564 &[][..]
565 } else {
566 unsafe { std::slice::from_raw_parts(d.ops, d.op_count) }
567 };
568 let mut chain = Vec::with_capacity(ops.len());
569 for op in ops {
570 let Some(verb) = kui_core::ColorOp::VERBS.get(op.op as usize) else {
571 return false;
572 };
573 let other = kstr(op.other);
574 let other = (!other.is_empty()).then_some(&*other);
575 let Some(step) = kui_core::ColorOp::parse(verb, other, op.t) else {
576 return false;
577 };
578 chain.push(step);
579 }
580 t = t.derive(kstr(d.name).into_owned(), &kstr(d.from), chain);
581 }
582 c.core().set_tokens(t);
583 true
584 })
585}
586
587/// A colour token by name, resolved for this frame's appearance, as
588/// `0xRRGGBBAA` through `out`. The calling origin's table is tried first,
589/// then the host's; a theme role's name (`surface`) answers with the role.
590/// False for a name nothing declared or one that is a length, which also
591/// raises `unknown-token` once per name.
592#[unsafe(no_mangle)]
593pub extern "C" fn kui_token_color(ptr: *mut KuiCtx, name: KuiStr, out: *mut u32) -> bool {
594 guard(false, || {
595 let Some(c) = (unsafe { ctx(ptr) }) else {
596 return false;
597 };
598 let Some(out) = (unsafe { out.as_mut() }) else {
599 return false;
600 };
601 let name = kstr(name);
602 match c.core().token_lookup().color(&name) {
603 Ok(col) => {
604 *out = col.to_hex();
605 true
606 }
607 Err(e) => {
608 c.core().warn_unknown_token(&e);
609 false
610 }
611 }
612 })
613}
614
615/// A length token by name, in logical px; a metrics role's name
616/// (`radius`) answers with the metric. False and `unknown-token` as for
617/// [`kui_token_color`].
618#[unsafe(no_mangle)]
619pub extern "C" fn kui_token_length(ptr: *mut KuiCtx, name: KuiStr, out: *mut f32) -> bool {
620 guard(false, || {
621 let Some(c) = (unsafe { ctx(ptr) }) else {
622 return false;
623 };
624 let Some(out) = (unsafe { out.as_mut() }) else {
625 return false;
626 };
627 let name = kstr(name);
628 match c.core().token_lookup().length(&name) {
629 Ok(v) => {
630 *out = v;
631 true
632 }
633 Err(e) => {
634 c.core().warn_unknown_token(&e);
635 false
636 }
637 }
638 })
639}
640
641/// The sizes the stock widgets are built from, in logical px.
642/// `KuiMetrics m = KUI_METRICS_INIT; kui_metrics(ctx, &m);` then
643/// `spec.radius = m.radius` makes a control of your own agree with the
644/// stock ones. False for a bad context, a NULL `out`, or a `size` below
645/// the first ABI's layout.
646#[unsafe(no_mangle)]
647pub extern "C" fn kui_metrics(ptr: *mut KuiCtx, out: *mut KuiMetrics) -> bool {
648 guard(false, || {
649 let Some(c) = (unsafe { ctx(ptr) }) else {
650 return false;
651 };
652 let m = KuiMetrics::of(c.core().metrics());
653 write_out(out, m)
654 })
655}
656
657/// Makes these the metrics every stock widget from the next node on is
658/// built from; NULL restores the stock set. Start from [`kui_metrics`] and
659/// change the fields you mean to change: a zeroed metric is zero, not
660/// "leave it". Nothing in the OS is followed; density is the host's call.
661#[unsafe(no_mangle)]
662pub extern "C" fn kui_metrics_set(ptr: *mut KuiCtx, metrics: *const KuiMetrics) {
663 guard((), || {
664 if let Some(c) = unsafe { ctx(ptr) } {
665 match unsafe { metrics.as_ref() } {
666 None => c.core().set_metrics(kui_core::Metrics::default()),
667 Some(m) => c.core().set_metrics(m.to_core()),
668 }
669 }
670 })
671}
672
673/// A `KuiTheme` the host filled, as a core [`kui_core::Theme`]. An
674/// appearance code past the end is `unknown`, the way every other code is.
675fn theme_of(t: &KuiTheme) -> kui_core::Theme {
676 kui_core::Theme {
677 appearance: Appearance::from_code(t.appearance).unwrap_or_default(),
678 disabled_opacity: t.disabled_opacity,
679 bg: Color::hex(t.bg),
680 surface: Color::hex(t.surface),
681 raised: Color::hex(t.raised),
682 sunken: Color::hex(t.sunken),
683 border: Color::hex(t.border),
684 border_strong: Color::hex(t.border_strong),
685 fg: Color::hex(t.fg),
686 muted: Color::hex(t.muted),
687 faint: Color::hex(t.faint),
688 accent: Color::hex(t.accent),
689 accent_hover: Color::hex(t.accent_hover),
690 accent_pressed: Color::hex(t.accent_pressed),
691 on_accent: Color::hex(t.on_accent),
692 accent_soft: Color::hex(t.accent_soft),
693 selection: Color::hex(t.selection),
694 focus_ring: Color::hex(t.focus_ring),
695 hover: Color::hex(t.hover),
696 pressed: Color::hex(t.pressed),
697 success: Color::hex(t.success),
698 warning: Color::hex(t.warning),
699 danger: Color::hex(t.danger),
700 scrollbar: Color::hex(t.scrollbar),
701 scrollbar_active: Color::hex(t.scrollbar_active),
702 }
703}
704
705/// The frame clock for transitions, in monotonic seconds from any origin.
706/// Set it before each [`kui_frame_begin`]; a host that never does sees
707/// transitions snap to their targets.
708#[unsafe(no_mangle)]
709pub extern "C" fn kui_set_time(ptr: *mut KuiCtx, now_secs: f64) {
710 guard((), || {
711 if let Some(c) = unsafe { ctx(ptr) } {
712 c.core().set_time(now_secs);
713 }
714 });
715}
716
717/// True when the last frame left a transition mid-flight: draw another
718/// frame without waiting for input.
719#[unsafe(no_mangle)]
720pub extern "C" fn kui_animating(ptr: *mut KuiCtx) -> bool {
721 guard(false, || {
722 unsafe { ctx(ptr) }.is_some_and(|c| c.core().animating())
723 })
724}
725
726/// Why the last frame wants another, as `KUI_OWED_*` bits:
727/// [`kui_animating`] taken apart by kind. A host draws another frame for
728/// any of them; a test masks `KUI_OWED_CYCLE` off to wait for transitions
729/// to settle under a keyframe cycle that never will.
730#[unsafe(no_mangle)]
731pub extern "C" fn kui_owed(ptr: *mut KuiCtx) -> u32 {
732 guard(0, || {
733 unsafe { ctx(ptr) }.map_or(0, |c| {
734 let o = c.core().owed();
735 (o.transition as u32 * KUI_OWED_TRANSITION)
736 | (o.cycle as u32 * KUI_OWED_CYCLE)
737 | (o.depart as u32 * KUI_OWED_DEPART)
738 | (o.requested as u32 * KUI_OWED_REQUESTED)
739 | (o.autoscroll as u32 * KUI_OWED_AUTOSCROLL)
740 | (o.scroll as u32 * KUI_OWED_SCROLL)
741 })
742 })
743}
744
745/// Turns the trace of why frames run on or off: the digest
746/// [`kui_frame_unchanged`] compares. [`kui_frame_cause`] is kept either
747/// way.
748#[unsafe(no_mangle)]
749pub extern "C" fn kui_set_frame_trace(ptr: *mut KuiCtx, on: bool) {
750 guard((), || {
751 if let Some(c) = unsafe { ctx(ptr) } {
752 c.core().set_frame_trace(on);
753 }
754 })
755}
756
757/// The `KUI_FRAME_CAUSE_*` bits of the frame being built, or between
758/// frames the last one's.
759#[unsafe(no_mangle)]
760pub extern "C" fn kui_frame_cause(ptr: *mut KuiCtx) -> u32 {
761 guard(0, || {
762 unsafe { ctx(ptr) }.map_or(0, |c| c.core().frame_cause().bits())
763 })
764}
765
766/// Adds `KUI_FRAME_CAUSE_*` bits to the next frame's reasons: the host's
767/// own (a wake, a resize, a blink) beside the input the `kui_input_*`
768/// calls record.
769#[unsafe(no_mangle)]
770pub extern "C" fn kui_note_frame_cause(ptr: *mut KuiCtx, cause: u32) {
771 guard((), || {
772 if let Some(c) = unsafe { ctx(ptr) } {
773 c.core()
774 .note_frame_cause(kui_core::FrameCause::from_bits(cause));
775 }
776 })
777}
778
779/// 1 when the last finished frame drew exactly what the one before drew,
780/// 0 when not, -1 when untraced ([`kui_set_frame_trace`]) or on the first
781/// traced frame.
782#[unsafe(no_mangle)]
783pub extern "C" fn kui_frame_unchanged(ptr: *mut KuiCtx) -> i32 {
784 guard(-1, || {
785 unsafe { ctx(ptr) }.map_or(-1, |c| match c.core().frame_unchanged() {
786 Some(same) => same as i32,
787 None => -1,
788 })
789 })
790}
791
792/// Rasterizes outline glyphs as LCD subpixel coverage
793/// (`KUI_QUAD_GLYPH_SUBPIXEL` quads, the atlas's RGB being per-channel
794/// coverage) instead of alpha masks. Only for a renderer that blends per
795/// channel (dual-source blending); flipping it re-rasterizes every glyph.
796#[unsafe(no_mangle)]
797pub extern "C" fn kui_set_subpixel_text(ptr: *mut KuiCtx, on: bool) {
798 guard((), || {
799 if let Some(c) = unsafe { ctx(ptr) } {
800 c.core().set_subpixel_text(on);
801 }
802 });
803}
804
805/// Byte budget for the shaped-text cache: past it the least recently drawn
806/// entries are evicted at the start of the next frame, never what the last
807/// frame drew. Default 64 MB (`DEFAULT_TEXT_CACHE_BYTES`).
808#[unsafe(no_mangle)]
809pub extern "C" fn kui_set_text_cache_budget(ptr: *mut KuiCtx, bytes: usize) {
810 guard((), || {
811 if let Some(c) = unsafe { ctx(ptr) } {
812 c.core().set_text_cache_budget(bytes);
813 }
814 });
815}
816
817/// What the shaped-text cache holds, in the estimated bytes the budget is
818/// charged against.
819#[unsafe(no_mangle)]
820pub extern "C" fn kui_text_cache_bytes(ptr: *mut KuiCtx) -> usize {
821 guard(0, || {
822 unsafe { ctx(ptr) }.map_or(0, |c| c.core().text_cache_bytes())
823 })
824}
825
826/// Drains the warnings the core raised since the last call (silent
827/// misconfigurations it noticed while finishing frames, each once) into
828/// `out`, up to `cap` (the rest wait for the next call), and returns the
829/// count. The strings stay valid
830/// until the next call on this context. A standalone context starts with
831/// the checks off ([`kui_set_diagnostics`] turns them on); `kui_run`
832/// prints them to stderr itself in debug builds.
833#[unsafe(no_mangle)]
834pub extern "C" fn kui_take_warnings(ptr: *mut KuiCtx, out: *mut KuiWarning, cap: usize) -> usize {
835 guard(0, || {
836 let Some(c) = (unsafe { ctx(ptr) }) else {
837 return 0;
838 };
839 if out.is_null() || cap == 0 {
840 return 0;
841 }
842 // Drained from the core into the context's queue, and handed out
843 // from there `cap` at a time: the rest wait, as kui.h says, where
844 // they were dropped.
845 let raised = c.core().take_warnings();
846 c.pending_warnings.extend(raised);
847 let n = c.pending_warnings.len().min(cap);
848 c.last_warnings = c.pending_warnings.drain(..n).collect();
849 for (i, w) in c.last_warnings.iter().enumerate() {
850 let s = |s: &str| KuiStr {
851 ptr: s.as_ptr(),
852 len: s.len(),
853 };
854 unsafe {
855 out.add(i).write(KuiWarning {
856 code: s(w.code),
857 key: w.key.0,
858 message: s(&w.message),
859 })
860 };
861 }
862 n
863 })
864}
865
866/// Turns the diagnostic checks behind [`kui_take_warnings`] on or off. Off
867/// by default for a standalone context; a development build opts in.
868#[unsafe(no_mangle)]
869pub extern "C" fn kui_set_diagnostics(ptr: *mut KuiCtx, on: bool) {
870 guard((), || {
871 if let Some(c) = unsafe { ctx(ptr) } {
872 c.core().set_diagnostics(on);
873 }
874 });
875}
876
877/// Turns the core's devtools panel on or off: the event stream, the
878/// runtime's facts and the tree, drawn by the core beside the host's tree
879/// (where [`kui_set_devtools_dock`] says). Its controls and its
880/// `Ctrl+Shift+<letter>` chords are handled inside the `kui_input_*`
881/// calls, so nothing of it reaches the host's events. `KUI_DEVTOOLS=1` in
882/// the environment makes the same call for a window `kui_run` opens; a
883/// headless context never reads it.
884#[unsafe(no_mangle)]
885pub extern "C" fn kui_set_devtools(ptr: *mut KuiCtx, on: bool) {
886 guard((), || {
887 if let Some(c) = unsafe { ctx(ptr) } {
888 c.core().set_devtools(on);
889 }
890 });
891}
892
893/// Where the devtools panel sits: `"left"`, `"right"`, `"bottom"`,
894/// `"window"` (one of its own, named `kui-devtools`, opened through an
895/// ordinary `KUI_CMD_OPEN` that the host builds nothing into) or `"off"`
896/// (hidden, chords still live); `"side"` means the right. Returns false
897/// for any other word.
898#[unsafe(no_mangle)]
899pub extern "C" fn kui_set_devtools_dock(ptr: *mut KuiCtx, dock: KuiStr) -> bool {
900 guard(false, || {
901 let Some(c) = (unsafe { ctx(ptr) }) else {
902 return false;
903 };
904 let Some(dock) = kui_core::DevtoolsDock::parse(&kstr(dock)) else {
905 return false;
906 };
907 c.core().set_devtools_dock(dock);
908 true
909 })
910}
911
912/// Whether the devtools panel is on.
913#[unsafe(no_mangle)]
914pub extern "C" fn kui_devtools(ptr: *mut KuiCtx) -> bool {
915 guard(false, || {
916 unsafe { ctx(ptr) }.is_some_and(|c| c.core().devtools())
917 })
918}
919
920/// Where the devtools panel sits, as a word [`kui_set_devtools_dock`]
921/// takes (`"right"` for the side); the string is static. False on a bad
922/// context.
923#[unsafe(no_mangle)]
924pub extern "C" fn kui_devtools_dock(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
925 guard(false, || {
926 let Some(c) = (unsafe { ctx(ptr) }) else {
927 return false;
928 };
929 let name = c.core().devtools_dock().name();
930 if let Some(out) = unsafe { out.as_mut() } {
931 *out = KuiStr {
932 ptr: name.as_ptr(),
933 len: name.len(),
934 };
935 }
936 true
937 })
938}
939
940/// Where the last frame laid the host out in its window, in logical px:
941/// the whole window with the devtools panel off or in its own window, the
942/// pane beside the dock otherwise, and zeros before the first frame.
943/// Scaled by the frame's scale it separates the host's quads from the
944/// dock's in [`kui_draw_data`], except the root's background, which also
945/// fills the window beneath the pane. False on a bad context, a NULL
946/// `out` or a short `size`.
947#[unsafe(no_mangle)]
948pub extern "C" fn kui_host_rect(ptr: *mut KuiCtx, out: *mut KuiLayoutRect) -> bool {
949 guard(false, || {
950 let Some(c) = (unsafe { ctx(ptr) }) else {
951 return false;
952 };
953 let r = c.core().host_rect();
954 write_out(
955 out,
956 KuiLayoutRect {
957 x: r.x,
958 y: r.y,
959 w: r.w,
960 h: r.h,
961 ..Default::default()
962 },
963 )
964 })
965}
966
967/// Seeds the devtools panel's theme override, which its `T` and `A`
968/// chords cycle from: `base` is `"light"`, `"dark"` or empty for the
969/// app's own; `accent` a `0xRRGGBBAA` colour, or 0 for none. False for
970/// any other base word.
971#[unsafe(no_mangle)]
972pub extern "C" fn kui_set_devtools_theme(ptr: *mut KuiCtx, base: KuiStr, accent: u32) -> bool {
973 guard(false, || {
974 let Some(c) = (unsafe { ctx(ptr) }) else {
975 return false;
976 };
977 let base = match kstr(base).as_ref() {
978 "" => None,
979 "light" => Some(kui_core::Appearance::Light),
980 "dark" => Some(kui_core::Appearance::Dark),
981 _ => return false,
982 };
983 let accent = (accent != 0).then(|| color_of(accent));
984 c.core().set_devtools_theme(base, accent);
985 true
986 })
987}
988
989/// Respells the chord that moves the keyboard into the devtools panel and
990/// back out (and brings a hidden panel back) from its default
991/// `"ctrl+shift+i"`: `"f12"`, `"mod+shift+d"` (`mod` is Command on macOS,
992/// Control elsewhere), any spelling a [`KuiMenuItem`]'s `accel` takes. The
993/// panel's other chords stay `Ctrl+Shift+<letter>`. False for a spelling
994/// kui cannot parse, which leaves the chord as it was.
995#[unsafe(no_mangle)]
996pub extern "C" fn kui_set_devtools_key(ptr: *mut KuiCtx, key: KuiStr) -> bool {
997 guard(false, || {
998 let Some(c) = (unsafe { ctx(ptr) }) else {
999 return false;
1000 };
1001 let Some(accel) = kui_core::Accel::parse(&kstr(key)) else {
1002 return false;
1003 };
1004 c.core().set_devtools_key(accel);
1005 true
1006 })
1007}
1008
1009/// The chord [`kui_set_devtools_key`] set, or the default, in its portable
1010/// spelling (`"ctrl+shift+i"`, `"f12"`, `"super+alt+d"`); borrowed until
1011/// the next call. False on a bad context.
1012#[unsafe(no_mangle)]
1013pub extern "C" fn kui_devtools_key(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
1014 guard(false, || {
1015 let Some(c) = (unsafe { ctx(ptr) }) else {
1016 return false;
1017 };
1018 c.devtools_key = c.core().devtools_key().spelling();
1019 if let Some(out) = unsafe { out.as_mut() } {
1020 *out = KuiStr {
1021 ptr: c.devtools_key.as_ptr(),
1022 len: c.devtools_key.len(),
1023 };
1024 }
1025 true
1026 })
1027}
1028
1029/// Declares a devtools tab an extension fills: `name` is the tab's
1030/// identity, `label` what the strip shows, `slot` the full
1031/// `namespace/slot` the extension names. While the tab is on show the
1032/// panel declares that slot in the tab's body; otherwise the extension is
1033/// not asked, and no `unknown-slot` is raised. Call it every frame, panel
1034/// on or off. False for a name already declared this frame
1035/// (`duplicate-tab`) or outside a frame.
1036#[unsafe(no_mangle)]
1037pub extern "C" fn kui_devtools_tab(
1038 ptr: *mut KuiCtx,
1039 name: KuiStr,
1040 label: KuiStr,
1041 slot: KuiStr,
1042) -> bool {
1043 guard(false, || {
1044 let Some(c) = (unsafe { ctx(ptr) }) else {
1045 return false;
1046 };
1047 c.core()
1048 .devtools_tab_declare(&kstr(name), &kstr(label), Some(&kstr(slot)))
1049 })
1050}
1051
1052/// Declares a devtools tab the host draws itself, and opens its content
1053/// node only while the tab is on show: true means the node is open, so
1054/// build inside it and [`kui_close`]; false means the tab was declared and
1055/// nothing opened, so skip the body and do not close. What the host builds
1056/// keeps its own keys and events, painted as a layer over the panel's tab
1057/// body and clipped to it:
1058/// `if (kui_devtools_tab_open(ctx, KUI_STR("syntax"), KUI_STR("Tree-sitter"))) { ...; kui_close(ctx); }`.
1059#[unsafe(no_mangle)]
1060pub extern "C" fn kui_devtools_tab_open(ptr: *mut KuiCtx, name: KuiStr, label: KuiStr) -> bool {
1061 guard(false, || {
1062 let Some(c) = (unsafe { ctx(ptr) }) else {
1063 return false;
1064 };
1065 let name = kstr(name);
1066 let core = c.core();
1067 if !core.devtools_tab_declare(&name, &kstr(label), None) || !core.devtools_tab_shown(&name)
1068 {
1069 return false;
1070 }
1071 core.devtools_tab_open(&name);
1072 true
1073 })
1074}
1075
1076/// The node the devtools tree tab has selected, as a key, or 0 for none:
1077/// what an inspector in a declared tab reads.
1078#[unsafe(no_mangle)]
1079pub extern "C" fn kui_devtools_selected(ptr: *mut KuiCtx) -> u64 {
1080 guard(0, || {
1081 unsafe { ctx(ptr) }.map_or(0, |c| c.core().devtools_selected().map_or(0, |k| k.0))
1082 })
1083}
1084
1085/// The tree row under the pointer, as a key, or 0.
1086#[unsafe(no_mangle)]
1087pub extern "C" fn kui_devtools_hovered(ptr: *mut KuiCtx) -> u64 {
1088 guard(0, || {
1089 unsafe { ctx(ptr) }.map_or(0, |c| c.core().devtools_hovered().map_or(0, |k| k.0))
1090 })
1091}
1092
1093/// The node the picker is over while picking, as a key, or 0.
1094#[unsafe(no_mangle)]
1095pub extern "C" fn kui_devtools_picked(ptr: *mut KuiCtx) -> u64 {
1096 guard(0, || {
1097 unsafe { ctx(ptr) }.map_or(0, |c| c.core().devtools_picked().map_or(0, |k| k.0))
1098 })
1099}
1100
1101/// Raises the devtools picker from outside the panel (an inspector asking
1102/// "which node?") or puts it away. While it is up the node under the
1103/// pointer is [`kui_devtools_picked`], and a press lands it in
1104/// [`kui_devtools_selected`]. Raised while a declared tab is on show the
1105/// pick leaves that tab up; otherwise it shows the tree tab. A hidden
1106/// panel comes back docked.
1107#[unsafe(no_mangle)]
1108pub extern "C" fn kui_set_devtools_pick(ptr: *mut KuiCtx, on: bool) {
1109 guard((), || {
1110 if let Some(c) = unsafe { ctx(ptr) } {
1111 c.core().set_devtools_pick(on);
1112 }
1113 });
1114}
1115
1116/// Whether the panel's picker is up.
1117#[unsafe(no_mangle)]
1118pub extern "C" fn kui_devtools_picking(ptr: *mut KuiCtx) -> bool {
1119 guard(false, || {
1120 unsafe { ctx(ptr) }.is_some_and(|c| c.core().devtools_picking())
1121 })
1122}
1123
1124/// Shows the devtools tab named `name`: one of the panel's own (`facts`,
1125/// `events`, `tree`) or a declared tab's. A declared name the panel does
1126/// not list yet is kept and shows once a frame declares it; the return
1127/// says whether the panel lists it now (false on a bad context too). A
1128/// hidden panel comes back docked; [`kui_set_devtools`] is still the
1129/// host's to call. Call it once, not every frame, or it pins the strip
1130/// against the user's own clicks.
1131#[unsafe(no_mangle)]
1132pub extern "C" fn kui_set_devtools_tab(ptr: *mut KuiCtx, name: KuiStr) -> bool {
1133 guard(false, || {
1134 unsafe { ctx(ptr) }.is_some_and(|c| c.core().set_devtools_tab(&kstr(name)))
1135 })
1136}
1137
1138/// The devtools tab currently selected, by name, panel on or off;
1139/// borrowed until the next call. False on a bad context.
1140#[unsafe(no_mangle)]
1141pub extern "C" fn kui_devtools_current_tab(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
1142 guard(false, || {
1143 let Some(c) = (unsafe { ctx(ptr) }) else {
1144 return false;
1145 };
1146 c.devtools_tab = c.core().devtools_current_tab();
1147 if let Some(out) = unsafe { out.as_mut() } {
1148 *out = KuiStr {
1149 ptr: c.devtools_tab.as_ptr(),
1150 len: c.devtools_tab.len(),
1151 };
1152 }
1153 true
1154 })
1155}
1156
1157/// Selects a node in the devtools tree tab and reveals it there, as the
1158/// picker does; 0 clears.
1159#[unsafe(no_mangle)]
1160pub extern "C" fn kui_set_devtools_selected(ptr: *mut KuiCtx, key: u64) {
1161 guard((), || {
1162 if let Some(c) = unsafe { ctx(ptr) } {
1163 c.core()
1164 .set_devtools_selected((key != 0).then_some(kui_core::Key(key)));
1165 }
1166 });
1167}
1168
1169/// The key legend the devtools facts tab shows: `count` pairs, the keys
1170/// in `keys` and what each does in `what`, index for index.
1171#[unsafe(no_mangle)]
1172pub extern "C" fn kui_set_devtools_legend(
1173 ptr: *mut KuiCtx,
1174 keys: *const KuiStr,
1175 what: *const KuiStr,
1176 count: usize,
1177) {
1178 guard((), || {
1179 let Some(c) = (unsafe { ctx(ptr) }) else {
1180 return;
1181 };
1182 let rows: Vec<(String, String)> = if keys.is_null() || what.is_null() {
1183 Vec::new()
1184 } else {
1185 let keys = unsafe { std::slice::from_raw_parts(keys, count) };
1186 let what = unsafe { std::slice::from_raw_parts(what, count) };
1187 keys.iter()
1188 .zip(what)
1189 .map(|(k, w)| (kstr(*k).into_owned(), kstr(*w).into_owned()))
1190 .collect()
1191 };
1192 let borrowed: Vec<(&str, &str)> =
1193 rows.iter().map(|(k, w)| (k.as_str(), w.as_str())).collect();
1194 c.core().set_devtools_legend(&borrowed);
1195 });
1196}
1197
1198/// Turns the per-frame node snapshot behind [`kui_nodes`] on or off. Off
1199/// by default: the copy costs a pass over every node each frame.
1200#[unsafe(no_mangle)]
1201pub extern "C" fn kui_set_inspect(ptr: *mut KuiCtx, on: bool) {
1202 guard((), || {
1203 if let Some(c) = unsafe { ctx(ptr) } {
1204 c.core().set_inspect(on);
1205 }
1206 });
1207}
1208
1209/// The last finished frame's nodes in tree order, as a list of maps, each
1210/// with `key`, `parent`, `depth`, `kind`, `label`, `rect`, `role`,
1211/// `text`, `flags`, `layer`, `origin`, `children`, the layout spec and
1212/// `events` (the node's payloads by handler name). Read it with
1213/// [`kui_value_at`] and [`kui_value_get`]. Empty until
1214/// `kui_set_inspect(ctx, true)` and a frame after it. Borrowed until the
1215/// next call; NULL on a bad context.
1216#[unsafe(no_mangle)]
1217pub extern "C" fn kui_nodes(ptr: *mut KuiCtx) -> *const KuiValue {
1218 guard(std::ptr::null(), || {
1219 let Some(c) = (unsafe { ctx(ptr) }) else {
1220 return std::ptr::null();
1221 };
1222 let list = c
1223 .core()
1224 .nodes()
1225 .iter()
1226 .map(|n| {
1227 let mut v = n.to_value(kui_core::Handles::INT);
1228 if let Value::Map(entries) = &mut v {
1229 entries.push((
1230 "events".into(),
1231 Value::Map(
1232 n.events
1233 .iter()
1234 .map(|(name, v)| ((*name).to_string(), v.clone()))
1235 .collect(),
1236 ),
1237 ));
1238 }
1239 v
1240 })
1241 .collect();
1242 c.nodes = Some(Box::new(KuiValue(Value::List(list))));
1243 c.nodes
1244 .as_deref()
1245 .map_or(std::ptr::null(), |v| v as *const KuiValue)
1246 })
1247}
1248
1249// ---------------------------------------------------------------------------
1250// Parity with the shared prop schema. `KuiSpec` has to be a static repr(C)
1251// layout, so it cannot read `kui_core::schema::PROPS` at runtime the way Lua
1252// and Node do — instead these tests pin it to the table: a schema row with
1253// no C counterpart fails `every_schema_prop_has_a_c_counterpart`.
1254
1255#[cfg(test)]
1256mod schema_parity;
1257
1258#[cfg(test)]
1259mod tests;
1260
1261// ---------------------------------------------------------------------------
1262// ABI parity with include/kui.h. The header is hand-written (it carries prose
1263// the generator would lose), so nothing in Rust makes it match the structs
1264// in `types`: a field added to `KuiSpec` but missing from - or misordered in - the
1265// header silently shifts every field after it at runtime. This module writes
1266// `target/kui-abi-assert.c`, a translation unit of `_Static_assert`s pinning
1267// each field's offset, size and C type to what Rust actually lays out, and
1268// each member of the enums the API reads as list indices to its position in
1269// the list; the `cbuild` tool compiles it against the header, in CI too.
1270
1271/// The two halves of the ABI handshake: a version a host can compare, and
1272/// a size on every struct the library writes into the host's memory.
1273#[cfg(test)]
1274mod abi_handshake;
1275
1276#[cfg(test)]
1277mod abi_parity;