use crate::components::color_picker::{ColorPicker, ColorPickerMsg}; use crate::css::C; use common::{BulbGroup, BulbGroupShape, BulbMap, BulbPrefs, ClientMessage, Param, ServerMessage}; use lighter_lib::{BulbId, BulbMode}; use seed::{attrs, button, div, empty, h1, h2, input, label, span, table, td, tr, C}; use seed::{prelude::*, IF}; use seed_router::Page; use std::collections::{BTreeMap, HashSet}; use std::fmt::Write; use std::iter::repeat; /// /lights page #[derive(Default)] pub struct Model { bulb_states: BTreeMap, select_mode: SelectMode, bulb_map: BulbMap, /// the currently selected bulbs on the map selected_groups: HashSet, /// the currently selected bulbs on the list selected_bulbs: HashSet, /// whether the currently selected map groups have been interacted with groups_interacted: bool, color_picker: ColorPicker, } #[derive(Default, Clone)] struct BulbState { mode: BulbMode, prefs: BTreeMap, } #[derive(Debug)] pub enum Msg { ServerMessage(ServerMessage), SelectGroup(usize), DeselectGroups, SelectBulb(BulbId), ColorPicker(ColorPickerMsg), SetBulbPower(bool), /// Set a script parameter value for all selected bulbs. SetParam { script: String, name: String, value: Param, }, SetSelectMode(SelectMode), } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub enum SelectMode { #[default] Map, List, } impl Page for Model { type Msg = Msg; fn new(orders: &mut impl Orders) -> Self { orders.subscribe(Msg::ServerMessage); orders.notify(ClientMessage::GetBulbs); Model::default() } fn update(&mut self, msg: Self::Msg, orders: &mut impl Orders) { match msg { Msg::ServerMessage(msg) => match msg { ServerMessage::BulbState { id, mode: new_mode, prefs, } => { *self.bulb_states.entry(id).or_default() = BulbState { mode: new_mode, prefs, }; } ServerMessage::BulbMap(bulb_map) => { self.bulb_map = bulb_map; self.selected_groups.clear(); } _ => {} }, Msg::DeselectGroups => { self.selected_groups.clear(); } Msg::SelectGroup(index) => { if self.groups_interacted { self.groups_interacted = false; // If this group is the only selected group, don't clear it if self.selected_groups.len() != 1 || !self.selected_groups.contains(&index) { self.selected_groups.clear(); } } if !self.selected_groups.insert(index) { self.selected_groups.remove(&index); } else { let bulb = self .bulb_map .groups .get(index) .and_then(|group| group.bulbs.first()) .and_then(|id| self.bulb_states.get(id)); if let Some(bulb) = bulb { self.color_picker.set_color(bulb.mode.color); } } } Msg::SelectBulb(bulb) => { if !self.selected_bulbs.remove(&bulb) { self.selected_bulbs.insert(bulb); } } Msg::ColorPicker(ColorPickerMsg::SetColor(color)) => { self.groups_interacted = true; self.for_selected_bulbs(|id, _| { let message = ClientMessage::SetBulbColor { id: id.clone(), color, }; orders.notify(message); }); } Msg::ColorPicker(msg) => { self.color_picker .update(msg, &mut orders.proxy(Msg::ColorPicker)); } Msg::SetBulbPower(power) => { self.groups_interacted = true; self.for_selected_bulbs(|id, _| { let message = ClientMessage::SetBulbPower { id: id.clone(), power, }; orders.notify(message); }); } Msg::SetParam { script, name, value, } => { self.for_selected_bulbs(|id, bulb| { bulb.prefs .get_mut(&script) .unwrap() //TOD .kvs .insert(name.clone(), value.clone()); let message = ClientMessage::SetBulbPref { bulb: id.clone(), script: script.clone(), name: name.clone(), value: value.clone(), }; orders.notify(message); }); } Msg::SetSelectMode(mode) => { self.select_mode = mode; } } } fn view(&self) -> Node { let bulb_map_width = self .bulb_map .groups .iter() .map(|group| group.x + group.shape.width()) .max() .unwrap_or(0); let bulb_map_height = self .bulb_map .groups .iter() .map(|group| group.y + group.shape.height()) .max() .unwrap_or(0); let bulb_group_map = |(i, group): (usize, &BulbGroup)| { let (w, h) = (group.shape.width(), group.shape.height()); let mut style = String::new(); write!( &mut style, "margin-left: {}rem; margin-top: {}rem; width: {}rem; height: {}rem;", group.x, group.y, w, h ) .ok(); if let BulbGroupShape::Circle { r } = group.shape { write!(&mut style, " border-radius: {r}rem;").ok(); } div![ &group.name[..1], if self.selected_groups.contains(&i) { C![C.bulb_group, C.bulb_group_selected] } else { C![C.bulb_group] }, attrs! { At::Style => style, }, ev(Ev::Click, move |event| { event.stop_propagation(); Msg::SelectGroup(i) }), ] }; let bulb_group_list = |(_i, group): (usize, &BulbGroup)| { div![ C![C.bulb_list_group], h1![&group.name], group.bulbs.iter().map(|bulb| { let select_ev = || { let bulb = bulb.clone(); ev(Ev::Click, |_| Msg::SelectBulb(bulb)) }; button![ C![C.bulb_list_bulb], select_ev(), div![ C![C.bulb_list_checkbox], label![ C![C.container], input![ attrs! { At::Type => "checkbox" }, IF!(self.selected_bulbs.contains(bulb) => attrs! { At::Checked => true }), select_ev(), ], div![C![C.checkmark]], ], ], span![bulb], ] }), ] }; // pick one (arbitrary) selected bulb to pull values for the controls from let selected_bulb = if let SelectMode::Map = self.select_mode { self.selected_groups .iter() .next() .and_then(|&index| self.bulb_map.groups.get(index)) .and_then(|group| group.bulbs.first()) .and_then(|id| self.bulb_states.get(id)) } else { self.selected_bulbs .iter() .next() .and_then(|id| self.bulb_states.get(id)) }; let script_param = |script: &str, name: &str, value: &Param| { let name = name.to_string(); let script = script.to_string(); match value { Param::String(value) => tr![ C![C.pref_line], td![&name], td![input![ C![C.pref_input], attrs! {At::Placeholder => &script}, attrs! {At::Value => value}, input_ev(Ev::Input, move |input| Msg::SetParam { script, name, value: Param::String(input), }) ]] ], &Param::Toggle(value) => { tr![ C![C.pref_line], button![ if value { C![C.pref_button_enabled] } else { C![C.pref_button] }, &name, input_ev(Ev::Click, move |_| Msg::SetParam { script, name, value: Param::Toggle(!value), }) ] ] } } }; div![ C![C.bulb_box], div![ C![C.selector_selector], button![ "Map", ev(Ev::Click, move |_| Msg::SetSelectMode(SelectMode::Map)) ], div![ C![C.selector_selector_arrow], IF!(self.select_mode == SelectMode::Map => attrs! { At::Style => "transform: rotateY(180deg);"}), ], button![ "List", ev(Ev::Click, move |_| Msg::SetSelectMode(SelectMode::List)) ], ], match self.select_mode { SelectMode::Map => div![ C![C.bulb_map], attrs! { At::Style => format!("min-width: {}rem; height: {}rem;", bulb_map_width, bulb_map_height), }, ev(Ev::Click, |_| Msg::DeselectGroups), self.bulb_map.groups.iter().enumerate().map(bulb_group_map), ], SelectMode::List => div![ C![C.bulb_list], self.bulb_map.groups.iter().enumerate().map(bulb_group_list), ], }, div![ C![C.bulb_controls], IF!(selected_bulb.is_none() => C![C.cross_out]), self.color_picker.view().map_msg(Msg::ColorPicker), button![ if selected_bulb.map(|b| b.mode.power).unwrap_or(false) { C![C.bulb_power_button, C.bulb_power_button_on] } else { C![C.bulb_power_button] }, { let target_power = selected_bulb.map(|b| !b.mode.power); ev(Ev::Click, move |_| target_power.map(Msg::SetBulbPower)) }, div![attrs! { At::Id => "switch_socket" }], div![attrs! { At::Id => "off_label" }, "Off"], div![attrs! { At::Id => "on_label" }, "On"], div![attrs! { At::Id => "lever_stem" }], div![attrs! { At::Id => "lever_face" }], ], ], div![ C![C.prefs_box], IF!(selected_bulb.is_none() => C![C.cross_out]), h2!["Settings"], if let Some(selected_bulb) = selected_bulb { table![selected_bulb .prefs .iter() .flat_map(|(script, prefs)| repeat(script).zip(prefs.kvs.iter())) .map(|(script, (name, value))| script_param(script, name, value))] } else { empty![] }, ], ] } } impl Model { fn for_selected_bulbs(&mut self, mut f: impl FnMut(&BulbId, &mut BulbState)) { if let SelectMode::Map = self.select_mode { for &index in &self.selected_groups { let Some(group) = self.bulb_map.groups.get(index) else { continue; }; for id in group.bulbs.iter() { if let Some(bulb) = self.bulb_states.get_mut(id) { f(id, bulb); } } } } else { for id in &self.selected_bulbs { if let Some(bulb) = self.bulb_states.get_mut(id) { f(id, bulb); }; } } } }