kui-core 0.1.0-alpha.45

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
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
//! A declared devtools tab (`docs/adr/0032-a-devtools-tab-mounts-a-slot.md`):
//! the tab body is a node in the panel, and the content is a layer
//! anchored to it — from the host's own view, lazily, or from an
//! extension's fill of the slot the panel declares. Both forms are driven
//! here through a bare core: the host form with `devtools_tab_with`, the
//! extension form with a stand-in extension in an `Extensions` list, the
//! way `tests/slots.rs` drives the runner's own filler.

use std::cell::RefCell;
use std::rc::Rc;

use kui_core::diag::{DUPLICATE_TAB, UNKNOWN_SLOT};
use kui_core::testing::{click_at, codes};
use kui_core::{
    Core, DevtoolsDock, Extension, Extensions, InputEvent, Key, KeyCode, KeyMods, KeyPress,
    NodeSpec, Slot, TextStyle, Ui, UiEvent, Value, widgets,
};

const VIEWPORT: kui_core::Size = kui_core::Size {
    w: 1000.0,
    h: 600.0,
};

/// The app: a button, then the declared tab. `built` counts the closure's
/// runs, which is the laziness under test.
fn view(ui: &mut Ui<'_>, built: &Rc<RefCell<u32>>) {
    ui.with(NodeSpec::column().fill(), |ui| {
        widgets::button(ui, "press", Value::map([("kind", Value::str("pressed"))]));
        ui.devtools_tab_with("syntax", "Tree-sitter", |ui| {
            *built.borrow_mut() += 1;
            ui.text("identifier 12:4", TextStyle::new(12.0));
            widgets::button(ui, "jump", Value::map([("kind", Value::str("jump"))]));
        });
    });
}

fn frame(core: &mut Core, built: &Rc<RefCell<u32>>) {
    let mut ui = core.frame(VIEWPORT, 1.0);
    view(&mut ui, built);
    ui.finish();
}

fn chord(c: char) -> InputEvent {
    InputEvent::KeyDown(KeyPress::new(
        KeyCode::Char(c),
        KeyMods::NONE.with_ctrl().with_shift(),
    ))
}

/// The rect of the node labelled `label`, from the inspect snapshot.
fn rect_of(core: &Core, label: &str) -> Option<kui_core::Rect> {
    core.nodes()
        .iter()
        .find(|n| n.label.as_deref() == Some(label))
        .map(|n| n.rect)
}

fn texts(core: &Core) -> Vec<String> {
    core.nodes().iter().filter_map(|n| n.text.clone()).collect()
}

