kui-core 0.1.0-alpha.40

kui contract: flat per-frame tree, clay-style flex layout, text stack, events as data, quad display list
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! `Core`'s menu surface: opening one, drawing the stock one into the
//! frame, and consuming the events its own rows produce.
//!
//! The menu the core opens is drawn by [`crate::widgets::context_menu`] —
//! the same public widget an app calls — through the moment `Ui::finish`
//! already reserves for content the view did not build (the `"root"`
//! fill). So there is no overlay layer here, no window of the core's own,
//! and nothing the conformance corpus cannot see: the menu is ordinary
//! nodes in an ordinary frame.
//!
//! Its rows post ordinary click events too, which is why this module
//! *takes them back*. A row the core built is found by key — recorded
//! while building, never guessed at from a payload — and the events those
//! keys produce never reach the app; what reaches the app is the item's
//! own payload, on the node the menu was opened over.

use crate::geom::Vec2;
/// The longest selection a definition panel is offered for. A word, a
/// term, a short phrase — past this it is a passage, and a dictionary has
/// nothing to say about a passage.
pub const LOOKUP_MAX: usize = 100;

use crate::input::UiEvent;
use crate::key::Key;
use crate::menu::{Menu, MenuAction, MenuItem, MenuRole};
use crate::runtime::Core;
use crate::tree::OriginId;
use crate::ui::Ui;
use crate::value::Value;
use crate::window::WindowId;

impl Core {
    /// The menu this window has open, if any.
    pub fn menu(&self) -> Option<&Menu> {
        self.menu.as_ref()
    }

    /// Tells the core that this host shows menus itself — an `NSMenu`, a
    /// `TrackPopupMenu`, whatever the platform has. The core then keeps the
    /// open menu as state and **does not
    /// draw it**: the host reads `menu()`, shows it, and reports back with
    /// [`Self::activate_menu_item`] or [`Self::close_menu`].
    ///
    /// Declared once by the driver, not per menu, because it is a fact
    /// about the host and not about any one menu. Off by default: a host
    /// that says nothing gets the drawn menu, which is every binding's
    /// starting point and the only thing a headless test can see.
    pub fn set_native_menus(&mut self, on: bool) {
        self.native_menus = on;
    }

    /// Tells the core that this host can show the platform's definition
    /// panel — macOS's Look Up. The standard Look Up row is then offered
    /// where it means something, and a force click over text asks for
    /// one; without it the core neither offers nor asks, because an item
    /// that does nothing is worse than an item that is not there.
    pub fn set_lookup_available(&mut self, on: bool) {
        self.lookup_available = on;
    }

    /// Whether the host said it can show a definition panel.
    pub fn lookup_available(&self) -> bool {
        self.lookup_available
    }

    /// Whether the host said it shows menus itself.
    pub fn native_menus(&self) -> bool {
        self.native_menus
    }

    /// The host's menu reports that item `i` was chosen: the same path a
    /// press on the drawn menu's row takes — the core performs what it
    /// can, queues what the host must do, and returns the events the app
    /// hears (one `menu` event on the node the menu was about).
    ///
    /// `None` when nothing was taken: no menu is open, or row `i` cannot
    /// be chosen — a disabled row, a separator — in which case the menu
    /// stays open and nothing is posted, since a native menu never reports
    /// such a row and the drawn one has no click on it, so a door that
    /// names one (`activateMenuItem(1)` over a select whose second option
    /// is disabled) should be answered the way the pointer would be,
    /// rather than handing the app a choice it disabled.
    /// An index past the end closes the menu and posts nothing
    /// (`Some` and empty), which is what a host reporting a row this build
    /// does not know should do.
    pub fn activate_menu_item(&mut self, i: usize) -> Option<Vec<UiEvent>> {
        let menu = self.menu.as_ref()?;
        let mut out = Vec::new();
        match menu.items.get(i) {
            Some(item) if !item.selectable() => return None,
            Some(_) => self.choose_menu_item(i, &mut out),
            None => {
                self.close_menu();
            }
        }
        // The same way out an input's events take: a row of the devtools'
        // own select is the panel's whichever menu showed it.
        self.outbound(&mut out);
        Some(out)
    }

