diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index c935deffed67..5bc770fe1775 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -351,7 +351,7 @@ &.blind { .word { & letter.extra { - display: none; + display: none !important; } & letter.incorrect { color: var(--correct-letter-color); @@ -533,45 +533,67 @@ } } - &.typed-effect-dots:not(.withLigatures) { + &.typed-effect-dots { /* transform already typed letters into appropriately colored dots */ - .word letter { - position: relative; - &::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 1em; - aspect-ratio: 1; - border-radius: 50%; - opacity: 0; + &:not(.withLigatures) .word, + &.withLigatures .word.broken-ligatures { + letter { + position: relative; + display: inline-block; + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 1em; + aspect-ratio: 1; + border-radius: 50%; + opacity: 0; + } } } - .typed letter { - color: var(--bg-color); - animation: typedEffectToDust 200ms ease-out 0ms 1 forwards !important; - &::after { - animation: typedEffectFadeIn 100ms ease-in 100ms 1 forwards; - background: var(--c-dot); + // unify dote spaceing + &.withLigatures .word.broken-ligatures { + display: inline-flex; + letter { + width: 0.5em; } } - &:not(.blind) { + + &:not(.withLigatures) .typed, + &.withLigatures .word.broken-ligatures.typed { + letter { + color: var(--bg-color); + animation: typedEffectToDust 200ms ease-out 0ms 1 forwards !important; + &::after { + animation: typedEffectFadeIn 100ms ease-in 100ms 1 forwards; + background: var(--c-dot); + } + } + } + + &:not(.withLigatures):not(.blind) { .word letter.incorrect::after { background: var(--c-dot--error); } } + &.withLigatures:not(.blind) .word.broken-ligatures letter.incorrect::after { + background: var(--c-dot--error); + } @media (prefers-reduced-motion) { - .typed letter { - animation: none !important; - transform: scale(0.4); - color: transparent; - &::after { + &:not(.withLigatures) .typed, + &.withLigatures .word.broken-ligatures.typed { + letter { animation: none !important; - opacity: 1; + transform: scale(0.4); + color: transparent; + &::after { + animation: none !important; + opacity: 1; + } } } } diff --git a/frontend/src/ts/test/break-ligatures.ts b/frontend/src/ts/test/break-ligatures.ts new file mode 100644 index 000000000000..6af798017914 --- /dev/null +++ b/frontend/src/ts/test/break-ligatures.ts @@ -0,0 +1,45 @@ +import Config from "../config"; +import { ElementWithUtils } from "../utils/dom"; + +function canBreak(wordEl: ElementWithUtils): boolean { + if (Config.typedEffect !== "dots") return false; + if (wordEl.hasClass("broken-ligatures")) return false; + + return !!wordEl.native.closest(".withLigatures"); +} + +function applyIfNeeded(wordEl: ElementWithUtils): void { + if (!canBreak(wordEl)) return; + + const { width } = wordEl.screenBounds(); + wordEl.setStyle({ width: `${width}px` }); + wordEl.addClass("broken-ligatures"); +} + +function reset(wordEl: ElementWithUtils): void { + if (!wordEl.hasClass("broken-ligatures")) return; + wordEl.removeClass("broken-ligatures"); + wordEl.setStyle({ width: "" }); +} + +export function set( + wordEl: ElementWithUtils, + areLigaturesBroken: boolean, +): void { + areLigaturesBroken ? applyIfNeeded(wordEl) : reset(wordEl); +} + +export function update(key: string, wordsEl: ElementWithUtils): void { + const words = wordsEl.qsa(".word.typed"); + + const shouldReset = + !wordsEl.hasClass("withLigatures") || + Config.typedEffect !== "dots" || + key === "fontFamily" || + key === "fontSize"; + + if (shouldReset) { + words.forEach(reset); + } + words.forEach(applyIfNeeded); +} diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 36b0b88279a6..f0d9541122c7 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -48,6 +48,7 @@ import * as SlowTimer from "../states/slow-timer"; import * as TestConfig from "./test-config"; import * as CompositionDisplay from "../elements/composition-display"; import * as AdController from "../controllers/ad-controller"; +import * as Ligatures from "./break-ligatures"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; import * as Keymap from "../elements/keymap"; import * as ThemeController from "../controllers/theme-controller"; @@ -141,6 +142,7 @@ export function updateActiveElement( if (previousActiveWord !== null) { if (direction === "forward") { previousActiveWord.addClass("typed"); + Ligatures.set(previousActiveWord, true); } else if (direction === "back") { // } @@ -157,6 +159,7 @@ export function updateActiveElement( newActiveWord.addClass("active"); newActiveWord.removeClass("error"); newActiveWord.removeClass("typed"); + Ligatures.set(newActiveWord, false); activeWordTop = newActiveWord.getOffsetTop(); activeWordHeight = newActiveWord.getOffsetHeight(); @@ -2072,11 +2075,15 @@ ConfigEvent.subscribe(({ key, newValue }) => { "colorfulMode", "showAllLines", "fontSize", + "fontFamily", "maxLineWidth", "tapeMargin", ].includes(key) ) { - updateWordWrapperClasses(); + if (key !== "fontFamily") updateWordWrapperClasses(); + if (["typedEffect", "fontFamily", "fontSize"].includes(key)) { + Ligatures.update(key, wordsEl); + } } if (["tapeMode", "tapeMargin"].includes(key)) { updateLiveStatsMargin();