diff --git a/src/ui/display.ts b/src/ui/display.ts index 1f62b67..03a68c7 100644 --- a/src/ui/display.ts +++ b/src/ui/display.ts @@ -291,9 +291,10 @@ export class FormulaDisplay { let nearest: { dist: number; g: any; key: string } | null = null; const cands: { dist: number; g: any; key: string; bl: number }[] = []; for (const [key, g] of lay.rows) { + // the stops of a right-to-left row run backwards: its extent is the span between the two ends, whichever is left const hxs = g.hx ?? g.xs; - const x1 = hxs[0]; - const x2 = hxs[hxs.length - 1]; + const x1 = Math.min(hxs[0], hxs[hxs.length - 1]); + const x2 = Math.max(hxs[0], hxs[hxs.length - 1]); const bands: { y: number; a: number; d: number }[] = key === '' ? lay.lines.map((l) => ({ y: l.y, a: l.a, d: l.d })) : [{ y: g.y, a: g.a, d: g.d }]; for (const bnd of bands) { // table cells and slots have padding around their content; empty slots show a small box @@ -327,7 +328,7 @@ export class FormulaDisplay { if (root && root.structs) { let ei = -1; for (let i = 0; i < root.structs.length; i++) { - if (root.structs[i] && px >= root.xs[i] && px <= root.xs[i + 1]) ei = i; + if (root.structs[i] && px >= Math.min(root.xs[i], root.xs[i + 1]) && px <= Math.max(root.xs[i], root.xs[i + 1])) ei = i; } if (ei >= 0) { let near: { dist: number; g: any; key: string } | null = null; @@ -335,8 +336,8 @@ export class FormulaDisplay { if (key === '' || !g.path.length || g.path[0][0] !== ei) continue; if (g.stops && !g.stops.some(Boolean)) continue; const bnd = { y: g.y, a: g.a, d: g.d }; - const x1 = g.xs[0]; - const x2 = g.xs[g.xs.length - 1]; + const x1 = Math.min(g.xs[0], g.xs[g.xs.length - 1]); + const x2 = Math.max(g.xs[0], g.xs[g.xs.length - 1]); const dx = px < x1 ? x1 - px : px > x2 ? px - x2 : 0; const dy = py < bnd.y - bnd.a ? bnd.y - bnd.a - py : py > bnd.y + bnd.d ? py - bnd.y - bnd.d : 0; const dist = dx * dx + dy * dy * 4; diff --git a/src/ui/editor.ts b/src/ui/editor.ts index 1325ada..0c823e1 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -432,6 +432,7 @@ export class MathEditor implements ToolbarHost { const pe = e as PointerEvent; if (pe.pointerType !== 'touch' || !pe.isPrimary) return; this.lastTouchAt = Date.now(); + this.touchPt = { x: pe.clientX, y: pe.clientY, t: this.lastTouchAt }; cancelPress(); pressAt = { x: pe.clientX, y: pe.clientY }; press = setTimeout(() => { @@ -1343,9 +1344,18 @@ export class MathEditor implements ToolbarHost { /* ---------------------------------------------------------------- mouse */ private contentPoint(e: MouseEvent): { x: number; y: number } { + // the mouse events a browser sends for a tap carry a "touch adjusted" position (snapped to the nearest clickable + // thing, a few pixels away from the finger): the caret goes where the finger touched, not where the browser moved it + const tp = this.touchPt; + if ((e.type === 'mousedown' || e.type === 'dblclick') && Date.now() - tp.t < 1000 && Math.hypot(e.clientX - tp.x, e.clientY - tp.y) < 48) { + return this.display.toContent(tp.x, tp.y); + } return this.display.toContent(e.clientX, e.clientY); } + /** where the last finger went down (client px) and when */ + private touchPt = { x: 0, y: 0, t: 0 }; + private longPressedAt = 0; private lastTouchAt = 0; diff --git a/tools/probe_rtl_touch.cjs b/tools/probe_rtl_touch.cjs new file mode 100644 index 0000000..b2b80b1 --- /dev/null +++ b/tools/probe_rtl_touch.cjs @@ -0,0 +1,51 @@ +// RTL vs LTR on a phone, directly on the formula: tap every caret position of every row, double tap, long press + handle drags +const { chromium } = require('playwright'); +const NS = 'xmlns="http://www.w3.org/1998/Math/MathML"'; +const F = { + flat: 'a+12=c-d', + arabic: 'س+ص=٣×ع', + frac: 'x+a+1b-c=y+2', + mix: 'مرحبا بالعالم+12=x2', +}; +(async () => { + const b = await chromium.launch({ channel: 'chrome' }); + for (const name of (process.env.ONLY||Object.keys(F).join(",")).split(",")) for (const rtl of [false, true]) { + const ctx = await b.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true, deviceScaleFactor: 2 }); + const p = await ctx.newPage(); + await p.goto(process.env.URL || 'http://localhost:8099/?output=0'); + await p.waitForFunction(() => window.editor && document.querySelector('#editorContainer .wrs_toolbar button')); + await p.waitForTimeout(600); + await p.evaluate(({ rtl, NS, m }) => editor.setMathML(`${m}`), { rtl, NS, m: F[name] }); + await p.waitForTimeout(500); + await p.touchscreen.tap(200, 700); await p.waitForTimeout(1200); + // every row, every caret position: client x/y of the position, then tap slightly inside the cell toward each neighbour + const rows = await p.evaluate(() => { + const d = editor.display, lay = d.layout, r = d.content.getBoundingClientRect(), out = []; + for (const [key, g] of lay.rows) { + const y = key === '' ? lay.lines[0].y : g.y; + out.push({ key, path: g.path, xs: g.xs.map((x) => r.left + x), hx: (g.hx || g.xs).map((x) => r.left + x), y: r.top + y - g.a / 2 }); + } + return out; + }); + let bad = 0, n = 0; + for (const row of rows) { + const hx = row.hx; + for (let i = 0; i < hx.length; i++) { + // tap exactly on the stop, then 2px toward the inside + for (const dx of [0]) { + const x = hx[i] + dx; + await p.touchscreen.tap(x, row.y); + await p.waitForTimeout(Number(process.env.GAP||650)); + const got = await p.evaluate(() => ({ p: JSON.stringify(editor.core.caret.path), i: editor.core.caret.index })); + n++; + // acceptable: same row & index, or a neighbouring stop at the same x + const ok = got.p === JSON.stringify(row.path) && (got.i === i || Math.abs(hx[got.i] - hx[i]) < 1.5); + if (!ok) { bad++; console.log(` ${name} ${rtl ? 'RTL' : 'LTR'} row "${row.key}" stop ${i} x=${Math.round(x)} y=${Math.round(row.y)} -> ${got.p} ${got.i}`); } + } + } + } + console.log(`${name} ${rtl ? 'RTL' : 'LTR'}: ${n} taps, ${bad} off`); + await ctx.close(); + } + await b.close(); +})().catch((e) => { console.error(e); process.exit(1); });