aethermap-gui 1.5.0

GUI client for aethermap input remapper
Documentation
use crate::gui::{Message, State};
use crate::theme;
use aethermap_common::LedPattern;
use aethermap_common::LedZone;
use iced::{
    widget::{button, column, container, horizontal_rule, row, slider, text, Column, Space},
    Alignment, Color, Element, Length, Theme,
};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct LedState {
    pub zone_colors: HashMap<LedZone, (u8, u8, u8)>,
    pub global_brightness: u8,
    pub zone_brightness: HashMap<LedZone, u8>,
    pub active_pattern: LedPattern,
}

impl Default for LedState {
    fn default() -> Self {
        Self {
            zone_colors: HashMap::new(),
            global_brightness: 100,
            zone_brightness: HashMap::new(),
            active_pattern: LedPattern::Static,
        }
    }
}

fn get_zone_color(state: &State, zone: LedZone) -> (u8, u8, u8) {
    if let Some(device_id) = &state.led_config_device {
        if let Some(led_state) = state.led_states.get(device_id) {
            if let Some(&color) = led_state.zone_colors.get(&zone) {
                return color;
            }
        }
    }
    (255, 255, 255)
}

fn led_color_style(
    zone: Option<LedZone>,
    zone_colors: &HashMap<LedZone, (u8, u8, u8)>,
) -> iced::theme::Container {
    let (r, g, b) = zone
        .and_then(|z| zone_colors.get(&z))
        .copied()
        .unwrap_or((255, 255, 255));

    struct LedColorStyle {
        r: u8,
        g: u8,
        b: u8,
    }

    impl iced::widget::container::StyleSheet for LedColorStyle {
        type Style = Theme;
        fn appearance(&self, _style: &Self::Style) -> iced::widget::container::Appearance {
            iced::widget::container::Appearance {
                background: Some(Color::from_rgb8(self.r, self.g, self.b).into()),
                ..Default::default()
            }
        }
    }

    iced::theme::Container::Custom(Box::new(LedColorStyle { r, g, b }))
}

fn view_led_rgb_sliders(state: &State) -> Element<'_, Message> {
    let zone = state.selected_led_zone.unwrap_or(LedZone::Logo);
    let (r, g, b) = state
        .pending_led_color
        .unwrap_or_else(|| get_zone_color(state, zone));

    Column::new()
        .spacing(8)
        .push(
            row![
                text("Red:").size(12).width(Length::Fixed(40.0)),
                text(format!("{}", r)).size(12).width(Length::Fixed(30.0)),
                slider(0..=255, r, move |v| { Message::LedSliderChanged(v, g, b) })
                    .width(Length::Fill)
            ]
            .spacing(8)
            .align_items(Alignment::Center),
        )
        .push(
            row![
                text("Green:").size(12).width(Length::Fixed(40.0)),
                text(format!("{}", g)).size(12).width(Length::Fixed(30.0)),
                slider(0..=255, g, move |v| { Message::LedSliderChanged(r, v, b) })
                    .width(Length::Fill)
            ]
            .spacing(8)
            .align_items(Alignment::Center),
        )
        .push(
            row![
                text("Blue:").size(12).width(Length::Fixed(40.0)),
                text(format!("{}", b)).size(12).width(Length::Fixed(30.0)),
                slider(0..=255, b, move |v| { Message::LedSliderChanged(r, g, v) })
                    .width(Length::Fill)
            ]
            .spacing(8)
            .align_items(Alignment::Center),
        )
        .into()
}

