Skip to content

impr(tape mode): support RTL languages (@NadAlaba) #5748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions frontend/src/styles/test.scss
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@
&.tape {
display: block;
white-space: nowrap;
width: 200vw;
.word {
margin: 0.25em 0.6em 0.25em 0;
display: inline-block;
Expand All @@ -314,8 +313,21 @@
}
}
&.withLigatures {
letter {
display: inline;
.word {
overflow-wrap: anywhere;
padding-bottom: 0.05em; // compensate for letter border

.hints {
overflow-wrap: initial;
}

letter {
display: inline;
}
}
.beforeNewline {
border-top: unset;
padding-bottom: 0.05em;
}
}
&.blurred {
Expand Down Expand Up @@ -743,8 +755,17 @@
}
}
&.withLigatures {
letter {
display: inline;
.word {
overflow-wrap: anywhere;
padding-bottom: 2px; // compensate for letter border

.hints {
overflow-wrap: initial;
}

letter {
display: inline;
}
}
}
}
Expand Down
16 changes: 13 additions & 3 deletions frontend/src/ts/test/caret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,29 @@ function getTargetPositionLeft(
} else {
const wordsWrapperWidth =
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
const tapeMargin =
wordsWrapperWidth *
(isLanguageRightToLeft
? 1 - Config.tapeMargin / 100
: Config.tapeMargin / 100);

result =
tapeMargin -
(fullWidthCaret && isLanguageRightToLeft ? fullWidthCaretWidth : 0);

if (Config.tapeMode === "word" && inputLen > 0) {
let currentWordWidth = 0;
let lastPositiveLetterWidth = 0;
for (let i = 0; i < inputLen; i++) {
if (invisibleExtraLetters && i >= wordLen) break;
currentWordWidth +=
$(currentWordNodeList[i] as HTMLElement).outerWidth(true) ?? 0;
const letterOuterWidth =
$(currentWordNodeList[i] as Element).outerWidth(true) ?? 0;
currentWordWidth += letterOuterWidth;
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
}
// if current letter has zero width move the caret to previous positive width letter
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
currentWordWidth -= lastPositiveLetterWidth;
if (isLanguageRightToLeft) currentWordWidth *= -1;
result += currentWordWidth;
}
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/ts/test/test-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,13 +454,6 @@ export async function init(): Promise<void | null> {
}
}

if (Config.tapeMode !== "off" && language.rightToLeft) {
Notifications.add("This language does not support tape mode.", 0, {
important: true,
});
UpdateConfig.setTapeMode("off");
}

const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
if (Config.lazyMode && !allowLazyMode) {
rememberLazyMode = true;
Expand Down
94 changes: 67 additions & 27 deletions frontend/src/ts/test/test-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,10 @@ export function showWords(): void {
}

updateActiveElement(undefined, true);
updateWordWrapperClasses();
setTimeout(() => {
void Caret.updatePosition();
}, 125);

updateWordWrapperClasses();
}

