Skip to main content

loam_egui/
media.rs

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
48/// Clicking while selected resets `rate` to 1.0.
49pub 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}