mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
feat: add scrollbar and fix selection on scroll
This commit is contained in:
parent
9804aa7443
commit
52c11d172a
10 changed files with 397 additions and 126 deletions
|
|
@ -5,6 +5,7 @@ import { logForDebugging } from '../../utils/debug.js'
|
|||
import { stopCapturingEarlyInput } from '../../utils/earlyInput.js'
|
||||
import { isMouseClicksDisabled } from '../../utils/fullscreen.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import type { DOMElement } from '../dom.js'
|
||||
import { EventEmitter } from '../events/emitter.js'
|
||||
import { InputEvent } from '../events/input-event.js'
|
||||
import { TerminalFocusEvent } from '../events/terminal-focus-event.js'
|
||||
|
|
@ -67,6 +68,9 @@ type Props = {
|
|||
// No-op (returns false) outside fullscreen mode (Ink.dispatchClick
|
||||
// gates on altScreenActive).
|
||||
readonly onClickAt: (col: number, row: number) => boolean
|
||||
readonly onMouseDownAt: (col: number, row: number, button: number) => DOMElement | undefined
|
||||
readonly onMouseUpAt: (target: DOMElement, col: number, row: number, button: number) => void
|
||||
readonly onMouseDragAt: (target: DOMElement, col: number, row: number, button: number) => void
|
||||
// Dispatch hover (onMouseEnter/onMouseLeave) as the pointer moves over
|
||||
// DOM elements. Called for mode-1003 motion events with no button held.
|
||||
// No-op outside fullscreen (Ink.dispatchHover gates on altScreenActive).
|
||||
|
|
@ -155,6 +159,7 @@ export default class App extends PureComponent<Props, State> {
|
|||
// repeat events (drag-then-release at same cell, etc.).
|
||||
lastHoverCol = -1
|
||||
lastHoverRow = -1
|
||||
mouseCaptureTarget: DOMElement | undefined
|
||||
|
||||
// Timestamp of last stdin chunk. Used to detect long gaps (tmux attach,
|
||||
// ssh reconnect, laptop wake) and trigger terminal mode re-assert.
|
||||
|
|
@ -578,6 +583,11 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
|||
|
||||
if (m.action === 'press') {
|
||||
if ((m.button & 0x20) !== 0 && baseButton === 3) {
|
||||
if (app.mouseCaptureTarget) {
|
||||
app.props.onMouseUpAt(app.mouseCaptureTarget, col, row, baseButton)
|
||||
app.mouseCaptureTarget = undefined
|
||||
}
|
||||
|
||||
// Mode-1003 motion with no button held. Dispatch hover; skip the
|
||||
// rest of this handler (no selection, no click-count side effects).
|
||||
// Lost-release recovery: no-button motion while isDragging=true means
|
||||
|
|
@ -611,6 +621,12 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
|||
}
|
||||
|
||||
if ((m.button & 0x20) !== 0) {
|
||||
if (app.mouseCaptureTarget) {
|
||||
app.props.onMouseDragAt(app.mouseCaptureTarget, col, row, baseButton)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Drag motion: mode-aware extension (char/word/line). onSelectionDrag
|
||||
// calls notifySelectionChange internally — no extra onSelectionChange.
|
||||
app.props.onSelectionDrag(col, row)
|
||||
|
|
@ -628,6 +644,15 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
|||
app.props.onSelectionChange()
|
||||
}
|
||||
|
||||
const capture = app.props.onMouseDownAt(col, row, baseButton)
|
||||
|
||||
if (capture) {
|
||||
app.mouseCaptureTarget = capture
|
||||
app.clickCount = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Fresh left press. Detect multi-click HERE (not on release) so the
|
||||
// word/line highlight appears immediately and a subsequent drag can
|
||||
// extend by word/line like native macOS. Previously detected on
|
||||
|
|
@ -677,6 +702,13 @@ export function handleMouseEvent(app: App, m: ParsedMouse): void {
|
|||
// isDragging=true and leave drag-to-scroll's timer running until the
|
||||
// scroll boundary. Only act on non-left releases when we ARE dragging
|
||||
// (so an unrelated middle/right click-release doesn't touch selection).
|
||||
if (app.mouseCaptureTarget) {
|
||||
app.props.onMouseUpAt(app.mouseCaptureTarget, col, row, baseButton)
|
||||
app.mouseCaptureTarget = undefined
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (baseButton !== 0) {
|
||||
if (!sel.isDragging) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { DOMElement } from '../dom.js'
|
|||
import type { ClickEvent } from '../events/click-event.js'
|
||||
import type { FocusEvent } from '../events/focus-event.js'
|
||||
import type { KeyboardEvent } from '../events/keyboard-event.js'
|
||||
import type { MouseEvent } from '../events/mouse-event.js'
|
||||
import type { Styles } from '../styles.js'
|
||||
import * as warn from '../warn.js'
|
||||
export type Props = Except<Styles, 'textWrap'> & {
|
||||
|
|
@ -31,6 +32,9 @@ export type Props = Except<Styles, 'textWrap'> & {
|
|||
* ancestors; call `event.stopImmediatePropagation()` to stop bubbling.
|
||||
*/
|
||||
onClick?: (event: ClickEvent) => void
|
||||
onMouseDown?: (event: MouseEvent) => void
|
||||
onMouseUp?: (event: MouseEvent) => void
|
||||
onMouseDrag?: (event: MouseEvent) => void
|
||||
onFocus?: (event: FocusEvent) => void
|
||||
onFocusCapture?: (event: FocusEvent) => void
|
||||
onBlur?: (event: FocusEvent) => void
|
||||
|
|
@ -52,7 +56,7 @@ export type Props = Except<Styles, 'textWrap'> & {
|
|||
* `<Box>` is an essential Ink component to build your layout. It's like `<div style="display: flex">` in the browser.
|
||||
*/
|
||||
function Box(t0: Props) {
|
||||
const $ = _c(42)
|
||||
const $ = _c(48)
|
||||
let autoFocus
|
||||
let children
|
||||
let flexDirection
|
||||
|
|
@ -66,8 +70,11 @@ function Box(t0: Props) {
|
|||
let onFocusCapture
|
||||
let onKeyDown
|
||||
let onKeyDownCapture
|
||||
let onMouseDown
|
||||
let onMouseDrag
|
||||
let onMouseEnter
|
||||
let onMouseLeave
|
||||
let onMouseUp
|
||||
let ref
|
||||
let style
|
||||
let tabIndex
|
||||
|
|
@ -87,11 +94,14 @@ function Box(t0: Props) {
|
|||
onFocusCapture: t11,
|
||||
onBlur: t12,
|
||||
onBlurCapture: t13,
|
||||
onMouseEnter: t14,
|
||||
onMouseLeave: t15,
|
||||
onKeyDown: t16,
|
||||
onKeyDownCapture: t17,
|
||||
...t18
|
||||
onMouseDown: t14,
|
||||
onMouseUp: t15,
|
||||
onMouseDrag: t16,
|
||||
onMouseEnter: t17,
|
||||
onMouseLeave: t18,
|
||||
onKeyDown: t19,
|
||||
onKeyDownCapture: t20,
|
||||
...t21
|
||||
} = t0
|
||||
|
||||
children = t1
|
||||
|
|
@ -103,11 +113,14 @@ function Box(t0: Props) {
|
|||
onFocusCapture = t11
|
||||
onBlur = t12
|
||||
onBlurCapture = t13
|
||||
onMouseEnter = t14
|
||||
onMouseLeave = t15
|
||||
onKeyDown = t16
|
||||
onKeyDownCapture = t17
|
||||
style = t18
|
||||
onMouseDown = t14
|
||||
onMouseUp = t15
|
||||
onMouseDrag = t16
|
||||
onMouseEnter = t17
|
||||
onMouseLeave = t18
|
||||
onKeyDown = t19
|
||||
onKeyDownCapture = t20
|
||||
style = t21
|
||||
flexWrap = t2 === undefined ? 'nowrap' : t2
|
||||
flexDirection = t3 === undefined ? 'row' : t3
|
||||
flexGrow = t4 === undefined ? 0 : t4
|
||||
|
|
@ -143,11 +156,14 @@ function Box(t0: Props) {
|
|||
$[11] = onFocusCapture
|
||||
$[12] = onKeyDown
|
||||
$[13] = onKeyDownCapture
|
||||
$[14] = onMouseEnter
|
||||
$[15] = onMouseLeave
|
||||
$[16] = ref
|
||||
$[17] = style
|
||||
$[18] = tabIndex
|
||||
$[14] = onMouseDown
|
||||
$[15] = onMouseUp
|
||||
$[16] = onMouseDrag
|
||||
$[17] = onMouseEnter
|
||||
$[18] = onMouseLeave
|
||||
$[19] = ref
|
||||
$[20] = style
|
||||
$[21] = tabIndex
|
||||
} else {
|
||||
autoFocus = $[1]
|
||||
children = $[2]
|
||||
|
|
@ -162,11 +178,14 @@ function Box(t0: Props) {
|
|||
onFocusCapture = $[11]
|
||||
onKeyDown = $[12]
|
||||
onKeyDownCapture = $[13]
|
||||
onMouseEnter = $[14]
|
||||
onMouseLeave = $[15]
|
||||
ref = $[16]
|
||||
style = $[17]
|
||||
tabIndex = $[18]
|
||||
onMouseDown = $[14]
|
||||
onMouseUp = $[15]
|
||||
onMouseDrag = $[16]
|
||||
onMouseEnter = $[17]
|
||||
onMouseLeave = $[18]
|
||||
ref = $[19]
|
||||
style = $[20]
|
||||
tabIndex = $[21]
|
||||
}
|
||||
|
||||
const t1 = style.overflowX ?? style.overflow ?? 'visible'
|
||||
|
|
@ -174,13 +193,13 @@ function Box(t0: Props) {
|
|||
let t3
|
||||
|
||||
if (
|
||||
$[19] !== flexDirection ||
|
||||
$[20] !== flexGrow ||
|
||||
$[21] !== flexShrink ||
|
||||
$[22] !== flexWrap ||
|
||||
$[23] !== style ||
|
||||
$[24] !== t1 ||
|
||||
$[25] !== t2
|
||||
$[22] !== flexDirection ||
|
||||
$[23] !== flexGrow ||
|
||||
$[24] !== flexShrink ||
|
||||
$[25] !== flexWrap ||
|
||||
$[26] !== style ||
|
||||
$[27] !== t1 ||
|
||||
$[28] !== t2
|
||||
) {
|
||||
t3 = {
|
||||
flexWrap,
|
||||
|
|
@ -191,35 +210,38 @@ function Box(t0: Props) {
|
|||
overflowX: t1,
|
||||
overflowY: t2
|
||||
}
|
||||
$[19] = flexDirection
|
||||
$[20] = flexGrow
|
||||
$[21] = flexShrink
|
||||
$[22] = flexWrap
|
||||
$[23] = style
|
||||
$[24] = t1
|
||||
$[25] = t2
|
||||
$[26] = t3
|
||||
$[22] = flexDirection
|
||||
$[23] = flexGrow
|
||||
$[24] = flexShrink
|
||||
$[25] = flexWrap
|
||||
$[26] = style
|
||||
$[27] = t1
|
||||
$[28] = t2
|
||||
$[29] = t3
|
||||
} else {
|
||||
t3 = $[26]
|
||||
t3 = $[29]
|
||||
}
|
||||
|
||||
let t4
|
||||
|
||||
if (
|
||||
$[27] !== autoFocus ||
|
||||
$[28] !== children ||
|
||||
$[29] !== onBlur ||
|
||||
$[30] !== onBlurCapture ||
|
||||
$[31] !== onClick ||
|
||||
$[32] !== onFocus ||
|
||||
$[33] !== onFocusCapture ||
|
||||
$[34] !== onKeyDown ||
|
||||
$[35] !== onKeyDownCapture ||
|
||||
$[36] !== onMouseEnter ||
|
||||
$[37] !== onMouseLeave ||
|
||||
$[38] !== ref ||
|
||||
$[39] !== t3 ||
|
||||
$[40] !== tabIndex
|
||||
$[30] !== autoFocus ||
|
||||
$[31] !== children ||
|
||||
$[32] !== onBlur ||
|
||||
$[33] !== onBlurCapture ||
|
||||
$[34] !== onClick ||
|
||||
$[35] !== onFocus ||
|
||||
$[36] !== onFocusCapture ||
|
||||
$[37] !== onKeyDown ||
|
||||
$[38] !== onKeyDownCapture ||
|
||||
$[39] !== onMouseDown ||
|
||||
$[40] !== onMouseUp ||
|
||||
$[41] !== onMouseDrag ||
|
||||
$[42] !== onMouseEnter ||
|
||||
$[43] !== onMouseLeave ||
|
||||
$[44] !== ref ||
|
||||
$[45] !== t3 ||
|
||||
$[46] !== tabIndex
|
||||
) {
|
||||
t4 = (
|
||||
<ink-box
|
||||
|
|
@ -231,8 +253,11 @@ function Box(t0: Props) {
|
|||
onFocusCapture={onFocusCapture}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseDrag={onMouseDrag}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseUp={onMouseUp}
|
||||
ref={ref}
|
||||
style={t3}
|
||||
tabIndex={tabIndex}
|
||||
|
|
@ -240,23 +265,26 @@ function Box(t0: Props) {
|
|||
{children}
|
||||
</ink-box>
|
||||
)
|
||||
$[27] = autoFocus
|
||||
$[28] = children
|
||||
$[29] = onBlur
|
||||
$[30] = onBlurCapture
|
||||
$[31] = onClick
|
||||
$[32] = onFocus
|
||||
$[33] = onFocusCapture
|
||||
$[34] = onKeyDown
|
||||
$[35] = onKeyDownCapture
|
||||
$[36] = onMouseEnter
|
||||
$[37] = onMouseLeave
|
||||
$[38] = ref
|
||||
$[39] = t3
|
||||
$[40] = tabIndex
|
||||
$[41] = t4
|
||||
$[30] = autoFocus
|
||||
$[31] = children
|
||||
$[32] = onBlur
|
||||
$[33] = onBlurCapture
|
||||
$[34] = onClick
|
||||
$[35] = onFocus
|
||||
$[36] = onFocusCapture
|
||||
$[37] = onKeyDown
|
||||
$[38] = onKeyDownCapture
|
||||
$[39] = onMouseDown
|
||||
$[40] = onMouseUp
|
||||
$[41] = onMouseDrag
|
||||
$[42] = onMouseEnter
|
||||
$[43] = onMouseLeave
|
||||
$[44] = ref
|
||||
$[45] = t3
|
||||
$[46] = tabIndex
|
||||
$[47] = t4
|
||||
} else {
|
||||
t4 = $[41]
|
||||
t4 = $[47]
|
||||
}
|
||||
|
||||
return t4
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ClickEvent } from './click-event.js'
|
||||
import type { FocusEvent } from './focus-event.js'
|
||||
import type { KeyboardEvent } from './keyboard-event.js'
|
||||
import type { MouseEvent } from './mouse-event.js'
|
||||
import type { PasteEvent } from './paste-event.js'
|
||||
import type { ResizeEvent } from './resize-event.js'
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ type FocusEventHandler = (event: FocusEvent) => void
|
|||
type PasteEventHandler = (event: PasteEvent) => void
|
||||
type ResizeEventHandler = (event: ResizeEvent) => void
|
||||
type ClickEventHandler = (event: ClickEvent) => void
|
||||
type MouseEventHandler = (event: MouseEvent) => void
|
||||
type HoverEventHandler = () => void
|
||||
|
||||
/**
|
||||
|
|
@ -33,6 +35,9 @@ export type EventHandlerProps = {
|
|||
onResize?: ResizeEventHandler
|
||||
|
||||
onClick?: ClickEventHandler
|
||||
onMouseDown?: MouseEventHandler
|
||||
onMouseUp?: MouseEventHandler
|
||||
onMouseDrag?: MouseEventHandler
|
||||
onMouseEnter?: HoverEventHandler
|
||||
onMouseLeave?: HoverEventHandler
|
||||
}
|
||||
|
|
@ -50,7 +55,10 @@ export const HANDLER_FOR_EVENT: Record<
|
|||
blur: { bubble: 'onBlur', capture: 'onBlurCapture' },
|
||||
paste: { bubble: 'onPaste', capture: 'onPasteCapture' },
|
||||
resize: { bubble: 'onResize' },
|
||||
click: { bubble: 'onClick' }
|
||||
click: { bubble: 'onClick' },
|
||||
mousedown: { bubble: 'onMouseDown' },
|
||||
mouseup: { bubble: 'onMouseUp' },
|
||||
mousedrag: { bubble: 'onMouseDrag' }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +76,9 @@ export const EVENT_HANDLER_PROPS = new Set<string>([
|
|||
'onPasteCapture',
|
||||
'onResize',
|
||||
'onClick',
|
||||
'onMouseDown',
|
||||
'onMouseUp',
|
||||
'onMouseDrag',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave'
|
||||
])
|
||||
|
|
|
|||
18
ui-tui/packages/hermes-ink/src/ink/events/mouse-event.ts
Normal file
18
ui-tui/packages/hermes-ink/src/ink/events/mouse-event.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Event } from './event.js'
|
||||
|
||||
export class MouseEvent extends Event {
|
||||
readonly col: number
|
||||
readonly row: number
|
||||
localCol = 0
|
||||
localRow = 0
|
||||
readonly cellIsBlank: boolean
|
||||
readonly button: number
|
||||
|
||||
constructor(col: number, row: number, cellIsBlank: boolean, button: number) {
|
||||
super()
|
||||
this.col = col
|
||||
this.row = row
|
||||
this.cellIsBlank = cellIsBlank
|
||||
this.button = button
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import type { DOMElement } from './dom.js'
|
||||
import { ClickEvent } from './events/click-event.js'
|
||||
import type { EventHandlerProps } from './events/event-handlers.js'
|
||||
import { MouseEvent } from './events/mouse-event.js'
|
||||
import { nodeCache } from './node-cache.js'
|
||||
|
||||
/**
|
||||
|
|
@ -101,6 +102,51 @@ export function dispatchClick(root: DOMElement, col: number, row: number, cellIs
|
|||
return handled
|
||||
}
|
||||
|
||||
type MouseHandler = 'onMouseDown' | 'onMouseUp' | 'onMouseDrag'
|
||||
|
||||
export function dispatchMouse(
|
||||
root: DOMElement,
|
||||
col: number,
|
||||
row: number,
|
||||
handlerName: MouseHandler,
|
||||
button: number,
|
||||
cellIsBlank = false,
|
||||
target?: DOMElement
|
||||
): DOMElement | undefined {
|
||||
let node: DOMElement | undefined = target ?? hitTest(root, col, row) ?? undefined
|
||||
|
||||
if (!node) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const event = new MouseEvent(col, row, cellIsBlank, button)
|
||||
let handled: DOMElement | undefined
|
||||
|
||||
while (node) {
|
||||
const handler = node._eventHandlers?.[handlerName] as ((event: MouseEvent) => void) | undefined
|
||||
|
||||
if (handler) {
|
||||
handled ??= node
|
||||
const rect = nodeCache.get(node)
|
||||
|
||||
if (rect) {
|
||||
event.localCol = col - rect.x
|
||||
event.localRow = row - rect.y
|
||||
}
|
||||
|
||||
handler(event)
|
||||
|
||||
if (event.didStopImmediatePropagation()) {
|
||||
return handled
|
||||
}
|
||||
}
|
||||
|
||||
node = node.parentNode
|
||||
}
|
||||
|
||||
return handled
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onMouseEnter/onMouseLeave as the pointer moves. Like DOM
|
||||
* mouseenter/mouseleave: does NOT bubble — moving between children does
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import * as dom from './dom.js'
|
|||
import { KeyboardEvent } from './events/keyboard-event.js'
|
||||
import { FocusManager } from './focus.js'
|
||||
import { emptyFrame, type Frame, type FrameEvent } from './frame.js'
|
||||
import { dispatchClick, dispatchHover } from './hit-test.js'
|
||||
import { dispatchClick, dispatchHover, dispatchMouse } from './hit-test.js'
|
||||
import instances from './instances.js'
|
||||
import { LogUpdate } from './log-update.js'
|
||||
import { nodeCache } from './node-cache.js'
|
||||
|
|
@ -1538,6 +1538,42 @@ export default class Ink {
|
|||
|
||||
return dispatchClick(this.rootNode, col, row, blank)
|
||||
}
|
||||
dispatchMouseDown(col: number, row: number, button: number): dom.DOMElement | undefined {
|
||||
if (!this.altScreenActive) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return dispatchMouse(
|
||||
this.rootNode,
|
||||
col,
|
||||
row,
|
||||
'onMouseDown',
|
||||
button,
|
||||
isEmptyCellAt(this.frontFrame.screen, col, row)
|
||||
)
|
||||
}
|
||||
dispatchMouseUp(target: dom.DOMElement, col: number, row: number, button: number): void {
|
||||
if (!this.altScreenActive) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatchMouse(this.rootNode, col, row, 'onMouseUp', button, isEmptyCellAt(this.frontFrame.screen, col, row), target)
|
||||
}
|
||||
dispatchMouseDrag(target: dom.DOMElement, col: number, row: number, button: number): void {
|
||||
if (!this.altScreenActive) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatchMouse(
|
||||
this.rootNode,
|
||||
col,
|
||||
row,
|
||||
'onMouseDrag',
|
||||
button,
|
||||
isEmptyCellAt(this.frontFrame.screen, col, row),
|
||||
target
|
||||
)
|
||||
}
|
||||
dispatchHover(col: number, row: number): void {
|
||||
if (!this.altScreenActive) {
|
||||
return
|
||||
|
|
@ -1764,6 +1800,9 @@ export default class Ink {
|
|||
onCursorDeclaration={this.setCursorDeclaration}
|
||||
onExit={this.unmount}
|
||||
onHoverAt={this.dispatchHover}
|
||||
onMouseDownAt={this.dispatchMouseDown}
|
||||
onMouseDragAt={this.dispatchMouseDrag}
|
||||
onMouseUpAt={this.dispatchMouseUp}
|
||||
onMultiClick={this.handleMultiClick}
|
||||
onOpenHyperlink={this.openHyperlink}
|
||||
onSelectionChange={this.notifySelectionChange}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue