Files
claude-code-mirror/claude-code源码-中文注释/src/ink/hit-test.ts
2026-04-03 13:01:19 +08:00

130 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { DOMElement } from './dom.js'
import { ClickEvent } from './events/click-event.js'
import type { EventHandlerProps } from './events/event-handlers.js'
import { nodeCache } from './node-cache.js'
/**
* 找到其渲染矩形包含 (col, row) 的最深的 DOM 元素。
*
* 使用 renderNodeToOutput 填充的 nodeCache — 矩形在屏幕
* 坐标中,所有偏移(包括 scrollTop 平移)已应用。
* 子元素以逆序遍历,以便后期兄弟元素(画在上面)获胜。
* 不在 nodeCache 中的节点(此帧未渲染,或缺少 yogaNode
* 及其子树被跳过。
*
* 即使命中节点没有 onClick 也返回命中节点 — dispatchClick 通过
* parentNode 向上查找处理器。
*/
export function hitTest(
node: DOMElement,
col: number,
row: number,
): DOMElement | null {
const rect = nodeCache.get(node)
if (!rect) return null
if (
col < rect.x ||
col >= rect.x + rect.width ||
row < rect.y ||
row >= rect.y + rect.height
) {
return null
}
// 后期兄弟元素画在上面;逆序遍历返回最顶层命中。
for (let i = node.childNodes.length - 1; i >= 0; i--) {
const child = node.childNodes[i]!
if (child.nodeName === '#text') continue
const hit = hitTest(child, col, row)
if (hit) return hit
}
return node
}
/**
* 在 (col, row) 处对根进行命中测试,并将 ClickEvent 从最深的
* 包含节点向上冒泡到 parentNode。只有具有 onClick 处理器的节点触发。
* 当处理器调用 stopImmediatePropagation() 时停止。如果至少一个
* onClick 处理器触发则返回 true。
*/
export function dispatchClick(
root: DOMElement,
col: number,
row: number,
cellIsBlank = false,
): boolean {
let target: DOMElement | undefined = hitTest(root, col, row) ?? undefined
if (!target) return false
// 点击转焦点:找到最近的 focusable 祖先并聚焦它。
// root 始终是 ink-root它拥有 FocusManager。
if (root.focusManager) {
let focusTarget: DOMElement | undefined = target
while (focusTarget) {
if (typeof focusTarget.attributes['tabIndex'] === 'number') {
root.focusManager.handleClickFocus(focusTarget)
break
}
focusTarget = focusTarget.parentNode
}
}
const event = new ClickEvent(col, row, cellIsBlank)
let handled = false
while (target) {
const handler = target._eventHandlers?.onClick as
| ((event: ClickEvent) => void)
| undefined
if (handler) {
handled = true
const rect = nodeCache.get(target)
if (rect) {
event.localCol = col - rect.x
event.localRow = row - rect.y
}
handler(event)
if (event.didStopImmediatePropagation()) return true
}
target = target.parentNode
}
return handled
}
/**
* 当指针移动时触发 onMouseEnter/onMouseLeave。类似于 DOM
* mouseenter/mouseleave不冒泡 — 在子元素之间移动不会
* 在父元素上重新触发。从命中节点向上遍历,收集每个
* 有悬停处理器的祖先;与先前悬停集合对比;
* 对退出的节点触发 leave对进入的节点触发 enter。
*
* 就地改变 `hovered`以便调用者App 实例)可以跨调用保留它。
* 当命中为空时清除集合(光标移动到未渲染的间隙或根矩形之外)。
*/
export function dispatchHover(
root: DOMElement,
col: number,
row: number,
hovered: Set<DOMElement>,
): void {
const next = new Set<DOMElement>()
let node: DOMElement | undefined = hitTest(root, col, row) ?? undefined
while (node) {
const h = node._eventHandlers as EventHandlerProps | undefined
if (h?.onMouseEnter || h?.onMouseLeave) next.add(node)
node = node.parentNode
}
for (const old of hovered) {
if (!next.has(old)) {
hovered.delete(old)
// 跳过已分离节点上的处理器(在鼠标事件之间移除)
if (old.parentNode) {
;(old._eventHandlers as EventHandlerProps | undefined)?.onMouseLeave?.()
}
}
}
for (const n of next) {
if (!hovered.has(n)) {
hovered.add(n)
;(n._eventHandlers as EventHandlerProps | undefined)?.onMouseEnter?.()
}
}
}