From 4044e4db5097b794aa575776e1f43a9746c0407b Mon Sep 17 00:00:00 2001 From: Kaloyan Danchev Date: Sat, 24 Oct 2026 15:32:16 +0300 Subject: [PATCH] Radial glow effects, RGB accent colors, and scroll fix - Replace linear ha_card_fill with radial glow using colored Shadow (the SDF degenerates to a circle for square rects with radius=half) - Add ha_entity_accent_color() to extract rgb_color from HA entities for RGB LED strip cards, falling back to warm orange - Apply radial glow to toggle, range, climate, media, and cover cards with clip rects to prevent bleed between cards - Fix scroll dispatch to include Curtain/Blinds card types - Media card glow emanates from album art center proportional to volume - Cover card: curtain glows from center, blinds from top Co-Authored-By: Claude Opus 4.6 --- src/main.rs | 4 +- src/view.rs | 122 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 94 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index e89da46..51cae28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1072,12 +1072,12 @@ fn run() -> Result<(), String> { mouse_y, .. } if state.overlay.is_none() => { - // HA card scroll: adjust range/climate/media values + // HA card scroll: adjust range/climate/media/cover values let ha_scroll = state.page == state::Page::Ha && matches!(&state.focus, state::Target::HaCard(room, index) if matches!( state.ha.config.rooms[*room].cards[*index].card_type, - state::HaCardType::Range | state::HaCardType::Climate | state::HaCardType::Media + state::HaCardType::Range | state::HaCardType::Climate | state::HaCardType::Media | state::HaCardType::Curtain | state::HaCardType::Blinds )) && y != 0; if ha_scroll { diff --git a/src/view.rs b/src/view.rs index 19832f6..56ac46b 100644 --- a/src/view.rs +++ b/src/view.rs @@ -179,6 +179,9 @@ impl Scene { c(0, alpha), )); } + fn glow(&mut self, rect: Rect, radius: f32, blur: f32, color: Color) { + self.draw.push(Draw::Shadow(rect, radius, blur, color)); + } fn pill(&mut self, rect: Rect, focused: bool, selected: bool) { self.shadow(rect, rect.h / 2., 18., 4., 0.06); if focused { @@ -1803,10 +1806,38 @@ fn ha_state_color(entity: Option<&crate::state::HaEntity>) -> Color { } } +fn ha_entity_accent_color(entity: Option<&crate::state::HaEntity>) -> Color { + if let Some(rgb) = entity + .and_then(|e| e.attributes.get("rgb_color")) + .and_then(|v| v.as_array()) + .filter(|a| a.len() >= 3) + { + let r = rgb[0].as_f64().unwrap_or(250.) as f32 / 255.; + let g = rgb[1].as_f64().unwrap_or(185.) as f32 / 255.; + let b = rgb[2].as_f64().unwrap_or(92.) as f32 / 255.; + [r, g, b, 1.] + } else { + c(0xfab95c, 1.) + } +} + fn ha_fill_color(card_type: &crate::state::HaCardType, entity: Option<&crate::state::HaEntity>) -> Color { use crate::state::HaCardType; match card_type { - HaCardType::Range => c(0xfab95c, 0.30), + HaCardType::Range => { + if let Some(rgb) = entity + .and_then(|e| e.attributes.get("rgb_color")) + .and_then(|v| v.as_array()) + .filter(|a| a.len() >= 3) + { + let r = rgb[0].as_f64().unwrap_or(250.) as f32 / 255.; + let g = rgb[1].as_f64().unwrap_or(185.) as f32 / 255.; + let b = rgb[2].as_f64().unwrap_or(92.) as f32 / 255.; + [r, g, b, 0.35] + } else { + c(0xfab95c, 0.30) + } + } HaCardType::Climate => { match entity.map(|e| e.state.as_str()) { Some("heat" | "heat_cool" | "auto") => c(0xf06040, 0.28), @@ -1873,9 +1904,16 @@ fn ha_toggle_card( let accent = ha_state_color(entity); let is_on = entity.is_some_and(|e| e.state == "on" || e.state == "open"); - let fill_color = ha_fill_color(&card.card_type, entity); + // Radial glow from icon if is_on { - ha_card_fill(out, rect, 1., fill_color); + let fill_color = ha_fill_color(&card.card_type, entity); + let icon_cx = rect.x + 40.; + let icon_cy = rect.y + 40.; + let glow_size = 80.; + let glow_rect = Rect::new(icon_cx - glow_size, icon_cy - glow_size, glow_size * 2., glow_size * 2.); + out.draw.push(Draw::Clip(Some(rect))); + out.glow(glow_rect, glow_size, glow_size * 0.8, fill_color); + out.draw.push(Draw::Clip(None)); } // Icon in tinted circle @@ -1952,12 +1990,23 @@ fn ha_range_card( .and_then(|v| v.as_f64()) .unwrap_or(0.); let pct = (brightness / 255. * 100.).round() as u8; - let accent = if is_on { c(0xfab95c, 1.) } else { c(0xbbbbbb, 0.5) }; + let accent = if is_on { + ha_entity_accent_color(entity) + } else { + c(0xbbbbbb, 0.5) + }; - // Fill background proportional to brightness + // Radial glow from icon center, clipped to card if is_on && pct > 0 { - let fill_color = ha_fill_color(&card.card_type, entity); - ha_card_fill(out, rect, pct as f32 / 100., fill_color); + let glow_alpha = (pct as f32 / 100.).clamp(0.1, 0.45); + let glow_color = [accent[0], accent[1], accent[2], glow_alpha]; + let icon_cx = rect.x + 40.; + let icon_cy = rect.y + 40.; + let glow_size = 80.; + let glow_rect = Rect::new(icon_cx - glow_size, icon_cy - glow_size, glow_size * 2., glow_size * 2.); + out.draw.push(Draw::Clip(Some(rect))); + out.glow(glow_rect, glow_size, glow_size * 0.8, glow_color); + out.draw.push(Draw::Clip(None)); } // Icon in tinted circle @@ -2033,11 +2082,18 @@ fn ha_climate_card( let hvac_mode = entity.map_or("off", |e| e.state.as_str()); let is_active = hvac_mode != "off" && hvac_mode != "unavailable"; - // Fill proportional to target temp (15-35°C range) + // Radial glow from icon if is_active { - let fill_pct = ((target_temp - 15.) / 20.).clamp(0., 1.) as f32; let fill_color = ha_fill_color(&card.card_type, entity); - ha_card_fill(out, rect, fill_pct, fill_color); + let glow_alpha = ((target_temp - 15.) / 20.).clamp(0.1, 0.40) as f32; + let glow_color = [fill_color[0], fill_color[1], fill_color[2], glow_alpha]; + let icon_cx = rect.x + 40.; + let icon_cy = rect.y + 40.; + let glow_size = 80.; + let glow_rect = Rect::new(icon_cx - glow_size, icon_cy - glow_size, glow_size * 2., glow_size * 2.); + out.draw.push(Draw::Clip(Some(rect))); + out.glow(glow_rect, glow_size, glow_size * 0.8, glow_color); + out.draw.push(Draw::Clip(None)); } // Icon in tinted circle @@ -2331,31 +2387,27 @@ fn ha_media_card( let accent = ha_state_color(entity); let is_active = entity.is_some_and(|e| e.state == "playing" || e.state == "paused"); - // Volume fill background + // Volume-driven radial glow from album art center let volume = entity .and_then(|e| e.attributes.get("volume_level")) .and_then(|v| v.as_f64()) .unwrap_or(0.) as f32; - if is_active && volume > 0. { - let fill_color = ha_fill_color(&card.card_type, entity); - ha_card_fill(out, rect, volume, fill_color); - } // Portrait layout: album art circle centered at top let art_size = 120.; let art_x = rect.x + (rect.w - art_size) / 2.; let art_y = rect.y + 24.; - // Glow behind album art - if is_active { - let glow_size = art_size * 1.8; - let glow_x = art_x + (art_size - glow_size) / 2.; - let glow_y = art_y + (art_size - glow_size) / 2.; - out.shape( - Rect::new(glow_x, glow_y, glow_size, glow_size), - glow_size / 2., - [accent[0], accent[1], accent[2], 0.12], - ); + // Radial glow from album art center + if is_active && volume > 0. { + let glow_alpha = (volume * 0.3).clamp(0.05, 0.30); + let art_cx = art_x + art_size / 2.; + let art_cy = art_y + art_size / 2.; + let glow_r = art_size; + let glow_rect = Rect::new(art_cx - glow_r, art_cy - glow_r, glow_r * 2., glow_r * 2.); + out.draw.push(Draw::Clip(Some(rect))); + out.glow(glow_rect, glow_r, glow_r * 0.7, [accent[0], accent[1], accent[2], glow_alpha]); + out.draw.push(Draw::Clip(None)); } out.shape( @@ -2466,17 +2518,27 @@ fn ha_cover_card( out.shape(rect, 22., c(0x0c1024, 0.5 * closed_pct)); } - // Daylight glow when open + // Daylight radial glow when open if is_open { let open_pct = (position / 100.).clamp(0., 1.); let fill_color = ha_fill_color(&card.card_type, entity); + let glow_alpha = (open_pct * fill_color[3]).clamp(0.05, 0.40); + let glow_color = [fill_color[0], fill_color[1], fill_color[2], glow_alpha]; + out.draw.push(Draw::Clip(Some(rect))); if is_curtain { - // Curtain: glow from center - ha_card_fill(out, rect, open_pct, fill_color); + // Curtain: glow from center of card + let cx = rect.x + rect.w / 2.; + let cy = rect.y + rect.h / 2.; + let r = rect.w * 0.5; + out.glow(Rect::new(cx - r, cy - r, r * 2., r * 2.), r, r * 0.7, glow_color); } else { - // Blinds: glow from top - ha_card_fill(out, rect, open_pct, fill_color); + // Blinds: glow from top center + let cx = rect.x + rect.w / 2.; + let cy = rect.y; + let r = rect.w * 0.5; + out.glow(Rect::new(cx - r, cy - r, r * 2., r * 2.), r, r * 0.7, glow_color); } + out.draw.push(Draw::Clip(None)); } let accent = if is_open { c(0xdcd2b4, 1.) } else { c(0xbbbbbb, 0.5) };