1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use crate::Bind;
use egui::{
    pos2, vec2, Event, Id, Key, KeyboardShortcut, ModifierNames, PointerButton, RichText, Sense,
    TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
};

/// A keybind (hotkey) widget for [egui].
pub struct Keybind<'a, B: Bind> {
    bind: &'a mut B,
    reset: B,
    text: &'a str,
    id: Id,
    reset_key: Option<Key>,
    modifier_names: &'a ModifierNames<'a>,
}

impl<'a, B: Bind> Keybind<'a, B> {
    /// Create a new [Keybind] for a given [Bind].
    ///
    /// # Arguments
    ///
    /// * `bind` - The bind to use for the [Keybind].
    /// * `id` - ID for the [Keybind] in [egui]'s memory.
    pub fn new(bind: &'a mut B, id: impl Into<Id>) -> Self {
        let prev_bind = bind.clone();
        Self {
            bind,
            reset: prev_bind,
            text: "",
            id: id.into(),
            reset_key: None,
            modifier_names: &ModifierNames::NAMES,
        }
    }

    /// Set the text of the [Keybind]. This will be displayed next to the
    /// keybind widget (and used for accessibility).
    ///
    /// You can remove the text by setting it to an empty string.
    /// By default there is no text.
    pub fn with_text(mut self, text: &'a str) -> Self {
        self.text = text;
        self
    }

    /// Set the bind of the [Keybind].
    ///
    /// By default this is the bind that was passed to `new`.
    pub fn with_bind(mut self, bind: &'a mut B) -> Self {
        self.bind = bind;
        self
    }

    /// Set the ID of the [Keybind] in [egui]'s memory.
    ///
    /// By default this is the ID that was passed in `new`.
    pub fn with_id(mut self, id: impl Into<Id>) -> Self {
        self.id = id.into();
        self
    }

    /// Set the key that resets the [Keybind]. If [None], the [Keybind] will
    /// never reset to its' previous value.
    ///
    /// By default this is [None].
    pub fn with_reset_key(mut self, key: Option<Key>) -> Self {
        self.reset_key = key;
        self
    }

    /// Set the bind that the [Keybind] will reset to after the reset key gets pressed.
    ///
    /// By default this is the same as the bind passed to `new`.
    pub fn with_reset(mut self, prev_bind: B) -> Self {
        self.reset = prev_bind;
        self
    }

    /// Set the modifier names to use for the [Keybind]. By default this is [`ModifierNames::NAMES`].
    pub fn with_modifier_names(mut self, modifier_names: &'a ModifierNames<'a>) -> Self {
        self.modifier_names = modifier_names;
        self
    }
}

/// Get the widget expecting value from egui's memory.
fn get_expecting(ui: &Ui, id: Id) -> bool {
    let expecting = ui.ctx().memory_mut(|memory| {
        *memory
            .data
            .get_temp_mut_or_default::<bool>(ui.make_persistent_id(id))
    });
    expecting
}

/// Set the widget expecting value in egui's memory.
fn set_expecting(ui: &Ui, id: Id, expecting: bool) {
    ui.ctx().memory_mut(|memory| {
        *memory
            .data
            .get_temp_mut_or_default(ui.make_persistent_id(id)) = expecting;
    });
}

impl<'a, B: Bind> Widget for Keybind<'a, B> {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        let text = self.bind.format(self.modifier_names, false);

        let galley = WidgetText::RichText(RichText::new(text.clone())).into_galley(
            ui,
            Some(false),
            0.0,
            TextStyle::Button,
        );

        let size = ui.spacing().interact_size.max(galley.size());
        let button_padding = ui.spacing().button_padding;
        let (rect, mut response) =
            ui.allocate_exact_size(size + button_padding * vec2(2.0, 1.0), Sense::click());

        // add widget info for accessibility. this generates a string like "Ctrl+T. Open the terminal"
        // if the keybind was created with `with_text`
        response.widget_info(|| {
            WidgetInfo::selected(WidgetType::Button, false, text.clone() + ". " + self.text)
        });

        // see if we're currently waiting for any key (pull from egui's memory)
        let mut expecting = get_expecting(ui, self.id);
        let prev_expecting = expecting;
        if response.clicked() {
            expecting = !expecting;
        }

        if expecting {
            if response.clicked_elsewhere() {
                // the user has clicked somewhere else, stop capturing input
                expecting = false;
            } else {
                // everything ok, capture keyboard input
                let kb = ui.input(|i| {
                    i.events.iter().find_map(|e| match e {
                        Event::Key {
                            key,
                            pressed: true,
                            modifiers,
                            repeat: false,
                        } => Some((*key, *modifiers)),
                        _ => None,
                    })
                });

                // capture mouse input
                let pointer = ui.input(|i| {
                    i.events.iter().find_map(|e| match e {
                        Event::PointerButton {
                            button,
                            pressed: true,
                            ..
                        } if *button != PointerButton::Primary
                            && *button != PointerButton::Secondary =>
                        {
                            Some(*button)
                        }
                        _ => None,
                    })
                });

                // set keybind
                if kb.is_some() || pointer.is_some() {
                    self.bind
                        .set(kb.map(|kb| KeyboardShortcut::new(kb.1, kb.0)), pointer);
                    response.mark_changed();
                    expecting = false;
                }
            }

            if let Some(reset_key) = self.reset_key {
                // the reset key was pressed
                if ui.input(|i| i.key_pressed(reset_key)) {
                    *self.bind = self.reset;
                    expecting = false;
                    response.mark_changed();
                }
            }
        }

        // paint
        if ui.is_rect_visible(rect) {
            // paint bg rect
            let visuals = ui.style().interact_selectable(&response, expecting);
            ui.painter().rect(
                rect.expand(visuals.expansion),
                visuals.rounding,
                visuals.bg_fill,
                visuals.bg_stroke,
            );

            // align text to center in rect that is shrinked to match button padding
            let mut text_pos = ui
                .layout()
                .align_size_within_rect(galley.size(), rect.shrink2(button_padding))
                .min;

            // align text to center of the button if it doesn't expand the rect
            if text_pos.x + galley.size().x + button_padding.x < rect.right() {
                text_pos.x += rect.size().x / 2.0 - galley.size().x / 2.0 - button_padding.x;
            }

            // paint text inside button
            galley.paint_with_visuals(ui.painter(), text_pos, &visuals);

            // compute galley for text outside on the left, if any
            if !self.text.is_empty() {
                let galley = WidgetText::RichText(RichText::new(self.text)).into_galley(
                    ui,
                    Some(true),
                    ui.available_width() - rect.right(),
                    TextStyle::Button,
                );
                let text_pos = pos2(
                    rect.right() + ui.spacing().icon_spacing,
                    rect.center().y - 0.5 * galley.size().y,
                );
                galley.paint_with_visuals(ui.painter(), text_pos, ui.style().noninteractive());
            }
        }

        if prev_expecting != expecting {
            set_expecting(ui, self.id, expecting);
        }
        response
    }
}