    /// What the selection would be looked *up* as, or `None` when it is
    /// not something to look up.
    ///
    /// A definition panel answers a word or a short phrase. Handed a
    /// paragraph it draws the whole thing back over the page as one
    /// enormous highlighted strip and then says "No Results Found" — seen
    /// in the field, 2026-09-09 — so a selection that spans lines, or runs
    /// past 100 characters, is neither offered nor asked about.
    /// A force click always passes: it selects one word.
    pub fn lookup_text(&self) -> Option<String> {
        let text = self.copy_selection().filter(|t| !t.trim().is_empty())?;
        let one_line = !text.contains('\n');
        (one_line && text.chars().count() <= LOOKUP_MAX).then_some(text)
    }

    /// A definition-panel request for whatever is selected: the text, and
    /// the baseline origin of its first line to anchor the panel at.
    pub(crate) fn lookup_action(&self) -> Option<MenuAction> {
        if !self.lookup_available {
            return None;
        }
        let text = self.lookup_text()?;
        let at = self.selection_anchor()?;
        Some(MenuAction::LookUp { text, at })
    }

    /// Opens one. A second replaces the first: a window has one menu, the
    /// way it has one selection and one focus.
    ///
    /// Nothing is drawn here. The next frame draws it, because the menu is
    /// state and the frame is a function of state — which is also what
    /// makes an app that never pumps another frame after a right-click a
    /// bug the app can see rather than a menu that appears out of turn.
    pub fn open_menu(&mut self, mut menu: Menu) {
        // `at` arrives in the host's coordinates (ADR 0024); the menu is
        // kept in the window's, which the drawn one and the native one
        // both place by.
        menu.at = menu.at.plus(self.dt_shift());
        self.open_menu_raw(menu);
    }

    /// `open_menu` for a point already in window coordinates — the
    /// right-click's own.
    pub(crate) fn open_menu_raw(&mut self, mut menu: Menu) {
        // Whose menu it is, unless the caller said: the node it is about.
        // An extension that opens a menu over its own node hears the rows
        // come back, the way it hears every other event it declared (ADR
        // 0014 decision 6).
        //
        // The node and not `self.origin()`, which is the *builder's*: it
        // is right during a view and stale afterwards — whatever ran last
        // in the frame that finished, an extension in any app that loads
        // one — so a host opening a menu from its own event handler would
        // have the rows answered to somebody else.
        if menu.origin == crate::tree::OriginId::HOST {
            menu.origin = self
                .tree
                .keys
                .iter()
                .position(|k| *k == menu.target)
                .map_or_else(|| self.origin(), |i| self.tree.origins[i]);
        }
        // Which editor the menu is about, remembered now rather than
        // looked up when a row is chosen: the menu's rows are focusable
        // (they have to be — the arrow keys are the composite's), so by
        // the time Copy runs, focus has moved off the field the user
        // right-clicked. The window's *selection* survives the press
        // (a press under `OriginId::MENU` is spared), but an editor's
        // lives with its focus.
        self.menu_editor = self.edit.focused();
        self.menu = Some(menu);
    }

    /// A select field built this frame (`widgets::select`): its key and
    /// the rows its menu will have. Read back by
    /// [`Self::consume_select_events`] when the field is clicked.
    pub(crate) fn declare_select(&mut self, key: Key, items: Vec<MenuItem>) {
        self.selects.push((key, items));
    }

