aethermap-gui 1.5.0

GUI client for aethermap input remapper
Documentation
use crate::gui::{Message, State};
use crate::theme;
use iced::{
    widget::{button, column, container, row, scrollable, text, text_input, Space},
    Alignment, Element, Length,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotkeyBinding {
    pub modifiers: Vec<String>,
    pub key: String,
    pub profile_name: String,
    pub device_id: Option<String>,
    pub layer_id: Option<usize>,
}

#[derive(Debug, Clone, Default)]
pub struct HotkeyBindingsView {
    pub device_id: String,
    pub bindings: Vec<HotkeyBinding>,
    pub editing_binding: Option<usize>,
    pub new_modifiers: Vec<String>,
    pub new_key: String,
    pub new_profile_name: String,
    pub new_layer_id: String,
}

fn modifier_checkbox<'a>(
    label: &str,
    modifier: &str,
    selected: &'a [String],
) -> Element<'a, Message> {
    let is_checked = selected.iter().any(|m| m.to_lowercase() == modifier);
    let btn = if is_checked {
        button(text(format!("[{}] ", label)).size(12))
    } else {
        button(text(format!("[ ] {}", label)).size(12))
    };
    btn.on_press(Message::ToggleHotkeyModifier(modifier.to_string()))
        .style(iced::theme::Button::Text)
        .into()
}

pub fn view(state: &State) -> Element<'_, Message> {
    let view = state.hotkey_view.as_ref().unwrap();

    let bindings_header = row![
        text("Hotkey Bindings").size(18),
        Space::with_width(Length::Fill),
        if view.editing_binding.is_some() {
            button("Cancel")
                .on_press(Message::EditHotkeyBinding(usize::MAX))
                .style(iced::theme::Button::Text)
        } else {
            button("Add Binding")
                .on_press(Message::EditHotkeyBinding(usize::MAX))
                .style(iced::theme::Button::Primary)
        },
    ]
    .align_items(Alignment::Center);

    let bindings_list = if view.bindings.is_empty() {
        column![
            Space::with_height(20),
            text("No bindings configured")
                .size(14)
                .style(iced::theme::Text::Color(iced::Color::from_rgb(
                    0.6, 0.6, 0.6
                ))),
            Space::with_height(8),
            text("Add a binding to switch profiles using keyboard shortcuts")
                .size(12)
                .style(iced::theme::Text::Color(iced::Color::from_rgb(
                    0.5, 0.5, 0.5
                ))),
        ]
        .align_items(Alignment::Center)
    } else {
        let mut list = column![].spacing(8);
        for (idx, binding) in view.bindings.iter().enumerate() {
            let is_editing = view.editing_binding == Some(idx);
            let modifiers_str = binding.modifiers.join("+");
            let indicator: Element<'_, Message> = if is_editing {
                container(text("\u{25b6}")).padding([0, 8]).into()
            } else {
                Space::with_width(20).into()
            };
            let rule_row = row![
                indicator,
                column![
                    text(format!(
                        "{}+{} \u{2192} {}",
                        modifiers_str, binding.key, binding.profile_name
                    ))
                    .size(14),
                    text(format!(
                        "Layer: {}",
                        binding
                            .layer_id
                            .map(|l| l.to_string())
                            .unwrap_or_else(|| "default".to_string())
                    ))
                    .size(12),
                ]
                .spacing(2),
                Space::with_width(Length::Fill),
                button("Edit")
                    .on_press(Message::EditHotkeyBinding(idx))
                    .style(iced::theme::Button::Text),
                button("Delete")
                    .on_press(Message::DeleteHotkeyBinding(idx))
                    .style(iced::theme::Button::Destructive),
            ]
            .spacing(8)
            .align_items(Alignment::Center);
            list = list.push(rule_row);
        }
        list
    };

    let edit_form = if view.editing_binding.is_some() {
        Some(
            column![
                Space::with_height(20),
                text(if view.editing_binding.unwrap_or(0) < view.bindings.len() {
                    "Edit Binding"
                } else {
                    "Add New Binding"
                })
                .size(16),
                Space::with_height(12),
                text("Modifiers:").size(14),
                row![
                    modifier_checkbox("Ctrl", "ctrl", &view.new_modifiers),
                    modifier_checkbox("Alt", "alt", &view.new_modifiers),
                    modifier_checkbox("Shift", "shift", &view.new_modifiers),
                    modifier_checkbox("Super", "super", &view.new_modifiers),
                ]
                .spacing(8),
                Space::with_height(8),
                row![
                    text("Key:").size(14),
                    Space::with_width(8),
                    text_input("1", &view.new_key)
                        .on_input(Message::HotkeyKeyChanged)
                        .padding(8)
                        .size(14),
                ]
                .spacing(4)
                .align_items(Alignment::Center),
                Space::with_height(8),
                row![
                    text("Profile:").size(14),
                    Space::with_width(8),
                    text_input("default", &view.new_profile_name)
                        .on_input(Message::HotkeyProfileNameChanged)
                        .padding(8)
                        .size(14),
                ]
                .spacing(4)
                .align_items(Alignment::Center),
                Space::with_height(8),
                row![
                    text("Layer (optional):").size(14),
                    Space::with_width(8),
                    text_input("0", &view.new_layer_id)
                        .on_input(Message::HotkeyLayerIdChanged)
                        .padding(8)
                        .size(14),
                ]
                .spacing(4)
                .align_items(Alignment::Center),
                Space::with_height(12),
                row![
                    Space::with_width(Length::Fill),
                    button("Save Binding")
                        .on_press(Message::SaveHotkeyBinding)
                        .style(iced::theme::Button::Primary),
                ]
                .align_items(Alignment::Center),
            ]
            .spacing(4),
        )
    } else {
        None
    };

    let mut content = column![
        bindings_header,
        Space::with_height(12),
        scrollable(bindings_list).height(Length::Fixed(200.0)),
    ]
    .spacing(4);

    if let Some(form) = edit_form {
        content = content.push(form);
    }

    container(content)
        .padding(20)
        .width(Length::Fill)
        .style(theme::styles::card)
        .into()
}