/// The host form: declared every frame, built only while it is the tab
/// on show, laid out over the panel's tab body, the host's to hear, the
/// dock's to Tab through.
#[test]
fn a_host_form_tab_is_lazy_and_lands_over_the_body() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    frame(&mut core, &built);
    frame(&mut core, &built);
    assert_eq!(
        *built.borrow(),
        0,
        "declared, not built: the events tab is up"
    );
    let strip: Vec<String> = core
        .nodes()
        .iter()
        .filter_map(|n| n.label.clone())
        .filter(|l| l == "kui-devtools/tab-custom:syntax")
        .collect();
    assert_eq!(
        strip.len(),
        1,
        "the strip lists the declared tab by its label"
    );

    // N walks facts → events → tree → the declared tab.
    core.handle_input(chord('N')); // tree
    core.handle_input(chord('N')); // Tree-sitter
    frame(&mut core, &built);
    assert_eq!(
        *built.borrow(),
        1,
        "the closure ran once the tab was on show"
    );
    // Exactly one tab of the strip is selected, and it is the declared one.
    let lit: Vec<String> = core
        .access_tree()
        .nodes
        .iter()
        .filter(|n| n.role == kui_core::Role::Tab && n.selected == Some(true))
        .filter_map(|n| n.name.clone())
        .collect();
    assert_eq!(lit, ["Tree-sitter"], "one lit tab, the declared one");
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 2);
    let body = rect_of(&core, "kui-devtools/tab/syntax").expect("the body is in the panel");
    let content = rect_of(&core, "jump").expect("the host's button is in the frame");
    assert!(
        body.w > 100.0 && body.h > 100.0,
        "the body takes the tab area: {body:?}"
    );
    assert!(
        content.x >= body.x && content.x + content.w <= body.x + body.w,
        "the content is laid out inside the body: {content:?} in {body:?}"
    );
    assert!(
        content.y >= body.y && content.y + content.h <= body.y + body.h,
        "vertically too: {content:?} in {body:?}"
    );
    assert!(
        texts(&core).iter().any(|t| t == "identifier 12:4"),
        "the content's text painted"
    );

    // Its events are the host's: a click on the tab's button comes out.
    let evs = click_at(&mut core, content.x + 2.0, content.y + 2.0);
    assert_eq!(evs.len(), 1, "one event, the host's: {evs:?}");
    assert_eq!(evs[0].kind(), Some("jump"));
    assert_eq!(evs[0].key, core.key_of("jump").unwrap());

    // The content is the dock's region, not the app's ring: Tab from the
    // app's button never reaches `jump`, and inside the dock it does.
    // (The click settled the region on the dock, since the content is
    // its; back to the main ring first.)
    assert_eq!(
        core.region(),
        core.key_of("kui-devtools"),
        "the click settled the dock's region"
    );
    core.focus_region(None);
    frame(&mut core, &built);
    core.set_focus(None);
    core.focus_next(true);
    let first = core.focus();
    core.focus_next(true);
    assert_eq!(core.focus(), first, "the app's ring is the one button");
    assert_ne!(first, core.key_of("jump"));
    core.set_focus(None);
    core.handle_input(chord('I'));
    frame(&mut core, &built);
    let dock = core.key_of("kui-devtools").unwrap();
    assert_eq!(core.region(), Some(dock));
    let mut seen = Vec::new();
    for _ in 0..40 {
        core.focus_next(true);
        seen.push(core.focus());
    }
    assert!(
        seen.contains(&core.key_of("jump")),
        "Tab inside the dock reaches the tab's button: {seen:?}"
    );

    // Another tab: the closure stops running, the content is gone.
    core.handle_input(chord('N')); // facts
    frame(&mut core, &built);
    let runs = *built.borrow();
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), runs, "not shown, not built");
    assert!(rect_of(&core, "jump").is_none());

    // The panel off: declared still (cheap), never built.
    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    frame(&mut core, &built);
    assert!(rect_of(&core, "jump").is_some(), "back on show");
    core.set_devtools(false);
    let runs = *built.borrow();
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), runs, "off, nothing built");
    assert!(rect_of(&core, "jump").is_none());
}