export function appendEmptyWordElement(): void {
Expand Down Expand Up @@ -589,12 +588,32 @@ export function updateWordsWrapperHeight(force = false): void {

function updateWordsMargin(): void {
if (Config.tapeMode !== "off") {
void scrollTape();
void scrollTape(true);
} else {
setTimeout(() => {
$("#words").css("margin-left", "unset");
$("#words .afterNewline").css("margin-left", "unset");
}, 0);
const wordsEl = document.getElementById("words") as HTMLElement;
const afterNewlineEls =
wordsEl.querySelectorAll<HTMLElement>(".afterNewline");
if (Config.smoothLineScroll) {
const jqWords = $(wordsEl);
jqWords.stop("leftMargin", true, false).animate(
{
marginLeft: 0,
},
{
duration: SlowTimer.get() ? 0 : 125,
queue: "leftMargin",
}
);
jqWords.dequeue("leftMargin");
$(afterNewlineEls)
.stop(true, false)
.animate({ marginLeft: 0 }, SlowTimer.get() ? 0 : 125);
} else {
wordsEl.style.marginLeft = `0`;
for (const afterNewline of afterNewlineEls) {
afterNewline.style.marginLeft = `0`;
}
}
}
}

Expand Down Expand Up @@ -807,11 +826,14 @@ function getNlCharWidth(
return nlChar.offsetWidth + letterMargin;
}

export async function scrollTape(): Promise<void> {
export async function scrollTape(noRemove = false): Promise<void> {
if (ActivePage.get() !== "test" || resultVisible) return;

await centeringActiveLine;

const currentLang = await JSONData.getCurrentLanguage(Config.language);
const isLanguageRTL = currentLang.rightToLeft;

// index of the active word in the collection of .word elements
const wordElementIndex = TestState.activeWordIndex - activeWordElementOffset;
const wordsWrapperWidth = (
Expand Down Expand Up @@ -889,7 +911,10 @@ export async function scrollTape(): Promise<void> {
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
const forWordLeft = Math.floor(child.offsetLeft);
const forWordWidth = Math.floor(child.offsetWidth);
if (forWordLeft < 0 - forWordWidth) {
if (
(!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
(isLanguageRTL && forWordLeft > wordsWrapperWidth)
) {
toRemove.push(child);
widthRemoved += wordOuterWidth;
wordsToRemoveCount++;
Expand All @@ -903,15 +928,20 @@ export async function scrollTape(): Promise<void> {
fullLineWidths -= nlCharWidth + wordRightMargin;
if (i < activeWordIndex) wordsWidthBeforeActive = fullLineWidths;

if (fullLineWidths < wordsEl.offsetWidth) {
/** words that are wider than limit can cause a barely visible bottom line shifting,
* increase limit if that ever happens, but keep the limit because browsers hate
* ridiculously wide margins which may cause the words to not be displayed
*/
const limit = 3 * wordsEl.offsetWidth;
if (fullLineWidths < limit) {
afterNewlinesNewMargins.push(fullLineWidths);
widthRemovedFromLine.push(widthRemoved);
} else {
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
afterNewlinesNewMargins.push(limit);
widthRemovedFromLine.push(widthRemoved);
if (i < lastElementIndex) {
// for the second .afterNewline after active word
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
afterNewlinesNewMargins.push(limit);
widthRemovedFromLine.push(widthRemoved);
}
break;
Expand All @@ -920,7 +950,7 @@ export async function scrollTape(): Promise<void> {
}

/* remove overflown elements */
if (toRemove.length > 0) {
if (toRemove.length > 0 && !noRemove) {
activeWordElementOffset += wordsToRemoveCount;
for (const el of toRemove) el.remove();
for (let i = 0; i < widthRemovedFromLine.length; i++) {
Expand All @@ -931,30 +961,40 @@ export async function scrollTape(): Promise<void> {
currentLineIndent - (widthRemovedFromLine[i] ?? 0)
}px`;
}
if (isLanguageRTL) widthRemoved *= -1;
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
}

/* calculate current word width to add to #words margin */
let currentWordWidth = 0;
if (Config.tapeMode === "letter") {
if (TestInput.input.current.length > 0) {
const letters = activeWordEl.querySelectorAll("letter");
for (let i = 0; i < TestInput.input.current.length; i++) {
const letter = letters[i] as HTMLElement;
if (
(Config.blindMode || Config.hideExtraLetters) &&
letter.classList.contains("extra")
) {
continue;
}
currentWordWidth += $(letter).outerWidth(true) ?? 0;
const inputLength = TestInput.input.current.length;
if (Config.tapeMode === "letter" && inputLength > 0) {
const letters = activeWordEl.querySelectorAll("letter");
let lastPositiveLetterWidth = 0;
for (let i = 0; i < inputLength; i++) {
const letter = letters[i] as HTMLElement;
if (
(Config.blindMode || Config.hideExtraLetters) &&
letter.classList.contains("extra")
) {
continue;
}
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
currentWordWidth += letterOuterWidth;
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
}
// if current letter has zero width move the tape to previous positive width letter
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
currentWordWidth -= lastPositiveLetterWidth;
}

/* change to new #words & .afterNewline margins */
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
const newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
let newMargin =
wordsWrapperWidth * (Config.tapeMargin / 100) -
wordsWidthBeforeActive -
currentWordWidth;
if (isLanguageRTL) newMargin = wordRightMargin - newMargin;

const jqWords = $(wordsEl);
if (Config.smoothLineScroll) {
Expand Down