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
13pub struct Bind<'a, B: BindTarget> {
15 id: Id,
16 value: &'a mut B,
17}
18
19impl<'a, B: BindTarget> Bind<'a, B> {
20 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
101pub 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}