Widget system Phase 4a: widget CRUD + grid placement UI
Build and push server image / test (push) Successful in 19s
Build and push server image / build-and-push (push) Successful in 2m32s

Adds the actual "Android home screen" placement experience: a new
Layout tab with a pointer-driven canvas for dragging/resizing widgets
and adding new ones from a type picker. Backed by a new
routers/api_widgets.py (create/move/delete), which re-validates
bounds, minimum footprint, and no-overlap server-side regardless of
what the client already checked. A widget added without an explicit
position lands in the first open space that fits it
(grid.find_open_rect), so users don't have to hunt for empty space
themselves.

Also fixes a real latent bug this surfaced: changing a frame's
orientation swaps the widget grid's long/short axis, which left
existing widget placements out of bounds on the new grid with no
render-time safeguard. Orientation changes now reset the layout to a
single full-panel widget (keeping the first widget's type, dropping
the rest), with a client-side confirm before it happens.
This commit is contained in:
2026-07-24 12:48:56 -04:00
parent 99069ba5fe
commit 5d4bb53b8a
11 changed files with 780 additions and 5 deletions
+15
View File
@@ -65,6 +65,21 @@ def overlaps(a: Rect, b: Rect) -> bool:
return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah
def find_open_rect(orientation: str, existing: list[Rect], w: int, h: int) -> Rect | None:
"""First w x h rect that's in-bounds and doesn't overlap any of
`existing`, scanning row-major (top-left first) -- used when creating
a widget without an explicit placement (see routers/api_widgets.py),
so adding one from a type picker doesn't require the caller to find
empty space itself first. None if no such rect fits anywhere."""
cols, rows = grid_dims(orientation)
for y in range(rows - h + 1):
for x in range(cols - w + 1):
candidate = (x, y, w, h)
if not any(overlaps(candidate, other) for other in existing):
return candidate
return None
def cell_to_pixels(orientation: str, panel_w: int, panel_h: int, rect: Rect) -> tuple[int, int, int, int]:
"""Grid rect -> pixel rect in logical (pre-rotation) canvas space --
against image_pipeline.logical_render_size(orientation)'s own