1use egui::{
2 pos2, vec2, CornerRadius, Pos2, Rect, Response, Sense, Shape, Stroke, StrokeKind, Ui, Vec2,
3};
4
5pub fn play_pause_button(ui: &mut Ui, size: Vec2, playing: bool) -> Response {
6 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
7 let style = ui.style().interact(&response);
8 ui.painter().rect(
9 rect,
10 CornerRadius::same(3),
11 style.bg_fill,
12 style.bg_stroke,
13 StrokeKind::Inside,
14 );
15 let color = style.fg_stroke.color;
16 let cx = rect.center().x;
17 let cy = rect.center().y;
18 if playing {
19 let bar_w = 4.0;
20 let bar_h = 12.0;
21 let gap = 3.0;
22 let half_gap = gap / 2.0;
23 ui.painter().rect_filled(
24 Rect::from_min_size(
25 pos2(cx - half_gap - bar_w, cy - bar_h / 2.0),
26 vec2(bar_w, bar_h),
27 ),
28 CornerRadius::ZERO,
29 color,
30 );
31 ui.painter().rect_filled(
32 Rect::from_min_size(pos2(cx + half_gap, cy - bar_h / 2.0), vec2(bar_w, bar_h)),
33 CornerRadius::ZERO,
34 color,
35 );
36 } else {
37 let r_h = 7.0;
38 let r_w = 8.0;
39 let p1 = pos2(cx - r_w * 0.4, cy - r_h);
40 let p2 = pos2(cx - r_w * 0.4, cy + r_h);
41 let p3 = pos2(cx + r_w * 0.7, cy);
42 ui.painter()
43 .add(Shape::convex_polygon(vec![p1, p2, p3], color, Stroke::NONE));
44 }
45 response
46}
47
48pub fn rate_toggle(
50 ui: &mut Ui,
51 size: Vec2,
52 rate: &mut f32,
53 value: f32,
54 double: bool,
55 forward: bool,
56) -> Response {
57 let selected = (*rate - value).abs() < 1e-6;
58 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
59 let style = ui.style().interact_selectable(&response, selected);
60 ui.painter().rect(
61 rect,
62 CornerRadius::same(2),
63 style.bg_fill,
64 style.bg_stroke,
65 StrokeKind::Inside,
66 );
67 let color = style.fg_stroke.color;
68 let cx = rect.center().x;
69 let cy = rect.center().y;
70 let r_w = 4.5_f32;
71 let r_h = 5.5_f32;
72 let triangle_at = |tip_x: f32| -> Vec<Pos2> {
73 if forward {
74 vec![
75 pos2(tip_x - r_w * 0.5, cy - r_h),
76 pos2(tip_x - r_w * 0.5, cy + r_h),
77 pos2(tip_x + r_w * 0.7, cy),
78 ]
79 } else {
80 vec![
81 pos2(tip_x + r_w * 0.5, cy - r_h),
82 pos2(tip_x + r_w * 0.5, cy + r_h),
83 pos2(tip_x - r_w * 0.7, cy),
84 ]
85 }
86 };
87 if double {
88 let offset = 4.0;
89 ui.painter().add(Shape::convex_polygon(
90 triangle_at(cx - offset),
91 color,
92 Stroke::NONE,
93 ));
94 ui.painter().add(Shape::convex_polygon(
95 triangle_at(cx + offset),
96 color,
97 Stroke::NONE,
98 ));
99 } else {
100 ui.painter()
101 .add(Shape::convex_polygon(triangle_at(cx), color, Stroke::NONE));
102 }
103 if response.clicked() {
104 *rate = if selected { 1.0 } else { value };
105 }
106 response.on_hover_text(format!("Set rate to ×{value} (click again to reset to ×1)"))
107}
108
109pub fn add_button(ui: &mut Ui, size: Vec2) -> Response {
110 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
111 let style = ui.style().interact(&response);
112 ui.painter().rect(
113 rect,
114 CornerRadius::same(2),
115 style.bg_fill,
116 style.bg_stroke,
117 StrokeKind::Inside,
118 );
119 let cx = rect.center().x;
120 let cy = rect.center().y;
121 let arm = 5.5_f32;
122 let thick = 2.0_f32;
123 let color = style.fg_stroke.color;
124 ui.painter().rect_filled(
125 Rect::from_center_size(pos2(cx, cy), vec2(arm * 2.0, thick)),
126 CornerRadius::ZERO,
127 color,
128 );
129 ui.painter().rect_filled(
130 Rect::from_center_size(pos2(cx, cy), vec2(thick, arm * 2.0)),
131 CornerRadius::ZERO,
132 color,
133 );
134 response
135}
136
137pub fn refresh_button(ui: &mut Ui, size: Vec2) -> Response {
138 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
139 let style = ui.style().interact(&response);
140 ui.painter().rect(
141 rect,
142 CornerRadius::same(2),
143 style.bg_fill,
144 style.bg_stroke,
145 StrokeKind::Inside,
146 );
147 let cx = rect.center().x;
148 let cy = rect.center().y;
149 let radius = 6.5_f32;
150 let stroke = Stroke::new(1.6, style.fg_stroke.color);
151 use std::f32::consts::PI;
152 let start_angle: f32 = -PI / 2.0 + 0.45;
153 let sweep: f32 = PI * 1.55;
154 let n = 16;
155 let points: Vec<Pos2> = (0..=n)
156 .map(|i| {
157 let t = i as f32 / n as f32;
158 let angle = start_angle + t * sweep;
159 pos2(cx + radius * angle.cos(), cy + radius * angle.sin())
160 })
161 .collect();
162 ui.painter().add(Shape::line(points, stroke));
163 let arrow_size = 3.5_f32;
164 let anchor = pos2(
165 cx + radius * start_angle.cos(),
166 cy + radius * start_angle.sin(),
167 );
168 let tan = start_angle - PI / 2.0;
169 let perp = tan + PI / 2.0;
170 let tip = pos2(
171 anchor.x + arrow_size * tan.cos(),
172 anchor.y + arrow_size * tan.sin(),
173 );
174 let base_l = pos2(
175 anchor.x + arrow_size * 0.8 * perp.cos(),
176 anchor.y + arrow_size * 0.8 * perp.sin(),
177 );
178 let base_r = pos2(
179 anchor.x - arrow_size * 0.8 * perp.cos(),
180 anchor.y - arrow_size * 0.8 * perp.sin(),
181 );
182 ui.painter().add(Shape::convex_polygon(
183 vec![tip, base_l, base_r],
184 style.fg_stroke.color,
185 Stroke::NONE,
186 ));
187 response
188}
189
190pub fn chevron_button(ui: &mut Ui, size: Vec2, up: bool, hover: &str) -> Response {
191 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
192 let style = ui.style().interact(&response);
193 ui.painter().rect(
194 rect,
195 CornerRadius::same(2),
196 style.bg_fill,
197 style.bg_stroke,
198 StrokeKind::Inside,
199 );
200 let cx = rect.center().x;
201 let cy = rect.center().y;
202 let dx = 6.0;
203 let dy = 4.0;
204 let stroke = Stroke::new(2.0, style.fg_stroke.color);
205 if up {
206 ui.painter()
207 .line_segment([pos2(cx - dx, cy + dy), pos2(cx, cy - dy)], stroke);
208 ui.painter()
209 .line_segment([pos2(cx + dx, cy + dy), pos2(cx, cy - dy)], stroke);
210 } else {
211 ui.painter()
212 .line_segment([pos2(cx - dx, cy - dy), pos2(cx, cy + dy)], stroke);
213 ui.painter()
214 .line_segment([pos2(cx + dx, cy - dy), pos2(cx, cy + dy)], stroke);
215 }
216 response.on_hover_text(hover)
217}
218
219pub fn dock_chevrons(ui: &mut Ui, size: Vec2, pointing_up: bool, hover: &str) -> Response {
220 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
221 let style = ui.style().interact(&response);
222
223 let half_w = (size.x * 0.34).clamp(2.0, 6.0);
224 let half_h = (size.y * 0.16).clamp(1.5, 4.0);
225 let gap = (size.y * 0.32).clamp(3.0, 8.0);
226
227 let stroke = Stroke::new(1.4, style.fg_stroke.color);
228 let cx = rect.center().x;
229 let cy = rect.center().y;
230 let chevron_centers = [cy - gap / 2.0, cy + gap / 2.0];
231
232 let painter = ui.painter();
233 for &y in &chevron_centers {
234 let (apex_y, wing_y) = if pointing_up {
235 (y - half_h, y + half_h)
236 } else {
237 (y + half_h, y - half_h)
238 };
239 painter.line_segment([pos2(cx - half_w, wing_y), pos2(cx, apex_y)], stroke);
240 painter.line_segment([pos2(cx, apex_y), pos2(cx + half_w, wing_y)], stroke);
241 }
242
243 response
244 .on_hover_text(hover)
245 .on_hover_cursor(egui::CursorIcon::PointingHand)
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn screen() -> Rect {
253 Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(800.0, 600.0))
254 }
255 fn click_at_centre<R>(
256 mut widget: impl FnMut(&mut Ui) -> Response,
257 result: impl Fn(&Response) -> R,
258 ) -> R {
259 let ctx = egui::Context::default();
260 let mut rect = Rect::ZERO;
261 let layout_input = egui::RawInput {
262 screen_rect: Some(screen()),
263 time: Some(0.0),
264 ..Default::default()
265 };
266 let _ = ctx.run(layout_input, |ctx| {
267 egui::CentralPanel::default().show(ctx, |ui| {
268 rect = widget(ui).rect;
269 });
270 });
271 let centre = rect.center();
272 let mut click_input = egui::RawInput {
273 screen_rect: Some(screen()),
274 time: Some(0.05),
275 ..Default::default()
276 };
277 click_input.events.push(egui::Event::PointerMoved(centre));
278 click_input.events.push(egui::Event::PointerButton {
279 pos: centre,
280 button: egui::PointerButton::Primary,
281 pressed: true,
282 modifiers: Default::default(),
283 });
284 click_input.events.push(egui::Event::PointerButton {
285 pos: centre,
286 button: egui::PointerButton::Primary,
287 pressed: false,
288 modifiers: Default::default(),
289 });
290 let mut out: Option<R> = None;
291 let _ = ctx.run(click_input, |ctx| {
292 egui::CentralPanel::default().show(ctx, |ui| {
293 let resp = widget(ui);
294 out = Some(result(&resp));
295 });
296 });
297 out.expect("widget closure ran in click frame")
298 }
299
300 #[test]
301 fn rate_toggle_selects_then_resets() {
302 let mut rate = 1.0_f32;
303 click_at_centre(
304 |ui| rate_toggle(ui, vec2(28.0, 29.0), &mut rate, 2.0, false, true),
305 |_| (),
306 );
307 assert_eq!(rate, 2.0);
308 click_at_centre(
309 |ui| rate_toggle(ui, vec2(28.0, 29.0), &mut rate, 2.0, false, true),
310 |_| (),
311 );
312 assert_eq!(rate, 1.0);
313 }
314}