// "Mein Zimmer" - the Home Assistant room panel. // // Its own violet hue rather than the player's teal, so the two pages read as siblings. // Lights and shutters are drawn from one `RoomCard` model in Home Assistant's own // config order; `kind` is what each card branches on. import { Theme } from "theme.slint"; import { RoomCard, RoomRow, SceneTile, Swatch } from "models.slint"; component ToggleSwitch inherits Rectangle { in property on; callback clicked(); width: 74px; height: 42px; border-radius: 21px; background: root.on ? Theme.room-accent : Theme.room-toggle-off; animate background { duration: 140ms; } Rectangle { x: root.on ? parent.width - self.width - 4px : 4px; y: 4px; width: 34px; height: 34px; border-radius: 17px; background: white; drop-shadow-color: #0f051d66; drop-shadow-blur: 6px; drop-shadow-offset-y: 2px; animate x { duration: 140ms; easing: ease-out; } } TouchArea { clicked => { root.clicked(); } } } // A flat labelled button - the shutter presets and the up/stop/down row. component RoomButton inherits Rectangle { in property label; in property active; in property active-fill: Theme.room-accent; in property text-size: 14px; callback clicked(); border-radius: 14px; background: root.active ? root.active-fill : (touch.pressed ? Theme.room-off-bg : Theme.room-btn); animate background { duration: 120ms; } Text { text: root.label; font-size: root.text-size; font-weight: 800; color: root.active ? white : Theme.room-btn-fg; vertical-alignment: center; horizontal-alignment: center; } touch := TouchArea { clicked => { root.clicked(); } } } component RoomCardFrame inherits Rectangle { in property glow; in property glow-tint; background: Theme.room-card.with-alpha(0.55); border-radius: 22px; // Standing in for the web card's `0 0 0 3px color-mix(...)` ring: a lit lamp's own // colour around its card is how the page reads at a glance from across the room. border-width: root.glow ? 3px : 0px; border-color: root.glow-tint; drop-shadow-color: #0f051d59; drop-shadow-blur: 24px; drop-shadow-offset-y: 8px; } component LightCard inherits RoomCardFrame { in property card; in property <[Swatch]> swatches; callback toggle(); callback set-level(int); callback set-swatch(int); glow: root.card.on; glow-tint: root.card.tint; VerticalLayout { padding-left: 20px; padding-right: 20px; padding-top: 18px; padding-bottom: 20px; spacing: 14px; HorizontalLayout { spacing: 14px; Rectangle { width: 56px; height: 56px; border-radius: 16px; background: root.card.on ? root.card.tint : Theme.room-off-bg; drop-shadow-color: root.card.on ? root.card.tint : transparent; drop-shadow-blur: 22px; // A pendant for a colour lamp, a ceiling fitting for a plain one - // drawn rather than iconographic, because there is no icon font here. Rectangle { width: 34px; height: 34px; x: 11px; y: 11px; if root.card.is-color: Path { width: 100%; height: 100%; viewbox-width: 48; viewbox-height: 48; stroke: root.card.on ? Theme.room-btn-fg : Theme.room-off-fg; stroke-width: 3px; commands: "M 24 7 A 17 17 0 1 1 23.9 7 Z"; } if !root.card.is-color: Path { width: 100%; height: 100%; viewbox-width: 48; viewbox-height: 48; stroke: root.card.on ? Theme.room-btn-fg : Theme.room-off-fg; stroke-width: 3px; commands: "M 9 30 L 24 11 L 39 30 Z M 24 4 L 24 11"; } } } VerticalLayout { alignment: center; horizontal-stretch: 1; Text { text: root.card.name; font-size: 19px; font-weight: 900; color: Theme.room-ink; overflow: elide; } } VerticalLayout { alignment: center; ToggleSwitch { on: root.card.on; clicked => { root.toggle(); } } } } // Five steps rather than a slider: a child aiming at a bar hits a level. HorizontalLayout { spacing: 6px; height: 46px; opacity: root.card.on ? 1.0 : 0.55; for step[i] in [1, 2, 3, 4, 5]: Rectangle { border-radius: 12px; background: root.card.on && root.card.level >= step ? root.card.tint : Theme.room-off-bg; animate background { duration: 120ms; } TouchArea { clicked => { root.set-level(step); } } } } if root.card.is-color: HorizontalLayout { spacing: 10px; height: 46px; alignment: start; for swatch[i] in root.swatches: Rectangle { width: 46px; height: 46px; border-radius: 23px; background: swatch.tint; border-width: 4px; border-color: root.card.on && root.card.swatch == i ? Theme.room-btn-fg : Theme.room-card.with-alpha(0.8); drop-shadow-color: #0f051d59; drop-shadow-blur: 8px; drop-shadow-offset-y: 3px; TouchArea { clicked => { root.set-swatch(i); } } } } } } component ShutterCard inherits RoomCardFrame { in property card; in property <[string]> presets; callback set-preset(int); callback open(); callback stop(); callback close(); VerticalLayout { padding-left: 20px; padding-right: 20px; padding-top: 18px; padding-bottom: 20px; spacing: 16px; HorizontalLayout { spacing: 14px; alignment: start; Rectangle { width: 56px; height: 56px; border-radius: 16px; background: Theme.room-icon-bg; Path { x: 11px; y: 11px; width: 34px; height: 34px; viewbox-width: 48; viewbox-height: 48; stroke: Theme.room-icon-fg; stroke-width: 3px; commands: "M 7 7 L 41 7 L 41 41 L 7 41 Z M 7 16 L 41 16 M 7 24 L 41 24 M 7 32 L 41 32"; } } VerticalLayout { alignment: center; Text { text: root.card.name; font-size: 19px; font-weight: 900; color: Theme.room-ink; overflow: elide; } Text { text: root.card.status; font-size: 13px; font-weight: 700; color: Theme.room-sub.with-alpha(0.8); } } } HorizontalLayout { spacing: 18px; // The window itself: sky behind, slats drawn down from the top to however // closed the shutter is. Real covers report their own position, so this // only ever reflects Home Assistant - no client-side movement animation. Rectangle { width: 104px; height: 140px; border-radius: 12px; border-width: 3px; border-color: Theme.room-icon-fg; clip: true; background: @linear-gradient(180deg, Theme.room-window-a 0%, Theme.room-window-b 100%); Rectangle { x: 0; y: 0; width: 100%; height: parent.height * clamp(root.card.closed-percent / 100, 0.0, 1.0); clip: true; animate height { duration: 200ms; easing: linear; } // The slats, as a fixed ladder clipped by the parent's height. for slat[i] in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]: Rectangle { y: i * 12px; width: 100%; height: 12px; background: Theme.room-slat-a; Rectangle { y: 9px; width: 100%; height: 3px; background: Theme.room-slat-b; } } } } VerticalLayout { spacing: 10px; horizontal-stretch: 1; if root.card.supports-position: VerticalLayout { spacing: 8px; for row[r] in [0, 1]: HorizontalLayout { spacing: 8px; height: 48px; for col[c] in [0, 1]: RoomButton { label: root.presets[r * 2 + c]; active: root.card.preset == r * 2 + c; clicked => { root.set-preset(r * 2 + c); } } } } HorizontalLayout { spacing: 8px; height: 52px; RoomButton { label: "▲"; text-size: 20px; active: root.card.moving == "up"; clicked => { root.open(); } } RoomButton { label: "■"; text-size: 18px; active: true; active-fill: Theme.room-stop; clicked => { root.stop(); } } RoomButton { label: "▼"; text-size: 20px; active: root.card.moving == "down"; clicked => { root.close(); } } } } } } } export component RoomView inherits Rectangle { in property <[RoomRow]> rows; in property <[SceneTile]> scenes; in property <[Swatch]> swatches; in property <[string]> presets; in property loading; callback toggle-light(string); callback set-level(string, int); callback set-swatch(string, int); callback set-preset(string, int); callback shutter(string, string); // entity, "open" | "stop" | "close" callback activate-scene(string); background: @linear-gradient(180deg, Theme.room-top 0%, Theme.room-mid 45%, Theme.room-deep 100%); VerticalLayout { padding-top: 18px; spacing: 6px; Text { text: "Mein Zimmer"; font-size: 34px; font-weight: 900; color: Theme.room-paper; horizontal-alignment: center; } if root.loading: VerticalLayout { alignment: center; Text { text: "Einen Moment …"; font-size: 22px; font-weight: 800; color: Theme.room-paper; horizontal-alignment: center; } } if !root.loading: Flickable { content-height: body.preferred-height; content-width: self.width; body := VerticalLayout { padding-left: 32px; padding-right: Theme.rail-clearance; padding-top: 14px; padding-bottom: 64px; spacing: 30px; alignment: start; if root.scenes.length > 0: VerticalLayout { spacing: 14px; Text { text: "Szenen"; font-size: 19px; font-weight: 900; color: Theme.room-paper; horizontal-alignment: center; } HorizontalLayout { alignment: center; spacing: 12px; for scene in root.scenes: Rectangle { height: 52px; width: pill.preferred-width; border-radius: 26px; background: scene.active ? Theme.room-paper : Theme.room-paper.with-alpha(0.18); animate background { duration: 140ms; } pill := HorizontalLayout { padding-left: 20px; padding-right: 20px; spacing: 8px; VerticalLayout { alignment: center; Text { text: "✨"; font-size: 22px; } } VerticalLayout { alignment: center; Text { text: scene.name; font-size: 15px; font-weight: 800; color: scene.active ? Theme.room-btn-fg : Theme.room-paper; } } } TouchArea { clicked => { root.activate-scene(scene.entity-id); } } } } } // Both kinds share one cell width, so the grid stays a grid whatever // mix of devices Home Assistant reports. for row in root.rows: HorizontalLayout { alignment: center; spacing: 20px; for card in row.cards: Rectangle { width: Theme.room-card-width; height: card.kind == "shutter" ? 268px : 246px; if card.kind == "light": LightCard { width: 100%; card: card; swatches: root.swatches; toggle => { root.toggle-light(card.entity-id); } set-level(n) => { root.set-level(card.entity-id, n); } set-swatch(i) => { root.set-swatch(card.entity-id, i); } } if card.kind == "shutter": ShutterCard { width: 100%; card: card; presets: root.presets; set-preset(i) => { root.set-preset(card.entity-id, i); } open => { root.shutter(card.entity-id, "open"); } stop => { root.shutter(card.entity-id, "stop"); } close => { root.shutter(card.entity-id, "close"); } } } } } } } }