scripts/CURSOR_AND_TEXT_HIT_TEST_ANALYSIS.md
Datum: 19. Januar 2026
Status: Verifizierung der Gemini-Analyse vor Implementation
cursor:pointer auf Button funktioniert nicht korrektText-Nodes (NodeType::Text) erzeugen keine eigenen Hit-Test-Bereiche im WebRender-Display-List.
Wenn der Mauszeiger über Text ist, gibt der Hit-Test den Container (Body, Button, Div) zurück, nicht den Text selbst.
+----------+------------------------------------+---------------------------+
| Marker | Zweck | Trigger Re-Render? |
+----------+------------------------------------+---------------------------+
| 0x0100 | DOM Node (Callbacks, Focus, Hover) | Ja |
| 0x0200 | Scrollbar-Komponenten | Nein (nur Scroll-Update) |
| 0x0300 | Selection (Text-Auswahl) | Ja |
| 0x0400 | Cursor (Cursor-Icon) | Nein |
| 0x0500 | Reserviert | - |
+----------+------------------------------------+---------------------------+
Definiert in: core/src/hit_test_tag.rs
┌─────────────────────────────────────────────────────────────────────────────┐
│ TEXT LAYOUT PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [1] DOM Traversal (fc.rs) │
│ ├── NodeType::Text("Hello") │
│ └── Erstellt: StyledRun { text, style, logical_start_byte } │
│ ❌ FEHLT: source_node_id │
│ │
│ [2] InlineContent Collection (fc.rs:4775-4805) │
│ └── InlineContent::Text(StyledRun) │
│ ❌ NodeId geht hier verloren! │
│ │
│ [3] Text Shaping (cache.rs) │
│ ├── StyledRun → VisualRun │
│ ├── VisualRun → ShapedCluster │
│ └── ShapedCluster enthält Glyphen-Positionen │
│ ❌ Keine Rückverfolgbarkeit zum ursprünglichen NodeId │
│ │
│ [4] Glyph Runs (glyphs.rs) │
│ └── SimpleGlyphRun { glyphs, font_hash, color... } │
│ ❌ Keine NodeId-Information │
│ │
│ [5] Display List (display_list.rs) │
│ └── DisplayListItem::Text { glyphs, font_hash, color, clip_rect } │
│ ❌ Kein HitTestArea für Text-Runs │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| Datei | Struktur | Problem |
|---|---|---|
| layout/src/text3/cache.rs#L811 | StyledRun | Fehlt source_node_id: NodeId |
| layout/src/solver3/fc.rs#L4775 | collect_inline_content() | NodeId wird nicht propagiert |
| layout/src/text3/glyphs.rs#L36 | SimpleGlyphRun | Fehlt NodeId |
| layout/src/solver3/display_list.rs | build_display_list() | Kein HitTestArea für Text |
| layout/src/hit_test.rs#L70 | CursorTypeHitTest::new() | Hat Text-Child-Hack |
| core/src/prop_cache.rs#L909 | restyle() | Gibt Tags an Container statt Text |
Ort: layout/src/hit_test.rs#L87-L118
// Wenn Container keinen expliziten Cursor hat, prüfe Text-Kinder
let hier = &node_hierarchy[*node_id];
if let Some(first_child) = hier.first_child_id(*node_id) {
let mut child_id = Some(first_child);
while let Some(cid) = child_id {
let child_data = &node_data_container[cid];
if matches!(child_data.get_node_type(), NodeType::Text(_)) {
// Found a text child - check its cursor property
let child_cursor = styled_dom.get_css_property_cache().get_cursor(...);
if let Some(child_cursor_prop) = child_cursor {
cursor_icon = translate_cursor(css_cursor);
break;
}
}
child_id = node_hierarchy[cid].next_sibling_id();
}
}
Problem: Dieser Hack prüft alle Kinder des Containers, nicht nur den Bereich unter dem Mauszeiger. Wenn Body einen Text-Child hat, zeigt der gesamte Body den I-Beam.
Ort: core/src/prop_cache.rs#L909-L947
let node_has_selectable_text = {
// Check if this node has immediate text children
let has_text_children = { /* ... prüft ob Container Text-Kinder hat */ };
if has_text_children {
// Check user-select property on this container
let user_select = self.get_user_select(&node_data, &node_id, &default_node_state)...;
!matches!(user_select, StyleUserSelect::None)
} else {
false
}
};
if node_has_selectable_text {
node_should_have_tag = true; // Container bekommt Tag, nicht der Text
}
Problem: Der Container bekommt den Hit-Test-Tag, nicht die einzelnen Text-Nodes. Das verhindert präzises Hit-Testing auf Text-Ebene.
Ort: core/src/ua_css.rs
/* Impliziter Cursor für Text */
Text { cursor: text; }
Problem: Diese Regel ist korrekt, aber nutzlos weil Text-Nodes keinen Hit-Test-Bereich haben.
Status: ✅ BESTÄTIGT
Aus layout/src/text3/cache.rs#L811-L817:
#[derive(Debug, Clone, Hash)]
pub struct StyledRun {
pub text: String,
pub style: Arc<StyleProperties>,
pub logical_start_byte: usize,
// ❌ KEIN source_node_id Feld!
}
Status: ✅ BESTÄTIGT
Aus layout/src/solver3/fc.rs#L4800:
content.push(InlineContent::Text(StyledRun {
text: text_content.to_string(),
style: Arc::new(get_style_properties(ctx.styled_dom, dom_id)),
logical_start_byte: 0,
// ❌ dom_id wird NICHT in StyledRun gespeichert!
}));
Status: ✅ BESTÄTIGT
Aus layout/src/text3/glyphs.rs#L32-L52:
pub struct SimpleGlyphRun {
pub glyphs: Vec<GlyphInstance>,
pub color: ColorU,
pub background_color: Option<ColorU>,
pub font_hash: u64,
pub font_size_px: f32,
// ❌ KEIN source_node_id Feld!
}
Status: ✅ BESTÄTIGT
Die DisplayListItem enum hat keinen Variant für Text-Hit-Testing. Text wird nur zum Rendering ausgegeben, ohne Hit-Test-Informationen.
Datei: layout/src/text3/cache.rs#L811
#[derive(Debug, Clone, Hash)]
pub struct StyledRun {
pub text: String,
pub style: Arc<StyleProperties>,
pub logical_start_byte: usize,
// NEU: Optional weil nicht alle Runs aus DOM kommen (z.B. list markers)
pub source_node_id: Option<NodeId>,
}
Datei: layout/src/solver3/fc.rs#L4800
content.push(InlineContent::Text(StyledRun {
text: text_content.to_string(),
style: Arc::new(get_style_properties(ctx.styled_dom, dom_id)),
logical_start_byte: 0,
source_node_id: Some(dom_id), // NEU
}));
Datei: layout/src/text3/glyphs.rs#L32
pub struct SimpleGlyphRun {
pub glyphs: Vec<GlyphInstance>,
pub color: ColorU,
// ... existing fields ...
pub source_node_id: Option<NodeId>, // NEU
}
Datei: layout/src/solver3/display_list.rs
Neue DisplayListItem Variante:
pub enum DisplayListItem {
// ... existing variants ...
/// Hit-test area for text selection and cursor resolution
TextHitArea {
bounds: LogicalRect,
dom_id: DomId,
node_id: NodeId,
text_run_index: u16,
},
}
Datei: layout/src/hit_test.rs#L70
impl CursorTypeHitTest {
pub fn new(hit_test: &FullHitTest, layout_window: &LayoutWindow) -> Self {
// 1. Suche zuerst in TAG_TYPE_CURSOR namespace (direkte Text-Hits)
// 2. Falls nicht gefunden, suche in TAG_TYPE_DOM_NODE (Container mit cursor property)
// 3. Kein Text-Child-Detection mehr nötig!
}
}
Datei: core/src/prop_cache.rs#L909
// Statt Container mit Text-Kindern zu taggen:
// ❌ if node_has_selectable_text { node_should_have_tag = true; }
// Direkt prüfen ob dieser Node ein Text ist:
// ✅ if matches!(node_data.get_node_type(), NodeType::Text(_)) {
// node_should_have_tag = true;
// }
| # | Datei | Änderung | Risiko |
|---|---|---|---|
| 1 | layout/src/text3/cache.rs | source_node_id zu StyledRun hinzufügen | Niedrig |
| 2 | layout/src/solver3/fc.rs | NodeId beim Erstellen von StyledRun übergeben | Niedrig |
| 3 | layout/src/text3/glyphs.rs | source_node_id zu SimpleGlyphRun hinzufügen | Niedrig |
| 4 | layout/src/solver3/display_list.rs | TextHitArea Item generieren | Mittel |
| 5 | layout/src/hit_test.rs | Text-Child-Hack entfernen | Hoch |
| 6 | core/src/prop_cache.rs | Tag Assignment korrigieren | Hoch |
cargo test in allen CratesDie Gemini-Analyse ist korrekt. Das Kernproblem ist:
Text-Nodes (
InlineContent::Text) erzeugen keine Hit-Test-Bereiche im WebRender Display-List.
Die existierenden "Hacks" (Text-Child-Detection in hit_test.rs, selectable-text in prop_cache.rs) sind Symptom-Behandlungen, die das Grundproblem nicht lösen.
Option<T> hinzufügen#[cfg(feature = "text_hittest")] verstecken bis stabil// layout/src/text3/cache.rs:811
pub struct StyledRun {
pub text: String,
pub style: Arc<StyleProperties>,
pub logical_start_byte: usize,
}
// layout/src/solver3/fc.rs:4800
content.push(InlineContent::Text(StyledRun {
text: text_content.to_string(),
style: Arc::new(get_style_properties(ctx.styled_dom, dom_id)),
logical_start_byte: 0,
}));
// layout/src/text3/glyphs.rs:32
pub struct SimpleGlyphRun {
pub glyphs: Vec<GlyphInstance>,
pub color: ColorU,
pub background_color: Option<ColorU>,
pub font_hash: u64,
pub font_size_px: f32,
pub text_decoration: TextDecoration,
pub is_ime_preview: bool,
}
// layout/src/hit_test.rs:87-118
// Wenn Container keinen expliziten Cursor hat, prüfe Text-Kinder
let hier = &node_hierarchy[*node_id];
if let Some(first_child) = hier.first_child_id(*node_id) {
// ... iteriert durch alle Kinder und prüft auf Text
}
// core/src/prop_cache.rs:909-947
let node_has_selectable_text = {
let has_text_children = { /* ... */ };
if has_text_children { /* ... */ }
};
if node_has_selectable_text {
node_should_have_tag = true;
}