/// A pick raised from a declared tab (`set_devtools_pick`) keeps that tab
/// up: the node under the pointer is `devtools_picked` while picking, the
/// press lands it in `devtools_selected`, and the tab — not the tree tab
/// — is what the panel shows after. Raised with no declared tab up, it is
/// the chord's pick and shows the tree.
#[test]
fn a_pick_raised_from_a_tab_lands_in_selected_and_keeps_the_tab() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    frame(&mut core, &built);
    frame(&mut core, &built);
    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    frame(&mut core, &built);
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_some(),
        "the tab is up"
    );
    assert!(!core.devtools_picking());
    core.set_devtools_pick(true);
    assert!(core.devtools_picking());
    frame(&mut core, &built);
    assert!(
        core.key_of("kui-devtools/picker").is_some(),
        "the overlay is up"
    );
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_some(),
        "and the declared tab stayed up"
    );
    let press = core.key_of("press").unwrap();
    let b = rect_of(&core, "press").unwrap();
    core.handle_input(InputEvent::CursorMoved(kui_core::Vec2::new(
        b.x + b.w / 2.0,
        b.y + b.h / 2.0,
    )));
    frame(&mut core, &built);
    assert_eq!(
        core.devtools_picked(),
        Some(press),
        "the node under the pointer"
    );
    let evs = click_at(&mut core, b.x + b.w / 2.0, b.y + b.h / 2.0);
    assert!(evs.is_empty(), "the press is the picker's: {evs:?}");
    assert!(!core.devtools_picking());
    assert_eq!(
        core.devtools_selected(),
        Some(press),
        "the pick landed in selected"
    );
    frame(&mut core, &built);
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_some(),
        "the tab is still the one on show"
    );
    assert!(core.key_of("kui-devtools/picker").is_none());
    // Put away from outside, too.
    core.set_devtools_pick(true);
    core.set_devtools_pick(false);
    assert!(!core.devtools_picking());
    // With no declared tab up, the door is the chord's pick: the tree tab.
    core.handle_input(chord('N')); // facts
    frame(&mut core, &built);
    core.set_devtools_pick(true);
    frame(&mut core, &built);
    assert!(core.key_of("kui-devtools/tab/syntax").is_none());
    assert!(
        core.key_of("kui-devtools/tree-filter").is_some()
            || core.key_of("kui-devtools/picker").is_some()
    );

    // A name kept for a tab no frame has declared yet (F67) is not a tab
    // up: a pick raised then is the chord's too, and shows the tree —
    // where keying "keep the tab" on the name being set showed the pick
    // in no tab at all (backlog RG14).
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    assert!(!core.set_devtools_tab("syntax"), "nothing declared yet");
    core.set_devtools_pick(true);
    assert_eq!(core.devtools_current_tab(), "tree", "the chord's pick");
    frame(&mut core, &built);
    assert!(
        core.key_of("kui-devtools/picker").is_some(),
        "the overlay is up"
    );
    assert!(
        core.key_of("kui-devtools/tree-filter").is_some(),
        "on the tree tab, not on the events tab the strip fell back to"
    );
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_none(),
        "the pending name gave way, as it does to the chord"
    );
    let press = core.key_of("press").unwrap();
    let b = rect_of(&core, "press").unwrap();
    core.handle_input(InputEvent::CursorMoved(kui_core::Vec2::new(
        b.x + b.w / 2.0,
        b.y + b.h / 2.0,
    )));
    frame(&mut core, &built);
    click_at(&mut core, b.x + b.w / 2.0, b.y + b.h / 2.0);
    assert_eq!(core.devtools_selected(), Some(press));
    assert_eq!(core.devtools_current_tab(), "tree", "and lands in the tree");
}

