diff --git a/client/index.html b/client/index.html
index d635c5f..e64def6 100644
--- a/client/index.html
+++ b/client/index.html
@@ -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
A4
- Widen
+ Expand
Shrink
diff --git a/client/js/main.js b/client/js/main.js
index c75a8e0..f87b415 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -1027,12 +1027,16 @@ async function enterSession(invitationText, name, notebook) {
renderer.sheetScale();
session.sendViewCommand('sheet');
});
- // "Widen" makes the sheet itself wider, until it fills the window at
- // the present zoom; nothing on it moves (Andy, 2026-10-08).
- element('zoom_widen').addEventListener('click', () => {
- const room = renderer.available().width / renderer.cssPixelsPerUnit;
- const widened = session.widenPage(room);
- if (widened) log(`Page widened to ${session.page.baseWidth / 100} cm`);
+ // "Expand" makes the sheet itself fill the window at the present zoom,
+ // in both directions; nothing on it moves (Andy, 2026-10-08 for the
+ // width, both ways 2026-10-11).
+ element('zoom_expand').addEventListener('click', () => {
+ const room = renderer.available();
+ 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', () => {
if (session.shrinkPage()) {
@@ -1062,7 +1066,7 @@ async function enterSession(invitationText, name, notebook) {
const hideTool = /** @type {HTMLElement} */ (document.querySelector('[data-tool=hide]'));
hideTool.hidden = !session.mayMakePages();
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();
}
showHostTools();
@@ -1413,7 +1417,7 @@ function showParticipants(session, renderer) {
// A checkbox with its label to the left, like Extend (Andy, 2026-10-09).
const allowLine = document.createElement('label');
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');
allow.type = 'checkbox';
allow.className = 'allow';
diff --git a/client/js/session.js b/client/js/session.js
index 4718df9..16dbd8e 100644
--- a/client/js/session.js
+++ b/client/js/session.js
@@ -479,28 +479,37 @@ export class Session {
}
/**
- * Make the sheet wider, for more room to write (Andy, 2026-10-08:
- * "Widen" enlarges the page rather than zooming). Everyone's page
- * widens; it is a change to the page, so it needs the right to make
- * pages, and it ends the chance to take back an image.
- * @param {number} width The page's new width, if wider than now.
- * @returns {boolean} Whether the page widened.
+ * Make the sheet fill the window, for more room to write (Andy,
+ * 2026-10-08 as "Widen", which only grew the width; both ways since
+ * 2026-10-11, since there is no reason width alone suits anyone).
+ * Everyone's page grows; it is a change to the page, so it needs the
+ * right to make pages, and it ends the chance to take back an image.
+ * @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 added = Math.round(width) - page.width;
- if (!this.mayMakePages() || page.number === 0 || added <= 0) return false;
- // Equally on both sides of the PAGE as it is, writing included (the
- // sheet may lie inside a page that writing has grown; measured from
- // the sheet, the page came out short, Andy, 2026-10-08): what is on
- // the page stays where it is seen. The same whole number of units on
- // each side, since the page is centred on the screen: an odd growth
- // split 1199 and 1200 moved the page's centre half a unit, a fraction
- // of a pixel that showed as a one-pixel move on Olion (2026-10-09).
- const half = Math.floor(added / 2);
- if (half === 0) return false;
- page.baseLeft = page.left - half;
- page.baseWidth = page.width + 2 * half;
+ if (!this.mayMakePages() || page.number === 0) return false;
+ // Equally on both sides, and top and bottom, of the PAGE as it is,
+ // writing included (the sheet may lie inside a page that writing has
+ // grown; measured from the sheet, the page came out short, Andy,
+ // 2026-10-08): what is on the page stays where it is seen. The same
+ // whole number of units on each side, since the page is centred on
+ // the screen: an odd growth split 1199 and 1200 moved the centre half
+ // a unit, a pixel's fraction that showed as a one-pixel move on Olion
+ // (2026-10-09). A dimension already filling the window does not grow.
+ const addLeft = Math.floor((Math.round(width) - page.width) / 2);
+ const addTop = Math.floor((Math.round(height) - page.height) / 2);
+ if (addLeft <= 0 && addTop <= 0) return false;
+ 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;
this.send(0, { type: 'page', page: page.descriptor() });
this.extentChanged(true);
@@ -510,7 +519,7 @@ export class Session {
/**
* 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
- * page like "Widen".
+ * page like "Expand".
* @returns {boolean} Whether the sheet changed.
*/
shrinkPage() {
diff --git a/client/kokkoja.css b/client/kokkoja.css
index 7c0fb25..5dfa10e 100644
--- a/client/kokkoja.css
+++ b/client/kokkoja.css
@@ -260,6 +260,11 @@ h2 {
flex: 1;
min-width: 0;
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;
background: var(--desk);
}
@@ -287,7 +292,8 @@ h2 {
.page {
position: relative;
- margin: 0 auto;
+ flex: none;
+ margin: auto;
background: white;
box-shadow: 0 1px 4px rgb(0 0 0 / 0.25);
touch-action: none;
diff --git a/docs/guest.md b/docs/guest.md
index cc177a5..eeaa939 100644
--- a/docs/guest.md
+++ b/docs/guest.md
@@ -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
follows, each in their own window. Your own Fit and A4 change only
your screen.
-* **Widen**, which only the host and co-hosts have, makes the page
- itself wider, for everyone, until it fills the
- window at the present zoom. The sheet grows equally on both sides,
- so nothing on the page moves and nothing is rescaled; there is simply
- more room at either side.
+* **Expand**, which only the host and co-hosts have, makes the page
+ itself fill the window, for everyone, at the present zoom, in both
+ directions. The sheet grows equally on every side, so nothing on the
+ page moves and nothing is rescaled; there is simply more room around
+ 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.
* **Extend** is a box, ticked to begin with. Ticked, the sheet grows
diff --git a/docs/host.md b/docs/host.md
index 22a08d7..824cfd7 100644
--- a/docs/host.md
+++ b/docs/host.md
@@ -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
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,
-until it fills your window at your present zoom: the quickest way to
-more room for writing beside an exercise. The sheet grows equally on
-both sides, so nothing on the page moves. The page keeps the new width
+**Expand**, among the view buttons, makes the page fill your window for
+everyone, at your present zoom, in both directions: the quickest way to
+more room for writing around an exercise. The sheet grows equally on
+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
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
@@ -266,7 +266,7 @@ notebook in the toolbar first.
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
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.
Unticking it takes it back; otherwise it lasts until the session
ends. What stays yours alone: making co-hosts, and bringing everyone's
diff --git a/docs/implementation.md b/docs/implementation.md
index fa69338..a5f8f34 100644
--- a/docs/implementation.md
+++ b/docs/implementation.md
@@ -113,7 +113,8 @@ from the host (or from an allowed guest, for pages and images).
## Pages
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
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
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 491ddaf..ed01aac 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -68,7 +68,7 @@ rather than in the browser's storage.
## The screen in one paragraph
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
and Session. Rest the pointer on any button for a second for its
explanation. Each participant writes on a layer of their own that
diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py
index 4a73843..b88694d 100644
--- a/tests/test_end_to_end.py
+++ b/tests/test_end_to_end.py
@@ -1418,23 +1418,26 @@ class EndToEndTest(unittest.TestCase):
self.assertEqual(placed["placed"]["y"], upward["origin"][1] + 50)
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
- # comes out as wide as the window, measured from the page's own
- # edges, not the sheet's (Andy, 2026-10-08).
+ # Expand on a page that writing has grown on one side: the page
+ # comes out as wide and as tall as the window, measured from the
+ # page's own edges, not the sheet's (Andy, 2026-10-08, 2026-10-11).
firefox.run("""
document.getElementById('desk').dispatchEvent(new WheelEvent('wheel',
{deltaY: 100, ctrlKey: true, clientX: 300, clientY: 300, bubbles: true, cancelable: true}));
""")
- firefox.run("document.getElementById('zoom_widen').click();")
- widened = self.wait_until(lambda s: s["page"]["width"] > grown["page"]["width"], "the page to widen")
+ firefox.run("document.getElementById('zoom_expand').click();")
+ 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("""
- const r = window.kokkoja.renderer;
const desk = document.getElementById('desk');
const style = getComputedStyle(desk);
- const room = desk.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
- return {page: document.getElementById('page').getBoundingClientRect().width, room};
+ const page = document.getElementById('page').getBoundingClientRect();
+ 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.
firefox.switch_to(guest_tab)
@@ -1654,8 +1657,8 @@ class EndToEndTest(unittest.TestCase):
time.sleep(0.5)
self.assertEqual(firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;"), host_scale)
- # Widen makes the sheet itself wider, for everyone, until it fills
- # the window at this zoom; the scale and what is in view stay.
+ # Expand makes the sheet fill the window, for everyone, at this
+ # zoom, in both directions; the scale and what is in view stay.
firefox.run("""
document.getElementById('desk').dispatchEvent(new WheelEvent('wheel',
{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)];
""")
box_before = image_box()
- firefox.run("document.getElementById('zoom_widen').click();")
- widened = zoom()
- # The sheet grew equally on both sides: nothing on it moved on the screen.
+ firefox.run("document.getElementById('zoom_expand').click();")
+ expanded = zoom()
+ # The sheet grew equally on every side: nothing on it moved on screen.
self.assertEqual(image_box(), box_before)
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 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)
- self.assertEqual(widened["zoom"], smaller["zoom"])
+ page_box = firefox.run("""
+ 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"]
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)
- self.wait_until(lambda s: s["page"] == wide_page, "the wider page to reach the student")
- self.assertTrue(firefox.run("return document.getElementById('zoom_widen').hidden;"))
+ self.wait_until(lambda s: s["page"] == wide_page, "the expanded page to reach the student")
+ self.assertTrue(firefox.run("return document.getElementById('zoom_expand').hidden;"))
self.assertEqual(firefox.run("return window.kokkoja.session.page.baseLeft;"),
-((wide_page["width"] - teacher["page"]["width"]) // 2))
- # The page file carries the sheet's left edge and reads back with it.
- widened_file = self.current_page("Student notebook", {}, number=3)
- self.assertRegex(widened_file, r'data-base="\d+ 2100 -\d+ 0"')
+ # The page file carries the sheet's edges and reads back with them.
+ expanded_file = self.current_page("Student notebook", {}, number=3)
+ self.assertRegex(expanded_file, r'data-base="\d+ \d+ -\d+ -\d+"')
firefox.switch_to(teacher_tab)
# 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();")
sheet_scale = firefox.run("return window.kokkoja.renderer.cssPixelsPerUnit;")
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 style = getComputedStyle(desk);
return desk.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
""") / 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_widen').click();")
- self.wait_until(lambda s: s["page"]["width"] > shrunk_page["page"]["width"], "the page widened again")
+ firefox.run("document.getElementById('zoom_expand').click();")
+ self.wait_until(lambda s: s["page"]["width"] > shrunk_page["page"]["width"], "the page expanded again")
firefox.run("document.getElementById('zoom_fit').click();")
teacher = self.state()