Skip to main content

egui_bind/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use egui::{
5    Align2, AsId, Event, FontId, Frame, Id, Key, PointerButton, Response, Sense, Ui, Widget,
6};
7
8mod target;
9pub use target::*;
10mod either;
11pub use either::*;
12
13/// Widget for showing the bind itself
14pub struct Bind<'a, B: BindTarget> {
15    id: Id,
16    value: &'a mut B,
17}
18
19impl<'a, B: BindTarget> Bind<'a, B> {
20    /// Creates a new bind widget
21    pub fn new(id_source: impl AsId, value: &'a mut B) -> Self {
22        Self {
23            id: Id::new(id_source),
24            value,
25        }
26    }
27}
28
29impl<B: BindTarget> Widget for Bind<'_, B> {
30    fn ui(self, ui: &mut Ui) -> Response {
31        let id = ui.make_persistent_id(self.id);
32        let changing = ui.memory_mut(|mem| mem.data.get_temp(id).unwrap_or(false));
33
34        let size = ui.spacing().interact_size;
35
36        let (mut r, p) = ui.allocate_painter(size, Sense::click());
37        let vis = ui.style().interact_selectable(&r, changing);
38
39        p.rect_filled(r.rect, vis.corner_radius, vis.bg_fill);
40
41        p.text(
42            r.rect.center(),
43            Align2::CENTER_CENTER,
44            self.value.format(),
45            FontId::default(),
46            vis.fg_stroke.color,
47        );
48
49        if changing {
50            let key = ui.input(|i| {
51                i.events
52                    .iter()
53                    .find(|e| {
54                        matches!(
55                            e,
56                            Event::Key { pressed: true, .. }
57                                | Event::PointerButton { pressed: true, .. }
58                        )
59                    })
60                    .cloned()
61            });
62
63            let (reset, changed) = match key {
64                Some(Event::Key {
65                    key: Key::Escape, ..
66                }) if B::CLEARABLE => {
67                    self.value.clear();
68                    (true, true)
69                }
70                Some(Event::Key { key, modifiers, .. }) if B::IS_KEY => {
71                    self.value.set_key(key, modifiers);
72                    (true, true)
73                }
74                Some(Event::PointerButton {
75                    button, modifiers, ..
76                }) if B::IS_POINTER && button != PointerButton::Primary => {
77                    self.value.set_pointer(button, modifiers);
78                    (true, true)
79                }
80                _ if r.clicked_elsewhere() => (true, false),
81                _ => (false, false),
82            };
83
84            if reset {
85                ui.memory_mut(|mem| mem.data.insert_temp(id, false));
86            }
87
88            if changed {
89                r.mark_changed();
90            }
91        }
92
93        if r.clicked() {
94            ui.memory_mut(|mem| mem.data.insert_temp(id, true));
95        }
96
97        r
98    }
99}
100
101/// Shows bind popup when clicked with secondary pointer button.
102pub fn show_bind_popup(
103    ui: &mut Ui,
104    bind: &mut impl BindTarget,
105    popup_id_source: impl AsId,
106    widget_response: &Response,
107) -> bool {
108    let popup_id = Id::new(popup_id_source);
109
110    if widget_response.secondary_clicked() {
111        egui::Popup::toggle_id(ui.ctx(), popup_id)
112    }
113
114    let mut should_close = false;
115    let was_opened = egui::Popup::is_id_open(ui.ctx(), popup_id);
116
117    let out = if was_opened {
118        egui::Popup::from_response(&widget_response)
119            .id(popup_id)
120            .align(egui::RectAlign::BOTTOM)
121            .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
122            .frame(Frame::popup(ui.style()).inner_margin(0))
123            .show(|ui| {
124                let r = ui.add(Bind::new(popup_id.with("_bind"), bind));
125
126                if r.changed() || ui.input(|i| i.key_down(Key::Escape)) {
127                    egui::Popup::close_id(ui.ctx(), popup_id);
128                    should_close = true;
129                }
130
131                r.changed()
132            })
133    } else {
134        None
135    };
136
137    if !should_close && was_opened {
138        egui::Popup::open_id(ui.ctx(), popup_id);
139    }
140
141    out.map_or(false, |inner_response| inner_response.inner)
142}