    /// A click on a select field is not the app's: taken back here and
    /// answered with the field's menu, opened under its bottom-left
    /// corner — the core's own menu, so what follows (the rows, a choice,
    /// a dismissal) is `consume_menu_events`' as for any other. The
    /// field's node is the target, so the choice is posted on it.
    pub(crate) fn consume_select_events(&mut self, out: &mut Vec<UiEvent>) {
        if self.selects.is_empty() {
            return;
        }
        let mut clicked: Option<Key> = None;
        out.retain(|ev| {
            let is = ev.payload.get_bool("select") == Some(true)
                && self.selects.iter().any(|(k, _)| *k == ev.key);
            if is {
                clicked = Some(ev.key);
            }
            !is
        });
        let Some(key) = clicked else {
            return;
        };
        let Some((_, items)) = self.selects.iter().find(|(k, _)| *k == key) else {
            return;
        };
        let items = items.clone();
        // Under the field, in window coordinates: the last frame laid the
        // field out, which is the frame the click was made against.
        let at = self
            .tree
            .keys
            .iter()
            .position(|k| *k == key)
            .map(|i| {
                let (p, s) = (self.tree.pos[i], self.tree.size[i]);
                Vec2::new(p.x, p.y + s.h)
            })
            .unwrap_or_default();
        self.open_menu_raw(Menu::new(key, at, items));
    }

    /// Closes it. Returns whether one was open.
    pub fn close_menu(&mut self) -> bool {
        self.menu.take().is_some()
    }

    /// What choosing an item left for the host: clipboard work, which is
    /// the host's in this library. Drained like the window and audio
    /// commands, and empty on every frame of an app whose menus are all
    /// its own.
    pub fn take_menu_actions(&mut self) -> Vec<MenuAction> {
        std::mem::take(&mut self.menu_actions)
    }

    /// Draws the open menu into the frame being built, if there is one.
    /// Called by `Ui::finish` after the extensions have filled their
    /// slots, so the menu is the last thing declared and therefore the
    /// frame's modal scope and its topmost float.
    pub(crate) fn build_menu(ui: &mut Ui<'_>) {
        if ui.core().native_menus {
            // The host is showing it. Nothing is drawn, so there are no
            // rows to click and `consume_menu_events` finds nothing to
            // take back.
            return;
        }
        let Some(menu) = ui.core().menu.clone() else {
            return;
        };
        // The widget floats against the host's viewport, whose origin is
        // the dock's edge under a left dock (ADR 0024): the window point
        // becomes a host one. Its nodes are opened under `OriginId::MENU`,
        // which is how `consume_menu_events` knows them.
        let at = menu.at.minus(ui.core().dt_shift());
        crate::widgets::context_menu(ui, at, &menu.items);
    }

    /// The stock items for a right-click on `region`, or none when there
    /// is nothing standard to offer there. An editor gets the four every
    /// platform's field has; a selection scope gets the two that mean
    /// anything without a text model behind them — nothing owns the text
    /// under a label, so it cannot be cut into or pasted over.
    ///
    /// Every item is always present and only its *enabling* moves: a menu
    /// whose rows shuffle depending on what happens to be possible is one
    /// nobody can use without reading it every time.
    pub(crate) fn default_menu_items(
        &self,
        editor: Option<Key>,
        scope: Option<Key>,
    ) -> Vec<MenuItem> {
        if let Some(key) = editor {
            let has = self.edit.copy_selection(key).is_some_and(|t| !t.is_empty());
            return vec![
                MenuItem::role(MenuRole::Cut).enabled(has),
                MenuItem::role(MenuRole::Copy).enabled(has),
                // The core cannot see a clipboard, so it cannot know
                // whether there is anything to paste; the host that can
                // will say so when it renders this natively (step 3).
                MenuItem::role(MenuRole::Paste),
                MenuItem::separator(),
                MenuItem::role(MenuRole::SelectAll),
            ];
        }
        if scope.is_some() {
            // `copy_selection`, not `selection_text`: a `cells` grid is a
            // scope too, and its selection is in cells rather than in
            // runs — reading only the text one left Copy dimmed over a
            // terminal with half its screen selected.
            let has = self.copy_selection().is_some_and(|t| !t.is_empty());
            let mut items = vec![
                MenuItem::role(MenuRole::Copy).enabled(has),
                MenuItem::role(MenuRole::SelectAll),
            ];
            // Only where the host can show one, and only with something
            // to look up: the row is the platform's, and a dead one would
            // be a promise this library cannot keep.
            if self.lookup_available {
                // Enabled only for something a dictionary can answer —
                // dimmed for a passage, and dimmed rather than missing, so
                // the rows a reader reaches for stay where they were.
                let can = self.lookup_text().is_some();
                items.insert(0, MenuItem::role(MenuRole::LookUp).enabled(has && can));
                items.insert(1, MenuItem::separator());
            }
            return items;
        }
        Vec::new()
    }

