diff --git a/config/sway/config b/config/sway/config index 7512c30..5c695a2 100644 --- a/config/sway/config +++ b/config/sway/config @@ -6,6 +6,11 @@ set $mod Mod4 # Wallpaper output * bg /usr/share/backgrounds/ekklesia/classroom-default.png fill +# UI scale first, so every surface below comes up at the right size +# instead of being resized underneath itself. This scales the whole +# session — Chrome, VLC and the dock alike — not just our own windows. +exec --no-startup-id /usr/local/bin/classroom-scale + # Notifications, started before the dock so nothing can race D-Bus into # activating an unstyled mako first. The wrapper resolves the styling at # the room's UI scale and execs mako, so the process is still just mako. diff --git a/docs/testing.md b/docs/testing.md index c7c36d7..3973148 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -21,14 +21,21 @@ before promoting to stable with `./update_stable.sh`. ## UI scale -- [ ] `UI_SCALE=1.0` looks exactly as it did before scaling existed -- [ ] `UI_SCALE=1.25`, then `sudo systemctl restart getty@tty1`: - dock, icons, clock and notifications all grow together -- [ ] `UI_SCALE=0.8` shrinks them together -- [ ] The reserved bottom strip grows and shrinks with the dock, and a - maximised Chrome window still stops above it +UI_SCALE sets sway's output scale, so it scales the whole session. +Change it, then `sudo systemctl restart getty@tty1`. + +- [ ] `UI_SCALE=1.0` is identical to a session with no scaling +- [ ] `swaymsg -t get_outputs` shows the mode still 1920x1080 at every + scale, with only the logical size changing +- [ ] At 1.25 and 1.5, **Chrome** grows: tabs, omnibox, toolbar icons, + menus and page content +- [ ] Chrome's buttons are still easy to hit with the mouse +- [ ] Dock, clock, notifications and the DVD bar grow by the same + amount — once, not twice +- [ ] `UI_SCALE=0.8` shrinks everything together - [ ] A nonsense value (`UI_SCALE=banana`) still gives a working shell - [ ] An extreme value (`UI_SCALE=9`) is clamped, not obeyed +- [ ] `journalctl` or the sway log shows `classroom-scale: session scale N` ## Reserved bottom shell (regression) @@ -128,6 +135,8 @@ With `DVD_ENABLED=true`: clock included — no wallpaper visible anywhere, at any point - [ ] A spinner and "Loading DVD…" show while the disc spins up - [ ] No partial VLC window is ever visible against the wallpaper +- [ ] The loading surface stays up until the disc menu is actually + showing — VLC's tiled black startup window is never visible - [ ] Video or the disc menu replaces the loading surface when ready - [ ] A disc that never opens still gives the desktop back (20s) - [ ] No VLC chrome anywhere: no menu bar, no fullscreen controller, diff --git a/docs/ui-scale.md b/docs/ui-scale.md index f752c0e..a5ca43a 100644 --- a/docs/ui-scale.md +++ b/docs/ui-scale.md @@ -2,7 +2,7 @@ Every classroom gets the same design at a size that suits its TV. A 32" panel a few feet from the children and a 65" set across a hall want -different sizes of the same dock, not different docks. +different sizes of the same room, not different rooms. ## Changing it @@ -18,43 +18,79 @@ Then restart the classroom shell: sudo systemctl restart getty@tty1 ``` -That ends the session and the autologin brings it straight back with the -new size. A reboot does the same thing. Nothing else needs touching, and -no CSS or source file is ever edited to change a room's size. - -## Values - -| UI_SCALE | Roughly | -| --- | --- | -| 0.8 | Small or close-up panel | -| 1.0 | Default. 1080p classroom TV, 32–50" | -| 1.25 | Large room, or a set viewed from further back | -| 1.5 | 65"+ | - -Anything outside 0.8–1.5 is clamped to that range, and anything that is -not a number at all falls back to 1.0, so a typo in device.conf cannot -produce an unusable shell. +That ends the session and the autologin brings it straight back at the +new size. A reboot does the same. Nothing else needs touching, and no +CSS or source file is ever edited to change a room's size. ## What it scales -- Dock height, and the reserved strip at the bottom of the screen -- Dock corner radius, padding and the spacing between buttons -- Button size, padding and corner radius -- Every dock icon -- Clock text size, padding, corner radius and its inset from the edge -- Notification width, height, padding, margin, corner radius, text size - and icon size +**Everything.** UI_SCALE is a property of the television, not of one +program, so it is applied where it reaches the whole session: sway's +output scale. -Colours, translucency and the hairline borders do not scale: those are -the design, not the size. At `UI_SCALE=1.0` the rendered stylesheet is -byte-identical to the one the shell used before scaling existed. +- Chrome's tabs, omnibox, toolbar icons and menus +- VLC, and any other application's own interface +- The dock, the clock, notifications and the DVD control bar +- Mouse hit targets, because they scale with the widgets -## How it works +The display mode is **not** changed. The panel stays at its native +1920x1080. Only the logical coordinate space shrinks, which is what +makes everything drawn in it larger: -`/etc/ekklesia-classroom/dock/style.css.in` and -`/etc/ekklesia-classroom/mako/config.in` are templates carrying -measurement tokens. `classroom-dock` resolves the stylesheet in memory at -startup; `classroom-notifications` resolves the notification config into -the session's runtime directory and hands it to mako. Neither template is -ever rewritten on disk, so an update cannot lose a room's setting and a -room's setting cannot drift from the packaged design. +| UI_SCALE | Logical space | Roughly | +| --- | --- | --- | +| 0.8 | 2400x1350 | Small or close-up panel | +| 1.0 | 1920x1080 | Default. 32–50" classroom TV | +| 1.25 | 1536x864 | Large room, or a set viewed from further back | +| 1.5 | 1280x720 | 65"+ | + +Anything outside 0.8–1.5 is clamped, and anything that is not a number +falls back to 1.0, so a typo in device.conf cannot produce an unusable +shell. + +## Measured + +Against 1920x1080 under sway, with Chrome 152: + +| | 1.0 | 1.25 | 1.5 | +| --- | --- | --- | --- | +| Chrome tab strip | 40px | 49px (x1.23) | 59px (x1.48) | +| sway title bar | 25px | 31px (x1.24) | 38px (x1.52) | +| Classroom dock | 74px | 92px (x1.24) | 110px (x1.49) | + +Everything scales once, together. + +## Why our own surfaces do not scale themselves + +`classroom-scale` runs first in the sway config and sets the output +scale. From then on the dock, the notifications and the DVD control bar +are measured in the same logical pixels sway is already scaling, so they +must render **unscaled** — `SHELL_SCALE` in `classroom_config.py` is +`1.0`. Scaling them again would apply UI_SCALE twice and the dock would +come out half again too big. + +The stylesheet templates still take a scale, and `SHELL_SCALE` is the +one place it comes from. If compositor scaling ever has to be abandoned, +setting it back to `ui_scale(config)` restores per-surface scaling +without touching a template. + +## Tradeoffs + +**GTK 3 has no fractional scaling.** At UI_SCALE=1.5 sway advertises an +integer scale of 2 to GTK, which renders at 2x, and sway scales the +result down to 1.5x. The dock is the right size but very slightly softer +than at 1.0. On a TV at classroom distance this is not visible. + +**XWayland is softer at fractional scale.** X11 clients have no concept +of a fractional output scale, so sway renders them at the logical size +and scales the buffer. Measured on terminal text, scale 1.5 produced +about 50% more intermediate-tone pixels than scale 1.0 — real, modest +softening. + +VLC is the only XWayland application in the classroom, and in DVD mode +none of its interface is on screen: only video, from a 720x480 disc +being upscaled regardless. Chrome runs on Wayland natively and is sharp +at every scale, which is what matters for reading. + +At UI_SCALE=1.0 nothing is scaled at all and the session is identical to +one with no scaling. diff --git a/lib/classroom_config.py b/lib/classroom_config.py index 840d9bd..8f9a298 100644 --- a/lib/classroom_config.py +++ b/lib/classroom_config.py @@ -60,6 +60,20 @@ def ui_scale(config): return min(max(value, UI_SCALE_MIN), UI_SCALE_MAX) +# The scale the classroom's own GTK surfaces render at. +# +# UI_SCALE is applied by sway to the whole output (see classroom-scale), +# which scales Chrome, VLC and every other application along with the +# dock. Our surfaces are measured in the same logical pixels sway is +# already scaling, so scaling them here as well would apply UI_SCALE +# twice and the dock would come out half again too big. +# +# This is the seam: if compositor scaling ever has to be abandoned, +# changing this back to ui_scale(config) restores per-surface scaling +# without touching the templates. +SHELL_SCALE = 1.0 + + def scaled(value, scale): - """A pixel measurement at the configured scale, never smaller than 1.""" + """A pixel measurement at the given scale, never smaller than 1.""" return max(1, int(round(value * scale))) diff --git a/packaging/debian/postinst b/packaging/debian/postinst index 001b53f..84a6d10 100755 --- a/packaging/debian/postinst +++ b/packaging/debian/postinst @@ -24,6 +24,7 @@ chmod 0755 /usr/local/bin/classroom-dvd-session chmod 0755 /usr/local/bin/classroom-dvd-css-setup chmod 0755 /usr/local/bin/classroom-notify-test chmod 0755 /usr/local/bin/classroom-notifications +chmod 0755 /usr/local/bin/classroom-scale chmod 0755 /usr/local/bin/classroom-chrome-setup chmod 0755 /usr/local/bin/classroom-provision diff --git a/scripts/classroom-dock b/scripts/classroom-dock index 6bf6723..90c6904 100755 --- a/scripts/classroom-dock +++ b/scripts/classroom-dock @@ -16,10 +16,10 @@ sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") import classroom_style as style # noqa: E402 from classroom_config import ( # noqa: E402 + SHELL_SCALE, enabled, read_device_config, scaled, - ui_scale, ) # Optional on purpose. The dock is the one thing the classroom cannot @@ -107,7 +107,7 @@ class ClassroomShell(Gtk.Window): self.set_resizable(False) self.config = read_device_config() - self.scale = ui_scale(self.config) + self.scale = SHELL_SCALE self.dvd_image = None self.disc_ready = None @@ -383,7 +383,7 @@ def load_css(scale): def main(): - load_css(ui_scale(read_device_config())) + load_css(SHELL_SCALE) shell = ClassroomShell() shell.connect( diff --git a/scripts/classroom-notifications b/scripts/classroom-notifications index 97ef84c..089bff3 100755 --- a/scripts/classroom-notifications +++ b/scripts/classroom-notifications @@ -13,7 +13,7 @@ from pathlib import Path sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") -from classroom_config import read_device_config, scaled, ui_scale # noqa: E402 +from classroom_config import SHELL_SCALE, scaled # noqa: E402 MAKO = "/usr/bin/mako" TEMPLATE = Path("/etc/ekklesia-classroom/mako/config.in") @@ -53,7 +53,7 @@ def render(scale): def main(): - scale = ui_scale(read_device_config()) + scale = SHELL_SCALE try: path = runtime_directory() / "mako.config" diff --git a/scripts/classroom-scale b/scripts/classroom-scale new file mode 100755 index 0000000..971bd66 --- /dev/null +++ b/scripts/classroom-scale @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +"""Apply the room's UI scale to the whole classroom session. + +UI_SCALE is a property of the television, not of one program, so it is +applied where it reaches everything: sway's output scale. Chrome's tabs +and omnibox, its menus, VLC, and the classroom's own dock and +notifications are then all measured in the same scaled logical pixels. + +The display mode is untouched — the panel stays at its native +resolution. Only the logical coordinate space shrinks, which is what +makes everything drawn in it larger. + +Run from the sway config before anything else starts, so surfaces come +up at the right size rather than being resized underneath themselves. +""" + +import os +import subprocess +import sys + +sys.path.insert(0, "/usr/local/lib/ekklesia-classroom") +sys.path.insert( + 0, + os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"), +) + +from classroom_config import read_device_config, ui_scale # noqa: E402 + + +def main(): + scale = ui_scale(read_device_config()) + + try: + result = subprocess.run( + ["swaymsg", "output", "*", "scale", f"{scale:g}"], + capture_output=True, + text=True, + ) + except OSError as error: + print(f"classroom-scale: {error}", file=sys.stderr) + return 1 + + if result.returncode != 0: + print( + f"classroom-scale: {result.stderr.strip() or 'sway refused the scale'}", + file=sys.stderr, + ) + return 1 + + print(f"classroom-scale: session scale {scale:g}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/build-release.sh b/tools/build-release.sh index 0e02155..b82776e 100755 --- a/tools/build-release.sh +++ b/tools/build-release.sh @@ -121,6 +121,10 @@ install -Dm755 \ "$ROOT/scripts/classroom-notifications" \ "$STAGE/usr/local/bin/classroom-notifications" +install -Dm755 \ + "$ROOT/scripts/classroom-scale" \ + "$STAGE/usr/local/bin/classroom-scale" + install -Dm755 \ "$ROOT/scripts/classroom-chrome-setup" \ "$STAGE/usr/local/bin/classroom-chrome-setup" @@ -138,6 +142,10 @@ install -Dm644 \ "$ROOT/lib/classroom_style.py" \ "$STAGE/usr/local/lib/ekklesia-classroom/classroom_style.py" +install -Dm644 \ + "$ROOT/lib/classroom_sway.py" \ + "$STAGE/usr/local/lib/ekklesia-classroom/classroom_sway.py" + install -Dm644 \ "$ROOT/lib/classroom_vlc.py" \ "$STAGE/usr/local/lib/ekklesia-classroom/classroom_vlc.py"