/// A tab is selected from the app's side (`set_devtools_tab`): a declared
/// name by name, one of the panel's own by its name, read back through
/// `devtools_current_tab`. A name the panel does not list yet is kept
/// and shows once a frame declares it — so a command that opens the tab
/// at startup, before any frame, lands — and a hidden panel comes back
/// docked, as the picker's does.
#[test]
fn a_tab_is_selected_from_the_app() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    frame(&mut core, &built);
    assert_eq!(core.devtools_current_tab(), "events", "the panel's default");
    assert_eq!(*built.borrow(), 0);
    assert!(
        core.set_devtools_tab("syntax"),
        "declared by the last frame: listed"
    );
    assert_eq!(core.devtools_current_tab(), "syntax");
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 1, "on show: the closure ran");
    assert!(core.key_of("kui-devtools/tab/syntax").is_some());
    // One of the panel's own, by name — in any case, since the strip
    // labels them `Facts`, `Events`, `Tree` and a caller writes what it
    // reads there; before RG14 "Tree" was refused and kept as a declared
    // name nobody would ever list.
    assert!(core.set_devtools_tab("tree"));
    assert_eq!(core.devtools_current_tab(), "tree");
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 1, "off show: the closure rests");
    assert!(core.key_of("kui-devtools/tab/syntax").is_none());
    assert!(core.key_of("kui-devtools/tree-filter").is_some());
    assert!(core.set_devtools_tab("Facts"), "the strip's own spelling");
    assert_eq!(core.devtools_current_tab(), "facts");
    assert!(core.set_devtools_tab("TREE"));
    assert_eq!(core.devtools_current_tab(), "tree");
    assert!(
        !core.set_devtools_tab("Syntax"),
        "a declared name is the app's spelling, matched exactly"
    );
    assert!(core.set_devtools_tab("tree"));
    // A name nobody declares: kept, not listed, the strip falls back.
    assert!(!core.set_devtools_tab("nope"), "not listed");
    assert_eq!(core.devtools_current_tab(), "tree", "the fallback");
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 1);
    assert!(core.key_of("kui-devtools/tree-filter").is_some());
    // The chord moves on from the fallback as it would from the tab.
    core.handle_input(chord('N'));
    assert_eq!(
        core.devtools_current_tab(),
        "syntax",
        "N: tree, then the declared one"
    );
    // A hidden panel comes back docked.
    core.set_devtools_dock(DevtoolsDock::Off);
    frame(&mut core, &built);
    let runs = *built.borrow();
    assert!(core.key_of("kui-devtools/tab/syntax").is_none(), "hidden");
    assert!(core.set_devtools_tab("syntax"));
    assert_eq!(core.devtools_dock(), DevtoolsDock::Right);
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), runs + 1, "back, on the tab");

    // Before any frame: the name is not listed yet, and shows once it is.
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    assert!(!core.set_devtools_tab("syntax"), "nothing declared yet");
    assert_eq!(
        core.devtools_current_tab(),
        "events",
        "the fallback until then"
    );
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 0, "the first frame declares it");
    assert_eq!(
        core.devtools_current_tab(),
        "syntax",
        "and now it is the one on show"
    );
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 1, "the second builds it");

    // With the panel off the selection is kept; `set_devtools` shows it.
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools_dock(DevtoolsDock::Right);
    frame(&mut core, &built);
    assert!(
        !core.set_devtools_tab("syntax"),
        "off, the panel lists nothing yet — the selection is kept"
    );
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 0, "off: nothing built");
    assert!(!core.devtools());
    core.set_devtools(true);
    frame(&mut core, &built);
    assert_eq!(
        core.devtools_current_tab(),
        "syntax",
        "on: listed from its first frame"
    );
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), 1, "and the tab selected earlier is up");
}

/// A declaration is made every frame, panel on or off: it must not pile
/// up across frames into a `duplicate-tab` on the second one, and a
/// window that is not the main one declares into nothing.
#[test]
fn a_declaration_every_frame_is_one_declaration_a_frame() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    for _ in 0..3 {
        frame(&mut core, &built);
    }
    let ws = core.take_warnings();
    assert!(
        !codes(&ws).contains(&DUPLICATE_TAB),
        "the panel off, three frames, no duplicate: {ws:?}"
    );
    core.set_devtools(true);
    for _ in 0..3 {
        frame(&mut core, &built);
    }
    let ws = core.take_warnings();
    assert!(
        !codes(&ws).contains(&DUPLICATE_TAB),
        "the panel on, three frames, no duplicate: {ws:?}"
    );
}

