screen-locker/screen_locker/_unlock_view.py
Krzysztof Rudnicki a79c4fdd05 Stop the lock screen scrolling itself, and make every screen fit 768px
This morning the lock screen scrolled to the bottom and back with nobody
touching the keyboard, on the 1366x768 panel. Reproduced under Xvfb with the
real app: one programmatic focus_set() produced three scrolls,
frac 0.000 -> 0.436 -> 0.145. The cause is in gatelock (fixed in v0.4.0,
pinned here): the viewport followed every <FocusIn>, and this app focuses a
widget on every repaint. It now follows only focus the *user* moved, and a
screen that fits cannot scroll at all.

The other half is this app's own layout, because scrolling was covering for
screens that did not fit. The manual-workout form needed 1643px of a 768px
screen -- SUBMIT and BACK were simply off the bottom:

* Every font size and pad now goes through the design-system tokens rather
  than a literal. A positive size means *points* to Tk, so "36" rendered
  ~37% larger than the 36px intended; that alone was most of the overflow,
  on all 88 call sites.
* The form's cells put their label beside the field instead of above it, and
  the three reflection boxes share rows instead of taking one each.

Result, measured by the new scripts/verify_screen_fits.py (a pre-commit hook
now, so this cannot come back quietly): the form is 752px of 768px, and every
other screen -- including heat-skip, which had no viewport at all -- fits with
room to spare.

Also fixed, all found by measuring rather than by reading: content that fits
is centred again (the reset-to-top was cancelling it on every repaint), the
scrollbar only appears when there is something to scroll, and unlock_screen
moved to _unlock_view so screen_lock.py stays under the 400-line cap.

Tests: 896 pass. The focus/scroll helpers shipped in 3e77f4e had no coverage
at all; test_focus_and_scroll.py covers them now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
2026-08-03 15:57:48 +02:00

72 lines
2.5 KiB
Python

"""The success screen shown once a workout has been credited.
Split out of ``screen_lock`` so that module stays inside the repo's 400-line
cap; this is pure view code (paint the credit, then hand off to the
commitment prompt or the close), which is exactly the kind of thing the other
``_ui_*`` mixins already own.
"""
from __future__ import annotations
from screen_locker._constants import EXTRA_BENEFITS_FILE
from screen_locker._extra_benefits import current_streak
_UNLOCK_DELAY_MS = 1500
# Workout kinds that carry a commitment prompt on the way out: they are the
# ones the user actually performed today, so "will you work out tomorrow?" is
# a question worth asking.
_COMMITTABLE = ("phone_verified", "runnerup_verified", "manual_workout")
class UnlockViewMixin:
"""Paints the unlock screen and routes to whatever follows it."""
def unlock_screen(self) -> None:
"""Apply workout credit and display the success message."""
credit = self._apply_workout_credit()
self.clear_container()
self._label(
"Great job! 💪",
role="display",
scale=1.5,
color=self._colors.success,
pad="lg",
)
if credit.shutdown_adjusted:
self._text(
"Shutdown time +2h later! 🎁",
role="title",
color=self._colors.warning,
)
if credit.extra_bonus_delta > 0:
self._text(
f"Extra workout today! +{credit.extra_bonus_delta}h tonight",
role="subtitle",
color=self._colors.warning,
)
if credit.new_debt is not None:
self._text(
f"Workout debt: {credit.new_debt}",
role="subtitle",
color=self._colors.warning
if credit.new_debt > 0
else self._colors.muted,
)
streak = current_streak(EXTRA_BENEFITS_FILE)
if streak >= 1:
self._text(
f"🔥 {streak}-week streak (5+ workouts each)",
role="label",
color=self._colors.muted,
)
self._text("Screen Unlocked!", role="display", pad="md")
if self.workout_data.get("type") in _COMMITTABLE:
self.root.after(
_UNLOCK_DELAY_MS,
lambda: self._show_commitment_prompt(on_done=self.close),
)
else:
self.root.after(_UNLOCK_DELAY_MS, self.close)