Skip to main content

aethermap_gui/views/
led.rs

1use crate::gui::{Message, State};
2use crate::theme;
3use aethermap_common::LedPattern;
4use aethermap_common::LedZone;
5use iced::{
6    widget::{button, column, container, horizontal_rule, row, slider, text, Column, Space},
7    Alignment, Color, Element, Length, Theme,
8};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
12pub struct LedState {
13    pub zone_colors: HashMap<LedZone, (u8, u8, u8)>,
14    pub global_brightness: u8,
15    pub zone_brightness: HashMap<LedZone, u8>,
16    pub active_pattern: LedPattern,
17}
18
19impl Default for LedState {
20    fn default() -> Self {
21        Self {
22            zone_colors: HashMap::new(),
23            global_brightness: 100,
24            zone_brightness: HashMap::new(),
25            active_pattern: LedPattern::Static,
26        }
27    }
28}
29
30fn get_zone_color(state: &State, zone: LedZone) -> (u8, u8, u8) {
31    if let Some(device_id) = &state.led_config_device {
32        if let Some(led_state) = state.led_states.get(device_id) {
33            if let Some(&color) = led_state.zone_colors.get(&zone) {
34                return color;
35            }
36        }
37    }
38    (255, 255, 255)
39}
40
41fn led_color_style(
42    zone: Option<LedZone>,
43    zone_colors: &HashMap<LedZone, (u8, u8, u8)>,
44) -> iced::theme::Container {
45    let (r, g, b) = zone
46        .and_then(|z| zone_colors.get(&z))
47        .copied()
48        .unwrap_or((255, 255, 255));
49
50    struct LedColorStyle {
51        r: u8,
52        g: u8,
53        b: u8,
54    }
55
56    impl iced::widget::container::StyleSheet for LedColorStyle {
57        type Style = Theme;
58        fn appearance(&self, _style: &Self::Style) -> iced::widget::container::Appearance {
59            iced::widget::container::Appearance {
60                background: Some(Color::from_rgb8(self.r, self.g, self.b).into()),
61                ..Default::default()
62            }
63        }
64    }
65
66    iced::theme::Container::Custom(Box::new(LedColorStyle { r, g, b }))
67}
68
69fn view_led_rgb_sliders(state: &State) -> Element<'_, Message> {
70    let zone = state.selected_led_zone.unwrap_or(LedZone::Logo);
71    let (r, g, b) = state
72        .pending_led_color
73        .unwrap_or_else(|| get_zone_color(state, zone));
74
75    Column::new()
76        .spacing(8)
77        .push(
78            row![
79                text("Red:").size(12).width(Length::Fixed(40.0)),
80                text(format!("{}", r)).size(12).width(Length::Fixed(30.0)),
81                slider(0..=255, r, move |v| { Message::LedSliderChanged(v, g, b) })
82                    .width(Length::Fill)
83            ]
84            .spacing(8)
85            .align_items(Alignment::Center),
86        )
87        .push(
88            row![
89                text("Green:").size(12).width(Length::Fixed(40.0)),
90                text(format!("{}", g)).size(12).width(Length::Fixed(30.0)),
91                slider(0..=255, g, move |v| { Message::LedSliderChanged(r, v, b) })
92                    .width(Length::Fill)
93            ]
94            .spacing(8)
95            .align_items(Alignment::Center),
96        )
97        .push(
98            row![
99                text("Blue:").size(12).width(Length::Fixed(40.0)),
100                text(format!("{}", b)).size(12).width(Length::Fixed(30.0)),
101                slider(0..=255, b, move |v| { Message::LedSliderChanged(r, g, v) })
102                    .width(Length::Fill)
103            ]
104            .spacing(8)
105            .align_items(Alignment::Center),
106        )
107        .into()
108}
109
110pub fn view(state: &State) -> Option<Element<'_, Message>> {
111    if let Some(ref device_id) = state.led_config_device {
112        let selected_zone = state.selected_led_zone.unwrap_or(LedZone::Logo);
113        let led_state = state.led_states.get(device_id);
114        let zone_colors = led_state.map(|s| &s.zone_colors);
115        let current_color = get_zone_color(state, selected_zone);
116
117        let zones = vec![
118            (LedZone::Logo, "Logo"),
119            (LedZone::Keys, "Keys"),
120            (LedZone::Thumbstick, "Thumbstick"),
121        ];
122
123        let zone_buttons: Vec<Element<'_, Message>> = zones
124            .into_iter()
125            .map(|(zone, label)| {
126                let is_selected = state.selected_led_zone == Some(zone);
127                button(text(label).size(12))
128                    .on_press(Message::SelectLedZone(zone))
129                    .style(if is_selected {
130                        iced::theme::Button::Primary
131                    } else {
132                        iced::theme::Button::Secondary
133                    })
134                    .padding([6, 12])
135                    .into()
136            })
137            .collect();
138
139        let preview = container(
140            container(
141                text(format!(
142                    "RGB({}, {}, {})",
143                    current_color.0, current_color.1, current_color.2
144                ))
145                .size(11)
146                .horizontal_alignment(iced::alignment::Horizontal::Center),
147            )
148            .width(Length::Fill)
149            .height(Length::Fill)
150            .align_x(iced::alignment::Horizontal::Center)
151            .align_y(iced::alignment::Vertical::Center),
152        )
153        .width(Length::Fixed(120.0))
154        .height(Length::Fixed(60.0))
155        .style(if let Some(colors) = zone_colors {
156            led_color_style(state.selected_led_zone, colors)
157        } else {
158            iced::theme::Container::Transparent
159        });
160
161        let patterns = vec![
162            (LedPattern::Static, "Static"),
163            (LedPattern::Breathing, "Breathing"),
164            (LedPattern::Rainbow, "Rainbow"),
165        ];
166
167        let current_pattern = led_state
168            .map(|s| s.active_pattern)
169            .unwrap_or(LedPattern::Static);
170
171        let pattern_buttons: Vec<Element<'_, Message>> = patterns
172            .into_iter()
173            .map(|(pattern, label)| {
174                let is_active = current_pattern == pattern;
175                button(text(label).size(11))
176                    .on_press(Message::SetLedPattern(device_id.clone(), pattern))
177                    .style(if is_active {
178                        iced::theme::Button::Primary
179                    } else {
180                        iced::theme::Button::Secondary
181                    })
182                    .padding([4, 10])
183                    .into()
184            })
185            .collect();
186
187        let brightness = led_state
188            .map(|s| s.global_brightness as f32)
189            .unwrap_or(100.0);
190
191        let dialog = container(
192            column![
193                row![
194                    text("LED Configuration").size(18),
195                    Space::with_width(Length::Fill),
196                    button(text("\u{00d7}").size(20))
197                        .on_press(Message::CloseLedConfig)
198                        .style(iced::theme::Button::Text)
199                        .padding([0, 8])
200                ]
201                .spacing(8)
202                .align_items(Alignment::Center),
203                horizontal_rule(1),
204                text(device_id).size(11).width(Length::Fill),
205                text("Zone:").size(13),
206                row(zone_buttons).spacing(8),
207                horizontal_rule(1),
208                text("Color:").size(13),
209                row![
210                    preview,
211                    column![
212                        text("Adjust RGB sliders below").size(11),
213                        text("to change color").size(11),
214                    ]
215                    .spacing(4)
216                ]
217                .spacing(12)
218                .align_items(Alignment::Center),
219                view_led_rgb_sliders(state),
220                horizontal_rule(1),
221                text(format!("Brightness: {}%", brightness as u8)).size(13),
222                slider(0.0..=100.0, brightness, move |v| {
223                    Message::SetLedBrightness(device_id.clone(), None, v as u8)
224                })
225                .width(Length::Fill),
226                horizontal_rule(1),
227                text("Pattern:").size(13),
228                row(pattern_buttons).spacing(8),
229                horizontal_rule(1),
230                row![
231                    Space::with_width(Length::Fill),
232                    button(text("Close").size(13))
233                        .on_press(Message::CloseLedConfig)
234                        .style(iced::theme::Button::Secondary)
235                        .padding([6, 16])
236                ]
237                .spacing(8)
238            ]
239            .spacing(12)
240            .padding(20),
241        )
242        .max_width(500)
243        .style(theme::styles::card);
244
245        Some(
246            container(dialog)
247                .width(Length::Fill)
248                .height(Length::Fill)
249                .align_x(iced::alignment::Horizontal::Center)
250                .align_y(iced::alignment::Vertical::Center)
251                .padding(40)
252                .style(iced::theme::Container::Transparent)
253                .into(),
254        )
255    } else {
256        None
257    }
258}