Add a per-widget text-size picker for calendar/tasks legibility
Build and push server image / test (push) Successful in 42s
Build and push server image / build-and-push (push) Successful in 3m33s
Build and push server image / deploy (push) Failing after 1m24s

Calendar and tasks pack the most body text at the smallest default
sizes, so those two gear-icon dialogs get a "Text size" card (Normal/
Large/X-Large) alongside the existing Border card -- a new Widget-level
font_scale column with its own POST .../font-scale endpoint, same
Widget-property-not-config-field shape as border_style. Threaded through
every classic (calendar_render.py) and modern (html_render.py/
calendar_html_render.py) size calc via one shared panel_style.
scaled_size() so row heights/max_rows already derived from font size
re-fit around the bigger text automatically.
This commit is contained in:
2026-08-02 03:23:27 +00:00
parent 09119e775f
commit 2868087467
22 changed files with 426 additions and 60 deletions
+33 -23
View File
@@ -512,10 +512,12 @@ _AGENDA_FONTS = {"large": (34, 22, 20), "medium": (24, 22, 16), "small": (18, 16
def _build_agenda(events: list[dict], browse_offset: int, target_w: int, target_h: int, tz: ZoneInfo,
palette_rgb: list | None = None, weather_cities: list[dict] | None = None,
weather_units: str = "fahrenheit") -> Image.Image:
weather_units: str = "fahrenheit", font_scale: float = 1.0) -> Image.Image:
img, draw, region = panel_style.card_canvas(target_w, target_h)
title_size, body_size, weather_size = _AGENDA_FONTS[_size_tier(target_w, target_h)]
title_size, body_size, weather_size = (
panel_style.scaled_size(v, font_scale) for v in _AGENDA_FONTS[_size_tier(target_w, target_h)]
)
title_font = panel_style.font_bold(title_size)
body_font = panel_style.font_regular(body_size)
weather_font = panel_style.font_regular(weather_size)
@@ -533,7 +535,7 @@ _TODAY_TOMORROW_FONTS = {"large": (26, 18, 16), "medium": (20, 15, 13), "small":
def _build_today_tomorrow(events: list[dict], browse_offset: int, target_w: int, target_h: int, tz: ZoneInfo,
palette_rgb: list | None = None, weather_cities: list[dict] | None = None,
weather_units: str = "fahrenheit") -> Image.Image:
weather_units: str = "fahrenheit", font_scale: float = 1.0) -> Image.Image:
"""Two _draw_agenda_day sections stacked vertically (below each other
rather than side-by-side -- narrower than tall doesn't leave enough
width per day for the event-row text at smaller sizes). browse_offset
@@ -542,7 +544,9 @@ def _build_today_tomorrow(events: list[dict], browse_offset: int, target_w: int,
both views."""
img, draw, (cx0, cy0, cw, ch) = panel_style.card_canvas(target_w, target_h)
title_size, body_size, weather_size = _TODAY_TOMORROW_FONTS[_size_tier(target_w, target_h)]
title_size, body_size, weather_size = (
panel_style.scaled_size(v, font_scale) for v in _TODAY_TOMORROW_FONTS[_size_tier(target_w, target_h)]
)
title_font = panel_style.font_bold(title_size)
body_font = panel_style.font_regular(body_size)
weather_font = panel_style.font_regular(weather_size)
@@ -572,7 +576,7 @@ def _build_week(events: list[dict], browse_offset: int, target_w: int, target_h:
week_start: int, palette_rgb: list | None = None,
weather_cities: list[dict] | None = None, weather_units: str = "fahrenheit",
days: int = 7, layout: str = "horizontal",
start_offset: int = 0) -> Image.Image:
start_offset: int = 0, font_scale: float = 1.0) -> Image.Image:
"""`days` (2-10, see routers/api_widgets.py's clamp) side-by-side
columns (layout="horizontal", the original fixed-at-7 behavior
generalized) or stacked bands (layout="vertical", reusing
@@ -597,9 +601,9 @@ def _build_week(events: list[dict], browse_offset: int, target_w: int, target_h:
if layout == "vertical":
title_base, body_base, weather_base = _WEEK_VERTICAL_FONTS[tier]
title_font = panel_style.font_bold(max(14, title_base - days))
body_font = panel_style.font_regular(max(11, body_base - days))
weather_font = panel_style.font_regular(max(9, weather_base - days))
title_font = panel_style.font_bold(panel_style.scaled_size(max(14, title_base - days), font_scale))
body_font = panel_style.font_regular(panel_style.scaled_size(max(11, body_base - days), font_scale))
weather_font = panel_style.font_regular(panel_style.scaled_size(max(9, weather_base - days), font_scale))
section_h = ch // days
for i in range(days):
section_y0 = cy0 + i * section_h
@@ -611,7 +615,9 @@ def _build_week(events: list[dict], browse_offset: int, target_w: int, target_h:
weather_cities, weather_font, weather_units)
return img
header_size, chip_size, weather_size = _WEEK_HORIZONTAL_FONTS[tier]
header_size, chip_size, weather_size = (
panel_style.scaled_size(v, font_scale) for v in _WEEK_HORIZONTAL_FONTS[tier]
)
header_font = panel_style.font_bold(header_size)
chip_font = panel_style.font_regular(chip_size)
weather_font = panel_style.font_regular(weather_size)
@@ -667,7 +673,7 @@ _MONTH_FONTS = {"large": (16, 18), "medium": (12, 13), "small": (12, 13)}
def _build_month(events: list[dict], browse_offset: int, target_w: int, target_h: int, tz: ZoneInfo,
week_start: int, palette_rgb: list | None = None) -> Image.Image:
week_start: int, palette_rgb: list | None = None, font_scale: float = 1.0) -> Image.Image:
"""Density dots per day, not literal event text -- real text at
typical month-cell size (~100x70px) is close to unreadable on a
6-color dithered e-ink panel. Capped at 4 visible dots, "+N" beyond.
@@ -677,7 +683,9 @@ def _build_month(events: list[dict], browse_offset: int, target_w: int, target_h
comment above MARGIN/BG/FG."""
img, draw, (cx0, cy0, cw, ch) = panel_style.card_canvas(target_w, target_h)
header_size, day_size = _MONTH_FONTS[_size_tier(target_w, target_h)]
header_size, day_size = (
panel_style.scaled_size(v, font_scale) for v in _MONTH_FONTS[_size_tier(target_w, target_h)]
)
header_font = panel_style.font_bold(header_size)
day_font_in_month = panel_style.font_bold(day_size)
day_font_out_of_month = panel_style.font_regular(day_size)
@@ -750,7 +758,7 @@ def _build(events: list[dict], view: str, browse_offset: int, target_w: int, tar
fetch_summary: str, week_start: int, palette_rgb: list | None = None,
weather_cities: list[dict] | None = None, weather_units: str = "fahrenheit",
week_days: int = 7, week_layout: str = "horizontal",
week_start_offset: int = 0) -> Image.Image:
week_start_offset: int = 0, font_scale: float = 1.0) -> Image.Image:
tz = ZoneInfo(timezone) if timezone else ZoneInfo("UTC")
effective_view = view
if view == "month" and not _month_view_fits(target_w, target_h):
@@ -758,13 +766,13 @@ def _build(events: list[dict], view: str, browse_offset: int, target_w: int, tar
if effective_view == "agenda":
img = _build_agenda(events, browse_offset, target_w, target_h, tz, palette_rgb,
weather_cities, weather_units)
weather_cities, weather_units, font_scale)
elif effective_view == "today_tomorrow":
img = _build_today_tomorrow(events, browse_offset, target_w, target_h, tz, palette_rgb,
weather_cities, weather_units)
weather_cities, weather_units, font_scale)
elif effective_view == "week":
img = _build_week(events, browse_offset, target_w, target_h, tz, week_start, palette_rgb,
weather_cities, weather_units, week_days, week_layout, week_start_offset)
weather_cities, weather_units, week_days, week_layout, week_start_offset, font_scale)
elif effective_view == "month":
# Never given weather -- no room for it at typical month-cell
# size, same reasoning that already keeps this view to density
@@ -772,10 +780,10 @@ def _build(events: list[dict], view: str, browse_offset: int, target_w: int, tar
# docstring). Colors are still passed through, though -- that's
# a different concern (legibility of individual events) than
# needing a whole extra strip of content.
img = _build_month(events, browse_offset, target_w, target_h, tz, week_start, palette_rgb)
img = _build_month(events, browse_offset, target_w, target_h, tz, week_start, palette_rgb, font_scale)
else:
img = _build_agenda(events, browse_offset, target_w, target_h, tz, palette_rgb,
weather_cities, weather_units)
weather_cities, weather_units, font_scale)
if fetch_summary:
# Drawn as a final overlay onto the already-composited img (not
@@ -814,13 +822,13 @@ def render_calendar_preview_png(events: list[dict], view: str, browse_offset: in
fetch_summary: str = "", manage: dict | None = None, week_start: int = 0,
weather_cities: list[dict] | None = None, weather_units: str = "fahrenheit",
week_days: int = 7, week_layout: str = "horizontal",
week_start_offset: int = 0) -> bytes:
week_start_offset: int = 0, font_scale: float = 1.0) -> bytes:
"""Same pipeline as render_calendar, but a normal browser-viewable
PNG in logical (upright) orientation -- mirrors
image_pipeline.render_preview_png's relationship to render_frame."""
target_w, target_h = logical_render_size(orientation)
img = _build(events, view, browse_offset, target_w, target_h, timezone, fetch_summary, week_start,
palette_rgb, weather_cities, weather_units, week_days, week_layout, week_start_offset)
palette_rgb, weather_cities, weather_units, week_days, week_layout, week_start_offset, font_scale)
img = _apply_manage_overlay(img, manage)
quantized = _quantize(img, palette_rgb, dither_strength=1.0)
buf = io.BytesIO()
@@ -835,12 +843,14 @@ _TASKS_FONTS = {"large": (24, 18), "medium": (20, 16), "small": (16, 13)}
def _build_tasks(tasks: list[dict], target_w: int, target_h: int, palette_rgb: list | None = None,
title: str = "Tasks") -> Image.Image:
title: str = "Tasks", font_scale: float = 1.0) -> Image.Image:
"""A tasks widget's entire region is the checklist -- unlike the old
week-view slot, there's no day columns/header to share space with,
so this is just _draw_tasks over the whole box."""
img, draw, region = panel_style.card_canvas(target_w, target_h)
title_size, body_size = _TASKS_FONTS[_size_tier(target_w, target_h)]
title_size, body_size = (
panel_style.scaled_size(v, font_scale) for v in _TASKS_FONTS[_size_tier(target_w, target_h)]
)
title_font = panel_style.font_bold(title_size)
body_font = panel_style.font_regular(body_size)
_draw_tasks(img, draw, region, tasks, title_font, body_font, palette_rgb, title)
@@ -860,12 +870,12 @@ def render_tasks(tasks: list[dict], orientation: str, palette_rgb: list | None,
def render_tasks_preview_png(tasks: list[dict], orientation: str, palette_rgb: list | None,
manage: dict | None = None, title: str = "Tasks") -> bytes:
manage: dict | None = None, title: str = "Tasks", font_scale: float = 1.0) -> bytes:
"""Same pipeline as render_tasks, but a normal browser-viewable PNG
in logical (upright) orientation -- mirrors render_calendar_preview_
png's relationship to render_calendar."""
target_w, target_h = logical_render_size(orientation)
img = _build_tasks(tasks, target_w, target_h, palette_rgb, title)
img = _build_tasks(tasks, target_w, target_h, palette_rgb, title, font_scale=font_scale)
img = _apply_manage_overlay(img, manage)
quantized = _quantize(img, palette_rgb, dither_strength=1.0)
buf = io.BytesIO()