/// A tab pick cancelled by Escape leaves nothing behind: the next chord
/// pick is the chord's, shown and revealed in the tree tab. And a
/// `custom` the app stopped declaring shows nothing and builds nothing —
/// the laziness rule and `shown()` agree on what the panel lists.
#[test]
fn a_cancelled_tab_pick_and_a_stale_tab_leave_nothing_behind() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    frame(&mut core, &built);
    frame(&mut core, &built);
    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    frame(&mut core, &built);
    core.set_devtools_pick(true);
    frame(&mut core, &built);
    let escape = InputEvent::KeyDown(KeyPress::new(KeyCode::Escape, KeyMods::default()));
    core.handle_input(escape);
    assert!(!core.devtools_picking(), "Escape put the tab's pick away");
    // The chord's pick: shows the tree tab, and the press lands there.
    core.handle_input(chord('P'));
    frame(&mut core, &built);
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_none(),
        "the chord's pick shows the tree tab"
    );
    let press = core.key_of("press").unwrap();
    let b = rect_of(&core, "press").unwrap();
    core.handle_input(InputEvent::CursorMoved(kui_core::Vec2::new(
        b.x + b.w / 2.0,
        b.y + b.h / 2.0,
    )));
    frame(&mut core, &built);
    click_at(&mut core, b.x + b.w / 2.0, b.y + b.h / 2.0);
    frame(&mut core, &built);
    assert_eq!(core.devtools_selected(), Some(press));
    assert!(
        core.key_of("kui-devtools/outline-selected").is_some(),
        "and the tree tab outlines the pick, as before the tab feature"
    );

    // Back to the declared tab, then stop declaring it: the panel falls
    // back to the tree tab, and the app's closure is not run for the
    // name the panel still remembers.
    core.handle_input(chord('N'));
    frame(&mut core, &built);
    assert!(core.key_of("kui-devtools/tab/syntax").is_some());
    let runs = *built.borrow();
    let plain = |core: &mut Core| {
        let mut ui = core.frame(VIEWPORT, 1.0);
        ui.with(NodeSpec::column().grow_width(), |ui| {
            widgets::button(ui, "press", Value::map([("kind", Value::str("pressed"))]));
        });
        ui.finish();
    };
    plain(&mut core);
    plain(&mut core);
    assert!(
        core.key_of("kui-devtools/tab/syntax").is_none(),
        "no body for a tab nobody declares"
    );
    assert!(
        !core.devtools_tab_shown("syntax"),
        "and the laziness rule says not shown, though the name is still remembered"
    );
    assert_eq!(core.devtools_shown_tab(), None);
    frame(&mut core, &built);
    assert_eq!(
        *built.borrow(),
        runs,
        "declared again: still not built, the panel lists it a frame later"
    );
    frame(&mut core, &built);
    assert_eq!(*built.borrow(), runs + 1, "listed now, so shown again");
}

/// A left dock is built before the host's view, so the body precedes the
/// content in tree order; the layer does not care which came first.
#[test]
fn a_left_dock_mounts_the_host_form_too() {
    let built = Rc::new(RefCell::new(0u32));
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Left);
    core.set_inspect(true);
    frame(&mut core, &built);
    frame(&mut core, &built);
    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    frame(&mut core, &built);
    frame(&mut core, &built);
    let body = rect_of(&core, "kui-devtools/tab/syntax").expect("the body");
    let content = rect_of(&core, "jump").expect("the content");
    assert!(body.x < 10.0, "a left dock: {body:?}");
    assert!(
        content.x >= body.x && content.x + content.w <= body.x + body.w,
        "{content:?} in {body:?}"
    );
}

/// A name declared twice in one frame is one tab and one warning; the
/// first declaration stands.
#[test]
fn a_tab_declared_twice_warns_and_keeps_the_first() {
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_inspect(true);
    let build = |core: &mut Core| {
        let mut ui = core.frame(VIEWPORT, 1.0);
        ui.with(NodeSpec::column().grow_width(), |ui| {
            ui.devtools_tab_with("x", "First", |_| {});
            ui.devtools_tab("x", "Second", "ext/x");
            ui.devtools_tab("y", "Other", "ext/y");
        });
        ui.finish();
    };
    build(&mut core);
    build(&mut core);
    let ws = core.take_warnings();
    assert_eq!(codes(&ws), [DUPLICATE_TAB], "{ws:?}");
    let labels: Vec<String> = core
        .nodes()
        .iter()
        .filter_map(|n| n.label.clone())
        .collect();
    assert!(
        labels.iter().any(|l| l == "kui-devtools/tab-custom:x"),
        "{labels:?}"
    );
    assert!(labels.iter().any(|l| l == "kui-devtools/tab-custom:y"));
    assert_eq!(
        labels
            .iter()
            .filter(|l| l.starts_with("kui-devtools/tab-custom:"))
            .count(),
        2,
        "two tabs, not three"
    );
    let texts = texts(&core);
    assert!(texts.iter().any(|t| t == "First"), "{texts:?}");
    assert!(!texts.iter().any(|t| t == "Second"));
}

// ---------------------------------------------------------------------------
// The extension form.