    /// A secondary press the app did not claim: opens the stock menu when
    /// the press landed on something with standard items, and does nothing
    /// at all otherwise — a right-click on a plain box has never opened a
    /// menu and does not start now.
    ///
    /// `claimed` is whether the node under the pointer declared
    /// `onContextMenu`. That declaration wins: the app asked to own the
    /// menu there, and one of the two has to win by declaration rather
    /// than by luck.
    pub(crate) fn auto_menu(&mut self, at: Vec2, claimed: bool) {
        if claimed {
            return;
        }
        let Some(region) = self.interaction.hit_at(at) else {
            return;
        };
        let (key, origin) = (region.key, region.origin);
        let editor = region.edit_origin.map(|_| key);
        let scope = region.select_scope;
        let items = self.default_menu_items(editor, scope);
        if items.is_empty() {
            return;
        }
        let target = editor.or(scope).unwrap_or(key);
        self.open_menu_raw(Menu::new(target, at, items).origin(origin));
        // The press told us which editor this is about, which is better
        // than what held focus: a right-click moves no focus (it must
        // leave a selection alone), so the field under the pointer is not
        // necessarily the focused one.
        self.menu_editor = editor;
    }

    /// Filters the events one input produced: anything belonging to the
    /// core's own menu is consumed and acted on, and what the app hears
    /// instead is the item's payload on the node the menu was about.
    ///
    /// Runs on the way out of `handle_input`, so a menu row cannot leak a
    /// click into an app that never declared one.
    pub(crate) fn consume_menu_events(&mut self, out: &mut Vec<UiEvent>) {
        if self.menu.is_none() {
            return;
        }
        let taken = Self::take_surface_events(out, OriginId::MENU);
        if let Some(i) = taken.row {
            self.choose_menu_item(i, out);
        } else if taken.dismissed {
            self.close_menu();
        }
    }

    /// Takes every event of one of the core's own surfaces out of `out`
    /// and reads what they said: a `dismiss` on the surface's modal root,
    /// a click on the `row`th item (its payload is `{row}`, the message
    /// `widgets::menu_row_tag` gave it, whether the pointer or Enter
    /// clicked it), a click on the `title`th menu of the bar (`{title}`).
    /// The one filter both menus consume through, so what a surface's
    /// nodes post is read in one place. Hover, focus and the rest of a
    /// surface's own events are taken with them: none of it is the app's.
    pub(crate) fn take_surface_events(out: &mut Vec<UiEvent>, origin: OriginId) -> Taken {
        let mut taken = Taken::default();
        let index = |v: &Value, name: &str| v.get(name).and_then(Value::as_int).map(|i| i as usize);
        out.retain(|ev| {
            if ev.origin != origin {
                return true;
            }
            if ev.kind() == Some("dismiss") {
                taken.dismissed = true;
            } else if let Some(i) = index(&ev.payload, "row") {
                taken.row = Some(i);
            } else if let Some(i) = index(&ev.payload, "title") {
                taken.title = Some(i);
            }
            false
        });
        taken
    }

    /// Performs one item and closes the menu. A standard role the core can
    /// finish it finishes; the clipboard three become a [`MenuAction`] for
    /// the host; everything else is the app's, and reaches it as an event
    /// on the node the menu was opened over.
    fn choose_menu_item(&mut self, i: usize, out: &mut Vec<UiEvent>) {
        let Some(menu) = self.menu.clone() else {
            return;
        };
        let Some(item) = menu.items.get(i).cloned() else {
            return;
        };
        self.close_menu();
        self.perform_menu_item(&item, menu.target, menu.origin, out);
    }

