1use cache::FrameCacheDyn;
4use egui::Popup;
5use egui::color_picker::{Alpha, color_picker_hsva_2d};
6use egui::ecolor::Hsva;
7use egui::style::WidgetVisuals;
8use egui::{
9 Button, Color32, ColorImage, ComboBox, Id, LayerId, Mesh, Order, Painter, PointerButton, Rect,
10 Sense, Shape, Stroke, StrokeKind, TextureHandle, TextureOptions, Ui, Vec2, lerp, pos2, vec2,
11};
12pub use gradient::{ColorInterpolator, Gradient, InterpolationMethod};
13
14mod cache;
15mod gradient;
16const TICK_OFFSET: f32 = 8.;
17
18const TICK_SQUARE_SIZE: f32 = 12.;
19
20const CHECKER_SIZE: f32 = 15.0;
21
22fn background_checkers(painter: &Painter, rect: Rect) {
23 let tex_mgr = painter.ctx().tex_manager();
24 let texture = painter.ctx().memory_mut(|mem| {
25 const NAME: &str = "checker_background";
26
27 let cache = mem.caches.cache::<FrameCacheDyn<TextureHandle, 1>>();
28 cache.get_or_else_insert(NAME, || {
29 let dark_color = Color32::from_gray(32);
30 let bright_color = Color32::from_gray(128);
31 let data = [dark_color, bright_color, bright_color, dark_color]
32 .iter()
33 .flat_map(|c| c.to_array())
34 .collect::<Vec<_>>();
35 let img = ColorImage::from_rgba_premultiplied([2, 2], &data);
36 let tex_id =
37 tex_mgr
38 .write()
39 .alloc(NAME.to_string(), img.into(), TextureOptions::NEAREST_REPEAT);
40 TextureHandle::new(tex_mgr, tex_id)
41 })
42 });
43 let mut mesh = Mesh::with_texture(texture.id());
44 let rect = rect.shrink(0.5); let uv = Rect::from_min_max(rect.min / CHECKER_SIZE, rect.max / CHECKER_SIZE);
46 mesh.add_rect_with_uv(rect, uv, Color32::WHITE);
47
48 painter.add(Shape::mesh(mesh));
49}
50
51fn draw_gradient(ui: &mut Ui, gradient: &Gradient, rect: Rect, use_alpha: bool) {
52 const TEX_SIZE: usize = 256;
53
54 let texture_mgr = ui.ctx().tex_manager();
55 let texture_cache_id = ui.auto_id_with("gradient_texture").with(use_alpha);
56 let texture = ui.memory_mut(|mem| {
57 let cache = mem.caches.cache::<FrameCacheDyn<TextureHandle, 1>>();
58 let mut tex = cache.get_or_else_insert(texture_cache_id, || {
59 let img = ColorImage::filled([TEX_SIZE, 1], Color32::BLACK);
60 let tex_id = texture_mgr.write().alloc(
61 "gradient".to_string(),
62 img.into(),
63 TextureOptions::LINEAR,
64 );
65 TextureHandle::new(texture_mgr, tex_id)
66 });
67 tex.set(
68 ColorImage::from_rgba_premultiplied(
69 [TEX_SIZE, 1],
70 &gradient
71 .linear_eval(TEX_SIZE, !use_alpha)
72 .iter()
73 .flat_map(|c| c.to_array())
74 .collect::<Vec<_>>(),
75 ),
76 TextureOptions::LINEAR,
77 );
78 tex
79 });
80
81 let mut mesh = Mesh::with_texture(texture.id());
83 mesh.add_rect_with_uv(
84 rect,
85 Rect::from_min_max(pos2(0., 0.), pos2(1., 1.)),
86 Color32::WHITE,
87 );
88 ui.painter().add(Shape::mesh(mesh));
89}
90
91fn gradient_box(
92 ui: &mut Ui,
93 gradient: &mut Gradient,
94 gradient_rect: Rect,
95 visuals: &WidgetVisuals,
96) -> Option<usize> {
97 const SOLID_HEIGHT: f32 = 8.;
98 background_checkers(
99 ui.painter(),
100 gradient_rect.with_max_y(gradient_rect.bottom()),
101 );
102
103 let mut new_stop = None;
104
105 let response = ui.allocate_rect(gradient_rect, Sense::click());
106 if response.double_clicked_by(PointerButton::Primary) {
107 let x = response.interact_pointer_pos().unwrap().x;
108 let t = ((x - gradient_rect.left()) / gradient_rect.width()).clamp(0., 1.);
109 let color = gradient.interpolator().sample_at(t).unwrap();
110 new_stop = Some(gradient.stops.len());
111 gradient.stops.push((t, color.into()));
112 }
113
114 for (y, use_alpha) in [
115 gradient_rect.top(),
116 gradient_rect.bottom() - SOLID_HEIGHT,
117 gradient_rect.bottom(),
118 ]
119 .windows(2)
120 .zip([true, false])
121 {
122 let (top, bottom) = (y[0], y[1]);
123 draw_gradient(
124 ui,
125 gradient,
126 gradient_rect.with_min_y(top).with_max_y(bottom),
127 use_alpha,
128 );
129 }
130 ui.painter()
131 .rect_stroke(gradient_rect, 0.0, visuals.bg_stroke, StrokeKind::Middle); new_stop
134}
135
136fn control_widgets(ui: &mut Ui, gradient: &mut Gradient, selected_stop: &mut Option<usize>) {
137 ui.horizontal(|ui| {
138 let add_button = Button::new("➕");
139 let add_button_response = ui.add(add_button).on_hover_text("Add stop");
140 if add_button_response.clicked() {
141 let t = if gradient.stops.len() <= 1 {
142 0.5
143 } else {
144 let sorted_stops = gradient.argsort();
145 *selected_stop = selected_stop.map(|idx| sorted_stops[idx]);
146 gradient.sort();
147
148 let insertion_idx = selected_stop.unwrap_or(gradient.stops.len() - 1).max(1);
149 let right_t = gradient.stops[insertion_idx].0;
150 let left_t = if insertion_idx > 0 {
151 gradient.stops[insertion_idx - 1].0
152 } else {
153 0.
154 };
155 0.5 * (left_t + right_t)
156 };
157 let col = gradient.interpolator().sample_at(t).unwrap();
158 gradient.stops.push((t, col.into()));
159 *selected_stop = Some(gradient.stops.len() - 1);
160 };
161 let remove_button = Button::new("➖");
162 let can_remove = selected_stop.is_some() && gradient.stops.len() > 1;
163 if can_remove {
164 let remove_button_response = ui.add(remove_button);
165 if remove_button_response.clicked() {
166 gradient.stops.remove(selected_stop.unwrap());
167 *selected_stop = None;
168 }
169 remove_button_response
170 } else {
171 ui.add_enabled(false, remove_button)
172 }
173 .on_hover_text("Remove stop");
174
175 ComboBox::from_id_salt(ui.auto_id_with(0))
176 .selected_text(gradient.interpolation_method.to_string())
177 .show_ui(ui, |ui| {
178 ui.selectable_value(
179 &mut gradient.interpolation_method,
180 InterpolationMethod::Linear,
181 InterpolationMethod::Linear.to_string(),
182 );
183 ui.selectable_value(
184 &mut gradient.interpolation_method,
185 InterpolationMethod::Constant,
186 InterpolationMethod::Constant.to_string(),
187 );
188 })
189 .response
190 .on_hover_text("Interpolation method");
191 });
192}
193
194fn gradient_stop(
195 ui: &mut Ui,
196 rect: Rect,
197 idx: usize,
198 (t, color): (&mut f32, &mut Hsva),
199 selected_stop: &mut Option<usize>,
200 did_interact: &mut bool,
201) {
202 let is_selected = matches!(selected_stop, Some(i) if *i == idx);
203 let popup_id = Id::new(ui.id()).with("popup").with(idx);
204 let x = lerp(rect.left()..=rect.right(), *t);
205
206 let tick_rect = Rect::from_center_size(
207 pos2(x, rect.bottom() + TICK_SQUARE_SIZE * 0.5 - TICK_OFFSET),
208 Vec2::splat(TICK_SQUARE_SIZE),
209 );
210 let mut tick_response = ui.allocate_rect(
211 tick_rect.expand(5.).with_min_y(rect.top()),
212 Sense::click_and_drag(),
213 );
214
215 if tick_response.dragged_by(PointerButton::Primary) {
216 *t = (*t + tick_response.drag_delta().x / rect.width()).clamp(0., 1.);
217 *selected_stop = Some(idx);
218 *did_interact = true;
219 }
220
221 Popup::menu(&tick_response).id(popup_id).show(|ui| {
222 ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
223 *selected_stop = Some(idx);
224 if color_picker_hsva_2d(ui, color, Alpha::BlendOrAdditive) {
225 tick_response.mark_changed();
226 }
227 *did_interact = true;
228 });
229
230 const COLOR_SLIDER_WIDTH: f32 = 200.;
231
232 let mut visuals = ui.style().interact_selectable(&tick_response, is_selected);
233 let mut painter = ui.painter().clone();
234
235 if is_selected {
236 visuals.fg_stroke.width = 3.;
237 visuals.bg_stroke = ui.style().visuals.widgets.hovered.bg_stroke;
238 visuals.bg_stroke.width = 3.;
239
240 painter.set_layer_id(LayerId::new(
242 Order::Middle,
243 ui.auto_id_with("selected stop"),
244 ));
245 }
246 painter.add(Shape::vline(
247 x,
248 tick_rect.top()..=rect.top(),
249 Stroke::new(visuals.fg_stroke.width, Color32::WHITE),
250 ));
251 painter.add(Shape::dashed_line(
252 &[pos2(x, tick_rect.top()), pos2(x, rect.top())],
253 Stroke::new(visuals.fg_stroke.width, Color32::BLACK),
254 2.,
255 2. + visuals.fg_stroke.width / 2.,
256 ));
257 painter.rect_filled(tick_rect, 0.0, color.to_opaque());
258 painter.rect_stroke(tick_rect, 0.0, visuals.bg_stroke, StrokeKind::Outside);
259 painter.rect_stroke(tick_rect, 0.0, visuals.fg_stroke, StrokeKind::Inside);
260}
261
262pub fn gradient_editor(ui: &mut Ui, gradient: &mut Gradient) {
264 ui.vertical(|ui| {
265 let selected_stop_id = ui.id().with("selected_stop");
266 let mut selected_stop: Option<usize> =
267 ui.memory_mut(|mem| mem.data.remove_temp(selected_stop_id));
268
269 control_widgets(ui, gradient, &mut selected_stop);
270 let minimum_size = vec2(
271 ui.spacing().slider_width,
272 ui.spacing().interact_size.y * 1.7,
273 );
274 ui.set_min_size(minimum_size);
275 let desired_size = minimum_size * vec2(4., 1.);
276 let requested_size = ui.available_size().max(minimum_size).min(desired_size);
277
278 let (rect, response) = ui.allocate_at_least(requested_size, Sense::hover());
279
280 let mut did_interact = false;
281
282 if ui.is_rect_visible(rect) {
283 let visuals = *ui.style().noninteractive();
284
285 let gradient_rect = rect
286 .with_max_y(rect.max.y - TICK_OFFSET)
287 .shrink2(vec2(TICK_SQUARE_SIZE * 0.5 + 2., 0.));
288
289 if let Some(new_stop) = gradient_box(ui, gradient, gradient_rect, &visuals) {
290 selected_stop = Some(new_stop);
291 };
292
293 for (idx, (t, color)) in gradient.stops.iter_mut().enumerate() {
294 gradient_stop(
295 ui,
296 gradient_rect,
297 idx,
298 (t, color),
299 &mut selected_stop,
300 &mut did_interact,
301 );
302 }
303 }
304
305 if response.clicked_elsewhere() && !did_interact {
306 selected_stop = None;
307 }
308
309 ui.memory_mut(|mem| {
310 if let Some(idx) = selected_stop {
311 mem.data.insert_temp(selected_stop_id, idx)
312 }
313 })
314 });
315}