From f147b4e38ff109e4d6986568b1cc93bad8aa8df3 Mon Sep 17 00:00:00 2001 From: ienaga Date: Mon, 18 Nov 2024 08:11:06 +0900 Subject: [PATCH] =?UTF-8?q?#154=20TextField=E3=81=AEhit=20test=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 9 +- packages/core/src/CoreUtil.ts | 11 +- .../service/PlayerDoubleClickEventService.ts | 29 ++ .../service/PlayerPointerDownEventService.ts | 80 +++++ .../service/PlayerPointerMoveEventService.ts | 34 ++ .../service/PlayerPointerUpEventService.ts | 29 ++ .../Player/usecase/PlayerHitTestUseCase.ts | 50 +-- .../TextFieldGenerateRenderQueueUseCase.ts | 3 +- .../TextFieldDrawOffscreenCanvasUseCase.ts | 119 +++++-- .../usecase/TextFieldRenderUseCase.ts | 1 + .../renderer/src/interface/ITextSetting.ts | 1 + packages/text/src/TextField.ts | 312 ++---------------- .../TextFieldBlinkingClearTimeoutService.ts | 11 +- .../usecase/TextFieldSetFocusIndexUseCase.ts | 209 ++++++++++++ .../usecase/TextFieldSetFocusUseCase.ts | 14 +- packages/text/src/TextUtil.ts | 6 +- 16 files changed, 572 insertions(+), 346 deletions(-) create mode 100644 packages/core/src/Player/service/PlayerDoubleClickEventService.ts create mode 100644 packages/core/src/Player/service/PlayerPointerDownEventService.ts create mode 100644 packages/core/src/Player/service/PlayerPointerMoveEventService.ts create mode 100644 packages/core/src/Player/service/PlayerPointerUpEventService.ts create mode 100644 packages/text/src/TextField/usecase/TextFieldSetFocusIndexUseCase.ts diff --git a/index.html b/index.html index d11a4211..ff28a04f 100644 --- a/index.html +++ b/index.html @@ -21,8 +21,13 @@ sprite.x = 50; sprite.y = 50; - // const textFiled = sprite.addChild(new TextField()); - // textFiled.text = "Hello Next2D"; + const textFiled = sprite.addChild(new TextField()); + textFiled.x = 150; + textFiled.y = 100; + textFiled.border = true; + textFiled.type = "input"; + textFiled.multiline = true; + textFiled.text = "Hello Next2D\nTest Mode On\n\nText Field"; const shape = sprite.addChild(new Shape()); shape diff --git a/packages/core/src/CoreUtil.ts b/packages/core/src/CoreUtil.ts index 0b320ffd..d9556cde 100644 --- a/packages/core/src/CoreUtil.ts +++ b/packages/core/src/CoreUtil.ts @@ -77,12 +77,21 @@ export const $getMainElement = (): HTMLDivElement => return $mainElement as NonNullable; }; +/** + * @description マウス、タップ時の画面のmatrix情報 + * Screen matrix information when mouse or tap is pressed + * + * @type {Float32Array} + * @protected + */ +export const $hitMatrix: Float32Array = new Float32Array([1, 0, 0, 1, 0, 0]); + /** * @description マウス、タップがヒットしたDisplayObjectを取得します。 * Get the DisplayObject that the mouse or tap hit. * * @type {IPlayerHitObject} - * @private + * @protected */ export const $hitObject: IPlayerHitObject = { "x": 0, diff --git a/packages/core/src/Player/service/PlayerDoubleClickEventService.ts b/packages/core/src/Player/service/PlayerDoubleClickEventService.ts new file mode 100644 index 00000000..5c43f654 --- /dev/null +++ b/packages/core/src/Player/service/PlayerDoubleClickEventService.ts @@ -0,0 +1,29 @@ +import type { DisplayObject } from "@next2d/display"; +import { $stage } from "@next2d/display"; +import { PointerEvent } from "@next2d/events"; + +/** + * @description ポインターのダブルタップイベントを処理します。 + * Processes the pointer double tap event. + * + * @param {D | null} display_object + * @return {void} + * @method + * @protected + */ +export const execute = (display_object: D | null = null): void => +{ + if (display_object) { + if (display_object.willTrigger(PointerEvent.DOUBLE_CLICK)) { + display_object.dispatchEvent( + new PointerEvent(PointerEvent.DOUBLE_CLICK) + ); + } + } else { + if ($stage.willTrigger(PointerEvent.DOUBLE_CLICK)) { + $stage.dispatchEvent( + new PointerEvent(PointerEvent.DOUBLE_CLICK) + ); + } + } +}; \ No newline at end of file diff --git a/packages/core/src/Player/service/PlayerPointerDownEventService.ts b/packages/core/src/Player/service/PlayerPointerDownEventService.ts new file mode 100644 index 00000000..7dde3232 --- /dev/null +++ b/packages/core/src/Player/service/PlayerPointerDownEventService.ts @@ -0,0 +1,80 @@ +import type { DisplayObject } from "@next2d/display"; +import type { TextField } from "@next2d/text"; +import { $stage } from "@next2d/display"; +import { PointerEvent } from "@next2d/events"; +import { + $setSelectedTextField, + $getSelectedTextField, + $textArea +} from "@next2d/text"; +import { + $hitObject, + $hitMatrix +} from "../../CoreUtil"; + +/** + * @description ポインターダウンイベントを処理します。 + * Processes the pointer down event. + * + * @param {D | null} display_object + * @return {void} + * @method + * @protected + */ +export const execute = ( + display_object: D | null = null, + page_x: number = 0, + page_y: number = 0 +): void => { + + if (display_object) { + + if (display_object.isText) { + + // 選択中のTextFieldがある場合はフォーカスを解除します。 + const selectedTextField = $getSelectedTextField(); + if (selectedTextField + && selectedTextField.instanceId !== display_object.instanceId + ) { + selectedTextField.focus = false; + } + + if (!(display_object as unknown as TextField).focus) { + (display_object as unknown as TextField).focus = true; + $setSelectedTextField(display_object as unknown as TextField); + } + + (display_object as unknown as TextField).setFocusIndex( + $hitObject.x - $hitMatrix[4], + $hitObject.y - $hitMatrix[5] + ); + + $textArea.style.top = `${page_x}px`; + $textArea.style.left = `${page_y}px`; + + } else { + // ヒットしたDisplayObjectポインターダウンイベントを発火します。 + if (display_object.willTrigger(PointerEvent.POINTER_DOWN)) { + display_object.dispatchEvent( + new PointerEvent(PointerEvent.POINTER_DOWN) + ); + } + } + + } else { + + // 選択中のTextFieldがある場合はフォーカスを解除します。 + const selectedTextField = $getSelectedTextField(); + if (selectedTextField) { + selectedTextField.focus = false; + $setSelectedTextField(null); + } + + // ステージ全体のポインターダウンイベントを発火します。 + if ($stage.willTrigger(PointerEvent.POINTER_DOWN)) { + $stage.dispatchEvent( + new PointerEvent(PointerEvent.POINTER_DOWN) + ); + } + } +}; \ No newline at end of file diff --git a/packages/core/src/Player/service/PlayerPointerMoveEventService.ts b/packages/core/src/Player/service/PlayerPointerMoveEventService.ts new file mode 100644 index 00000000..adf3950c --- /dev/null +++ b/packages/core/src/Player/service/PlayerPointerMoveEventService.ts @@ -0,0 +1,34 @@ +import type { DisplayObject } from "@next2d/display"; +import type { TextField } from "@next2d/text"; +import { $player } from "../../Player"; +import { + $hitObject, + $hitMatrix +} from "../../CoreUtil"; + +/** + * @description ポインタームーブイベントを処理します。 + * Processes the pointer move event. + * + * @param {D | null} display_object + * @return {void} + * @method + * @protected + */ +export const execute = ( + display_object: D | null = null, + page_x: number = 0, + page_y: number = 0 +): void => { + + console.log(page_x, page_y); + if (display_object) { + if (display_object.isText && $player.mouseState === "down") { + (display_object as unknown as TextField).setFocusIndex( + $hitObject.x - $hitMatrix[4], + $hitObject.y - $hitMatrix[5], + true + ); + } + } +}; \ No newline at end of file diff --git a/packages/core/src/Player/service/PlayerPointerUpEventService.ts b/packages/core/src/Player/service/PlayerPointerUpEventService.ts new file mode 100644 index 00000000..f6b13d8f --- /dev/null +++ b/packages/core/src/Player/service/PlayerPointerUpEventService.ts @@ -0,0 +1,29 @@ +import type { DisplayObject } from "@next2d/display"; +import { $stage } from "@next2d/display"; +import { PointerEvent } from "@next2d/events"; + +/** + * @description ポインターアップイベントを処理します。 + * Processes the pointer up event. + * + * @param {DisplayObject | null} display_object + * @return {void} + * @method + * @protected + */ +export const execute = (display_object: D | null = null): void => +{ + if (display_object) { + if (display_object.willTrigger(PointerEvent.POINTER_UP)) { + display_object.dispatchEvent( + new PointerEvent(PointerEvent.POINTER_UP) + ); + } + } else { + if ($stage.willTrigger(PointerEvent.POINTER_UP)) { + $stage.dispatchEvent( + new PointerEvent(PointerEvent.POINTER_UP) + ); + } + } +}; \ No newline at end of file diff --git a/packages/core/src/Player/usecase/PlayerHitTestUseCase.ts b/packages/core/src/Player/usecase/PlayerHitTestUseCase.ts index 94d48a08..929ebfed 100644 --- a/packages/core/src/Player/usecase/PlayerHitTestUseCase.ts +++ b/packages/core/src/Player/usecase/PlayerHitTestUseCase.ts @@ -1,19 +1,19 @@ +import type { DisplayObject } from "@next2d/display"; import { $player } from "../../Player"; import { $stage } from "@next2d/display"; import { PointerEvent as Next2D_PointerEvent } from "@next2d/events"; +import { execute as playerPointerDownEventService } from "../service/PlayerPointerDownEventService"; +import { execute as playerDoubleClickEventService } from "../service/PlayerDoubleClickEventService"; +import { execute as playerPointerUpEventService } from "../service/PlayerPointerUpEventService"; +import { execute as playerPointerMoveEventService } from "../service/PlayerPointerMoveEventService"; import { $devicePixelRatio, $hitContext, $getMainElement, - $hitObject + $hitObject, + $hitMatrix } from "../../CoreUtil"; -/** - * @type {Float32Array} - * @private - */ -const $matrix: Float32Array = new Float32Array([1, 0, 0, 1, 0, 0]); - /** * @type {string} * @private @@ -68,15 +68,15 @@ export const execute = (event: PointerEvent, canvas: HTMLCanvasElement): void => $hitObject.hit = null; // hit test - $matrix[4] = ($player.rendererWidth - $stage.stageWidth * $player.rendererScale) / 2; - $matrix[5] = ($player.rendererHeight - $stage.stageHeight * $player.rendererScale) / 2; + $hitMatrix[4] = ($player.rendererWidth - $stage.stageWidth * $player.rendererScale) / 2; + $hitMatrix[5] = ($player.rendererHeight - $stage.stageHeight * $player.rendererScale) / 2; // reset $hitContext.beginPath(); $hitContext.setTransform(1, 0, 0, 1, 0, 0); // ヒット判定 - $stage.$mouseHit($hitContext, $matrix, $hitObject); + $stage.$mouseHit($hitContext, $hitMatrix, $hitObject); // ヒットしたオブジェクトがある場合 if ($hitObject.hit) { @@ -88,21 +88,19 @@ export const execute = (event: PointerEvent, canvas: HTMLCanvasElement): void => canvas.style.cursor = $currentCursor = $hitObject.pointer; } - switch (true) { - - // ヒットしたオブジェクトがある場合 - case $hitObject.hit === null: - break; + const hitDisplayObject = $hitObject.hit as DisplayObject | null; + switch (event.type) { - // ヒットしたオブジェクトがない場合 - default: + case Next2D_PointerEvent.POINTER_MOVE: + playerPointerMoveEventService( + hitDisplayObject, event.pageX, event.pageY + ); break; - } - - switch (event.type) { case Next2D_PointerEvent.POINTER_DOWN: + clearTimeout($timerId); + if (!$wait) { // 初回のタップであればダブルタップを待機モードに変更 @@ -114,18 +112,24 @@ export const execute = (event: PointerEvent, canvas: HTMLCanvasElement): void => $wait = false; }, 300); + playerPointerDownEventService( + hitDisplayObject, + event.pageX, + event.pageY + ); + } else { // ダブルタップを終了 $wait = false; + playerDoubleClickEventService(hitDisplayObject); + } break; case Next2D_PointerEvent.POINTER_UP: - break; - - case Next2D_PointerEvent.POINTER_MOVE: + playerPointerUpEventService(hitDisplayObject); break; } diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index ca00d12a..db9d8760 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -49,7 +49,7 @@ export const execute = ( point_y: number ): void => { - if (!text_field.visible || !text_field.text) { + if (!text_field.visible) { render_queue.push(0); return ; } @@ -253,6 +253,7 @@ export const execute = ( render_queue.push(Math.abs(text_field.xMax - text_field.xMin)); render_queue.push(Math.abs(text_field.yMax - text_field.yMin)); render_queue.push(text_field.focusIndex); + render_queue.push(text_field.selectIndex); render_queue.push(+text_field.focusVisible); render_queue.push(text_field.thickness); render_queue.push(text_field.thicknessColor); diff --git a/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.ts index 83ddaab7..2681ef9c 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.ts @@ -87,6 +87,76 @@ export const execute = ( context.setTransform(x_scale, 0, 0, y_scale, tx * x_scale, ty * y_scale); context.beginPath(); + if (text_setting.selectIndex > -1 + && text_setting.focusIndex > -1 + ) { + const range = text_data.textTable.length - 1; + + let minIndex = 0; + let maxIndex = 0; + if (text_setting.focusIndex <= text_setting.selectIndex) { + minIndex = Math.min(text_setting.focusIndex, range); + maxIndex = Math.min(text_setting.selectIndex, range); + } else { + minIndex = Math.min(text_setting.selectIndex, range); + maxIndex = Math.min(text_setting.focusIndex - 1, range); + } + + const textObject = text_data.textTable[minIndex]; + const lineObject = text_data.lineTable[textObject.line]; + const offsetAlign = textFiledGetAlignOffsetService( + text_data, lineObject, text_setting + ); + + let x = 0; + if (minIndex && textObject.mode === "text") { + let idx = minIndex; + while (idx) { + const textObject = text_data.textTable[--idx]; + if (textObject.mode !== "text") { + break; + } + x += textObject.w; + } + } + + context.fillStyle = "#b4d7ff"; + + let w = 0; + for (let idx = minIndex; idx <= maxIndex; ++idx) { + + const textObject = text_data.textTable[idx]; + if (textObject.mode === "text") { + + w += textObject.w; + + if (idx !== maxIndex) { + continue; + } + } + + let y = 0; + const line = textObject.mode === "text" + ? textObject.line + : textObject.line - 1; + + for (let idx = 0; idx < line; ++idx) { + y += text_data.heightTable[idx]; + } + + context.beginPath(); + context.rect( + x, y, + w + offsetAlign, + text_data.heightTable[line] + ); + context.fill(); + + x = 0; + w = 0; + } + } + const rawWidth = text_setting.rawWidth; let scrollX = 0; if (text_setting.scrollX > 0) { @@ -103,22 +173,6 @@ export const execute = ( } const limitHeight = rawHeight + scrollY; - if (!text_data.textTable.length - && text_setting.focusIndex > -1 - && text_setting.focusVisible - ) { - - const color = $intToRGBA(text_setting.defaultColor); - - context.strokeStyle = `rgba(${color.R},${color.G},${color.B},${color.A})`; - context.beginPath(); - context.moveTo(0, 0); - context.lineTo(0, 0 + (text_setting.defaultSize || 12)); - context.stroke(); - - return canvas; - } - // setup let offsetWidth = 0; let offsetHeight = 0; @@ -172,7 +226,9 @@ export const execute = ( context.fillStyle = `rgba(${color.R},${color.G},${color.B},${color.A})`; // focus line - if (text_setting.focusVisible && text_setting.focusIndex === idx) { + if (text_setting.focusVisible + && text_setting.focusIndex === idx + ) { const x = offsetWidth + offsetAlign + 0.1; let line = textObject.line; @@ -185,12 +241,11 @@ export const execute = ( ? textObject.h : text_data.ascentTable[line - 1]; - if (line > 0 && !text_data.ascentTable[line - 1]) { - line = textObject.line; - y = text_data.ascentTable[line - 1]; - } else { + if (line > 0) { line = textObject.line - 1; y = text_data.ascentTable[line]; + } else { + y = textObject.h; } } @@ -274,6 +329,28 @@ export const execute = ( } } + if (text_setting.focusVisible + && text_setting.focusIndex >= text_data.textTable.length + ) { + const textObject = text_data.textTable[text_setting.focusIndex - 1]; + if (textObject) { + const color = $intToRGBA(textObject.textFormat.color || 0); + context.strokeStyle = `rgba(${color.R},${color.G},${color.B},${color.A})`; + + const x = offsetWidth + offsetAlign + 0.1; + const y = offsetHeight + verticalAlign; + + context.beginPath(); + if (textObject.mode === "text") { + context.moveTo(x, y - textObject.y); + } else { + context.moveTo(x, y + textObject.h); + } + context.lineTo(x, y); + context.stroke(); + } + } + context.restore(); return canvas; diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index 9e84219d..ad5f620a 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -125,6 +125,7 @@ export const execute = (render_queue: Float32Array, index: number): number => "rawWidth": render_queue[index++], "rawHeight": render_queue[index++], "focusIndex": render_queue[index++], + "selectIndex": render_queue[index++], "focusVisible": Boolean(render_queue[index++]), "thickness": render_queue[index++], "thicknessColor": render_queue[index++], diff --git a/packages/renderer/src/interface/ITextSetting.ts b/packages/renderer/src/interface/ITextSetting.ts index 60e4b225..f2864db9 100644 --- a/packages/renderer/src/interface/ITextSetting.ts +++ b/packages/renderer/src/interface/ITextSetting.ts @@ -12,6 +12,7 @@ export interface ITextSetting { rawHeight: number; autoSize: ITextFieldAutoSize; focusIndex: number; + selectIndex: number; focusVisible: boolean; thickness: number; thicknessColor: number; diff --git a/packages/text/src/TextField.ts b/packages/text/src/TextField.ts index 8cf57d86..05a42992 100644 --- a/packages/text/src/TextField.ts +++ b/packages/text/src/TextField.ts @@ -19,6 +19,7 @@ import { execute as textFieldReplaceTextUseCase } from "./TextField/usecase/Text import { execute as textFieldCopyUseCase } from "./TextField/usecase/TextFieldCopyUseCase"; import { execute as textFieldInsertTextUseCase } from "./TextField/usecase/TextFieldInsertTextUseCase"; import { execute as textFieldApplyChangesService } from "./TextField/service/TextFieldApplyChangesService"; +import { execute as textFieldSetFocusIndexUseCase } from "./TextField/usecase/TextFieldSetFocusIndexUseCase"; import { $clamp, $toColorInt @@ -957,7 +958,7 @@ export class TextField extends InteractiveObject set text (text: string) { text = `${text}`; - if (text === this._$text) { + if (text !== "" && text === this._$text) { return ; } @@ -1220,293 +1221,22 @@ export class TextField extends InteractiveObject textFieldInsertTextUseCase(this, this._$copyText); } - // /** - // * @param {number} stage_x - // * @param {number} stage_y - // * @return {void} - // * @method - // * @private - // */ - // _$setIndex (stage_x: number, stage_y: number): void - // { - // if (this.type !== "input") { - // return ; - // } - - // const textData: TextData = textFieldGetTextDataUseCase(this); - // if (!textData.textTable.length) { - // this.focusIndex = 0; - // this.selectIndex = -1; - // this.setBlinkingTimer(); - // return ; - // } - - // const width: number = this.width; - // const height: number = this.height; - - // let tx: number = 0; - // if (this._$scrollX > 0) { - // tx += this._$scrollX * (this.textWidth - width) / width; - // } - - // let ty: number = 0; - // if (this._$scrollY) { - // ty += this._$scrollY * (this.textHeight - height) / height; - // } - - // const eventType: string = $getEventType(); - // const point: Point = this.globalToLocal(new Point(stage_x, stage_y)); - // const x: number = point.x + tx; - // const y: number = point.y + ty; - - // let w: number = 2; - // let yMin: number = 2; - // let yMax: number = yMin + textData.heightTable[0]; - // for (let idx: number = 1; idx < textData.textTable.length; ++idx) { - - // const textObject: ITextObject = textData.textTable[idx]; - - // switch (textObject.mode) { - - // case "break": - // case "wrap": - // if (x > w && y > yMin - // && yMax > y - // && width > x - // ) { - // const index: number = idx; - // switch (eventType) { - - // case $TOUCH_MOVE: - // case $MOUSE_MOVE: - // if (this._$selectIndex !== index && this._$focusIndex === index) { - // this._$selectIndex = index; - - // if (this._$focusIndex !== index) { - // this._$focusVisible = false; - // $clearTimeout(this._$timerId); - - // this._$doChanged(); - // $doUpdated(); - // } - // } - // break; - - // default: - // if (this._$focusIndex !== index || this._$selectIndex > -1) { - // this._$focusIndex = index; - // this._$selectIndex = -1; - // this.setBlinkingTimer(); - // } - // break; - // } - - // return ; - // } - - // w = 2; - // yMin += textData.heightTable[textObject.line - 1]; - // yMax = yMin + textData.heightTable[textObject.line]; - // break; - - // case "text": - // if (idx === textData.textTable.length - 1 - // && x > w && y > yMin && yMax > y - // && width > x - // ) { - - // const index: number = textData.textTable.length; - // switch (eventType) { - - // case $TOUCH_MOVE: - // case $MOUSE_MOVE: - // if (this._$selectIndex !== index) { - // this._$selectIndex = index; - - // if (this._$focusIndex !== index) { - // this._$focusVisible = false; - // $clearTimeout(this._$timerId); - - // this._$doChanged(); - // $doUpdated(); - // } - // } - // break; - - // default: - // if (this._$focusIndex !== index || this._$selectIndex > -1) { - // this._$focusIndex = index; - // this._$selectIndex = -1; - // this.setBlinkingTimer(); - // } - // break; - - // } - - // return ; - // } - - // if (x > w && y > yMin - // && yMax > y - // && w + textObject.w > x - // ) { - - // let index: number = idx; - // switch (eventType) { - // case $TOUCH_MOVE: - // case $MOUSE_MOVE: - - // if (this._$focusIndex > index) { // left - // if (this._$focusIndex === index + 1) { - // if (w + textObject.w / 2 < x) { - // index = -1; - // } - // } else { - // if (w + textObject.w / 2 < x) { - // index += 1; - // } - // } - // } else { // right - // if (this._$focusIndex === index) { - // if (w + textObject.w / 2 > x) { - // index = -1; - // } - // } else { - // if (w + textObject.w / 2 > x) { - // index -= 1; - // } - // } - // } - - // if (this._$selectIndex !== index) { - // this._$selectIndex = index; - - // if (this._$selectIndex > -1) { - // this._$focusVisible = false; - // $clearTimeout(this._$timerId); - // } - - // this._$doChanged(); - // $doUpdated(); - // } - // break; - - // default: - - // if (w + textObject.w / 2 < x) { - // const textObject: ITextObject = textData.textTable[index + 1]; - // if (!textObject || textObject.mode === "text") { - // index += 1; - // } - // } - - // if (this._$focusIndex !== index || this._$selectIndex > -1) { - // this._$focusIndex = index; - // this._$selectIndex = -1; - // this.setBlinkingTimer(); - // } - // break; - - // } - // return ; - // } - - // w += textObject.w; - // break; - - // default: - // break; - - // } - // } - - // switch (eventType) { - - // case $TOUCH_MOVE: - // case $MOUSE_MOVE: - // // reset - // this._$focusIndex = -1; - // this._$selectIndex = -1; - // break; - - // default: - // this._$focusIndex = textData.textTable.length; - // this._$selectIndex = -1; - // this.setBlinkingTimer(); - // break; - - // } - // } - - // /** - // * @param {CanvasRenderingContext2D} context - // * @param {Float32Array} matrix - // * @param {object} options - // * @return {boolean} - // * @method - // * @private - // */ - // _$mouseHit ( - // context: CanvasRenderingContext2D, - // matrix: Float32Array, - // options: PlayerHitObjectImpl - // ): boolean { - - // if (!this._$visible) { - // return false; - // } - - // return this._$hit(context, matrix, options); - // } - - // /** - // * @param {CanvasRenderingContext2D} context - // * @param {Float32Array} matrix - // * @param {object} options - // * @return {boolean} - // * @method - // * @private - // */ - // _$hit ( - // context: CanvasRenderingContext2D, - // matrix: Float32Array, - // options: PlayerHitObjectImpl - // ): boolean { - - // let multiMatrix: Float32Array = matrix; - // const rawMatrix: Float32Array = this._$transform._$rawMatrix(); - // if (rawMatrix[0] !== 1 || rawMatrix[1] !== 0 - // || rawMatrix[2] !== 0 || rawMatrix[3] !== 1 - // || rawMatrix[4] !== 0 || rawMatrix[5] !== 0 - // ) { - // multiMatrix = $multiplicationMatrix(matrix, rawMatrix); - // } - - // const baseBounds: IBounds = this._$getBounds(null); - - // const bounds: IBounds = $boundsMatrix(baseBounds, multiMatrix); - // const xMax = +bounds.xMax; - // const xMin = +bounds.xMin; - // const yMax = +bounds.yMax; - // const yMin = +bounds.yMin; - // $poolBoundsObject(bounds); - // $poolBoundsObject(baseBounds); - - // const width: number = Math.ceil(Math.abs(xMax - xMin)); - // const height: number = Math.ceil(Math.abs(yMax - yMin)); - - // context.setTransform(1, 0, 0, 1, xMin, yMin); - // context.beginPath(); - // context.moveTo(0, 0); - // context.lineTo(width, 0); - // context.lineTo(width, height); - // context.lineTo(0, height); - // context.lineTo(0, 0); - - // if (multiMatrix !== matrix) { - // $poolFloat32Array6(multiMatrix); - // } - - // return context.isPointInPath(options.x, options.y); - // } -} + /** + * @description テキストフィールドのフォーカス位置を設定します。 + * Sets the focus position of the text field. + * + * @param {number} stage_x + * @param {number} stage_y + * @param {boolean} [selected=false] + * @return {void} + * @method + * @private + */ + setFocusIndex ( + stage_x: number, + stage_y: number, + selected: boolean = false + ): void { + textFieldSetFocusIndexUseCase(this, stage_x, stage_y, selected); + } +} \ No newline at end of file diff --git a/packages/text/src/TextField/service/TextFieldBlinkingClearTimeoutService.ts b/packages/text/src/TextField/service/TextFieldBlinkingClearTimeoutService.ts index e356df5d..58edd6c8 100644 --- a/packages/text/src/TextField/service/TextFieldBlinkingClearTimeoutService.ts +++ b/packages/text/src/TextField/service/TextFieldBlinkingClearTimeoutService.ts @@ -1,4 +1,7 @@ -import { $getBlinkingTimerId } from "../../TextUtil"; +import { + $getBlinkingTimerId, + $setBlinkingTimerId +} from "../../TextUtil"; /** * @description テキストの点滅のタイマーをクリアします。 @@ -10,5 +13,9 @@ import { $getBlinkingTimerId } from "../../TextUtil"; */ export const execute = (): void => { - clearTimeout($getBlinkingTimerId()); + const timerId = $getBlinkingTimerId(); + if (timerId !== undefined) { + clearTimeout(timerId); + } + $setBlinkingTimerId(void 0); }; \ No newline at end of file diff --git a/packages/text/src/TextField/usecase/TextFieldSetFocusIndexUseCase.ts b/packages/text/src/TextField/usecase/TextFieldSetFocusIndexUseCase.ts new file mode 100644 index 00000000..8a7018c7 --- /dev/null +++ b/packages/text/src/TextField/usecase/TextFieldSetFocusIndexUseCase.ts @@ -0,0 +1,209 @@ +import type { TextField } from "../../TextField"; +import { Point } from "@next2d/geom"; +import { execute as textFieldGetTextDataUseCase } from "./TextFieldGetTextDataUseCase"; +import { execute as textFieldBlinkingUseCase } from "./TextFieldBlinkingUseCase"; +import { execute as textFieldApplyChangesService } from "../service/TextFieldApplyChangesService"; +import { execute as textFieldBlinkingClearTimeoutService } from "../service/TextFieldBlinkingClearTimeoutService"; +import { $getBlinkingTimerId } from "../../TextUtil"; + +/** + * @description テキストフィールドのフォーカスしてるテキスト位置(インデックス)を設定します。 + * Set the text position (index) that the text field is focusing on. + * + * @param {TextField} text_field + * @param {number} stage_x + * @param {number} stage_y + * @param {boolean} [selected=false] + * @return {void} + * @method + * @protected + */ +export const execute = ( + text_field: TextField, + stage_x: number, + stage_y: number, + selected: boolean = false +): void => { + + if (text_field.type !== "input") { + return ; + } + + const textData = textFieldGetTextDataUseCase(text_field); + if (2 > textData.textTable.length) { + text_field.focusIndex = 1; + text_field.selectIndex = -1; + + if ($getBlinkingTimerId() === undefined) { + textFieldBlinkingUseCase(text_field); + } + + return ; + } + + const width = text_field.width; + const height = text_field.height; + + let tx = 0; + if (text_field.scrollX > 0) { + tx += text_field.scrollX * (text_field.textWidth - width) / width; + } + + let ty = 0; + if (text_field.scrollY > 0) { + ty += text_field.scrollY * (text_field.textHeight - height) / height; + } + + const point = text_field.globalToLocal(new Point(stage_x, stage_y)); + const x = point.x + tx; + const y = point.y + ty; + + let w = 2; + let yMin = 2; + let yMax = yMin + textData.heightTable[0]; + for (let idx = 1; idx < textData.textTable.length; ++idx) { + + const textObject = textData.textTable[idx]; + if (!textObject) { + continue; + } + + switch (textObject.mode) { + + case "break": + case "wrap": + if (x > w && y > yMin + && yMax > y + && width > x + ) { + const index = idx; + if (selected) { + if (text_field.selectIndex !== index + && text_field.focusIndex === index + ) { + text_field.selectIndex = index; + + if (text_field.focusIndex !== index) { + text_field.focusVisible = false; + textFieldBlinkingClearTimeoutService(); + textFieldApplyChangesService(text_field); + } + } + } else { + if (text_field.focusIndex !== index || text_field.selectIndex > -1) { + text_field.focusIndex = index; + text_field.selectIndex = -1; + textFieldApplyChangesService(text_field); + } + } + + return ; + } + + w = 2; + yMin += textData.heightTable[textObject.line - 1]; + yMax = yMin + textData.heightTable[textObject.line]; + break; + + case "text": + if (x > w && y > yMin + && yMax > y + && w + textObject.w > x + ) { + let index = idx; + if (selected) { + if (text_field.focusIndex > index) { + // left + if (text_field.focusIndex === index + 1) { + if (w + textObject.w / 2 < x) { + index = -1; + } + } else { + if (w + textObject.w / 2 < x) { + index += 1; + } + } + } else { + // right + if (text_field.focusIndex === index) { + if (w + textObject.w / 2 > x) { + index = -1; + } + } else { + if (w + textObject.w / 2 > x) { + index -= 1; + } + } + } + + if (text_field.selectIndex !== index) { + text_field.selectIndex = index; + + if (text_field.selectIndex > -1) { + text_field.focusVisible = false; + textFieldBlinkingClearTimeoutService(); + } + + textFieldApplyChangesService(text_field); + } + } else { + if (w + textObject.w / 2 < x) { + const textObject = textData.textTable[index + 1]; + if (!textObject || textObject.mode === "text") { + index += 1; + } + } + + if (text_field.focusIndex !== index || text_field.selectIndex > -1) { + text_field.focusIndex = index; + text_field.selectIndex = -1; + textFieldApplyChangesService(text_field); + } + } + return ; + } + + if (idx === textData.textTable.length - 1 + && x > w && y > yMin && yMax > y + && width > x + ) { + const index = textData.textTable.length; + if (selected) { + if (text_field.selectIndex !== index) { + text_field.selectIndex = index; + + if (text_field.focusIndex !== index) { + text_field.focusVisible = false; + textFieldBlinkingClearTimeoutService(); + textFieldApplyChangesService(text_field); + } + } + } else { + if (text_field.focusIndex !== index || text_field.selectIndex > -1) { + text_field.focusIndex = index; + text_field.selectIndex = -1; + textFieldApplyChangesService(text_field); + } + } + return ; + } + + w += textObject.w; + break; + + default: + break; + + } + + if (!selected) { + text_field.focusIndex = textData.textTable.length; + text_field.selectIndex = -1; + if ($getBlinkingTimerId() === undefined) { + textFieldBlinkingUseCase(text_field); + } else { + textFieldApplyChangesService(text_field); + } + } + } +}; \ No newline at end of file diff --git a/packages/text/src/TextField/usecase/TextFieldSetFocusUseCase.ts b/packages/text/src/TextField/usecase/TextFieldSetFocusUseCase.ts index 7dd90bb9..932161dd 100644 --- a/packages/text/src/TextField/usecase/TextFieldSetFocusUseCase.ts +++ b/packages/text/src/TextField/usecase/TextFieldSetFocusUseCase.ts @@ -1,8 +1,12 @@ import type { TextField } from "../../TextField"; import { FocusEvent } from "@next2d/events"; -import { $textArea } from "../../TextUtil"; import { execute as textFieldApplyChangesService } from "../service/TextFieldApplyChangesService"; import { execute as textFieldBlinkingClearTimeoutService } from "../service/TextFieldBlinkingClearTimeoutService"; +import { execute as textFieldBlinkingUseCase } from "./TextFieldBlinkingUseCase"; +import { + $textArea, + $getBlinkingTimerId +} from "../../TextUtil"; /** * @description フォーカス @@ -23,7 +27,13 @@ export const execute = (text_field: TextField, name: string): void => if (text_field.focus) { $textArea.focus(); - // todo set stage x,y + if ($getBlinkingTimerId() === undefined) { + if (text_field.focusIndex === -1) { + text_field.focusIndex = 1; + text_field.selectIndex = -1; + } + textFieldBlinkingUseCase(text_field); + } } else { // params reset diff --git a/packages/text/src/TextUtil.ts b/packages/text/src/TextUtil.ts index 4b09af4b..259b2215 100644 --- a/packages/text/src/TextUtil.ts +++ b/packages/text/src/TextUtil.ts @@ -183,7 +183,7 @@ export const $clamp = ( * @type {NodeJS.Timeout} * @private */ -let $timerId: NodeJS.Timeout; +let $timerId: NodeJS.Timeout | void; /** * @description テキスト点滅のタイマーIDを返却 @@ -192,7 +192,7 @@ let $timerId: NodeJS.Timeout; * @return {NodeJS.Timeout} * @protected */ -export const $getBlinkingTimerId = (): NodeJS.Timeout => +export const $getBlinkingTimerId = (): NodeJS.Timeout | void => { return $timerId; }; @@ -205,7 +205,7 @@ export const $getBlinkingTimerId = (): NodeJS.Timeout => * @return {void} * @protected */ -export const $setBlinkingTimerId = (timer_id: NodeJS.Timeout): void => +export const $setBlinkingTimerId = (timer_id: NodeJS.Timeout | void): void => { $timerId = timer_id; }; \ No newline at end of file