    /// One item, performed and posted, wherever it was chosen from: the
    /// open context menu's row, a host's native menu, or a menu of the
    /// application menu bar. The two callers differ only in what they close first
    /// and what node the event lands on, so everything after that is here
    /// and cannot drift between them.
    ///
    /// `target` is the node the event is posted on and `origin` who hears
    /// it; the standard roles act on [`Core::menu_editor`], which each
    /// caller sets when its menu opens — an editor's selection lives with
    /// its focus, and a menu's rows take that focus.
    pub(crate) fn perform_menu_item(
        &mut self,
        item: &MenuItem,
        target: Key,
        origin: crate::tree::OriginId,
        out: &mut Vec<UiEvent>,
    ) {
        match item.role {
            MenuRole::Separator => return,
            MenuRole::SelectAll => {
                // The one standard item that needs nobody: an editor
                // selects its own text, a scope selects its runs.
                match self.menu_editor {
                    Some(key) => {
                        self.move_focus(Some(key));
                        // The editor directly, not back through
                        // `handle_input`: this runs *inside* one already,
                        // and the events the nested call returned were
                        // dropped on the floor.
                        self.edit_with_fonts(|edit, fs| {
                            edit.apply_key(
                                key,
                                crate::input::EditKey::SelectAll,
                                crate::input::Mods::default(),
                                fs,
                            )
                        });
                    }
                    None => {
                        let scope = self.selection().map_or(target, |s| s.scope);
                        self.select_all_in(scope);
                    }
                }
            }
            MenuRole::Copy => {
                // `copy_selection` again, for the reason it is used to
                // enable the row: a `cells` grid's selection is in cells,
                // and reading only the text one left Copy lit over a
                // terminal and then copied nothing when it was chosen.
                let text = match self.menu_editor {
                    Some(key) => self.edit.copy_selection(key),
                    None => self.copy_selection().filter(|t| !t.is_empty()),
                };
                if let Some(text) = text {
                    let html = self.selection_html();
                    self.menu_actions
                        .push(MenuAction::SetClipboard { text, html });
                }
            }
            MenuRole::Cut => {
                // Only an editor can be cut from: nothing owns the text
                // behind a static selection, so there is nothing to take
                // it out of. The item is simply not offered there.
                if let Some(key) = self.menu_editor
                    && let Some(text) = self.cut_editor(key)
                {
                    self.move_focus(Some(key));
                    // No `html`: an editor's text is one style, and what
                    // was cut is gone anyway.
                    self.menu_actions
                        .push(MenuAction::SetClipboard { text, html: None });
                    // And the edit it is: every other mutation posts one
                    // (`apply_text`, `apply_key`, a reader's `setValue`),
                    // so an app mirroring the field hears this one too
                    // (AR15).
                    self.push_edit_event(key, "changed", out);
                }
            }
            MenuRole::Paste => self.queue_paste(),
            MenuRole::LookUp => {
                if let Some(action) = self.lookup_action() {
                    self.menu_actions.push(action);
                }
            }
            MenuRole::Custom => {}
        }
        // Every chosen item posts, including the standard ones: an app
        // that wants to know its editor was cut from does not have to
        // guess, and one that does not simply ignores the event.
        let payload = item.id.clone().unwrap_or_else(|| Value::str(item.text()));
        out.push(UiEvent {
            origin,
            window: WindowId::MAIN,
            key: target,
            payload: Value::map([
                ("kind", Value::str("menu")),
                ("role", Value::str(item.role.name())),
                ("item", payload),
            ]),
            slot: None,
        });
    }
}

/// What one input said to one of the core's surfaces (`Core::take_surface_events`).
#[derive(Default)]
pub(crate) struct Taken {
    pub dismissed: bool,
    pub row: Option<usize>,
    pub title: Option<usize>,
}