Expand replaces Widen: fill the window in both directions

From the development repository at 3f1da52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Opus 4.8 committed 2026-10-11 19:43:17 +02:00
1 parent 51e66db9a1
commit cc0b8dbd31
9 files changed
+102 -72

No files matched your search

+2 -2
View File
@@ -78,8 +78,8 @@
data-help="Show the whole page in the window. Pressed by the host, it does so on every screen. Ctrl (Command on a Mac) with the scroll wheel, or with a pen stroke up or down, zooms about the pointer.">Fit</button> data-help="Show the whole page in the window. Pressed by the host, it does so on every screen. Ctrl (Command on a Mac) with the scroll wheel, or with a pen stroke up or down, zooms about the pointer.">Fit</button>
<button id="zoom_sheet" type="button" <button id="zoom_sheet" type="button"
data-help="Show the page at the scale a new page has: one A4 sheet filling the window. A shrunk page is then seen at its size on the sheet. Pressed by the host, it does so on every screen.">A4</button> data-help="Show the page at the scale a new page has: one A4 sheet filling the window. A shrunk page is then seen at its size on the sheet. Pressed by the host, it does so on every screen.">A4</button>
<button id="zoom_widen" type="button" hidden <button id="zoom_expand" type="button" hidden
data-help="Make the page wider, for everyone, until it fills the window at this zoom. Nothing on it moves.">Widen</button> data-help="Make the page fill the window, for everyone, at this zoom, in both directions. Nothing on it moves.">Expand</button>
<button id="zoom_shrink" type="button" hidden <button id="zoom_shrink" type="button" hidden
data-help="Make the page, for everyone, just large enough for what is on it, with the margin around. Nothing on it moves.">Shrink</button> data-help="Make the page, for everyone, just large enough for what is on it, with the margin around. Nothing on it moves.">Shrink</button>
<label class="toggle" data-help="Let the page grow when your writing reaches an edge. Off, your writing stops at the edge. Images always get the room they need."> <label class="toggle" data-help="Let the page grow when your writing reaches an edge. Off, your writing stops at the edge. Images always get the room they need.">
+12 -8
View File
@@ -1027,12 +1027,16 @@ async function enterSession(invitationText, name, notebook) {
renderer.sheetScale(); renderer.sheetScale();
session.sendViewCommand('sheet'); session.sendViewCommand('sheet');
}); });
// "Widen" makes the sheet itself wider, until it fills the window at // "Expand" makes the sheet itself fill the window at the present zoom,
// the present zoom; nothing on it moves (Andy, 2026-10-08). // in both directions; nothing on it moves (Andy, 2026-10-08 for the
element('zoom_widen').addEventListener('click', () => { // width, both ways 2026-10-11).
const room = renderer.available().width / renderer.cssPixelsPerUnit; element('zoom_expand').addEventListener('click', () => {
const widened = session.widenPage(room); const room = renderer.available();
if (widened) log(`Page widened to ${session.page.baseWidth / 100} cm`); const expanded = session.expandPage(room.width / renderer.cssPixelsPerUnit,
room.height / renderer.cssPixelsPerUnit);
if (expanded) {
log(`Page expanded to ${session.page.baseWidth / 100} by ${session.page.baseHeight / 100} cm`);
}
}); });
element('zoom_shrink').addEventListener('click', () => { element('zoom_shrink').addEventListener('click', () => {
if (session.shrinkPage()) { if (session.shrinkPage()) {
@@ -1062,7 +1066,7 @@ async function enterSession(invitationText, name, notebook) {
const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]')); const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]'));
hideTool.hidden = !session.mayMakePages(); hideTool.hidden = !session.mayMakePages();
if (hideTool.hidden && tool === 'hide') /** @type {HTMLElement} */ (document.querySelector('[data-tool=pen]')).click(); if (hideTool.hidden && tool === 'hide') /** @type {HTMLElement} */ (document.querySelector('[data-tool=pen]')).click();
element('zoom_widen').hidden = !session.mayMakePages(); element('zoom_expand').hidden = !session.mayMakePages();
element('zoom_shrink').hidden = !session.mayMakePages(); element('zoom_shrink').hidden = !session.mayMakePages();
} }
showHostTools(); showHostTools();
@@ -1413,7 +1417,7 @@ function showParticipants(session, renderer) {
// A checkbox with its label to the left, like Extend (Andy, 2026-10-09). // A checkbox with its label to the left, like Extend (Andy, 2026-10-09).
const allowLine = document.createElement('label'); const allowLine = document.createElement('label');
allowLine.className = 'allow_line'; allowLine.className = 'allow_line';
allowLine.setAttribute('data-help', 'Make this participant a co-host: they may then start pages, put images on the page and arrange them, load notebook pages, widen, shrink and hide, as you can.'); allowLine.setAttribute('data-help', 'Make this participant a co-host: they may then start pages, put images on the page and arrange them, load notebook pages, expand, shrink and hide, as you can.');
const allow = document.createElement('input'); const allow = document.createElement('input');
allow.type = 'checkbox'; allow.type = 'checkbox';
allow.className = 'allow'; allow.className = 'allow';
+30 -21
View File
@@ -479,28 +479,37 @@ export class Session {
} }
/** /**
* Make the sheet wider, for more room to write (Andy, 2026-10-08: * Make the sheet fill the window, for more room to write (Andy,
* "Widen" enlarges the page rather than zooming). Everyone's page * 2026-10-08 as "Widen", which only grew the width; both ways since
* widens; it is a change to the page, so it needs the right to make * 2026-10-11, since there is no reason width alone suits anyone).
* pages, and it ends the chance to take back an image. * Everyone's page grows; it is a change to the page, so it needs the
* @param {number} width The page's new width, if wider than now. * right to make pages, and it ends the chance to take back an image.
* @returns {boolean} Whether the page widened. * @param {number} width The page's new width, if wider than now.
* @param {number} height The page's new height, if taller than now.
* @returns {boolean} Whether the page grew.
*/ */
widenPage(width) { expandPage(width, height) {
const page = this.page; const page = this.page;
const added = Math.round(width) - page.width; if (!this.mayMakePages() || page.number === 0) return false;
if (!this.mayMakePages() || page.number === 0 || added <= 0) return false; // Equally on both sides, and top and bottom, of the PAGE as it is,
// Equally on both sides of the PAGE as it is, writing included (the // writing included (the sheet may lie inside a page that writing has
// sheet may lie inside a page that writing has grown; measured from // grown; measured from the sheet, the page came out short, Andy,
// the sheet, the page came out short, Andy, 2026-10-08): what is on // 2026-10-08): what is on the page stays where it is seen. The same
// the page stays where it is seen. The same whole number of units on // whole number of units on each side, since the page is centred on
// each side, since the page is centred on the screen: an odd growth // the screen: an odd growth split 1199 and 1200 moved the centre half
// split 1199 and 1200 moved the page's centre half a unit, a fraction // a unit, a pixel's fraction that showed as a one-pixel move on Olion
// of a pixel that showed as a one-pixel move on Olion (2026-10-09). // (2026-10-09). A dimension already filling the window does not grow.
const half = Math.floor(added / 2); const addLeft = Math.floor((Math.round(width) - page.width) / 2);
if (half === 0) return false; const addTop = Math.floor((Math.round(height) - page.height) / 2);
page.baseLeft = page.left - half; if (addLeft <= 0 && addTop <= 0) return false;
page.baseWidth = page.width + 2 * half; if (addLeft > 0) {
page.baseLeft = page.left - addLeft;
page.baseWidth = page.width + 2 * addLeft;
}
if (addTop > 0) {
page.baseTop = page.top - addTop;
page.baseHeight = page.height + 2 * addTop;
}
page.imagesRevision += 1; page.imagesRevision += 1;
this.send(0, { type: 'page', page: page.descriptor() }); this.send(0, { type: 'page', page: page.descriptor() });
this.extentChanged(true); this.extentChanged(true);
@@ -510,7 +519,7 @@ export class Session {
/** /**
* Make the sheet the smallest that holds everything on the page with * Make the sheet the smallest that holds everything on the page with
* the margin around it ("Shrink", Andy, 2026-10-08). A change to the * the margin around it ("Shrink", Andy, 2026-10-08). A change to the
* page like "Widen". * page like "Expand".
* @returns {boolean} Whether the sheet changed. * @returns {boolean} Whether the sheet changed.
*/ */
shrinkPage() { shrinkPage() {
+7 -1
View File
@@ -260,6 +260,11 @@ h2 {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
overflow: auto; overflow: auto;
/* Centre the page in the desk both ways, so that growing the sheet
("Expand") equally on every side leaves what is on it where it is
seen. margin:auto on an overflowing flex child computes to 0, so a
page larger than the desk still scrolls to every edge. */
display: flex;
padding: 0.5rem; padding: 0.5rem;
background: var(--desk); background: var(--desk);
} }
@@ -287,7 +292,8 @@ h2 {
.page { .page {
position: relative; position: relative;
margin: 0 auto; flex: none;
margin: auto;
background: white; background: white;
box-shadow: 0 1px 4px rgb(0 0 0 / 0.25); box-shadow: 0 1px 4px rgb(0 0 0 / 0.25);
touch-action: none; touch-action: none;
+5 -5
View File
@@ -153,11 +153,11 @@ two gestures change the view, and none of them changes the page:
* When the host presses **Fit** or **A4**, every participant's view * When the host presses **Fit** or **A4**, every participant's view
follows, each in their own window. Your own Fit and A4 change only follows, each in their own window. Your own Fit and A4 change only
your screen. your screen.
* **Widen**, which only the host and co-hosts have, makes the page * **Expand**, which only the host and co-hosts have, makes the page
itself wider, for everyone, until it fills the itself fill the window, for everyone, at the present zoom, in both
window at the present zoom. The sheet grows equally on both sides, directions. The sheet grows equally on every side, so nothing on the
so nothing on the page moves and nothing is rescaled; there is simply page moves and nothing is rescaled; there is simply more room around
more room at either side. it.
* **Shrink**, also for the host and co-hosts, makes the page, for everyone, just large enough for what is on it, * **Shrink**, also for the host and co-hosts, makes the page, for everyone, just large enough for what is on it,
with the usual margin around. Nothing on it moves either. with the usual margin around. Nothing on it moves either.
* **Extend** is a box, ticked to begin with. Ticked, the sheet grows * **Extend** is a box, ticked to begin with. Ticked, the sheet grows
+5 -5
View File
@@ -225,10 +225,10 @@ is the "write your answer here" box of a worksheet: draw it on the scan
where the answer belongs, and the student types into their own box or where the answer belongs, and the student types into their own box or
beside yours. Only its owner can type in a box, as with any text. beside yours. Only its owner can type in a box, as with any text.
**Widen**, among the view buttons, makes the page wider for everyone, **Expand**, among the view buttons, makes the page fill your window for
until it fills your window at your present zoom: the quickest way to everyone, at your present zoom, in both directions: the quickest way to
more room for writing beside an exercise. The sheet grows equally on more room for writing around an exercise. The sheet grows equally on
both sides, so nothing on the page moves. The page keeps the new width every side, so nothing on the page moves. The page keeps the new size
in everyone's notebook. **Shrink** is its opposite: the page becomes in everyone's notebook. **Shrink** is its opposite: the page becomes
just large enough for everything on it, images and writing, with the just large enough for everything on it, images and writing, with the
margin of 5 mm around, which trims a page down to its exercise before you keep margin of 5 mm around, which trims a page down to its exercise before you keep
@@ -266,7 +266,7 @@ notebook in the toolbar first.
In the **Participants** list of the Session window, each guest's name In the **Participants** list of the Session window, each guest's name
has a box **Co-host** beside it. Ticking it makes that guest a has a box **Co-host** beside it. Ticking it makes that guest a
co-host: they may then start new pages, put images on the page and co-host: they may then start new pages, put images on the page and
arrange them, load pages from their notebook, widen, shrink and hide, arrange them, load pages from their notebook, expand, shrink and hide,
exactly as you can, and the buttons for it appear on their toolbar. exactly as you can, and the buttons for it appear on their toolbar.
Unticking it takes it back; otherwise it lasts until the session Unticking it takes it back; otherwise it lasts until the session
ends. What stays yours alone: making co-hosts, and bringing everyone's ends. What stays yours alone: making co-hosts, and bringing everyone's
+2 -1
View File
@@ -113,7 +113,8 @@ from the host (or from an allowed guest, for pages and images).
## Pages ## Pages
Page coordinates are in units of 0.1 mm. A blank page is an A4 sheet Page coordinates are in units of 0.1 mm. A blank page is an A4 sheet
in landscape, 2970 by 2100 units. "Widen" makes the sheet wider, and in landscape, 2970 by 2100 units. "Expand" makes the sheet fill the
window in both directions, and
"Shrink" makes it the bounding box of everything on the page plus the "Shrink" makes it the bounding box of everything on the page plus the
greater of the two margins, exactly, on every side; both are changes to the page like adding greater of the two margins, exactly, on every side; both are changes to the page like adding
an image, carried in the page descriptor (its `left` and `top` edges and an image, carried in the page descriptor (its `left` and `top` edges and
+1 -1
View File
@@ -68,7 +68,7 @@ rather than in the browser's storage.
## The screen in one paragraph ## The screen in one paragraph
The toolbar holds the pen, text, eraser and hide tools, colours and The toolbar holds the pen, text, eraser and hide tools, colours and
line widths, the view commands (Fit, A4, Widen, Shrink, Extend), the line widths, the view commands (Fit, A4, Expand, Shrink, Extend), the
host's page buttons (New, Image, Notebooks), your notebook, Keep, Export host's page buttons (New, Image, Notebooks), your notebook, Keep, Export
and Session. Rest the pointer on any button for a second for its and Session. Rest the pointer on any button for a second for its
explanation. Each participant writes on a layer of their own that explanation. Each participant writes on a layer of their own that
+38 -28
View File
@@ -1418,23 +1418,26 @@ class EndToEndTest(unittest.TestCase):
self.assertEqual(placed["placed"]["y"], upward["origin"][1] + 50) self.assertEqual(placed["placed"]["y"], upward["origin"][1] + 50)
self.assertEqual(placed["placed"]["x"], upward["origin"][0] + (upward["page"]["width"] - 300) // 2) self.assertEqual(placed["placed"]["x"], upward["origin"][0] + (upward["page"]["width"] - 300) // 2)
# Widen on a page that writing has grown on one side: the page # Expand on a page that writing has grown on one side: the page
# comes out as wide as the window, measured from the page's own # comes out as wide and as tall as the window, measured from the
# edges, not the sheet's (Andy, 2026-10-08). # page's own edges, not the sheet's (Andy, 2026-10-08, 2026-10-11).
firefox.run(""" firefox.run("""
document.getElementById('desk').dispatchEvent(new WheelEvent('wheel', document.getElementById('desk').dispatchEvent(new WheelEvent('wheel',
{deltaY: 100, ctrlKey: true, clientX: 300, clientY: 300, bubbles: true, cancelable: true})); {deltaY: 100, ctrlKey: true, clientX: 300, clientY: 300, bubbles: true, cancelable: true}));
""") """)
firefox.run("document.getElementById('zoom_widen').click();") firefox.run("document.getElementById('zoom_expand').click();")
widened = self.wait_until(lambda s: s["page"]["width"] > grown["page"]["width"], "the page to widen") self.wait_until(lambda s: s["page"]["width"] > grown["page"]["width"]
and s["page"]["height"] > grown["page"]["height"], "the page to expand")
measured = firefox.run(""" measured = firefox.run("""
const r = window.kokkoja.renderer;
const desk = document.getElementById('desk'); const desk = document.getElementById('desk');
const style = getComputedStyle(desk); const style = getComputedStyle(desk);
const room = desk.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); const page = document.getElementById('page').getBoundingClientRect();
return {page: document.getElementById('page').getBoundingClientRect().width, room}; return {width: page.width, height: page.height,
room: desk.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
roomHeight: desk.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)};
""") """)
self.assertAlmostEqual(measured["page"], measured["room"], delta=1) self.assertAlmostEqual(measured["width"], measured["room"], delta=1)
self.assertAlmostEqual(measured["height"], measured["roomHeight"], delta=1)
# The writer's tab is closed for the next test; the host's is cleared. # The writer's tab is closed for the next test; the host's is cleared.
firefox.switch_to(guest_tab) firefox.switch_to(guest_tab)
@@ -1654,8 +1657,8 @@ class EndToEndTest(unittest.TestCase):
time.sleep(0.5) time.sleep(0.5)
self.assertEqual(firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;"), host_scale) self.assertEqual(firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;"), host_scale)
# Widen makes the sheet itself wider, for everyone, until it fills # Expand makes the sheet fill the window, for everyone, at this
# the window at this zoom; the scale and what is in view stay. # zoom, in both directions; the scale and what is in view stay.
firefox.run(""" firefox.run("""
document.getElementById('desk').dispatchEvent(new WheelEvent('wheel', document.getElementById('desk').dispatchEvent(new WheelEvent('wheel',
{deltaY: 100, ctrlKey: true, clientX: 300, clientY: 300, bubbles: true, cancelable: true})); {deltaY: 100, ctrlKey: true, clientX: 300, clientY: 300, bubbles: true, cancelable: true}));
@@ -1666,29 +1669,36 @@ class EndToEndTest(unittest.TestCase):
return [Math.round(b.left), Math.round(b.top), Math.round(b.width)]; return [Math.round(b.left), Math.round(b.top), Math.round(b.width)];
""") """)
box_before = image_box() box_before = image_box()
firefox.run("document.getElementById('zoom_widen').click();") firefox.run("document.getElementById('zoom_expand').click();")
widened = zoom() expanded = zoom()
# The sheet grew equally on both sides: nothing on it moved on the screen. # The sheet grew equally on every side: nothing on it moved on screen.
self.assertEqual(image_box(), box_before) self.assertEqual(image_box(), box_before)
self.assertLess(firefox.run("return window.kokkoja.session.page.baseLeft;"), 0) self.assertLess(firefox.run("return window.kokkoja.session.page.baseLeft;"), 0)
desk_width = firefox.run(""" self.assertLess(firefox.run("return window.kokkoja.session.page.baseTop;"), 0)
room = firefox.run("""
const desk = document.getElementById('desk'); const desk = document.getElementById('desk');
const style = getComputedStyle(desk); const style = getComputedStyle(desk);
return desk.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); return {width: desk.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight),
height: desk.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)};
""") """)
self.assertAlmostEqual(widened["width"], desk_width, delta=1) page_box = firefox.run("""
self.assertEqual(widened["zoom"], smaller["zoom"]) const b = document.getElementById('page').getBoundingClientRect();
return {width: b.width, height: b.height};
""")
self.assertAlmostEqual(page_box["width"], room["width"], delta=1)
self.assertAlmostEqual(page_box["height"], room["height"], delta=1)
self.assertEqual(expanded["zoom"], smaller["zoom"])
wide_page = self.state()["page"] wide_page = self.state()["page"]
self.assertGreater(wide_page["width"], teacher["page"]["width"]) self.assertGreater(wide_page["width"], teacher["page"]["width"])
self.assertEqual(wide_page["height"], teacher["page"]["height"]) self.assertGreater(wide_page["height"], teacher["page"]["height"])
firefox.switch_to(student_tab) firefox.switch_to(student_tab)
self.wait_until(lambda s: s["page"] == wide_page, "the wider page to reach the student") self.wait_until(lambda s: s["page"] == wide_page, "the expanded page to reach the student")
self.assertTrue(firefox.run("return document.getElementById('zoom_widen').hidden;")) self.assertTrue(firefox.run("return document.getElementById('zoom_expand').hidden;"))
self.assertEqual(firefox.run("return window.kokkoja.session.page.baseLeft;"), self.assertEqual(firefox.run("return window.kokkoja.session.page.baseLeft;"),
-((wide_page["width"] - teacher["page"]["width"]) // 2)) -((wide_page["width"] - teacher["page"]["width"]) // 2))
# The page file carries the sheet's left edge and reads back with it. # The page file carries the sheet's edges and reads back with them.
widened_file = self.current_page("Student notebook", {}, number=3) expanded_file = self.current_page("Student notebook", {}, number=3)
self.assertRegex(widened_file, r'data-base="\d+ 2100 -\d+ 0"') self.assertRegex(expanded_file, r'data-base="\d+ \d+ -\d+ -\d+"')
firefox.switch_to(teacher_tab) firefox.switch_to(teacher_tab)
# Shrink makes the sheet just large enough for what is on it, with # Shrink makes the sheet just large enough for what is on it, with
@@ -1725,15 +1735,15 @@ class EndToEndTest(unittest.TestCase):
firefox.run("document.getElementById('zoom_sheet').click();") firefox.run("document.getElementById('zoom_sheet').click();")
sheet_scale = firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;") sheet_scale = firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;")
self.assertLess(sheet_scale, fitted_scale) self.assertLess(sheet_scale, fitted_scale)
self.assertAlmostEqual(sheet_scale, min(desk_width / 2970, firefox.run(""" self.assertAlmostEqual(sheet_scale, min(room["width"] / 2970, firefox.run("""
const desk = document.getElementById('desk'); const desk = document.getElementById('desk');
const style = getComputedStyle(desk); const style = getComputedStyle(desk);
return desk.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); return desk.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
""") / 2100), places=4) """) / 2100), places=4)
# Widened again, from the shrunk sheet, for the room the rest needs. # Expanded again, from the shrunk sheet, for the room the rest needs.
firefox.run("document.getElementById('zoom_fit').click();") firefox.run("document.getElementById('zoom_fit').click();")
firefox.run("document.getElementById('zoom_widen').click();") firefox.run("document.getElementById('zoom_expand').click();")
self.wait_until(lambda s: s["page"]["width"] > shrunk_page["page"]["width"], "the page widened again") self.wait_until(lambda s: s["page"]["width"] > shrunk_page["page"]["width"], "the page expanded again")
firefox.run("document.getElementById('zoom_fit').click();") firefox.run("document.getElementById('zoom_fit').click();")
teacher = self.state() teacher = self.state()