pub fn view(state: &State) -> Option<Element<'_, Message>> {
    if let Some(ref device_id) = state.led_config_device {
        let selected_zone = state.selected_led_zone.unwrap_or(LedZone::Logo);
        let led_state = state.led_states.get(device_id);
        let zone_colors = led_state.map(|s| &s.zone_colors);
        let current_color = get_zone_color(state, selected_zone);

        let zones = vec![
            (LedZone::Logo, "Logo"),
            (LedZone::Keys, "Keys"),
            (LedZone::Thumbstick, "Thumbstick"),
        ];

        let zone_buttons: Vec<Element<'_, Message>> = zones
            .into_iter()
            .map(|(zone, label)| {
                let is_selected = state.selected_led_zone == Some(zone);
                button(text(label).size(12))
                    .on_press(Message::SelectLedZone(zone))
                    .style(if is_selected {
                        iced::theme::Button::Primary
                    } else {
                        iced::theme::Button::Secondary
                    })
                    .padding([6, 12])
                    .into()
            })
            .collect();

        let preview = container(
            container(
                text(format!(
                    "RGB({}, {}, {})",
                    current_color.0, current_color.1, current_color.2
                ))
                .size(11)
                .horizontal_alignment(iced::alignment::Horizontal::Center),
            )
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(iced::alignment::Horizontal::Center)
            .align_y(iced::alignment::Vertical::Center),
        )
        .width(Length::Fixed(120.0))
        .height(Length::Fixed(60.0))
        .style(if let Some(colors) = zone_colors {
            led_color_style(state.selected_led_zone, colors)
        } else {
            iced::theme::Container::Transparent
        });

        let patterns = vec![
            (LedPattern::Static, "Static"),
            (LedPattern::Breathing, "Breathing"),
            (LedPattern::Rainbow, "Rainbow"),
        ];

        let current_pattern = led_state
            .map(|s| s.active_pattern)
            .unwrap_or(LedPattern::Static);

        let pattern_buttons: Vec<Element<'_, Message>> = patterns
            .into_iter()
            .map(|(pattern, label)| {
                let is_active = current_pattern == pattern;
                button(text(label).size(11))
                    .on_press(Message::SetLedPattern(device_id.clone(), pattern))
                    .style(if is_active {
                        iced::theme::Button::Primary
                    } else {
                        iced::theme::Button::Secondary
                    })
                    .padding([4, 10])
                    .into()
            })
            .collect();

        let brightness = led_state
            .map(|s| s.global_brightness as f32)
            .unwrap_or(100.0);

        let dialog = container(
            column![
                row![
                    text("LED Configuration").size(18),
                    Space::with_width(Length::Fill),
                    button(text("\u{00d7}").size(20))
                        .on_press(Message::CloseLedConfig)
                        .style(iced::theme::Button::Text)
                        .padding([0, 8])
                ]
                .spacing(8)
                .align_items(Alignment::Center),
                horizontal_rule(1),
                text(device_id).size(11).width(Length::Fill),
                text("Zone:").size(13),
                row(zone_buttons).spacing(8),
                horizontal_rule(1),
                text("Color:").size(13),
                row![
                    preview,
                    column![
                        text("Adjust RGB sliders below").size(11),
                        text("to change color").size(11),
                    ]
                    .spacing(4)
                ]
                .spacing(12)
                .align_items(Alignment::Center),
                view_led_rgb_sliders(state),
                horizontal_rule(1),
                text(format!("Brightness: {}%", brightness as u8)).size(13),
                slider(0.0..=100.0, brightness, move |v| {
                    Message::SetLedBrightness(device_id.clone(), None, v as u8)
                })
                .width(Length::Fill),
                horizontal_rule(1),
                text("Pattern:").size(13),
                row(pattern_buttons).spacing(8),
                horizontal_rule(1),
                row![
                    Space::with_width(Length::Fill),
                    button(text("Close").size(13))
                        .on_press(Message::CloseLedConfig)
                        .style(iced::theme::Button::Secondary)
                        .padding([6, 16])
                ]
                .spacing(8)
            ]
            .spacing(12)
            .padding(20),
        )
        .max_width(500)
        .style(theme::styles::card);

        Some(
            container(dialog)
                .width(Length::Fill)
                .height(Length::Fill)
                .align_x(iced::alignment::Horizontal::Center)
                .align_y(iced::alignment::Vertical::Center)
                .padding(40)
                .style(iced::theme::Container::Transparent)
                .into(),
        )
    } else {
        None
    }
}