1use crate::Theme;
26use egui::{Color32, Pos2, Rect, Sense, Stroke, Ui, Vec2};
27use egui_cha::ViewCtx;
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct GradientStop {
32 pub position: f32,
33 pub color: Color32,
34}
35
36impl GradientStop {
37 pub fn new(position: f32, color: Color32) -> Self {
38 Self {
39 position: position.clamp(0.0, 1.0),
40 color,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub struct Gradient {
48 pub stops: Vec<GradientStop>,
49}
50
51impl Gradient {
52 pub fn new() -> Self {
53 Self {
54 stops: vec![
55 GradientStop::new(0.0, Color32::BLACK),
56 GradientStop::new(1.0, Color32::WHITE),
57 ],
58 }
59 }
60
61 pub fn from_stops(mut stops: Vec<GradientStop>) -> Self {
62 stops.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
63 Self { stops }
64 }
65
66 pub fn sample(&self, t: f32) -> Color32 {
67 let t = t.clamp(0.0, 1.0);
68
69 if self.stops.is_empty() {
70 return Color32::BLACK;
71 }
72 if self.stops.len() == 1 {
73 return self.stops[0].color;
74 }
75
76 let mut left = &self.stops[0];
77 let mut right = &self.stops[self.stops.len() - 1];
78
79 for i in 0..self.stops.len() - 1 {
80 if self.stops[i].position <= t && self.stops[i + 1].position >= t {
81 left = &self.stops[i];
82 right = &self.stops[i + 1];
83 break;
84 }
85 }
86
87 let range = right.position - left.position;
88 if range < 0.0001 {
89 return left.color;
90 }
91
92 let factor = (t - left.position) / range;
93 Color32::from_rgba_unmultiplied(
94 lerp_u8(left.color.r(), right.color.r(), factor),
95 lerp_u8(left.color.g(), right.color.g(), factor),
96 lerp_u8(left.color.b(), right.color.b(), factor),
97 lerp_u8(left.color.a(), right.color.a(), factor),
98 )
99 }
100
101 pub fn add_stop(&mut self, position: f32) {
102 let color = self.sample(position);
103 self.stops.push(GradientStop::new(position, color));
104 self.stops
105 .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
106 }
107
108 pub fn add_stop_with_color(&mut self, position: f32, color: Color32) {
109 self.stops.push(GradientStop::new(position, color));
110 self.stops
111 .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
112 }
113
114 pub fn remove_stop(&mut self, index: usize) {
115 if self.stops.len() > 2 && index < self.stops.len() {
116 self.stops.remove(index);
117 }
118 }
119
120 pub fn move_stop(&mut self, index: usize, new_position: f32) {
121 if let Some(stop) = self.stops.get_mut(index) {
122 stop.position = new_position.clamp(0.0, 1.0);
123 }
124 self.stops
125 .sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap());
126 }
127}
128
129impl Default for Gradient {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
136 ((a as f32) * (1.0 - t) + (b as f32) * t) as u8
137}
138
139#[derive(Debug, Clone)]
141pub enum GradientEvent {
142 AddStop(f32),
143 MoveStop { index: usize, position: f32 },
144 RemoveStop(usize),
145 SetStopColor { index: usize, color: Color32 },
146 SelectStop(Option<usize>),
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub enum GradientDirection {
152 #[default]
153 Horizontal,
154 Vertical,
155}
156
157pub struct GradientEditor<'a> {
159 gradient: &'a Gradient,
160 width: f32,
161 height: f32,
162 direction: GradientDirection,
163 selected_stop: Option<usize>,
164 show_stop_values: bool,
165 editable: bool,
166}
167
168impl<'a> GradientEditor<'a> {
169 pub fn new(gradient: &'a Gradient) -> Self {
170 Self {
171 gradient,
172 width: 300.0,
173 height: 40.0,
174 direction: GradientDirection::Horizontal,
175 selected_stop: None,
176 show_stop_values: true,
177 editable: true,
178 }
179 }
180
181 pub fn width(mut self, width: f32) -> Self {
182 self.width = width;
183 self
184 }
185
186 pub fn height(mut self, height: f32) -> Self {
187 self.height = height;
188 self
189 }
190
191 pub fn direction(mut self, direction: GradientDirection) -> Self {
192 self.direction = direction;
193 self
194 }
195
196 pub fn selected(mut self, index: Option<usize>) -> Self {
197 self.selected_stop = index;
198 self
199 }
200
201 pub fn show_values(mut self, show: bool) -> Self {
202 self.show_stop_values = show;
203 self
204 }
205
206 pub fn editable(mut self, editable: bool) -> Self {
207 self.editable = editable;
208 self
209 }
210
211 pub fn show_with<Msg>(
212 self,
213 ctx: &mut ViewCtx<'_, Msg>,
214 on_event: impl Fn(GradientEvent) -> Msg,
215 ) {
216 let event = self.show_internal(ctx.ui);
217 if let Some(e) = event {
218 ctx.emit(on_event(e));
219 }
220 }
221
222 pub fn show(self, ui: &mut Ui) -> Option<GradientEvent> {
223 self.show_internal(ui)
224 }
225
226 fn show_internal(self, ui: &mut Ui) -> Option<GradientEvent> {
227 let theme = Theme::current(ui.ctx());
228 let mut event: Option<GradientEvent> = None;
229
230 let stop_handle_size = theme.spacing_md;
231 let stop_area_height = stop_handle_size + theme.spacing_xs;
232 let values_height = if self.show_stop_values {
233 theme.font_size_xs + theme.spacing_xs
234 } else {
235 0.0
236 };
237
238 let total_height = self.height + stop_area_height + values_height + theme.spacing_xs;
239
240 let (rect, _response) =
241 ui.allocate_exact_size(Vec2::new(self.width, total_height), Sense::hover());
242
243 if !ui.is_rect_visible(rect) {
244 return None;
245 }
246
247 let bar_rect = Rect::from_min_size(
249 Pos2::new(rect.min.x, rect.min.y),
250 Vec2::new(self.width, self.height),
251 );
252
253 let bar_response = if self.editable {
255 let resp = ui.allocate_rect(bar_rect, Sense::click());
256 Some((resp.double_clicked(), resp.interact_pointer_pos()))
257 } else {
258 None
259 };
260
261 struct StopInfo {
263 idx: usize,
264 stop_x: f32,
265 hovered: bool,
266 dragged: bool,
267 drag_pos: Option<Pos2>,
268 clicked: bool,
269 secondary_clicked: bool,
270 }
271
272 let stop_y = bar_rect.max.y + theme.spacing_xs;
273 let mut stop_infos: Vec<StopInfo> = Vec::with_capacity(self.gradient.stops.len());
274
275 for (idx, stop) in self.gradient.stops.iter().enumerate() {
276 let stop_x = match self.direction {
277 GradientDirection::Horizontal => bar_rect.min.x + stop.position * bar_rect.width(),
278 GradientDirection::Vertical => bar_rect.center().x,
279 };
280
281 let handle_pos = Pos2::new(stop_x, stop_y);
282 let handle_rect =
283 Rect::from_center_size(handle_pos, Vec2::new(stop_handle_size, stop_handle_size));
284
285 if self.editable {
286 let resp = ui.allocate_rect(handle_rect.expand(4.0), Sense::click_and_drag());
287 stop_infos.push(StopInfo {
288 idx,
289 stop_x,
290 hovered: resp.hovered(),
291 dragged: resp.dragged(),
292 drag_pos: resp.interact_pointer_pos(),
293 clicked: resp.clicked(),
294 secondary_clicked: resp.secondary_clicked(),
295 });
296 } else {
297 stop_infos.push(StopInfo {
298 idx,
299 stop_x,
300 hovered: false,
301 dragged: false,
302 drag_pos: None,
303 clicked: false,
304 secondary_clicked: false,
305 });
306 }
307 }
308
309 let painter = ui.painter();
311
312 let checker_size = 8.0;
314 let cols = (bar_rect.width() / checker_size) as usize + 1;
315 let rows = (bar_rect.height() / checker_size) as usize + 1;
316 for row in 0..rows {
317 for col in 0..cols {
318 let is_dark = (row + col) % 2 == 0;
319 let color = if is_dark {
320 Color32::from_gray(60)
321 } else {
322 Color32::from_gray(100)
323 };
324 let check_rect = Rect::from_min_size(
325 Pos2::new(
326 bar_rect.min.x + col as f32 * checker_size,
327 bar_rect.min.y + row as f32 * checker_size,
328 ),
329 Vec2::splat(checker_size),
330 )
331 .intersect(bar_rect);
332 painter.rect_filled(check_rect, 0.0, color);
333 }
334 }
335
336 let steps = 64;
338 for i in 0..steps {
339 let t1 = i as f32 / steps as f32;
340 let t2 = (i + 1) as f32 / steps as f32;
341
342 let (x1, x2) = match self.direction {
343 GradientDirection::Horizontal => (
344 bar_rect.min.x + t1 * bar_rect.width(),
345 bar_rect.min.x + t2 * bar_rect.width(),
346 ),
347 GradientDirection::Vertical => (bar_rect.min.x, bar_rect.max.x),
348 };
349
350 let (y1, y2) = match self.direction {
351 GradientDirection::Horizontal => (bar_rect.min.y, bar_rect.max.y),
352 GradientDirection::Vertical => (
353 bar_rect.min.y + t1 * bar_rect.height(),
354 bar_rect.min.y + t2 * bar_rect.height(),
355 ),
356 };
357
358 let color = self.gradient.sample(t1);
359 painter.rect_filled(
360 Rect::from_min_max(Pos2::new(x1, y1), Pos2::new(x2 + 1.0, y2)),
361 0.0,
362 color,
363 );
364 }
365
366 painter.rect_stroke(
368 bar_rect,
369 theme.radius_sm,
370 Stroke::new(theme.border_width, theme.border),
371 egui::StrokeKind::Inside,
372 );
373
374 if let Some((double_clicked, pos)) = bar_response {
376 if double_clicked {
377 if let Some(pos) = pos {
378 let t = match self.direction {
379 GradientDirection::Horizontal => {
380 (pos.x - bar_rect.min.x) / bar_rect.width()
381 }
382 GradientDirection::Vertical => (pos.y - bar_rect.min.y) / bar_rect.height(),
383 };
384 event = Some(GradientEvent::AddStop(t.clamp(0.0, 1.0)));
385 }
386 }
387 }
388
389 for (info, stop) in stop_infos.iter().zip(self.gradient.stops.iter()) {
391 let is_selected = self.selected_stop == Some(info.idx);
392
393 let tri_height = stop_handle_size * 0.6;
395 let tri_half_width = stop_handle_size * 0.5;
396 let tri_top = Pos2::new(info.stop_x, bar_rect.max.y);
397 let tri_left = Pos2::new(info.stop_x - tri_half_width, bar_rect.max.y + tri_height);
398 let tri_right = Pos2::new(info.stop_x + tri_half_width, bar_rect.max.y + tri_height);
399
400 let is_hovered = info.hovered || info.dragged;
401
402 painter.add(egui::Shape::convex_polygon(
403 vec![tri_top, tri_left, tri_right],
404 stop.color,
405 Stroke::NONE,
406 ));
407
408 let outline_color = if is_selected {
409 theme.primary
410 } else if is_hovered {
411 theme.text_primary
412 } else {
413 theme.border
414 };
415 painter.add(egui::Shape::closed_line(
416 vec![tri_top, tri_left, tri_right],
417 Stroke::new(if is_selected { 2.0 } else { 1.0 }, outline_color),
418 ));
419
420 let swatch_rect = Rect::from_min_size(
422 Pos2::new(info.stop_x - stop_handle_size / 2.0, tri_right.y + 2.0),
423 Vec2::new(stop_handle_size, stop_handle_size / 2.0),
424 );
425 painter.rect_filled(swatch_rect, theme.radius_sm, stop.color);
426 painter.rect_stroke(
427 swatch_rect,
428 theme.radius_sm,
429 Stroke::new(1.0, outline_color),
430 egui::StrokeKind::Outside,
431 );
432
433 if self.show_stop_values && is_hovered {
435 let value_text = format!("{:.0}%", stop.position * 100.0);
436 painter.text(
437 Pos2::new(info.stop_x, swatch_rect.max.y + theme.spacing_xs),
438 egui::Align2::CENTER_TOP,
439 &value_text,
440 egui::FontId::proportional(theme.font_size_xs),
441 theme.text_muted,
442 );
443 }
444 }
445
446 if self.gradient.stops.len() > 1 {
448 let line_y = stop_y + stop_handle_size * 0.3;
449 for i in 0..self.gradient.stops.len() - 1 {
450 let x1 = bar_rect.min.x + self.gradient.stops[i].position * bar_rect.width();
451 let x2 = bar_rect.min.x + self.gradient.stops[i + 1].position * bar_rect.width();
452 painter.line_segment(
453 [
454 Pos2::new(x1 + stop_handle_size / 2.0, line_y),
455 Pos2::new(x2 - stop_handle_size / 2.0, line_y),
456 ],
457 Stroke::new(1.0, theme.border),
458 );
459 }
460 }
461
462 let info_text = format!("{} stops", self.gradient.stops.len());
464 painter.text(
465 Pos2::new(rect.max.x - theme.spacing_sm, rect.min.y + theme.spacing_xs),
466 egui::Align2::RIGHT_TOP,
467 &info_text,
468 egui::FontId::proportional(theme.font_size_xs),
469 theme.text_muted,
470 );
471
472 for info in stop_infos.iter() {
474 if event.is_some() {
475 break;
476 }
477
478 if info.clicked {
479 event = Some(GradientEvent::SelectStop(Some(info.idx)));
480 } else if info.dragged {
481 if let Some(pos) = info.drag_pos {
482 let new_pos = match self.direction {
483 GradientDirection::Horizontal => {
484 (pos.x - bar_rect.min.x) / bar_rect.width()
485 }
486 GradientDirection::Vertical => (pos.y - bar_rect.min.y) / bar_rect.height(),
487 };
488 event = Some(GradientEvent::MoveStop {
489 index: info.idx,
490 position: new_pos.clamp(0.0, 1.0),
491 });
492 }
493 } else if info.secondary_clicked && self.gradient.stops.len() > 2 {
494 event = Some(GradientEvent::RemoveStop(info.idx));
495 }
496 }
497
498 event
499 }
500}