facett-core/src/navgraph/harvest.rs
//! **Harvest** — the navgraph over ordinary egui, with no facett component and no
//! `navgraph::field` call anywhere in the app.
//!
//! Owner, 2026-09-28: *"navgraph must work on ordinary egui also, of course"*. Every
//! pass, egui itself records every widget it laid out (`PassState::widgets`: id, rect,
//! layer, sense, and the id of the `Ui` it was added to). At the end of the pass this
//! module reads that record and adds each navigable widget (it senses clicks, or it can
//! take keyboard focus) that facett did not already register:
//!
//! * **where** — a widget on the background layer hangs under the deepest facett scope
//! whose rect contains it (so a plain `TextEdit` inside a deck pane lands in that
//! pane), else under the app root. A widget on any other layer (a `Window`, an `Area`,
//! a popup) hangs under one `Surface` node for that layer.
//! * **groups** — two or more widgets added to the same `Ui` (a toolbar row, a form
//! column) become one `Group` node for that `Ui`.
//! * **labels** — from the AccessKit node egui wrote for the widget (dev mode turns
//! AccessKit on, so the labels exist).
//! * **numbers** — draw order, marked `~`, unless a declared order (`NavOrder`, keyed by
//! the ids below) says otherwise.
//!
//! Harvested ids are stable across frames: `egui` (the root of a plain app),
//! `layer-<hex>` (a window's layer id), `g<hex>` (a `Ui` id) and `w<hex>` (a widget id),
//! each hex being the egui `Id`'s 64-bit value.
//!
//! Labels and text that are not navigable (a `ui.label`, a plain `Ui`) are not
//! harvested: the navgraph numbers what you can move focus to.
use std::fmt::Write as _;
use egui::{Context, Id, IdMap, LayerId, Order, Rect};
use super::{GRAPH_ID, Key0, NavGraph, NavKind};
/// One navigable widget read off egui's per-pass record. The slots are reused frame to
/// frame (their label `String` included).
#[derive(Clone, Debug)]
pub(super) struct Harvested {
id: Id,
parent_ui: Id,
layer: LayerId,
rect: Rect,
label: String,
dest: u32,
}
/// The harvest's reusable buffers (kept in the [`NavGraph`]).
#[derive(Clone, Default)]
pub(super) struct HarvestBuf {
items: Vec<Harvested>,
len: usize,
by_widget: IdMap<()>,
groups: IdMap<u32>,
key: String,
}
/// The app root a plain egui app gets when nothing registered one.
pub const ROOT: &str = "egui";
impl NavGraph {
/// Add every navigable widget egui laid out this pass that is not already a node.
pub(super) fn harvest(&mut self, ctx: &Context) {
let mut buf = std::mem::take(&mut self.harvest);
buf.len = 0;
buf.by_widget.clear();
buf.groups.clear();
for n in self.nodes() {
if let Some(w) = n.widget {
buf.by_widget.insert(w, ());
}
}
let own = LayerId::new(Order::Debug, Id::new(GRAPH_ID));
// 1. Copy the facts out (inside the context lock: no other context call here).
ctx.viewport(|v| {
for (layer, rects) in v.this_pass.widgets.layers() {
if *layer == own || matches!(layer.order, Order::Debug | Order::Tooltip) {
continue;
}
for r in rects {
let navigable = r.sense.senses_click() || r.sense.is_focusable();
if !navigable || !r.rect.is_positive() || buf.by_widget.contains_key(&r.id) {
continue;
}
if buf.len == buf.items.len() {
buf.items.push(Harvested {
id: r.id,
parent_ui: r.parent_id,
layer: *layer,
rect: r.rect,
label: String::new(),
dest: 0,
});
} else {
let h = &mut buf.items[buf.len];
h.id = r.id;
h.parent_ui = r.parent_id;
h.layer = *layer;
h.rect = r.rect;
h.label.clear();
}
buf.len += 1;
}
}
});
if buf.len == 0 {
self.harvest = buf;
return;
}
// 2. Labels, from the AccessKit node egui already wrote for each widget.
for h in &mut buf.items[..buf.len] {
let label = &mut h.label;
ctx.accesskit_node_builder(h.id, |node| {
if let Some(l) = node.label() {
label.push_str(l);
}
});
}
// 3. Where each goes: the root, a facett scope, or its layer's surface.
let root = match self.nodes().iter().position(|n| n.parent.is_none()) {
Some(i) => i as u32,
None => match self.register(Key0::Str(ROOT), ROOT, NavKind::App, ctx.content_rect(), None, None) {
Some(i) => {
self.nodes[i as usize].harvested = true;
i
}
None => {
self.harvest = buf;
return;
}
},
};
let registered = self.len;
for h in &mut buf.items[..buf.len] {
h.dest = if h.layer == LayerId::background() {
self.deepest_scope(h.rect.center(), registered).unwrap_or(root)
} else {
buf.key.clear();
let _ = write!(buf.key, "layer-{:016x}", h.layer.id.value());
match self.register(Key0::Str(&buf.key), "", NavKind::Surface, h.rect, None, Some(root)) {
Some(s) => {
self.nodes[s as usize].harvested = true;
s
}
None => root,
}
};
*buf.groups.entry(h.parent_ui.with(h.dest)).or_insert(0) += 1;
}
// 4. Register: grouped by their `Ui` when two or more share one.
for i in 0..buf.len {
let (id, parent_ui, rect, dest) = {
let h = &buf.items[i];
(h.id, h.parent_ui, h.rect, h.dest)
};
let mut parent = dest;
if buf.groups.get(&parent_ui.with(dest)).copied().unwrap_or(0) >= 2 {
buf.key.clear();
let _ = write!(buf.key, "g{:016x}", parent_ui.value());
if let Some(g) = self.register(Key0::Str(&buf.key), "", NavKind::Group, rect, None, Some(dest)) {
self.nodes[g as usize].harvested = true;
parent = g;
}
}
buf.key.clear();
let _ = write!(buf.key, "w{:016x}", id.value());
let label = std::mem::take(&mut buf.items[i].label);
if let Some(n) = self.register(Key0::Str(&buf.key), &label, NavKind::Field, rect, Some(id), Some(parent)) {
self.nodes[n as usize].harvested = true;
}
buf.items[i].label = label;
}
self.harvest = buf;
}
/// The deepest registered non-field node (among the first `limit`) containing `p`.
fn deepest_scope(&self, p: egui::Pos2, limit: usize) -> Option<u32> {
let mut best: Option<(u8, u32)> = None;
for (i, n) in self.nodes[..limit].iter().enumerate() {
if n.kind != NavKind::Field && n.rect.contains(p) && best.is_none_or(|(d, _)| n.depth > d) {
best = Some((n.depth, i as u32));
}
}
best.map(|(_, i)| i)
}
}