use cache::FrameCacheDyn;
use egui::Popup;
use egui::color_picker::{Alpha, color_picker_hsva_2d};
use egui::ecolor::Hsva;
use egui::style::WidgetVisuals;
use egui::{
Button, Color32, ColorImage, ComboBox, Id, LayerId, Mesh, Order, Painter, PointerButton, Rect,
Sense, Shape, Stroke, StrokeKind, TextureHandle, TextureOptions, Ui, Vec2, lerp, pos2, vec2,
};
pub use gradient::{ColorInterpolator, Gradient, InterpolationMethod};
mod cache;
mod gradient;
const TICK_OFFSET: f32 = 8.;
const TICK_SQUARE_SIZE: f32 = 12.;
const CHECKER_SIZE: f32 = 15.0;
fn background_checkers(painter: &Painter, rect: Rect) {
let tex_mgr = painter.ctx().tex_manager();
let texture = painter.ctx().memory_mut(|mem| {
const NAME: &str = "checker_background";
let cache = mem.caches.cache::<FrameCacheDyn<TextureHandle, 1>>();
cache.get_or_else_insert(NAME, || {
let dark_color = Color32::from_gray(32);
let bright_color = Color32::from_gray(128);
let data = [dark_color, bright_color, bright_color, dark_color]
.iter()
.flat_map(|c| c.to_array())
.collect::<Vec<_>>();
let img = ColorImage::from_rgba_premultiplied([2, 2], &data);
let tex_id =
tex_mgr
.write()
.alloc(NAME.to_string(), img.into(), TextureOptions::NEAREST_REPEAT);
TextureHandle::new(tex_mgr, tex_id)
})
});
let mut mesh = Mesh::with_texture(texture.id());
let rect = rect.shrink(0.5); let uv = Rect::from_min_max(rect.min / CHECKER_SIZE, rect.max / CHECKER_SIZE);
mesh.add_rect_with_uv(rect, uv, Color32::WHITE);
painter.add(Shape::mesh(mesh));
}
fn draw_gradient(ui: &mut Ui, gradient: &Gradient, rect: Rect, use_alpha: bool) {
const TEX_SIZE: usize = 256;
let texture_mgr = ui.ctx().tex_manager();
let texture_cache_id = ui.auto_id_with("gradient_texture").with(use_alpha);
let texture = ui.memory_mut(|mem| {
let cache = mem.caches.cache::<FrameCacheDyn<TextureHandle, 1>>();
let mut tex = cache.get_or_else_insert(texture_cache_id, || {
let img = ColorImage::filled([TEX_SIZE, 1], Color32::BLACK);
let tex_id = texture_mgr.write().alloc(
"gradient".to_string(),
img.into(),
TextureOptions::LINEAR,
);
TextureHandle::new(texture_mgr, tex_id)
});
tex.set(
ColorImage::from_rgba_premultiplied(
[TEX_SIZE, 1],
&gradient
.linear_eval(TEX_SIZE, !use_alpha)
.iter()
.flat_map(|c| c.to_array())
.collect::<Vec<_>>(),
),
TextureOptions::LINEAR,
);
tex
});
let mut mesh = Mesh::with_texture(texture.id());
mesh.add_rect_with_uv(
rect,
Rect::from_min_max(pos2(0., 0.), pos2(1., 1.)),
Color32::WHITE,
);
ui.painter().add(Shape::mesh(mesh));
}
fn gradient_box(
ui: &mut Ui,
gradient: &mut Gradient,
gradient_rect: Rect,
visuals: &WidgetVisuals,
) -> Option<usize> {
const SOLID_HEIGHT: f32 = 8.;
background_checkers(
ui.painter(),
gradient_rect.with_max_y(gradient_rect.bottom()),
);
let mut new_stop = None;
let response = ui.allocate_rect(gradient_rect, Sense::click());
if response.double_clicked_by(PointerButton::Primary) {
let x = response.interact_pointer_pos().unwrap().x;
let t = ((x - gradient_rect.left()) / gradient_rect.width()).clamp(0., 1.);
let color = gradient.interpolator().sample_at(t).unwrap();
new_stop = Some(gradient.stops.len());
gradient.stops.push((t, color.into()));
}
for (y, use_alpha) in [
gradient_rect.top(),
gradient_rect.bottom() - SOLID_HEIGHT,
gradient_rect.bottom(),
]
.windows(2)
.zip([true, false])
{
let (top, bottom) = (y[0], y[1]);
draw_gradient(
ui,
gradient,
gradient_rect.with_min_y(top).with_max_y(bottom),
use_alpha,
);
}
ui.painter()
.rect_stroke(gradient_rect, 0.0, visuals.bg_stroke, StrokeKind::Middle);
new_stop
}
fn control_widgets(ui: &mut Ui, gradient: &mut Gradient, selected_stop: &mut Option<usize>) {
ui.horizontal(|ui| {
let add_button = Button::new("âž•");
let add_button_response = ui.add(add_button).on_hover_text("Add stop");
if add_button_response.clicked() {
let t = if gradient.stops.len() <= 1 {
0.5
} else {
let sorted_stops = gradient.argsort();
*selected_stop = selected_stop.map(|idx| sorted_stops[idx]);
gradient.sort();
let insertion_idx = selected_stop.unwrap_or(gradient.stops.len() - 1).max(1);
let right_t = gradient.stops[insertion_idx].0;
let left_t = if insertion_idx > 0 {
gradient.stops[insertion_idx - 1].0
} else {
0.
};
0.5 * (left_t + right_t)
};
let col = gradient.interpolator().sample_at(t).unwrap();
gradient.stops.push((t, col.into()));
*selected_stop = Some(gradient.stops.len() - 1);
};
let remove_button = Button::new("âž–");
let can_remove = selected_stop.is_some() && gradient.stops.len() > 1;
if can_remove {
let remove_button_response = ui.add(remove_button);
if remove_button_response.clicked() {
gradient.stops.remove(selected_stop.unwrap());
*selected_stop = None;
}
remove_button_response
} else {
ui.add_enabled(false, remove_button)
}
.on_hover_text("Remove stop");
ComboBox::from_id_salt(ui.auto_id_with(0))
.selected_text(gradient.interpolation_method.to_string())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut gradient.interpolation_method,
InterpolationMethod::Linear,
InterpolationMethod::Linear.to_string(),
);
ui.selectable_value(
&mut gradient.interpolation_method,
InterpolationMethod::Constant,
InterpolationMethod::Constant.to_string(),
);
})
.response
.on_hover_text("Interpolation method");
});
}
fn gradient_stop(
ui: &mut Ui,
rect: Rect,
idx: usize,
(t, color): (&mut f32, &mut Hsva),
selected_stop: &mut Option<usize>,
did_interact: &mut bool,
) {
let is_selected = matches!(selected_stop, Some(i) if *i == idx);
let popup_id = Id::new(ui.id()).with("popup").with(idx);
let x = lerp(rect.left()..=rect.right(), *t);
let tick_rect = Rect::from_center_size(
pos2(x, rect.bottom() + TICK_SQUARE_SIZE * 0.5 - TICK_OFFSET),
Vec2::splat(TICK_SQUARE_SIZE),
);
let mut tick_response = ui.allocate_rect(
tick_rect.expand(5.).with_min_y(rect.top()),
Sense::click_and_drag(),
);
if tick_response.dragged_by(PointerButton::Primary) {
*t = (*t + tick_response.drag_delta().x / rect.width()).clamp(0., 1.);
*selected_stop = Some(idx);
*did_interact = true;
}
Popup::menu(&tick_response).id(popup_id).show(|ui| {
ui.spacing_mut().slider_width = COLOR_SLIDER_WIDTH;
*selected_stop = Some(idx);
if color_picker_hsva_2d(ui, color, Alpha::BlendOrAdditive) {
tick_response.mark_changed();
}
*did_interact = true;
});
const COLOR_SLIDER_WIDTH: f32 = 200.;
let mut visuals = ui.style().interact_selectable(&tick_response, is_selected);
let mut painter = ui.painter().clone();
if is_selected {
visuals.fg_stroke.width = 3.;
visuals.bg_stroke = ui.style().visuals.widgets.hovered.bg_stroke;
visuals.bg_stroke.width = 3.;
painter.set_layer_id(LayerId::new(
Order::Middle,
ui.auto_id_with("selected stop"),
));
}
painter.add(Shape::vline(
x,
tick_rect.top()..=rect.top(),
Stroke::new(visuals.fg_stroke.width, Color32::WHITE),
));
painter.add(Shape::dashed_line(
&[pos2(x, tick_rect.top()), pos2(x, rect.top())],
Stroke::new(visuals.fg_stroke.width, Color32::BLACK),
2.,
2. + visuals.fg_stroke.width / 2.,
));
painter.rect_filled(tick_rect, 0.0, color.to_opaque());
painter.rect_stroke(tick_rect, 0.0, visuals.bg_stroke, StrokeKind::Outside);
painter.rect_stroke(tick_rect, 0.0, visuals.fg_stroke, StrokeKind::Inside);
}
pub fn gradient_editor(ui: &mut Ui, gradient: &mut Gradient) {
ui.vertical(|ui| {
let selected_stop_id = ui.id().with("selected_stop");
let mut selected_stop: Option<usize> =
ui.memory_mut(|mem| mem.data.remove_temp(selected_stop_id));
control_widgets(ui, gradient, &mut selected_stop);
let minimum_size = vec2(
ui.spacing().slider_width,
ui.spacing().interact_size.y * 1.7,
);
ui.set_min_size(minimum_size);
let desired_size = minimum_size * vec2(4., 1.);
let requested_size = ui.available_size().max(minimum_size).min(desired_size);
let (rect, response) = ui.allocate_at_least(requested_size, Sense::hover());
let mut did_interact = false;
if ui.is_rect_visible(rect) {
let visuals = *ui.style().noninteractive();
let gradient_rect = rect
.with_max_y(rect.max.y - TICK_OFFSET)
.shrink2(vec2(TICK_SQUARE_SIZE * 0.5 + 2., 0.));
if let Some(new_stop) = gradient_box(ui, gradient, gradient_rect, &visuals) {
selected_stop = Some(new_stop);
};
for (idx, (t, color)) in gradient.stops.iter_mut().enumerate() {
gradient_stop(
ui,
gradient_rect,
idx,
(t, color),
&mut selected_stop,
&mut did_interact,
);
}
}
if response.clicked_elsewhere() && !did_interact {
selected_stop = None;
}
ui.memory_mut(|mem| {
if let Some(idx) = selected_stop {
mem.data.insert_temp(selected_stop_id, idx)
}
})
});
}