1use egui::{Rect, Vec2, Color32, Sense, Widget, Ui,
2 FontId, Response, Pos2, LayerId, Order, Align2,
3 Rounding, vec2
4};
5
6pub struct Timeline<'a> {
7 progress: f64,
8 total: f64,
9 accent_color: Color32,
10 height: f32,
11 radius_factor: f32,
12 font_size: f32,
13 seek_position: &'a mut f64,
14}
15
16impl<'a> Timeline<'a> {
17 pub fn new(progress: f64, total: f64, seek_position: &'a mut f64) -> Self {
18 Self {
19 progress,
20 total,
21 accent_color: Color32::from_rgb(0, 155, 255),
22 height: 8.0,
23 radius_factor: 0.3,
24 font_size: 12.0,
25 seek_position,
26 }
27 }
28
29 pub fn accent_color(mut self, color: Color32) -> Self {
30 self.accent_color = color;
31 self
32 }
33
34 pub fn height(mut self, height: f32) -> Self {
35 self.height = height;
36 self
37 }
38
39 pub fn radius_factor(mut self, factor: f32) -> Self {
40 self.radius_factor = factor;
41 self
42 }
43
44 pub fn font_size(mut self, font_size: f32) -> Self {
45 self.font_size = font_size;
46 self
47 }
48}
49
50impl<'a> Widget for Timeline<'a> {
51 fn ui(self, ui: &mut Ui) -> Response {
52 let font_id = FontId::new(self.font_size, egui::FontFamily::Proportional);
53 let time_font_width = ui.ctx().fonts(|fonts| {
54 fonts.layout_no_wrap(time_to_display(0.0), font_id, egui::Color32::WHITE).rect.width()
55 });
56
57 let desired_size_x = ui.available_size().x;
58 let desired_size_y = self.height;
59 let desired_size: Vec2 = vec2(desired_size_x, desired_size_y);
60
61 let (rect, mut response) =
62 ui.allocate_exact_size(desired_size, Sense::click_and_drag());
63 let visuals = ui.style().interact(&response);
64
65 let mut outer_rect = rect.expand(visuals.expansion);
66 outer_rect.set_left(rect.left() + time_font_width + 5.0);
67 outer_rect.set_right(rect.right() - time_font_width - 5.0);
68
69 if ui.is_rect_visible(rect) {
70 let radius = self.radius_factor * rect.height();
71 ui.painter()
72 .rect(outer_rect, radius, visuals.bg_fill, visuals.bg_stroke);
73
74 if response.hovered() {
75 if let Some(mouse_position) = ui.input(|i| i.pointer.hover_pos()) {
76 let seek_time;
77 if mouse_position.x < outer_rect.min.x {
78 seek_time = 0.0;
79 } else if mouse_position.x > outer_rect.max.x {
80 seek_time = self.total;
81 } else {
82 seek_time = self.total * (mouse_position.x - outer_rect.min.x) as f64 / outer_rect.width() as f64;
83 }
84
85 draw_tooltip(
86 ui,
87 Pos2::new(mouse_position.x, outer_rect.min.y - self.font_size - 10.0),
88 time_to_display(seek_time),
89 visuals.text_color(),
90 visuals.bg_fill,
91 self.font_size
92 );
93
94 }
95 }
96
97 let mut fill_rect = outer_rect;
98 let mut seek_rect = outer_rect;
99
100 fill_rect.set_width(fill_rect.width() * self.progress as f32 / self.total as f32);
101 ui.painter().rect_filled(
102 fill_rect,
103 radius,
104 self.accent_color
105 );
106
107 if response.is_pointer_button_down_on() || response.dragged() {
108 if let Some(pt) = response.interact_pointer_pos() {
109 seek_rect.max.x = pt.x;
110 if seek_rect.width() > outer_rect.width() {
111 seek_rect.set_width(outer_rect.width());
112 }
113
114 let seek_color = {
115 let [r, g, b, _] = self.accent_color.to_array();
116 Color32::from_rgba_unmultiplied(
117 ((r as f32 * 1.1).min(255.0)) as u8,
118 ((g as f32 * 1.1).min(255.0)) as u8,
119 ((b as f32 * 1.1).min(255.0)) as u8,
120 128,
121 )
122 };
123
124 ui.painter().rect_filled(seek_rect, radius, seek_color);
125
126 if pt.x < seek_rect.min.x {
127 *self.seek_position = 0.0;
128 } else if pt.x > rect.max.x {
129 *self.seek_position = self.total;
130 } else {
131 *self.seek_position =
132 self.total * seek_rect.width() as f64 / outer_rect.width() as f64;
133 }
134
135 response.mark_changed();
136 }
137 }
138 }
139
140 ui.painter().text(
141 rect.left_top() + Vec2::new(0.0 , self.height / 2.0 - self.font_size / 2.0),
142 Align2::LEFT_TOP,
143 time_to_display(self.progress),
144 FontId::proportional(self.font_size),
145 visuals.text_color(),
146 );
147
148 ui.painter().text(
149 rect.right_top() + Vec2::new(-time_font_width, self.height / 2.0 - self.font_size / 2.0),
150 Align2::LEFT_TOP,
151 time_to_display(self.total),
152 FontId::proportional(self.font_size),
153 visuals.text_color(),
154 );
155
156 response
157 }
158}
159
160fn time_to_display(seconds: f64) -> String {
161 let is: i64 = seconds.round() as i64;
162 let hours = is / (60 * 60);
163 let mins = (is % (60 * 60)) / 60;
164 let secs = seconds - 60.0 * mins as f64 - 60.0 * 60.0 * hours as f64; format!("{}:{:0>2}:{:0>4.1}", hours, mins, secs)
167}
168
169fn draw_tooltip(
170 ui: &Ui,
171 pos: Pos2,
172 tooltip_text: impl ToString,
173 text_color: Color32,
174 tooltip_color: Color32,
175 font_size: f32
176) {
177 let layer_id = LayerId::new(Order::Foreground, ui.id().with("foreground_layer"));
178 let foreground_painter = ui.ctx().layer_painter(layer_id);
179
180 let font_id = FontId::new(font_size, egui::FontFamily::Proportional);
181 let tooltip_font_width = ui.ctx().fonts(|fonts| {
182 fonts.layout_no_wrap(tooltip_text.to_string(), font_id, egui::Color32::WHITE).rect.width()
183 });
184
185 let rect = Rect::from_min_size(pos, vec2(tooltip_font_width + 8.0, font_size + 6.0));
186 let rounding = Rounding::same(5.0);
187
188 foreground_painter.rect_filled(rect, rounding, tooltip_color);
189
190 foreground_painter.text(
191 rect.center(),
192 Align2::CENTER_CENTER,
193 tooltip_text,
194 FontId::proportional(font_size),
195 text_color,
196 );
197}