Skip to content

rhwp-studio: 텍스트 드래그 선택 중 커서와 스크롤 위치가 튀는 현상 #661

@postmelee

Description

@postmelee

개요

rhwp-studio에서 텍스트를 드래그 선택할 때 선택 하이라이트가 텍스트 영역 밖으로 길게 튀는 문제는 #658 에서 별도로 수정 중이다. 다만 수정 후 확인 과정에서, 특정 위치에서 드래그 선택을 시작하거나 이어갈 때 커서 위치와 페이지 스크롤 위치가 순간적으로 튀는 별도 증상이 확인되었다.

이 이슈는 #658의 selection rect overflow 문제가 아니라, 드래그 중 hitTest/caret rect/pageIndex/auto-scroll 상호작용으로 보이는 스크롤 점프 문제를 추적한다.

2026-05-07.15.06.50.mov

관찰 환경

  • 앱: rhwp-studio web
  • 문서: samples/exam_social.hwp
  • URL: 로컬 Vite dev server (http://127.0.0.1:7701/)
  • 확대율: 120%
  • 관찰 위치: 2/4쪽, 구역 2/2, 사회탐구 영역 페이지 하단 좌측 텍스트 박스 부근
  • 상황: 하단 좌측 박스 문단에서 텍스트 드래그 선택을 시작/진행할 때, 선택 영역 자체는 텍스트 범위 안에 머물지만 커서 또는 페이지 스크롤 위치가 순간적으로 위/아래로 튀는 것처럼 보임

재현 절차

  1. rhwp-studio dev server를 실행한다.
  2. samples/exam_social.hwp를 연다.
  3. 확대율을 120%로 설정한다.
  4. 2/4쪽, 구역 2/2의 사회탐구 영역 페이지로 이동한다.
  5. 하단 좌측 7번 문항의 박스 내부 문단에서 텍스트를 드래그 선택한다.
  6. 드래그 시작점 또는 진행 방향에 따라 커서/스크롤 위치가 순간적으로 튀는지 확인한다.

기대 동작

  • 드래그 중 포인터 위치에 대응하는 텍스트 선택만 갱신되어야 한다.
  • 포인터가 편집 영역 상/하단 auto-scroll 영역에 접근하지 않았다면 scrollTop이 임의로 바뀌지 않아야 한다.
  • hitTest가 산출한 페이지와 caret rect 재계산 결과가 달라져도 현재 드래그 위치를 다른 페이지로 해석하면 안 된다.

실제 동작

[CursorState] 캐럿 페이지 불일치 (WASM=0, hitTest=1) -> hitTest 폴백

이 경고는 hitTest가 반환한 cursorRect.pageIndexCursorState.updateRect()getCursorRect*로 재계산한 pageIndex가 불일치하는 경로가 있음을 시사한다.

의심 지점

1. 드래그 중 rAF 내부에서 원본 MouseEvent를 뒤늦게 재해석

rhwp-studio/src/engine/input-handler-mouse.ts

if (this.isDragging) {
  if (this.dragRafId) return;
  this.dragRafId = requestAnimationFrame(() => {
    this.dragRafId = 0;
    if (!this.isDragging) return;
    const hit = this.hitTestFromEvent(e);
    if (hit && hit.paragraphIndex < 0xFFFFFF00) {
      this.cursor.moveTo(hit);
      this.updateCaretDuringDrag();
    }
  });
  return;
}

MouseEventclientX/clientY는 화면 좌표인데, rAF 실행 전후에 scrollTop이 바뀌면 hitTestFromEvent(e)가 같은 포인터 좌표를 다른 문서 좌표로 해석할 가능성이 있다.

2. 드래그 중에도 caret 기준 자동 스크롤을 매 프레임 수행

rhwp-studio/src/engine/input-handler.ts

private updateCaretDuringDrag(): void {
  ...
  this.caret.updateLive(rect, zoom);
  this.scrollCaretIntoView(rect);
  ...
}

scrollCaretIntoView()는 caret rect가 화면 margin 밖이라고 판단하면 즉시 container.scrollTop을 변경한다.

if (caretDocY < scrollTop + margin) {
  this.container.scrollTop = Math.max(0, caretDocY - margin);
} else if (caretDocY + caretHeight > scrollTop + viewHeight - margin) {
  this.container.scrollTop = caretDocY + caretHeight - viewHeight + margin;
}

텍스트 드래그 선택 중에는 일반 커서 이동과 달리 포인터가 편집 영역 상/하단에 가까울 때만 auto-scroll하는 것이 더 안전해 보인다.

3. CursorState.updateRect()의 pageIndex 불일치 폴백

rhwp-studio/src/engine/cursor.ts

if (this.rect && this.position.cursorRect &&
    this.rect.pageIndex !== this.position.cursorRect.pageIndex) {
  console.warn('[CursorState] 캐럿 페이지 불일치 (WASM=%d, hitTest=%d) -> hitTest 폴백',
    this.rect.pageIndex, this.position.cursorRect.pageIndex);
  this.rect = { ...this.position.cursorRect };
}

JS 레벨에서는 hitTest 폴백이 있으나, 이 경고가 드래그 중 반복되면 caret rect와 auto-scroll 판단이 불안정해질 수 있다.

4. Rust getCursorRect*가 현재 page hint 없이 후보 페이지를 앞에서부터 탐색

src/document_core/queries/cursor_rect.rs

  • get_cursor_rect_native()find_pages_for_paragraph() 결과를 앞에서부터 순회한다.
  • get_cursor_rect_in_cell_native()도 표가 포함된 본문 문단의 후보 페이지를 앞에서부터 순회한다.

동일 문단/표가 여러 페이지 후보에 걸리는 경우, 현재 마우스가 위치한 pageIndex와 다른 페이지의 rect를 먼저 반환할 가능성이 있다.

예상 해결 방향

  1. 드래그 선택 중에는 scrollCaretIntoView()를 기본 호출하지 않는다.
  2. 대신 포인터가 #scroll-container 상/하단 일정 margin 안으로 들어왔을 때만 명시적 auto-scroll을 수행한다.
  3. rAF에서는 원본 MouseEvent를 다시 해석하지 말고, mousemove 발생 시점의 clientX/clientY 또는 hit 결과를 별도 값으로 고정한 뒤 렌더만 지연한다.
  4. hitTest 결과에 포함된 cursorRect를 드래그 중 우선 사용하거나, Rust getCursorRect* API에 현재 pageIndex hint를 전달하는 보조 API를 추가한다.
  5. 회귀 검증은 exam_social.hwp 2/4쪽 하단 좌측 박스에서 드래그 선택 시 scrollTop 변화량과 cursor rect pageIndex 안정성을 확인하는 E2E/임시 진단 스크립트로 추가한다.

관련 이슈

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions