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(``), { 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); });