Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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)
    }
}