#[derive(Default)]
struct Ext {
    seen: Rc<RefCell<Vec<(String, Value)>>>,
    heard: Rc<RefCell<Vec<Value>>>,
}

impl Extension for Ext {
    fn name(&self) -> &str {
        "ts"
    }
    fn slots(&self) -> &[String] {
        std::slice::from_ref(Box::leak(Box::new("panel".to_string())))
    }
    fn view(&mut self, slot: &Slot<'_>, ui: &mut Ui<'_>) -> Result<(), String> {
        self.seen
            .borrow_mut()
            .push((slot.full_name(), slot.params.clone()));
        ui.text("from the plugin", TextStyle::new(12.0));
        widgets::button(ui, "plug", Value::map([("kind", Value::str("plug"))]));
        Ok(())
    }
    fn on_event(&mut self, ev: &UiEvent) -> Vec<Value> {
        self.heard.borrow_mut().push(ev.payload.clone());
        vec![Value::map([("kind", Value::str("reply"))])]
    }
}

fn host_view(ui: &mut Ui<'_>) {
    ui.with(NodeSpec::column().fill(), |ui| {
        widgets::button(ui, "press", Value::map([("kind", Value::str("pressed"))]));
        ui.devtools_tab("syntax", "Tree-sitter", "ts/panel");
    });
}

fn frame_with(core: &mut Core, exts: &mut Extensions) {
    let mut ui = core.frame_with(VIEWPORT, 1.0, exts);
    host_view(&mut ui);
    ui.finish();
}

/// The extension form: the slot is declared only while the tab is on
/// show — and raises no `unknown-slot` otherwise — the fill lands over
/// the body with the panel's facts as params, its events go to the
/// extension by origin, and its replies reach the host.
#[test]
fn an_extension_form_tab_is_a_slot_the_panel_declares_when_shown() {
    let seen = Rc::new(RefCell::new(Vec::new()));
    let heard = Rc::new(RefCell::new(Vec::new()));
    let mut exts = Extensions::try_from(vec![Box::new(Ext {
        seen: seen.clone(),
        heard: heard.clone(),
    }) as Box<dyn Extension>])
    .unwrap();
    let mut core = Core::new();
    core.set_devtools(true);
    core.set_devtools_dock(DevtoolsDock::Right);
    core.set_inspect(true);
    frame_with(&mut core, &mut exts);
    frame_with(&mut core, &mut exts);
    assert!(
        seen.borrow().is_empty(),
        "not on show: the extension is not asked"
    );
    let ws = core.take_warnings();
    assert!(
        !codes(&ws).contains(&UNKNOWN_SLOT),
        "a tab's slot counts as declared with the tab not on show: {ws:?}"
    );

    core.handle_input(chord('N'));
    core.handle_input(chord('N'));
    frame_with(&mut core, &mut exts);
    assert_eq!(seen.borrow().len(), 1, "asked once the tab is on show");
    let (name, params) = seen.borrow()[0].clone();
    assert_eq!(name, "ts/panel");
    assert!(
        params.get("selected").is_some(),
        "the panel's facts: {params:?}"
    );
    frame_with(&mut core, &mut exts);
    let body = rect_of(&core, "kui-devtools/tab/syntax").expect("the body");
    let content = rect_of(&core, "plug").expect("the fill's button");
    assert!(
        content.x >= body.x && content.x + content.w <= body.x + body.w,
        "{content:?} in {body:?}"
    );
    assert!(texts(&core).iter().any(|t| t == "from the plugin"));

    // The fill's click is the extension's, by origin; its reply is the
    // host's, because the panel declared the slot for the host.
    let evs = click_at(&mut core, content.x + 2.0, content.y + 2.0);
    assert_eq!(evs.len(), 1);
    let mut out = Vec::new();
    exts.route(evs, |ev| out.push(ev));
    assert_eq!(heard.borrow().len(), 1, "the extension heard its click");
    assert_eq!(out.len(), 1, "one reply for the host: {out:?}");
    assert_eq!(out[0].kind(), Some("reply"));
    let _ = Key::ROOT;
}