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