mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 18:07:20 +01:00
217 lines
7.4 KiB
Python
217 lines
7.4 KiB
Python
"""Widget builders for the gate's Body tab.
|
|
|
|
Split from :mod:`diet_guard._gatelock_body` (the controller) for the 250-line
|
|
cap. Everything here only builds and packs; every value lives in
|
|
:class:`~diet_guard._gatelock_body_vars.BodyVars` and every action is a
|
|
callback.
|
|
|
|
Layout, top to bottom: the goal sentence ("To lose 0.5 kg / week on
|
|
Moderate eat 2019 kcal", every choice a drop-down), then weight with the
|
|
graph beside BMI, ideal weight and resting burn, then workouts and the week
|
|
plan, then the profile (set up once -- first instead while incomplete) and
|
|
last the body-fat log.
|
|
|
|
.. note::
|
|
Builds ``tkinter`` widgets, so it **must** appear in
|
|
``diet_guard.tests._gate_fixtures._GATE_TK_MODULES``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tkinter as tk
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gatelock import ButtonStyle, make_button
|
|
|
|
from diet_guard import _gatelock_glyphs as g
|
|
from diet_guard._body_energy import (
|
|
ACTIVITY_CHOICES,
|
|
DIRECTIONS,
|
|
PRESET_AMOUNTS,
|
|
TIME_UNITS,
|
|
WEIGHT_UNITS,
|
|
)
|
|
from diet_guard._gatelock_body_glyphs import ACTIVITY
|
|
from diet_guard._gatelock_body_graph import GRAPH_RANGES
|
|
from diet_guard._gatelock_body_vars import BodyWidgets
|
|
from diet_guard._gatelock_calendar_types import _ACCENT, _COLORS, _FIELD_BG, _MUTED
|
|
from diet_guard._gatelock_spacing import MD, SM, XS
|
|
from diet_guard._gatelock_typography import BODY, LABEL, SUBTITLE, TITLE
|
|
from diet_guard._gatelock_ui import BG, FG
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from diet_guard._gatelock_body_vars import BodyCallbacks, BodyVars
|
|
|
|
__all__ = ["GRAPH_H", "GRAPH_W", "build_body_frame"]
|
|
|
|
GRAPH_W = 720
|
|
GRAPH_H = 300
|
|
_MONO = ("Courier", LABEL)
|
|
_SECONDARY = ButtonStyle(variant="secondary", bold=False)
|
|
_TOGGLE = {"bg": BG, "fg": FG, "selectcolor": _FIELD_BG, "activebackground": BG}
|
|
|
|
|
|
def _font(size: int = LABEL) -> tuple[str, int]:
|
|
return (_COLORS.typography.font_family, size)
|
|
|
|
|
|
def _label(parent: tk.Misc, text: str, size: int = LABEL) -> None:
|
|
tk.Label(parent, text=text, font=_font(size), bg=BG, fg=FG).pack(
|
|
side="left", padx=(0, XS)
|
|
)
|
|
|
|
|
|
def _entry(parent: tk.Misc, var: tk.StringVar, width: int, size: int = BODY) -> None:
|
|
tk.Entry(
|
|
parent,
|
|
textvariable=var,
|
|
width=width,
|
|
font=_font(size),
|
|
bg=_FIELD_BG,
|
|
fg=FG,
|
|
insertbackground=FG,
|
|
).pack(side="left", padx=(0, SM))
|
|
|
|
|
|
def _row(parent: tk.Misc) -> tk.Frame:
|
|
row = tk.Frame(parent, bg=BG)
|
|
row.pack(anchor="w", pady=(XS, XS))
|
|
return row
|
|
|
|
|
|
def _block(parent: tk.Misc, var: tk.StringVar) -> None:
|
|
tk.Label(
|
|
parent, textvariable=var, font=_MONO, bg=BG, fg=FG, justify="left", anchor="w"
|
|
).pack(anchor="w", pady=(SM, 0))
|
|
|
|
|
|
def _button(parent: tk.Misc, text: str, command: Callable[[], None]) -> None:
|
|
make_button(parent, _COLORS, text, command, _SECONDARY).pack(
|
|
side="left", padx=(0, SM)
|
|
)
|
|
|
|
|
|
def _menu(
|
|
parent: tk.Misc, var: tk.StringVar, values: list[str], cb: BodyCallbacks
|
|
) -> None:
|
|
"""A drop-down that re-answers the goal whenever a value is picked."""
|
|
menu = tk.OptionMenu(parent, var, *values, command=lambda _v: cb.goal_changed())
|
|
menu.config(font=_font(SUBTITLE), bg=_FIELD_BG, fg=_ACCENT, highlightthickness=0)
|
|
menu.pack(side="left", padx=(0, XS))
|
|
|
|
|
|
def _build_goal(parent: tk.Misc, vars_: BodyVars, cb: BodyCallbacks) -> None:
|
|
"""``[↓] [0.5][v] [kg] / [◑] [▄] → 2019 kcal``: every choice a glyph."""
|
|
goal = vars_.goal
|
|
row = _row(parent)
|
|
_menu(row, goal.direction, [g.DIRECTION[d] for d in DIRECTIONS], cb)
|
|
_entry(row, goal.amount, 5, SUBTITLE)
|
|
_menu(row, goal.amount, [f"{a:g}" for a in PRESET_AMOUNTS], cb)
|
|
_menu(row, goal.weight_unit, list(WEIGHT_UNITS), cb)
|
|
_label(row, "/", SUBTITLE)
|
|
_menu(row, goal.time_unit, [g.SPAN[t] for t in TIME_UNITS], cb)
|
|
_menu(row, goal.activity, [ACTIVITY[k] for k in ACTIVITY_CHOICES], cb)
|
|
tk.Label(
|
|
row, textvariable=goal.answer, font=(*_font(TITLE), "bold"), bg=BG, fg=_ACCENT
|
|
).pack(side="left", padx=(MD, 0))
|
|
_button(row, g.OK, cb.goal_changed)
|
|
_block(parent, vars_.blocks["table"])
|
|
|
|
|
|
def _build_profile(parent: tk.Misc, vars_: BodyVars, cb: BodyCallbacks) -> None:
|
|
row = _row(parent)
|
|
_label(row, g.BIRTH)
|
|
_entry(row, vars_.profile.birth, 11)
|
|
_label(row, "cm")
|
|
_entry(row, vars_.profile.height, 6)
|
|
sex = vars_.profile.sex
|
|
for value, text in (("m", g.MALE), ("f", g.FEMALE)):
|
|
tk.Radiobutton(row, text=text, value=value, variable=sex, **_TOGGLE).pack(
|
|
side="left"
|
|
)
|
|
_button(row, g.SAVE, cb.save_profile)
|
|
_block(parent, vars_.blocks["summary"])
|
|
|
|
|
|
def _dated_row(
|
|
parent: tk.Misc,
|
|
label: str,
|
|
vars_pair: tuple[tk.StringVar, tk.StringVar],
|
|
actions: tuple[Callable[[], None], Callable[[], None]],
|
|
) -> None:
|
|
row = _row(parent)
|
|
_label(row, label)
|
|
_entry(row, vars_pair[0], 6)
|
|
_label(row, g.CLOCK)
|
|
_entry(row, vars_pair[1], 11)
|
|
_button(row, g.SAVE, actions[0])
|
|
_button(row, g.DELETE, actions[1])
|
|
|
|
|
|
def _build_left(col: tk.Misc, vars_: BodyVars, cb: BodyCallbacks) -> tk.Canvas:
|
|
logs = vars_.logs
|
|
pair = (logs.weight, logs.weight_day)
|
|
_dated_row(col, f"{g.AMOUNT} kg", pair, (cb.save_weight, cb.delete_weight))
|
|
row = _row(col)
|
|
for value, text in (("weight", g.AMOUNT), ("fat", "%")):
|
|
tk.Radiobutton(
|
|
row,
|
|
text=text,
|
|
value=value,
|
|
variable=logs.graph,
|
|
command=cb.recalc,
|
|
**_TOGGLE,
|
|
).pack(side="left")
|
|
for text, days in GRAPH_RANGES:
|
|
_button(row, text, lambda d=days: cb.set_range(d))
|
|
canvas = tk.Canvas(col, width=GRAPH_W, height=GRAPH_H, bg=BG, highlightthickness=0)
|
|
canvas.pack(anchor="w", pady=(SM, 0))
|
|
tk.Label(col, textvariable=vars_.status, font=_font(), bg=BG, fg=_MUTED).pack(
|
|
anchor="w"
|
|
)
|
|
_block(col, vars_.blocks["workouts"])
|
|
return canvas
|
|
|
|
|
|
def _build_right(col: tk.Misc, vars_: BodyVars, cb: BodyCallbacks) -> None:
|
|
for name in ("bmi", "ideal", "bmr"):
|
|
_block(col, vars_.blocks[name])
|
|
row = _row(col)
|
|
_label(row, f"{g.AVERAGE} kcal")
|
|
_entry(row, vars_.plan.target, 6)
|
|
# Seven day boxes under dots, the weekend pair larger (no day names).
|
|
for dot, var in zip(g.WEEKDAY_DOTS, vars_.plan.days, strict=True):
|
|
_label(row, dot)
|
|
_entry(row, var, 5)
|
|
_button(row, g.OK, cb.recalc)
|
|
_block(col, vars_.blocks["plan"])
|
|
|
|
|
|
def build_body_frame(
|
|
parent: tk.Misc, vars_: BodyVars, cb: BodyCallbacks, *, profile_first: bool
|
|
) -> BodyWidgets:
|
|
"""Build the whole tab into ``parent``; return what the controller needs."""
|
|
frame = tk.Frame(parent, bg=BG)
|
|
inner = tk.Frame(frame, bg=BG)
|
|
inner.pack(fill="both", expand=True, padx=MD)
|
|
if profile_first:
|
|
_build_profile(inner, vars_, cb)
|
|
_build_goal(inner, vars_, cb)
|
|
columns = tk.Frame(inner, bg=BG)
|
|
columns.pack(anchor="w", pady=(MD, 0))
|
|
left = tk.Frame(columns, bg=BG)
|
|
left.pack(side="left", anchor="n", padx=(0, MD))
|
|
right = tk.Frame(columns, bg=BG)
|
|
right.pack(side="left", anchor="n")
|
|
canvas = _build_left(left, vars_, cb)
|
|
_build_right(right, vars_, cb)
|
|
if not profile_first:
|
|
_build_profile(inner, vars_, cb)
|
|
logs = vars_.logs
|
|
pair = (logs.body_fat, logs.fat_day)
|
|
_dated_row(inner, "%", pair, (cb.save_body_fat, cb.delete_body_fat))
|
|
_block(inner, vars_.blocks["bodyfat"])
|
|
return BodyWidgets(frame=frame, canvas=canvas)
|