Skip to content

Commit f650130

Browse files
committed
support mixed language directions in tape mode
also center current letter, so that it stays in the same position in RTL and LTR tape
1 parent 4ec8aea commit f650130

2 files changed

Lines changed: 86 additions & 53 deletions

File tree

frontend/src/ts/elements/caret.ts

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -446,59 +446,53 @@ export class Caret {
446446
}
447447

448448
const spaceWidth = getTotalInlineMargin(options.word.native);
449-
let width = spaceWidth;
450-
if (this.isFullWidth() && options.side === "beforeLetter") {
451-
width = letter.getOffsetWidth();
449+
let width = letter.getOffsetWidth();
450+
if (options.side === "afterLetter") {
451+
width = spaceWidth;
452452
}
453453

454454
let left = 0;
455455
let top = 0;
456456

457-
const tapeOffset =
458-
wordsWrapperCache.getOffsetWidth() * (Config.tapeMargin / 100);
457+
const isTestRightToLeft = options.isDirectionReversed
458+
? !options.isLanguageRightToLeft
459+
: options.isLanguageRightToLeft;
460+
461+
let tapeOffsetRatio = Config.tapeMargin / 100;
462+
if (isTestRightToLeft) tapeOffsetRatio = 1 - tapeOffsetRatio;
463+
const tapeOffset = wordsWrapperCache.getOffsetWidth() * tapeOffsetRatio;
459464

460465
// yes, this is all super verbose, but its easier to maintain and understand
461466
if (isWordRTL) {
462467
if (!checkRtlByLetter && isFullMatch) options.word.addClass("wordRtl");
463468
let afterLetterCorrection = 0;
464-
if (options.side === "afterLetter") {
465-
if (this.isFullWidth()) {
466-
afterLetterCorrection += spaceWidth * -1;
467-
} else {
468-
afterLetterCorrection += letter.getOffsetWidth() * -1;
469-
}
469+
if (this.isFullWidth() && options.side === "afterLetter") {
470+
afterLetterCorrection += spaceWidth * -1;
471+
} else if (!this.isFullWidth() && options.side === "beforeLetter") {
472+
afterLetterCorrection += width;
470473
}
471474
if (Config.tapeMode === "off") {
472-
if (!this.isFullWidth()) {
473-
left += letter.getOffsetWidth();
474-
}
475475
left += letter.getOffsetLeft();
476476
left += options.word.getOffsetLeft();
477477
left += afterLetterCorrection;
478478
} else if (Config.tapeMode === "word") {
479-
if (!this.isFullWidth()) {
480-
left += letter.getOffsetWidth();
481-
}
482-
left += options.word.getOffsetWidth() * -1;
483479
left += letter.getOffsetLeft();
484480
left += afterLetterCorrection;
485481
if (this.isMainCaret && lockedMainCaretInTape) {
486-
left += wordsWrapperCache.getOffsetWidth() - tapeOffset;
482+
left += tapeOffset - options.word.getOffsetWidth();
483+
left += spaceWidth * 0.5; // center current letter
487484
} else {
488485
left += options.word.getOffsetLeft();
489-
left += options.word.getOffsetWidth();
490486
}
491487
} else if (Config.tapeMode === "letter") {
492-
if (this.isFullWidth()) {
493-
left += width * -1;
494-
}
495488
if (this.isMainCaret && lockedMainCaretInTape) {
496-
left += wordsWrapperCache.getOffsetWidth() - tapeOffset;
489+
left += tapeOffset;
490+
if (this.isFullWidth()) left += width * -1;
491+
left += spaceWidth * 0.5; // center current letter
497492
} else {
498493
left += letter.getOffsetLeft();
499494
left += options.word.getOffsetLeft();
500495
left += afterLetterCorrection;
501-
left += width;
502496
}
503497
}
504498
} else {
@@ -515,12 +509,14 @@ export class Caret {
515509
left += afterLetterCorrection;
516510
if (this.isMainCaret && lockedMainCaretInTape) {
517511
left += tapeOffset;
512+
left += spaceWidth * -0.5; // center current letter
518513
} else {
519514
left += options.word.getOffsetLeft();
520515
}
521516
} else if (Config.tapeMode === "letter") {
522517
if (this.isMainCaret && lockedMainCaretInTape) {
523518
left += tapeOffset;
519+
left += spaceWidth * -0.5; // center current letter
524520
} else {
525521
left += letter.getOffsetLeft();
526522
left += options.word.getOffsetLeft();

frontend/src/ts/test/test-ui.ts

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ import {
6969
import { getTheme } from "../states/theme";
7070
import { skipBreakdownEvent } from "../states/header";
7171
import { wordsHaveNewline } from "../states/test";
72+
import { isWordRightToLeft } from "../utils/strings";
73+
import { getTotalInlineMargin } from "../utils/misc";
7274

7375
export const updateHintsPositionDebounced = Misc.debounceUntilResolved(
7476
updateHintsPosition,
@@ -995,9 +997,7 @@ export async function scrollTape(noAnimation = false): Promise<void> {
995997
}
996998
}
997999

998-
const wordRightMargin = parseFloat(
999-
window.getComputedStyle(activeWordEl.native).marginRight,
1000-
);
1000+
const spaceWidth = getTotalInlineMargin(activeWordEl.native);
10011001

10021002
/*calculate .afterNewline & #words new margins + determine elements to remove*/
10031003
for (let i = 0; i <= lastElementIndex; i++) {
@@ -1024,7 +1024,7 @@ export async function scrollTape(noAnimation = false): Promise<void> {
10241024
} else if (child.hasClass("afterNewline")) {
10251025
if (leadingNewLine) continue;
10261026
const nlCharWidth = getNlCharWidth(wordsChildrenArr[i - 3]);
1027-
fullLineWidths -= nlCharWidth + wordRightMargin;
1027+
fullLineWidths -= nlCharWidth + spaceWidth;
10281028
if (i < activeWordIndex) wordsWidthBeforeActive = fullLineWidths;
10291029

10301030
/** words that are wider than limit can cause a barely visible bottom line shifting,
@@ -1067,43 +1067,80 @@ export async function scrollTape(noAnimation = false): Promise<void> {
10671067
}
10681068

10691069
/* calculate current word width to add to #words margin */
1070+
let inputWord = TestInput.input.current;
1071+
const targetWord = TestWords.words.getCurrent() || inputWord; // fallback for zen mode
1072+
const [isActiveWordRTL, _] = isWordRightToLeft(
1073+
targetWord,
1074+
TestState.isLanguageRightToLeft,
1075+
TestState.isDirectionReversed,
1076+
);
10701077
let currentWordWidth = 0;
1071-
const inputLength = TestInput.input.current.length;
1072-
if (Config.tapeMode === "letter" && inputLength > 0) {
1073-
const letters = activeWordEl.qsa("letter");
1074-
let lastPositiveLetterWidth = 0;
1075-
for (let i = 0; i < inputLength; i++) {
1076-
const letter = letters[i];
1077-
if (
1078-
(Config.blindMode || Config.hideExtraLetters) &&
1079-
letter?.hasClass("extra")
1080-
) {
1081-
continue;
1082-
}
1083-
const letterOuterWidth = letter?.getOffsetWidth() ?? 0;
1084-
currentWordWidth += letterOuterWidth;
1085-
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
1078+
if (Config.tapeMode === "letter") {
1079+
let inputLength = inputWord.length;
1080+
const targetWordLength = targetWord.length;
1081+
if (Config.blindMode || Config.hideExtraLetters) {
1082+
inputLength = Math.min(targetWordLength, inputLength);
1083+
}
1084+
1085+
let letterIndex;
1086+
let side: "beforeLetter" | "afterLetter";
1087+
if (
1088+
inputLength < targetWordLength ||
1089+
(Config.mode === "zen" && inputLength === 0)
1090+
) {
1091+
side = "beforeLetter";
1092+
letterIndex = inputLength;
1093+
} else {
1094+
side = "afterLetter";
1095+
letterIndex = inputLength - 1;
10861096
}
1097+
10871098
// if current letter has zero width move the tape to previous positive width letter
1088-
if (letters[inputLength]?.getOffsetWidth() === 0) {
1089-
currentWordWidth -= lastPositiveLetterWidth;
1099+
let i = letterIndex;
1100+
const letters = activeWordEl.qsa("letter");
1101+
let ltr;
1102+
while ((ltr = letters[i]) && ltr.getOffsetWidth() === 0 && i > 0) i--;
1103+
let currentLetterOffset = ltr?.getOffsetLeft() ?? 0;
1104+
let currentLetterWidth = ltr?.getOffsetWidth() ?? 0;
1105+
if (side === "afterLetter") currentLetterWidth = spaceWidth;
1106+
1107+
if (
1108+
(isActiveWordRTL && side === "beforeLetter") ||
1109+
(!isActiveWordRTL && side === "afterLetter")
1110+
) {
1111+
currentLetterOffset += currentLetterWidth;
1112+
}
1113+
1114+
if (isTestRightToLeft) {
1115+
currentWordWidth = activeWordEl.getOffsetWidth() - currentLetterOffset;
1116+
} else {
1117+
currentWordWidth = currentLetterOffset;
10901118
}
10911119
}
10921120

1121+
if (Config.tapeMode === "word" && isTestRightToLeft !== isActiveWordRTL) {
1122+
currentWordWidth += activeWordEl.getOffsetWidth();
1123+
}
1124+
10931125
/* change to new #words & .afterNewline margins */
1094-
const tapeMarginPx = wordsWrapperWidth * (Config.tapeMargin / 100);
1095-
let newMarginOffset = wordsWidthBeforeActive + currentWordWidth;
1096-
let newMargin = tapeMarginPx - newMarginOffset;
1126+
let tapeMarginPx = wordsWrapperWidth * (Config.tapeMargin / 100);
1127+
let typedWidth = -1 * (wordsWidthBeforeActive + currentWordWidth);
1128+
let newMargin = tapeMarginPx + typedWidth;
10971129
if (isTestRightToLeft) {
1098-
newMarginOffset *= -1;
1099-
newMargin = wordRightMargin - newMargin;
1130+
typedWidth *= -1;
1131+
newMargin *= -1;
1132+
newMargin += spaceWidth;
11001133
}
11011134

1135+
// center current letter
1136+
if (isActiveWordRTL) newMargin += 0.5 * spaceWidth;
1137+
else newMargin -= 0.5 * spaceWidth;
1138+
11021139
const duration = noAnimation ? 0 : 125;
11031140
const ease = "inOut(1.25)";
11041141

11051142
const caretScrollOptions = {
1106-
newValue: newMarginOffset * -1,
1143+
newValue: typedWidth,
11071144
duration: Config.smoothLineScroll ? duration : 0,
11081145
ease,
11091146
};
@@ -2013,7 +2050,7 @@ qs("#wordsInput")?.on("focusout", () => {
20132050
if (!isInputElementFocused()) {
20142051
OutOfFocus.show();
20152052
}
2016-
Caret.hide();
2053+
//Caret.hide();
20172054
});
20182055

20192056
qs(".pageTest")?.onChild("click", "#showWordHistoryButton", () => {

0 commit comments

Comments
 (0)