A press that only brings the window to the front draws nothing

From the development repository at 62e0967.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Opus 4.8 committed 2026-10-11 19:32:13 +02:00
1 parent 2de843143d
commit 51e66db9a1
5 files changed
+78 -10

No files matched your search

+1 -1
View File
@@ -1223,7 +1223,7 @@ async function enterSession(invitationText, name, notebook) {
session.start();
// For looking at the session from the browser's console during development.
Object.assign(window, { kokkoja: {
session, renderer, writer, loadImage, textEditor,
session, renderer, writer, loadImage, textEditor, logText,
notebook: { listNotebooks, listKeptPages, readPage, readCurrentPage, notebooksInBrowser, discardNotebook, discardPage },
} });
}
+32 -1
View File
@@ -16,6 +16,31 @@ const ERASER_BUTTON = 32;
*/
const NOMINAL_PRESSURE = 0.4;
/**
* A press this soon after the window gained focus is taken to be the
* click that brought the window to the front, and draws nothing (Andy,
* 2026-10-11): clicking the page to return from the terminal, with the
* Pen chosen, left a stray dot. Measured on Hollis (Firefox, Linux):
* the window's focus event fires about 4 ms before such a press, the
* document already reporting focus, so neither document.hasFocus() nor
* the pointer's movement (a pen hovers either way) can tell it from an
* ordinary press; only the time since the focus can. A quarter second
* is far longer than that press and far shorter than any deliberate
* stroke made as a separate act after focusing the window.
*/
export const FOCUS_SETTLE_MS = 250;
/**
* The end-to-end tests turn the focusing-click rule off with
* `?no_focus_settle` in the address: a browser driven by WebDriver gains
* window focus on the first pointer action after every load or tab
* switch, so it cannot tell such a switch from a click that raised the
* window, and the rule would refuse legitimate test strokes. The rule
* itself is tested with a dispatched focus event in one tab. A real
* user never sets this, and it disables nothing but this nicety.
*/
const focusSettleOn = !new URLSearchParams(location.search).has('no_focus_settle');
/**
* @typedef {object} PenTarget
* @property {(clientX: number, clientY: number) => [number, number]} toPage
@@ -100,6 +125,8 @@ export function attachPenInput(surface, target) {
let pressY = 0;
/** @type {PointerEvent | null} The eraser's press, not yet erased with. */
let eraseWaiting = null;
/** When the window last gained focus, for the focusing-click rule. */
let lastFocusAt = -Infinity;
/** The eraser's press erases now, if it was waiting. */
function eraseFromPress() {
@@ -188,6 +215,10 @@ export function attachPenInput(surface, target) {
if (activePointer !== null) return refuse(`pointer ${activePointer} still active`);
if (event.target !== surface) return refuse('not on the page');
if (event.pointerType === 'pen') penSeen = true;
// A press that only brought the window to the front draws nothing.
if (focusSettleOn && performance.now() - lastFocusAt < FOCUS_SETTLE_MS) {
return refuse('it brought the window to the front');
}
// Once a pen has been used, a touch is taken to be the resting hand.
if (event.pointerType === 'touch' && penSeen) return refuse('a touch after a pen: the resting hand');
if (event.pointerType === 'mouse' && event.button !== 0) return refuse('not the main button');
@@ -325,7 +356,7 @@ export function attachPenInput(surface, target) {
// learned on his machine before the rule is written (a window focus
// is faked by the test harness on every tab switch, so it cannot be
// learned here). The press trace already carries "in front"/"behind".
window.addEventListener('focus', () => trace('window: focus'));
window.addEventListener('focus', () => { lastFocusAt = performance.now(); trace('window: focus'); });
window.addEventListener('blur', () => trace('window: blur'));
document.addEventListener('visibilitychange', () => trace(`visibility: ${document.visibilityState}`));
+3 -1
View File
@@ -81,7 +81,9 @@ from the palette joins the row and stays there in this browser. White
is for painting over. The four lines to the right of the colours are
the line widths: 0.4, 0.7, 1.2 and 2.4 mm at ordinary pressure.
If a pen is in use, a hand resting on a touch screen is ignored.
If a pen is in use, a hand resting on a touch screen is ignored. A
click on the page that only brings the browser window to the front, when
you come back to it from another window, makes no mark.
### Text
+1 -1
View File
@@ -283,7 +283,7 @@ the zoom step of the wheel and the zoom per 100 pixels of a pen drag
(`renderer.js`), the dead zone of that drag (`pen_input.js`), the margin
by which the page grows (`page_model.js`), the tooltip delay (1 s,
`DELAY_MS` in `tooltip.js`) and how long after one tooltip the next
button's comes at once (1 s, `WARM_MS` there), the hold that moves an image (600 ms, with 6 pixels of
button's comes at once (1 s, `WARM_MS` there), the time within which a press after the window comes to the front is taken for the click that raised it and draws nothing (250 ms, `FOCUS_SETTLE_MS` in `pen_input.js`), the hold that moves an image (600 ms, with 6 pixels of
slack, `pen_input.js`), the reach of an image's corner (24 pixels,
`renderer.js`), the smallest an image can be scaled to (1 cm,
`page_model.js`), how often a dragged image's place is sent (40 ms,
+41 -6
View File
@@ -570,12 +570,17 @@ class EndToEndTest(unittest.TestCase):
cls.stop()
raise
def address(self, name, join=None):
def address(self, name, join=None, focus_settle=False):
"""The Kokkoja address of a participant's own local program, with
its token, and an invitation to join if given."""
its token, and an invitation to join if given. By default it
carries `?no_focus_settle`, which turns off the focusing-click
rule: a driven browser gains focus on the first draw after a load
or tab switch, which the rule would take for a focusing click
(see test_a_focusing_click_draws_nothing, which keeps it on)."""
base, token = self.programs[name]
query = "" if focus_settle else "?no_focus_settle"
joining = "" if join is None else f"&join={urllib.parse.quote(join, safe='')}"
return f"{base}#token={token}{joining}"
return f"{base}{query}#token={token}{joining}"
@classmethod
def start(cls, command):
@@ -708,7 +713,7 @@ class EndToEndTest(unittest.TestCase):
# filled in, says where the notebooks are, and offers the installed
# program to anyone who has it.
guest_tab = firefox.new_tab()
firefox.go(link)
firefox.go(link.replace("#", "?no_focus_settle#", 1))
self.wait_until_true(lambda: firefox.run("return document.getElementById('invitation_entry').value;"),
lambda v: v and v.startswith("ws://"), "the invitation in the start screen")
self.assertEqual(firefox.run("return document.getElementById('invitation_entry').value;"),
@@ -1063,6 +1068,33 @@ class EndToEndTest(unittest.TestCase):
firefox.switch_to(host_tab)
firefox.go("about:blank")
def test_a_focusing_click_draws_nothing(self):
"""Andy, 2026-10-11: clicking the page to bring the browser to the
front from the terminal, with the Pen chosen, left a stray dot. A
press within 250 ms of the window gaining focus draws nothing.
(The window focus is dispatched here: the test harness raises the
real window on the first draw after a tab switch, so a two-tab
test cannot feign the terminal case; this does it in one tab.)"""
firefox = self.firefox
firefox.go(self.address("Late", focus_settle=True))
self.enter_name("Focuser", "start_session")
self.wait_until(lambda s: s["page"]["number"] == 1, "the page")
strokes = lambda: len(self.state()["layers"].get("Focuser", []))
# The window gains focus, then a press right after: nothing drawn.
firefox.run("window.dispatchEvent(new Event('focus'));")
firefox.drag("#page", 200, 300, 400, 320)
time.sleep(0.4)
self.assertEqual(strokes(), 0)
self.assertIn("refused: it brought the window to the front",
firefox.run("return window.kokkoja.logText();"))
# Once the window has been focused a while, a stroke draws.
firefox.run("window.dispatchEvent(new Event('focus'));")
time.sleep(0.4)
firefox.drag("#page", 200, 400, 400, 420)
self.wait_until(lambda s: len(s["layers"].get("Focuser", [])) == 1,
"a stroke once the window had been focused a while")
firefox.go("about:blank")
def test_undo(self):
"""Andy, 2026-10-11: one's latest gesture is taken back, then the
one before, by the Undo button, Ctrl+Z, or two quick taps of the
@@ -2024,8 +2056,11 @@ class EndToEndTest(unittest.TestCase):
self.assertEqual(len(self.kept_pages("Teacher notebook")), 2)
# A new session in the same notebook: the page the last session
# left there is kept, without anyone having asked.
firefox.go(self.programs["Teacher"][0])
# left there is kept, without anyone having asked. (The param
# that turns off the focusing-click rule, as address() adds, so
# that drawing after this navigation is not taken for a focusing
# click by the driven browser.)
firefox.go(self.programs["Teacher"][0] + "?no_focus_settle")
firefox.run("location.reload();")
time.sleep(0.5)
self.enter_name("Teacher", "start_session")