Add slider modal for Range, Climate, and Cover cards
OK press on Range/Climate/Curtain/Blinds cards now opens a modal overlay with slider controls instead of direct value adjustment. Scroll wheel and D-pad left/right adjust values within the modal. RGB lights with color_temp attribute get a second slider for color temperature (mireds). Adds SetColorTemp HA effect for the light service call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4044e4db50
commit
fcc7a41f02
3 files changed
+308
-11
No files matched your search
+17
-3
@@ -372,6 +372,15 @@ fn ha_dispatch(service: &ha_client::HaService, effect: state::HaEffect) {
|
|||||||
data: serde_json::json!({ "position": position }),
|
data: serde_json::json!({ "position": position }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
HaEffect::SetColorTemp(entity_id, color_temp) => {
|
||||||
|
service.send(ha_client::HaCommand::CallService {
|
||||||
|
id: next_id(),
|
||||||
|
domain: "light".into(),
|
||||||
|
service: "turn_on".into(),
|
||||||
|
target: serde_json::json!({ "entity_id": entity_id }),
|
||||||
|
data: serde_json::json!({ "color_temp": color_temp }),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,15 +1081,20 @@ fn run() -> Result<(), String> {
|
|||||||
mouse_y,
|
mouse_y,
|
||||||
..
|
..
|
||||||
} if state.overlay.is_none() => {
|
} if state.overlay.is_none() => {
|
||||||
// HA card scroll: adjust range/climate/media/cover values
|
// HA card/slider scroll: adjust range/climate/media/cover values
|
||||||
let ha_scroll = state.page == state::Page::Ha
|
let ha_slider_scroll = state.page == state::Page::Ha
|
||||||
|
&& state.ha.slider_open
|
||||||
|
&& matches!(&state.focus, state::Target::HaSliderBrightness | state::Target::HaSliderColorTemp)
|
||||||
|
&& y != 0;
|
||||||
|
let ha_scroll = !ha_slider_scroll
|
||||||
|
&& state.page == state::Page::Ha
|
||||||
&& matches!(&state.focus, state::Target::HaCard(room, index)
|
&& matches!(&state.focus, state::Target::HaCard(room, index)
|
||||||
if matches!(
|
if matches!(
|
||||||
state.ha.config.rooms[*room].cards[*index].card_type,
|
state.ha.config.rooms[*room].cards[*index].card_type,
|
||||||
state::HaCardType::Range | state::HaCardType::Climate | state::HaCardType::Media | state::HaCardType::Curtain | state::HaCardType::Blinds
|
state::HaCardType::Range | state::HaCardType::Climate | state::HaCardType::Media | state::HaCardType::Curtain | state::HaCardType::Blinds
|
||||||
))
|
))
|
||||||
&& y != 0;
|
&& y != 0;
|
||||||
if ha_scroll {
|
if ha_slider_scroll || ha_scroll {
|
||||||
pending = state.scroll_ha(y);
|
pending = state.scroll_ha(y);
|
||||||
} else {
|
} else {
|
||||||
let over_favorites = state.page == state::Page::Home
|
let over_favorites = state.page == state::Page::Home
|
||||||
|
|||||||
+132
-8
@@ -70,6 +70,9 @@ pub enum Target {
|
|||||||
HaVacuumMode,
|
HaVacuumMode,
|
||||||
HaVacuumStart,
|
HaVacuumStart,
|
||||||
HaVacuumClose,
|
HaVacuumClose,
|
||||||
|
HaSliderBrightness,
|
||||||
|
HaSliderColorTemp,
|
||||||
|
HaSliderClose,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
@@ -166,6 +169,7 @@ pub enum HaEffect {
|
|||||||
MediaPrev(String),
|
MediaPrev(String),
|
||||||
SetVolume(String, f32),
|
SetVolume(String, f32),
|
||||||
SetCoverPosition(String, u8),
|
SetCoverPosition(String, u8),
|
||||||
|
SetColorTemp(String, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const KEYS: [&str; 40] = [
|
pub const KEYS: [&str; 40] = [
|
||||||
@@ -174,8 +178,9 @@ pub const KEYS: [&str; 40] = [
|
|||||||
"Delete", "Clear", "Done",
|
"Delete", "Clear", "Done",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum HaCardType {
|
pub enum HaCardType {
|
||||||
|
#[default]
|
||||||
Toggle,
|
Toggle,
|
||||||
Range,
|
Range,
|
||||||
Climate,
|
Climate,
|
||||||
@@ -235,6 +240,9 @@ pub struct HaState {
|
|||||||
pub vacuum_selected: Vec<bool>,
|
pub vacuum_selected: Vec<bool>,
|
||||||
pub vacuum_mode: usize,
|
pub vacuum_mode: usize,
|
||||||
pub vacuum_open: bool,
|
pub vacuum_open: bool,
|
||||||
|
pub slider_open: bool,
|
||||||
|
pub slider_entity: String,
|
||||||
|
pub slider_card_type: HaCardType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -801,6 +809,9 @@ impl State {
|
|||||||
if self.ha.vacuum_open {
|
if self.ha.vacuum_open {
|
||||||
return self.ha_vacuum_direction(direction, controls);
|
return self.ha_vacuum_direction(direction, controls);
|
||||||
}
|
}
|
||||||
|
if self.ha.slider_open {
|
||||||
|
return self.ha_slider_direction(direction, controls);
|
||||||
|
}
|
||||||
if let Target::HaRoom(room) = self.focus {
|
if let Target::HaRoom(room) = self.focus {
|
||||||
match direction {
|
match direction {
|
||||||
Direction::Left if room > 0 => {
|
Direction::Left if room > 0 => {
|
||||||
@@ -1220,35 +1231,93 @@ impl State {
|
|||||||
4
|
4
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ha_entity_brightness(&self, entity_id: &str) -> f32 {
|
pub fn ha_entity_brightness(&self, entity_id: &str) -> f32 {
|
||||||
self.ha.entities.get(entity_id)
|
self.ha.entities.get(entity_id)
|
||||||
.and_then(|e| e.attributes.get("brightness"))
|
.and_then(|e| e.attributes.get("brightness"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.) as f32
|
.unwrap_or(0.) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ha_entity_target_temp(&self, entity_id: &str) -> f32 {
|
pub fn ha_entity_target_temp(&self, entity_id: &str) -> f32 {
|
||||||
self.ha.entities.get(entity_id)
|
self.ha.entities.get(entity_id)
|
||||||
.and_then(|e| e.attributes.get("temperature"))
|
.and_then(|e| e.attributes.get("temperature"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(20.) as f32
|
.unwrap_or(20.) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ha_entity_volume(&self, entity_id: &str) -> f32 {
|
pub fn ha_entity_volume(&self, entity_id: &str) -> f32 {
|
||||||
self.ha.entities.get(entity_id)
|
self.ha.entities.get(entity_id)
|
||||||
.and_then(|e| e.attributes.get("volume_level"))
|
.and_then(|e| e.attributes.get("volume_level"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.) as f32
|
.unwrap_or(0.) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ha_entity_position(&self, entity_id: &str) -> f32 {
|
pub fn ha_entity_position(&self, entity_id: &str) -> f32 {
|
||||||
self.ha.entities.get(entity_id)
|
self.ha.entities.get(entity_id)
|
||||||
.and_then(|e| e.attributes.get("current_position"))
|
.and_then(|e| e.attributes.get("current_position"))
|
||||||
.and_then(|v| v.as_f64())
|
.and_then(|v| v.as_f64())
|
||||||
.unwrap_or(0.) as f32
|
.unwrap_or(0.) as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ha_entity_color_temp(&self, entity_id: &str) -> u32 {
|
||||||
|
self.ha.entities.get(entity_id)
|
||||||
|
.and_then(|e| e.attributes.get("color_temp"))
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(300) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ha_entity_min_mireds(&self, entity_id: &str) -> u32 {
|
||||||
|
self.ha.entities.get(entity_id)
|
||||||
|
.and_then(|e| e.attributes.get("min_mireds"))
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(153) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ha_entity_max_mireds(&self, entity_id: &str) -> u32 {
|
||||||
|
self.ha.entities.get(entity_id)
|
||||||
|
.and_then(|e| e.attributes.get("max_mireds"))
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(500) as u32
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scroll_ha(&mut self, delta: i32) -> Vec<Effect> {
|
pub fn scroll_ha(&mut self, delta: i32) -> Vec<Effect> {
|
||||||
|
// Slider modal scroll — adjust the focused slider
|
||||||
|
if self.ha.slider_open {
|
||||||
|
let eid = self.ha.slider_entity.clone();
|
||||||
|
match self.focus {
|
||||||
|
Target::HaSliderBrightness => {
|
||||||
|
match self.ha.slider_card_type {
|
||||||
|
HaCardType::Range => {
|
||||||
|
let brightness = self.ha_entity_brightness(&eid);
|
||||||
|
let new = (brightness + delta as f32 * 25.5).clamp(0., 255.);
|
||||||
|
return vec![Effect::Ha(HaEffect::SetBrightness(eid, new))];
|
||||||
|
}
|
||||||
|
HaCardType::Climate => {
|
||||||
|
let temp = self.ha_entity_target_temp(&eid);
|
||||||
|
let new = temp + delta as f32 * 0.5;
|
||||||
|
return vec![Effect::Ha(HaEffect::SetClimateTemp(eid, new))];
|
||||||
|
}
|
||||||
|
HaCardType::Curtain | HaCardType::Blinds => {
|
||||||
|
let pos = self.ha_entity_position(&eid);
|
||||||
|
let new = (pos + delta as f32 * 10.).clamp(0., 100.) as u8;
|
||||||
|
return vec![Effect::Ha(HaEffect::SetCoverPosition(eid, new))];
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Target::HaSliderColorTemp => {
|
||||||
|
let ct = self.ha_entity_color_temp(&eid);
|
||||||
|
let min = self.ha_entity_min_mireds(&eid);
|
||||||
|
let max = self.ha_entity_max_mireds(&eid);
|
||||||
|
let step = ((max - min) / 10).max(1);
|
||||||
|
let new = (ct as i64 + delta as i64 * step as i64).clamp(min as i64, max as i64) as u32;
|
||||||
|
return vec![Effect::Ha(HaEffect::SetColorTemp(eid, new))];
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
if let Target::HaCard(room, index) = &self.focus {
|
if let Target::HaCard(room, index) = &self.focus {
|
||||||
let card = &self.ha.config.rooms[*room].cards[*index];
|
let card = &self.ha.config.rooms[*room].cards[*index];
|
||||||
match card.card_type {
|
match card.card_type {
|
||||||
@@ -1334,6 +1403,41 @@ impl State {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ha_slider_direction(&mut self, direction: Direction, controls: &[Control]) -> Vec<Effect> {
|
||||||
|
let has_color_temp = matches!(self.ha.slider_card_type, HaCardType::Range)
|
||||||
|
&& self.ha.entities.get(&self.ha.slider_entity)
|
||||||
|
.and_then(|e| e.attributes.get("color_temp"))
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
match &self.focus {
|
||||||
|
Target::HaSliderBrightness => match direction {
|
||||||
|
Direction::Up => self.focus(Target::HaSliderClose, controls),
|
||||||
|
Direction::Down if has_color_temp => {
|
||||||
|
self.focus(Target::HaSliderColorTemp, controls);
|
||||||
|
}
|
||||||
|
Direction::Left | Direction::Right => {
|
||||||
|
let delta = if matches!(direction, Direction::Left) { -1 } else { 1 };
|
||||||
|
return self.scroll_ha(delta);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Target::HaSliderColorTemp => match direction {
|
||||||
|
Direction::Up => self.focus(Target::HaSliderBrightness, controls),
|
||||||
|
Direction::Left | Direction::Right => {
|
||||||
|
let delta = if matches!(direction, Direction::Left) { -1 } else { 1 };
|
||||||
|
return self.scroll_ha(delta);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Target::HaSliderClose => match direction {
|
||||||
|
Direction::Down => self.focus(Target::HaSliderBrightness, controls),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
pub fn options(&mut self) {
|
pub fn options(&mut self) {
|
||||||
if self.overlay.is_some() || self.moving.is_some() {
|
if self.overlay.is_some() || self.moving.is_some() {
|
||||||
return;
|
return;
|
||||||
@@ -1709,12 +1813,24 @@ impl State {
|
|||||||
HaCardType::Media => {
|
HaCardType::Media => {
|
||||||
return vec![Effect::Ha(HaEffect::MediaPlayPause(card.entity_id.clone()))];
|
return vec![Effect::Ha(HaEffect::MediaPlayPause(card.entity_id.clone()))];
|
||||||
}
|
}
|
||||||
HaCardType::Curtain | HaCardType::Blinds => {
|
HaCardType::Range | HaCardType::Climate | HaCardType::Curtain | HaCardType::Blinds => {
|
||||||
return vec![Effect::Ha(HaEffect::Toggle(card.entity_id.clone()))];
|
self.ha.slider_open = true;
|
||||||
|
self.ha.slider_entity = card.entity_id.clone();
|
||||||
|
self.ha.slider_card_type = card.card_type.clone();
|
||||||
|
self.focus = Target::HaSliderBrightness;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Target::HaSliderBrightness | Target::HaSliderColorTemp => {
|
||||||
|
// scroll handles these — OK does nothing on slider tracks
|
||||||
|
}
|
||||||
|
Target::HaSliderClose => {
|
||||||
|
self.ha.slider_open = false;
|
||||||
|
self.focus = Target::HaCard(self.ha.room,
|
||||||
|
self.ha.config.rooms[self.ha.room].cards.iter()
|
||||||
|
.position(|c| c.entity_id == self.ha.slider_entity)
|
||||||
|
.unwrap_or(0));
|
||||||
|
}
|
||||||
Target::HaVacuumRoom(index) => {
|
Target::HaVacuumRoom(index) => {
|
||||||
if index < self.ha.vacuum_selected.len() {
|
if index < self.ha.vacuum_selected.len() {
|
||||||
self.ha.vacuum_selected[index] = !self.ha.vacuum_selected[index];
|
self.ha.vacuum_selected[index] = !self.ha.vacuum_selected[index];
|
||||||
@@ -1994,6 +2110,14 @@ impl State {
|
|||||||
self.scroll = 0.;
|
self.scroll = 0.;
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
if self.page == Page::Ha && self.ha.slider_open {
|
||||||
|
self.ha.slider_open = false;
|
||||||
|
self.focus = Target::HaCard(self.ha.room,
|
||||||
|
self.ha.config.rooms[self.ha.room].cards.iter()
|
||||||
|
.position(|c| c.entity_id == self.ha.slider_entity)
|
||||||
|
.unwrap_or(0));
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
if self.page == Page::Ha && self.ha.vacuum_open {
|
if self.page == Page::Ha && self.ha.vacuum_open {
|
||||||
self.ha.vacuum_open = false;
|
self.ha.vacuum_open = false;
|
||||||
self.focus = Target::HaCard(self.ha.room,
|
self.focus = Target::HaCard(self.ha.room,
|
||||||
|
|||||||
+159
@@ -1754,6 +1754,11 @@ fn ha(out: &mut Scene, state: &State, assets: &Assets) {
|
|||||||
ha_vacuum_modal(out, state, assets, vac_cfg);
|
ha_vacuum_modal(out, state, assets, vac_cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Slider modal (overlays everything) ──────────────────────
|
||||||
|
if state.ha.slider_open {
|
||||||
|
ha_slider_modal(out, state, assets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Glass card base ──────────────────────────────────────────────
|
// ── Glass card base ──────────────────────────────────────────────
|
||||||
@@ -2836,6 +2841,160 @@ fn ha_vacuum_modal(
|
|||||||
out.control(Target::HaVacuumStart, start_rect, false);
|
out.control(Target::HaVacuumStart, start_rect, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ha_slider_modal(out: &mut Scene, state: &State, assets: &Assets) {
|
||||||
|
use crate::state::HaCardType;
|
||||||
|
let entity_id = &state.ha.slider_entity;
|
||||||
|
let entity = state.ha.entities.get(entity_id);
|
||||||
|
let card_type = &state.ha.slider_card_type;
|
||||||
|
|
||||||
|
let overlay_rect = Rect::new(0., 0., 1920., 1080.);
|
||||||
|
out.shape(overlay_rect, 0., c(0x080b12, 0.75));
|
||||||
|
out.modal = Some(overlay_rect);
|
||||||
|
|
||||||
|
let has_color_temp = matches!(card_type, HaCardType::Range)
|
||||||
|
&& entity.and_then(|e| e.attributes.get("color_temp")).is_some();
|
||||||
|
let modal_h = if has_color_temp { 400. } else { 280. };
|
||||||
|
let modal = Rect::new(480., (1080. - modal_h) / 2., 960., modal_h);
|
||||||
|
ha_glass(out, modal, false);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
let title = match card_type {
|
||||||
|
HaCardType::Range => "Light",
|
||||||
|
HaCardType::Climate => "Climate",
|
||||||
|
HaCardType::Curtain => "Curtain",
|
||||||
|
HaCardType::Blinds => "Blinds",
|
||||||
|
_ => "Control",
|
||||||
|
};
|
||||||
|
// Find card name from config
|
||||||
|
let card_name = state.ha.config.rooms.iter()
|
||||||
|
.flat_map(|r| r.cards.iter())
|
||||||
|
.find(|c| c.entity_id == *entity_id)
|
||||||
|
.map(|c| c.name.as_str())
|
||||||
|
.unwrap_or(title);
|
||||||
|
out.text(
|
||||||
|
card_name,
|
||||||
|
Rect::new(modal.x + 40., modal.y + 28., 600., 32.),
|
||||||
|
text(28., 600, -0.03),
|
||||||
|
WHITE,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
let close_rect = Rect::new(modal.x + modal.w - 72., modal.y + 24., 40., 40.);
|
||||||
|
out.shape(close_rect, 20., c(0xffffff, 0.08));
|
||||||
|
out.icon(assets, "close", close_rect.inset(10.), c(0xeeeeee, 0.7));
|
||||||
|
let close_focused = state.focus == Target::HaSliderClose;
|
||||||
|
if close_focused {
|
||||||
|
out.border(close_rect.inset(-3.), 23., 2., WHITE);
|
||||||
|
}
|
||||||
|
out.control(Target::HaSliderClose, close_rect, false);
|
||||||
|
|
||||||
|
// Primary slider (brightness / temperature / position)
|
||||||
|
let slider_y = modal.y + 88.;
|
||||||
|
let slider_x = modal.x + 40.;
|
||||||
|
let slider_w = modal.w - 80.;
|
||||||
|
let slider_h = 72.;
|
||||||
|
let slider_rect = Rect::new(slider_x, slider_y, slider_w, slider_h);
|
||||||
|
let primary_focused = state.focus == Target::HaSliderBrightness;
|
||||||
|
|
||||||
|
out.shape(slider_rect, 16., c(0xffffff, 0.06));
|
||||||
|
out.border(slider_rect, 16., 1., c(0xffffff, 0.10));
|
||||||
|
if primary_focused {
|
||||||
|
out.border(slider_rect.inset(-4.), 20., 2., WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value bar and label
|
||||||
|
let (label, fraction) = match card_type {
|
||||||
|
HaCardType::Range => {
|
||||||
|
let brightness = state.ha_entity_brightness(entity_id);
|
||||||
|
let pct = (brightness / 255. * 100.).round() as u8;
|
||||||
|
(format!("Brightness: {}%", pct), brightness / 255.)
|
||||||
|
}
|
||||||
|
HaCardType::Climate => {
|
||||||
|
let temp = state.ha_entity_target_temp(entity_id);
|
||||||
|
let min = 16_f32;
|
||||||
|
let max = 30_f32;
|
||||||
|
let frac = ((temp - min) / (max - min)).clamp(0., 1.);
|
||||||
|
(format!("Temperature: {:.1}°C", temp), frac)
|
||||||
|
}
|
||||||
|
HaCardType::Curtain | HaCardType::Blinds => {
|
||||||
|
let pos = state.ha_entity_position(entity_id);
|
||||||
|
(format!("Position: {}%", pos.round() as u8), pos / 100.)
|
||||||
|
}
|
||||||
|
_ => ("Value".to_string(), 0.),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filled bar
|
||||||
|
let bar_inset = 4.;
|
||||||
|
let bar_rect = Rect::new(
|
||||||
|
slider_rect.x + bar_inset,
|
||||||
|
slider_rect.y + bar_inset,
|
||||||
|
(slider_rect.w - bar_inset * 2.) * fraction.clamp(0., 1.),
|
||||||
|
slider_rect.h - bar_inset * 2.,
|
||||||
|
);
|
||||||
|
let accent = ha_entity_accent_color(entity);
|
||||||
|
let bar_color = [accent[0], accent[1], accent[2], 0.35];
|
||||||
|
out.shape(bar_rect, 12., bar_color);
|
||||||
|
|
||||||
|
out.text(
|
||||||
|
&label,
|
||||||
|
Rect::new(slider_rect.x + 20., slider_rect.y + 24., slider_w - 40., 24.),
|
||||||
|
text(18., 500, -0.02),
|
||||||
|
WHITE,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
out.control(Target::HaSliderBrightness, slider_rect, false);
|
||||||
|
|
||||||
|
// Color temperature slider (only for Range lights with color_temp)
|
||||||
|
if has_color_temp {
|
||||||
|
let ct_y = slider_y + slider_h + 20.;
|
||||||
|
let ct_rect = Rect::new(slider_x, ct_y, slider_w, slider_h);
|
||||||
|
let ct_focused = state.focus == Target::HaSliderColorTemp;
|
||||||
|
|
||||||
|
out.shape(ct_rect, 16., c(0xffffff, 0.06));
|
||||||
|
out.border(ct_rect, 16., 1., c(0xffffff, 0.10));
|
||||||
|
if ct_focused {
|
||||||
|
out.border(ct_rect.inset(-4.), 20., 2., WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ct = state.ha_entity_color_temp(entity_id);
|
||||||
|
let min_m = state.ha_entity_min_mireds(entity_id);
|
||||||
|
let max_m = state.ha_entity_max_mireds(entity_id);
|
||||||
|
let range = (max_m - min_m).max(1) as f32;
|
||||||
|
let frac = (ct as f32 - min_m as f32) / range;
|
||||||
|
let kelvin = if ct > 0 { 1_000_000 / ct } else { 4000 };
|
||||||
|
|
||||||
|
// Warm-to-cool gradient bar
|
||||||
|
let ct_bar = Rect::new(
|
||||||
|
ct_rect.x + bar_inset,
|
||||||
|
ct_rect.y + bar_inset,
|
||||||
|
(ct_rect.w - bar_inset * 2.) * frac.clamp(0., 1.),
|
||||||
|
ct_rect.h - bar_inset * 2.,
|
||||||
|
);
|
||||||
|
// Warm amber color for the filled portion
|
||||||
|
out.shape(ct_bar, 12., c(0xffaa44, 0.35));
|
||||||
|
|
||||||
|
out.text(
|
||||||
|
&format!("Color Temp: {}K", kelvin),
|
||||||
|
Rect::new(ct_rect.x + 20., ct_rect.y + 24., slider_w - 40., 24.),
|
||||||
|
text(18., 500, -0.02),
|
||||||
|
WHITE,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
out.control(Target::HaSliderColorTemp, ct_rect, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint text at bottom
|
||||||
|
let hint_y = modal.y + modal_h - 44.;
|
||||||
|
out.text(
|
||||||
|
"Scroll or ◀ ▶ to adjust",
|
||||||
|
Rect::new(slider_x, hint_y, slider_w, 20.),
|
||||||
|
text(14., 400, -0.01),
|
||||||
|
c(0xeeeeee, 0.4),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn settings(out: &mut Scene, state: &State, assets: &Assets) {
|
fn settings(out: &mut Scene, state: &State, assets: &Assets) {
|
||||||
let language = state.preferences.language;
|
let language = state.preferences.language;
|
||||||
let s = state.scroll;
|
let s = state.scroll;
|
||||||
|
|||||||
Reference in new issue
Block a user