use egui::{Color32, Pos2, Rect, Sense, Stroke, StrokeKind, Vec2};
#[derive(Clone, Copy, Default)]
pub struct CurveResponse {
pub changed: bool,
pub committed: bool,
}
const HANDLE: f32 = 5.0;
const PICK: f32 = 11.0;
fn normalize<T>(keys: &mut [(f32, T)]) {
keys.sort_by(|a, b| a.0.total_cmp(&b.0));
if let Some(first) = keys.first_mut() {
first.0 = 0.0;
}
if let Some(last) = keys.last_mut() {
last.0 = 1.0;
}
}
fn nearest_stop(keys: &[(f32, [f32; 4])], rect: Rect, p: Pos2) -> Option<usize> {
let to_x = |ratio: f32| rect.left() + ratio.clamp(0.0, 1.0) * rect.width();
keys.iter()
.enumerate()
.map(|(i, k)| (i, (to_x(k.0) - p.x).abs()))
.filter(|(_, d)| *d <= PICK)
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(i, _)| i)
}
pub struct CurveEditor<'k> {
keys: &'k mut Vec<(f32, f32)>,
y_min: f32,
y_max: f32,
height: f32,
}
impl<'k> CurveEditor<'k> {
pub fn new(keys: &'k mut Vec<(f32, f32)>) -> Self {
Self {
keys,
y_min: 0.0,
y_max: 1.0,
height: 120.0,
}
}
pub fn y_range(mut self, min: f32, max: f32) -> Self {
self.y_min = min;
self.y_max = max.max(min + f32::EPSILON);
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
pub fn show(self, ui: &mut egui::Ui) -> CurveResponse {
let mut out = CurveResponse::default();
if self.keys.is_empty() {
self.keys.push((0.0, self.y_min));
self.keys.push((1.0, self.y_max));
out.changed = true;
}
let width = ui.available_width().max(80.0);
let (rect, resp) =
ui.allocate_exact_size(Vec2::new(width, self.height), Sense::click_and_drag());
let painter = ui.painter_at(rect);
let vis = ui.visuals();
painter.rect_filled(rect, 4.0, vis.extreme_bg_color);
painter.rect_stroke(
rect,
4.0,
vis.widgets.noninteractive.bg_stroke,
StrokeKind::Inside,
);
let to_screen = |ratio: f32, value: f32| -> Pos2 {
let nx = ratio.clamp(0.0, 1.0);
let ny = ((value - self.y_min) / (self.y_max - self.y_min)).clamp(0.0, 1.0);
Pos2::new(
rect.left() + nx * rect.width(),
rect.bottom() - ny * rect.height(),
)
};
let from_screen = |p: Pos2| -> (f32, f32) {
let nx = ((p.x - rect.left()) / rect.width()).clamp(0.0, 1.0);
let ny = ((rect.bottom() - p.y) / rect.height()).clamp(0.0, 1.0);
(nx, self.y_min + ny * (self.y_max - self.y_min))
};
for i in 0..self.keys.len().saturating_sub(1) {
let a = to_screen(self.keys[i].0, self.keys[i].1);
let b = to_screen(self.keys[i + 1].0, self.keys[i + 1].1);
painter.line_segment([a, b], Stroke::new(2.0, vis.widgets.active.fg_stroke.color));
}
let active_id = ui.id().with("curve-drag");
let mut dragging: Option<usize> = ui.ctx().data(|d| d.get_temp::<usize>(active_id));
if resp.drag_started() {
if let Some(p) = resp.interact_pointer_pos() {
dragging = self
.keys
.iter()
.enumerate()
.map(|(i, k)| (i, to_screen(k.0, k.1).distance(p)))
.filter(|(_, d)| *d <= PICK)
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(i, _)| i);
match dragging {
Some(i) => ui.ctx().data_mut(|d| {
d.insert_temp(active_id, i);
}),
None => ui.ctx().data_mut(|d| {
d.remove::<usize>(active_id);
}),
}
}
}
if resp.dragged() {
if let (Some(i), Some(p)) = (dragging, resp.interact_pointer_pos()) {
let (r, v) = from_screen(p);
let last = self.keys.len() - 1;
self.keys[i].1 = v;
if i != 0 && i != last {
self.keys[i].0 = r;
}
out.changed = true;
}
}
if resp.drag_stopped() {
if dragging.is_some() {
normalize(self.keys);
out.committed = true;
}
ui.ctx().data_mut(|d| d.remove::<usize>(active_id));
dragging = None;
}
if resp.double_clicked() {
if let Some(p) = resp.interact_pointer_pos() {
let on_handle = self
.keys
.iter()
.any(|k| to_screen(k.0, k.1).distance(p) <= PICK);
if !on_handle {
let (r, v) = from_screen(p);
self.keys.push((r.clamp(0.001, 0.999), v));
normalize(self.keys);
out.committed = true;
out.changed = true;
}
}
}
if resp.secondary_clicked() && self.keys.len() > 1 {
if let Some(p) = resp.interact_pointer_pos() {
if let Some((i, d)) = self
.keys
.iter()
.enumerate()
.map(|(i, k)| (i, to_screen(k.0, k.1).distance(p)))
.min_by(|a, b| a.1.total_cmp(&b.1))
{
if d <= PICK {
self.keys.remove(i);
normalize(self.keys);
out.committed = true;
out.changed = true;
}
}
}
}
for (i, k) in self.keys.iter().enumerate() {
let c = to_screen(k.0, k.1);
let hot = dragging == Some(i);
painter.circle_filled(
c,
if hot { HANDLE + 1.0 } else { HANDLE },
vis.selection.bg_fill,
);
painter.circle_stroke(c, HANDLE, Stroke::new(1.0, vis.extreme_bg_color));
}
out
}
}
pub struct GradientBar<'k> {
keys: &'k mut Vec<(f32, [f32; 4])>,
height: f32,
}
impl<'k> GradientBar<'k> {
pub fn new(keys: &'k mut Vec<(f32, [f32; 4])>) -> Self {
Self { keys, height: 28.0 }
}
pub fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
pub fn show(self, ui: &mut egui::Ui) -> CurveResponse {
let mut out = CurveResponse::default();
if self.keys.is_empty() {
self.keys.push((0.0, [0.0, 0.0, 0.0, 1.0]));
self.keys.push((1.0, [1.0, 1.0, 1.0, 1.0]));
out.changed = true;
}
let width = ui.available_width().max(80.0);
let (rect, resp) =
ui.allocate_exact_size(Vec2::new(width, self.height), Sense::click_and_drag());
let painter = ui.painter_at(rect);
let to_x = |ratio: f32| rect.left() + ratio.clamp(0.0, 1.0) * rect.width();
let to_ratio = |x: f32| ((x - rect.left()) / rect.width()).clamp(0.0, 1.0);
let col = |c: &[f32; 4]| {
Color32::from(egui::Rgba::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]))
};
for i in 0..self.keys.len().saturating_sub(1) {
let a = &self.keys[i];
let b = &self.keys[i + 1];
let mut mesh = egui::Mesh::default();
let (ca, cb) = (col(&a.1), col(&b.1));
let (xa, xb) = (to_x(a.0), to_x(b.0));
mesh.colored_vertex(Pos2::new(xa, rect.top()), ca);
mesh.colored_vertex(Pos2::new(xa, rect.bottom()), ca);
mesh.colored_vertex(Pos2::new(xb, rect.top()), cb);
mesh.colored_vertex(Pos2::new(xb, rect.bottom()), cb);
mesh.add_triangle(0, 1, 2);
mesh.add_triangle(2, 1, 3);
painter.add(mesh);
}
painter.rect_stroke(
rect,
2.0,
ui.visuals().widgets.noninteractive.bg_stroke,
StrokeKind::Inside,
);
let sel_id = ui.id().with("grad-sel");
let mut selected: usize = ui.ctx().data(|d| d.get_temp(sel_id).unwrap_or(0));
let drag_id = ui.id().with("grad-drag");
let mut dragging: Option<usize> = ui.ctx().data(|d| d.get_temp::<usize>(drag_id));
if resp.drag_started() {
if let Some(p) = resp.interact_pointer_pos() {
dragging = nearest_stop(self.keys, rect, p);
if let Some(i) = dragging {
selected = i;
ui.ctx().data_mut(|d| {
d.insert_temp(drag_id, i);
});
} else {
ui.ctx().data_mut(|d| {
d.remove::<usize>(drag_id);
});
}
}
}
if resp.dragged() {
if let (Some(i), Some(p)) = (dragging, resp.interact_pointer_pos()) {
let last = self.keys.len() - 1;
if i != 0 && i != last {
self.keys[i].0 = to_ratio(p.x);
out.changed = true;
}
}
}
if resp.drag_stopped() {
if let Some(i) = dragging {
let val = self.keys[i];
normalize(self.keys);
selected = self.keys.iter().position(|k| *k == val).unwrap_or(0);
out.committed = true;
}
ui.ctx().data_mut(|d| {
d.remove::<usize>(drag_id);
});
}
if resp.clicked() {
if let Some(i) = resp
.interact_pointer_pos()
.and_then(|p| nearest_stop(self.keys, rect, p))
{
selected = i;
}
}
if resp.double_clicked() {
if let Some(p) = resp.interact_pointer_pos() {
if nearest_stop(self.keys, rect, p).is_none() {
let r = to_ratio(p.x).clamp(0.001, 0.999);
self.keys.push((r, [1.0, 1.0, 1.0, 1.0]));
normalize(self.keys);
selected = self.keys.iter().position(|k| k.0 == r).unwrap_or(0);
out.committed = true;
out.changed = true;
}
}
}
if resp.secondary_clicked() && self.keys.len() > 1 {
if let Some(i) = resp
.interact_pointer_pos()
.and_then(|p| nearest_stop(self.keys, rect, p))
{
self.keys.remove(i);
out.committed = true;
out.changed = true;
}
}
selected = selected.min(self.keys.len() - 1);
for (i, k) in self.keys.iter().enumerate() {
let x = to_x(k.0);
let r = Rect::from_center_size(
Pos2::new(x, rect.bottom() - HANDLE),
Vec2::new(HANDLE * 2.0, HANDLE * 2.0),
);
let outline = if i == selected {
ui.visuals().selection.stroke.color
} else {
ui.visuals().extreme_bg_color
};
painter.rect_filled(r, 1.0, col(&k.1));
painter.rect_stroke(r, 1.0, Stroke::new(1.5, outline), StrokeKind::Outside);
}
let mut rgba = self.keys[selected].1;
if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
self.keys[selected].1 = rgba;
out.changed = true;
out.committed = true;
}
ui.ctx().data_mut(|d| d.insert_temp(sel_id, selected));
out
}
}