theme config, layout preview, container alignment
Server: ThemeConfig entity + CRUD (GET/PUT /theme), SQLite persistence, ThemeUpdate broadcast to ESP32 on save and initial connect. Client: render engine uses theme colors, full-screen redraw on theme change. SPA: theme page with color pickers + presets, layout preview with TS port of layout engine, justify/align controls on containers. DisplayHint refactored to struct (kind + h_align + v_align).
This commit is contained in:
93
spa/src/lib/layout-engine.ts
Normal file
93
spa/src/lib/layout-engine.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { LayoutNode, LayoutChild } from "@/api/types"
|
||||
|
||||
export interface BoundingBox {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function computeLayout(
|
||||
layout: LayoutNode,
|
||||
bounds: BoundingBox,
|
||||
): Map<number, BoundingBox> {
|
||||
const out = new Map<number, BoundingBox>()
|
||||
computeNode(layout, bounds, out)
|
||||
return out
|
||||
}
|
||||
|
||||
function computeNode(
|
||||
node: LayoutNode,
|
||||
bounds: BoundingBox,
|
||||
out: Map<number, BoundingBox>,
|
||||
) {
|
||||
if (node.type === "leaf") {
|
||||
if (node.widget_id !== undefined) {
|
||||
out.set(node.widget_id, bounds)
|
||||
}
|
||||
} else {
|
||||
computeContainer(node, bounds, out)
|
||||
}
|
||||
}
|
||||
|
||||
function computeContainer(
|
||||
container: LayoutNode,
|
||||
bounds: BoundingBox,
|
||||
out: Map<number, BoundingBox>,
|
||||
) {
|
||||
const pad = container.padding ?? 0
|
||||
const inner: BoundingBox = {
|
||||
x: bounds.x + pad,
|
||||
y: bounds.y + pad,
|
||||
width: Math.max(0, bounds.width - pad * 2),
|
||||
height: Math.max(0, bounds.height - pad * 2),
|
||||
}
|
||||
|
||||
const children = container.children ?? []
|
||||
if (children.length === 0) return
|
||||
|
||||
const isRow = container.direction === "row"
|
||||
const totalAxis = isRow ? inner.width : inner.height
|
||||
const gap = container.gap ?? 0
|
||||
const totalGap = gap * Math.max(0, children.length - 1)
|
||||
const available = Math.max(0, totalAxis - totalGap)
|
||||
|
||||
let fixedTotal = 0
|
||||
let flexTotal = 0
|
||||
for (const child of children) {
|
||||
if (child.sizing.type === "fixed") {
|
||||
fixedTotal += child.sizing.value
|
||||
} else {
|
||||
flexTotal += child.sizing.value
|
||||
}
|
||||
}
|
||||
|
||||
const flexSpace = Math.max(0, available - fixedTotal)
|
||||
|
||||
const childSizes: number[] = children.map((child: LayoutChild) => {
|
||||
if (child.sizing.type === "fixed") return child.sizing.value
|
||||
if (flexTotal > 0) {
|
||||
return Math.floor((flexSpace * child.sizing.value) / flexTotal)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const childrenTotal = childSizes.reduce((a, b) => a + b, 0)
|
||||
const remaining = Math.max(0, totalAxis - childrenTotal - totalGap)
|
||||
|
||||
// Default justify: Start
|
||||
let offset = 0
|
||||
let justifyGap = gap
|
||||
// SpaceBetween / SpaceEvenly could be added here if justify_content is on the DTO
|
||||
void remaining // unused for now, Start justification
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childSize = childSizes[i]
|
||||
const childBounds: BoundingBox = isRow
|
||||
? { x: inner.x + offset, y: inner.y, width: childSize, height: inner.height }
|
||||
: { x: inner.x, y: inner.y + offset, width: inner.width, height: childSize }
|
||||
|
||||
computeNode(children[i].node, childBounds, out)
|
||||
offset += childSize + justifyGap
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user