[color] add HSV-based color picker for struct Srgba

This commit is contained in:
Emil Ernerfeldt 2020-09-06 21:30:52 +02:00
parent fc3582fbe1
commit d8e0b3bff6
15 changed files with 451 additions and 23 deletions

View file

@ -10,9 +10,12 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [ ] Clipboard copy/paste
* [ ] Move focus with tab
* [ ] Horizontal slider
* [ ] Color picker
* [/] Color picker
* [x] linear rgb <-> sRGB
* Containers:
* [x] HSV
* [x] Color edit button with popup color picker
* [ ] Easily edit users own (s)RGBA quadruplets (`&mut [u8;4]`/`[f32;4]`)
* Containers
* [ ] Scroll areas
* [x] Vertical scrolling
* [x] Scroll-wheel input

47
egui/src/cache.rs Normal file
View file

@ -0,0 +1,47 @@
use std::hash::{Hash, Hasher};
const SIZE: usize = 8 * 1024;
/// Very stupid/simple key-value cache. TODO: improve
#[derive(Clone)]
pub struct Cache<K, V>([Option<(K, V)>; SIZE]);
impl<K, V> Default for Cache<K, V>
where
K: Copy,
V: Copy,
{
fn default() -> Self {
Self([None; SIZE])
}
}
impl<K, V> std::fmt::Debug for Cache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Cache")
}
}
impl<K, V> Cache<K, V>
where
K: Hash + PartialEq,
{
pub fn get(&self, key: &K) -> Option<&V> {
let bucket = (hash(key) % (SIZE as u64)) as usize;
match &self.0[bucket] {
Some((k, v)) if k == key => Some(v),
_ => None,
}
}
pub fn set(&mut self, key: K, value: V) {
let bucket = (hash(&key) % (SIZE as u64)) as usize;
self.0[bucket] = Some((key, value));
}
}
fn hash(value: impl Hash) -> u64 {
let mut hasher = ahash::AHasher::default();
value.hash(&mut hasher);
hasher.finish()
}

View file

@ -12,8 +12,15 @@ pub fn show_tooltip(ctx: &Arc<Context>, add_contents: impl FnOnce(&mut Ui)) {
}
}
/// Show a tooltip at the current mouse position (if any).
pub fn show_tooltip_text(ctx: &Arc<Context>, text: impl Into<String>) {
show_tooltip(ctx, |ui| {
ui.add(crate::widgets::Label::new(text));
})
}
/// Show a pop-over window.
pub fn show_popup(
fn show_popup(
ctx: &Arc<Context>,
id: Id,
window_pos: Pos2,
@ -21,7 +28,7 @@ pub fn show_popup(
) -> Response {
use containers::*;
Area::new(id)
.order(Order::Foreground)
.order(Order::Tooltip)
.fixed_pos(window_pos)
.interactable(false)
.show(ctx, |ui| Frame::popup(&ctx.style()).show(ui, add_contents))

View file

@ -452,6 +452,7 @@ struct Widgets {
radio: usize,
slider_value: f32,
angle: f32,
color: Srgba,
single_line_text_input: String,
multiline_text_input: String,
}
@ -464,6 +465,7 @@ impl Default for Widgets {
count: 0,
slider_value: 3.4,
angle: TAU / 8.0,
color: (Rgba::new(0.0, 1.0, 0.5, 1.0) * 0.75).into(),
single_line_text_input: "Hello World!".to_owned(),
multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
}
@ -527,6 +529,13 @@ impl Widgets {
}
ui.separator();
ui.horizontal_centered(|ui| {
ui.add(Label::new("Click to select a different text color: ").text_color(self.color));
ui.color_edit_button(&mut self.color);
});
ui.separator();
ui.horizontal(|ui| {
ui.add(label!("Single line text input:"));
ui.add(

View file

@ -12,6 +12,8 @@ pub enum Order {
Middle,
/// Popups, menus etc that should always be painted on top of windows
Foreground,
/// Foreground objects can also have tooltips
Tooltip,
/// Debug layer, always painted last / on top
Debug,
}

View file

@ -46,6 +46,7 @@
mod animation_manager;
pub mod app;
pub(crate) mod cache;
pub mod containers;
mod context;
pub mod demos;

View file

@ -1,8 +1,13 @@
use std::collections::{HashMap, HashSet};
use crate::{
area, collapsing_header, menu, resize, scroll_area, widgets::text_edit, window, Id, Layer,
Pos2, Rect,
area,
cache::Cache,
collapsing_header, menu,
paint::color::{Hsva, Srgba},
resize, scroll_area,
widgets::text_edit,
window, Id, Layer, Pos2, Rect,
};
/// The data that Egui persists between frames.
@ -34,6 +39,14 @@ pub struct Memory {
pub(crate) temp_edit_string: Option<String>,
pub(crate) areas: Areas,
/// Used by color picker
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) color_cache: Cache<Srgba, Hsva>,
/// Which popup-window is open (if any)?
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) popup: Option<Id>,
}
/// Say there is a button in a scroll area.

View file

@ -1,9 +1,13 @@
use crate::math::clamp;
/// This format is used for space-efficient color representation.
///
/// Instead of manipulating this directly it is often better
/// to first convert it to either `Rgba` or `Hsva`.
///
/// 0-255 gamma space `sRGBA` color with premultiplied alpha.
/// Alpha channel is in linear space.
/// This format is used for space-efficient color representation.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Srgba(pub [u8; 4]);
@ -21,10 +25,14 @@ impl std::ops::IndexMut<usize> for Srgba {
}
pub const fn srgba(r: u8, g: u8, b: u8, a: u8) -> Srgba {
Srgba([r, g, b, a])
Srgba::new(r, g, b, a)
}
impl Srgba {
pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self([r, g, b, a])
}
pub const fn gray(l: u8) -> Self {
Self([l, l, l, 255])
}
@ -36,6 +44,11 @@ impl Srgba {
pub const fn additive_luminance(l: u8) -> Self {
Self([l, l, l, 0])
}
/// Returns an opaque version of self
pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into()
}
}
// ----------------------------------------------------------------------------
@ -93,6 +106,12 @@ impl Rgba {
Self([l * a, l * a, l * a, a])
}
/// Transparent black
pub fn black_alpha(a: f32) -> Self {
debug_assert!(0.0 <= a && a <= 1.0);
Self([0.0, 0.0, 0.0, a])
}
/// Transparent white
pub fn white_alpha(a: f32) -> Self {
debug_assert!(0.0 <= a && a <= 1.0);
@ -108,6 +127,40 @@ impl Rgba {
alpha * self[3],
])
}
pub fn r(&self) -> f32 {
self.0[0]
}
pub fn g(&self) -> f32 {
self.0[1]
}
pub fn b(&self) -> f32 {
self.0[2]
}
pub fn a(&self) -> f32 {
self.0[3]
}
/// How perceptually intense (bright) is the color?
pub fn intensity(&self) -> f32 {
0.3 * self.r() + 0.59 * self.g() + 0.11 * self.b()
}
/// Returns an opaque version of self
pub fn to_opaque(&self) -> Self {
if self.a() == 0.0 {
// additive or fully transparent
Self::new(self.r(), self.g(), self.b(), 1.0)
} else {
// un-multiply alpha
Self::new(
self.r() / self.a(),
self.g() / self.a(),
self.b() / self.a(),
1.0,
)
}
}
}
impl std::ops::Add for Rgba {

View file

@ -117,6 +117,22 @@ impl Triangles {
self.vertices.push(bottom_right);
}
/// Uniformly colored rectangle.
pub fn add_colored_rect(&mut self, rect: Rect, color: Srgba) {
self.add_rect(
Vertex {
pos: rect.min,
uv: WHITE_UV,
color,
},
Vertex {
pos: rect.max,
uv: WHITE_UV,
color,
},
)
}
/// This is for platforms that only support 16-bit index buffers.
///
/// Splits this mesh into many smaller meshes (if needed).

View file

@ -435,7 +435,7 @@ impl Stroke {
ui.label(format!("{}: ", text));
ui.add(DragValue::f32(width).speed(0.1).range(0.0..=5.0))
.tooltip_text("Width");
ui_color(ui, color, "Color");
ui.color_edit_button(color);
});
}
}
@ -449,13 +449,9 @@ fn ui_slider_vec2(ui: &mut Ui, value: &mut Vec2, range: std::ops::RangeInclusive
});
}
// TODO: improve color picker
fn ui_color(ui: &mut Ui, srgba: &mut Srgba, text: &str) {
ui.horizontal_centered(|ui| {
ui.label(format!("{} sRGBA: ", text));
ui.add(DragValue::u8(&mut srgba[0])).tooltip_text("r");
ui.add(DragValue::u8(&mut srgba[1])).tooltip_text("g");
ui.add(DragValue::u8(&mut srgba[2])).tooltip_text("b");
ui.add(DragValue::u8(&mut srgba[3])).tooltip_text("a");
ui.label(format!("{}: ", text));
ui.color_edit_button(srgba);
});
}

View file

@ -80,9 +80,22 @@ pub struct Response {
pub has_kb_focus: bool,
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Response")
.field("rect", &self.rect)
.field("sense", &self.sense)
.field("hovered", &self.hovered)
.field("clicked", &self.clicked)
.field("double_clicked", &self.double_clicked)
.field("active", &self.active)
.field("has_kb_focus", &self.has_kb_focus)
.finish()
}
}
impl Response {
/// Show some stuff if the item was hovered
pub fn tooltip(&mut self, add_contents: impl FnOnce(&mut Ui)) -> &mut Self {
pub fn tooltip(self, add_contents: impl FnOnce(&mut Ui)) -> Self {
if self.hovered {
crate::containers::show_tooltip(&self.ctx, add_contents);
}
@ -90,9 +103,9 @@ impl Response {
}
/// Show this text if the item was hovered
pub fn tooltip_text(&mut self, text: impl Into<String>) -> &mut Self {
self.tooltip(|popup| {
popup.add(crate::widgets::Label::new(text));
pub fn tooltip_text(self, text: impl Into<String>) -> Self {
self.tooltip(|ui| {
ui.add(crate::widgets::Label::new(text));
})
}
}

View file

@ -481,6 +481,12 @@ impl Ui {
response
}
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button(&mut self, srgba: &mut Srgba) {
widgets::color_picker::color_edit_button(self, srgba)
}
/// Ask to allocate a certain amount of space and return a Painter for that region.
///
/// You may get back a `Painter` with a smaller or larger size than what you desired,
@ -603,6 +609,11 @@ impl Ui {
self.inner_layout(Layout::vertical(Align::Min), initial_size, add_contents)
}
pub fn vertical_centered<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) {
let initial_size = vec2(0.0, self.available().height());
self.inner_layout(Layout::vertical(Align::Center), initial_size, add_contents)
}
pub fn inner_layout<R>(
&mut self,
layout: Layout,

View file

@ -8,6 +8,7 @@
use crate::{layout::Direction, *};
pub mod color_picker;
mod slider;
pub(crate) mod text_edit;

View file

@ -0,0 +1,256 @@
use crate::{
paint::{color::*, *},
*,
};
fn contrast_color(color: impl Into<Rgba>) -> Srgba {
if color.into().intensity() < 0.5 {
color::WHITE
} else {
color::BLACK
}
}
/// Number of vertices per dimension in the color sliders.
/// We need at least 6 for hues, and more for smooth 2D areas.
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
const N: u32 = 6 * 3;
fn background_checkers(painter: &Painter, rect: Rect) {
let mut top_color = Srgba::gray(128);
let mut bottom_color = Srgba::gray(32);
let checker_size = Vec2::splat(rect.height() / 2.0);
let n = (rect.width() / checker_size.x).round() as u32;
let mut triangles = Triangles::default();
for i in 0..n {
let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
triangles.add_colored_rect(
Rect::from_min_size(pos2(x, rect.top()), checker_size),
top_color,
);
triangles.add_colored_rect(
Rect::from_min_size(pos2(x, rect.center().y), checker_size),
bottom_color,
);
std::mem::swap(&mut top_color, &mut bottom_color);
}
painter.add(PaintCmd::Triangles(triangles));
}
fn show_color(ui: &mut Ui, color: Srgba, desired_size: Vec2) -> Rect {
let rect = ui.allocate_space(desired_size);
background_checkers(ui.painter(), rect);
ui.painter().add(PaintCmd::Rect {
rect,
corner_radius: 2.0,
fill: color,
stroke: Stroke::new(3.0, color.to_opaque()),
});
rect
}
fn color_button(ui: &mut Ui, color: Srgba) -> Response {
let desired_size = Vec2::splat(ui.style().spacing.clickable_diameter);
let rect = ui.allocate_space(desired_size);
let rect = rect.expand2(ui.style().spacing.button_expand);
let id = ui.make_position_id();
let response = ui.interact(rect, id, Sense::click());
let visuals = ui.style().interact(&response);
background_checkers(ui.painter(), rect);
ui.painter().add(PaintCmd::Rect {
rect,
corner_radius: visuals.corner_radius.min(2.0),
fill: color,
stroke: visuals.fg_stroke,
});
response
}
fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Srgba) -> Response {
#![allow(clippy::identity_op)]
let desired_size = vec2(
ui.style().spacing.slider_width,
ui.style().spacing.clickable_diameter * 2.0,
);
let rect = ui.allocate_space(desired_size);
let id = ui.make_position_id();
let response = ui.interact(rect, id, Sense::click_and_drag());
if response.active {
if let Some(mpos) = ui.input().mouse.pos {
*value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
}
}
let visuals = ui.style().interact(&response);
background_checkers(ui.painter(), rect); // for alpha:
{
// fill color:
let mut triangles = Triangles::default();
for i in 0..=N {
let t = i as f32 / (N as f32);
let color = color_at(t);
let x = lerp(rect.left()..=rect.right(), t);
triangles.colored_vertex(pos2(x, rect.top()), color);
triangles.colored_vertex(pos2(x, rect.bottom()), color);
if i < N {
triangles.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
triangles.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
}
}
ui.painter().add(PaintCmd::Triangles(triangles));
}
ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline
{
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *value);
let r = rect.height() / 4.0;
let picked_color = color_at(*value);
ui.painter().add(PaintCmd::Path {
points: vec![
pos2(x - r, rect.bottom()),
pos2(x + r, rect.bottom()),
pos2(x, rect.center().y),
],
closed: true,
fill: picked_color,
stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
});
}
response
}
fn color_slider_2d(
ui: &mut Ui,
x_value: &mut f32,
y_value: &mut f32,
color_at: impl Fn(f32, f32) -> Srgba,
) -> Response {
let desired_size = Vec2::splat(ui.style().spacing.slider_width);
let rect = ui.allocate_space(desired_size);
let id = ui.make_position_id();
let response = ui.interact(rect, id, Sense::click_and_drag());
if response.active {
if let Some(mpos) = ui.input().mouse.pos {
*x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0);
*y_value = remap_clamp(mpos.y, rect.bottom()..=rect.top(), 0.0..=1.0);
}
}
let visuals = ui.style().interact(&response);
let mut triangles = Triangles::default();
for xi in 0..=N {
for yi in 0..=N {
let xt = xi as f32 / (N as f32);
let yt = yi as f32 / (N as f32);
let color = color_at(xt, yt);
let x = lerp(rect.left()..=rect.right(), xt);
let y = lerp(rect.bottom()..=rect.top(), yt);
triangles.colored_vertex(pos2(x, y), color);
if xi < N && yi < N {
let x_offset = 1;
let y_offset = N + 1;
let tl = yi * y_offset + xi;
triangles.add_triangle(tl, tl + x_offset, tl + y_offset);
triangles.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
}
}
}
ui.painter().add(PaintCmd::Triangles(triangles)); // fill
ui.painter().rect_stroke(rect, 0.0, visuals.bg_stroke); // outline
// Show where the slider is at:
let x = lerp(rect.left()..=rect.right(), *x_value);
let y = lerp(rect.bottom()..=rect.top(), *y_value);
let picked_color = color_at(*x_value, *y_value);
ui.painter().add(PaintCmd::Circle {
center: pos2(x, y),
radius: rect.width() / 12.0,
fill: picked_color,
stroke: Stroke::new(visuals.fg_stroke.width, contrast_color(picked_color)),
});
response
}
fn color_picker_hsva_2d(ui: &mut Ui, hsva: &mut Hsva) {
ui.vertical_centered(|ui| {
let current_color_size = vec2(
ui.style().spacing.slider_width,
ui.style().spacing.clickable_diameter * 2.0,
);
let current_color_rect = show_color(ui, (*hsva).into(), current_color_size);
if ui.hovered(current_color_rect) {
show_tooltip_text(ui.ctx(), "Current color");
}
let opaque = Hsva { a: 1.0, ..*hsva };
let Hsva { h, s, v, a } = hsva;
color_slider_2d(ui, h, s, |h, s| Hsva::new(h, s, 1.0, 1.0).into())
.tooltip_text("Hue - Saturation");
color_slider_2d(ui, v, s, |v, s| Hsva { v, s, ..opaque }.into())
.tooltip_text("Value - Saturation");
ui.label("Alpha:");
color_slider_1d(ui, a, |a| Hsva { a, ..opaque }.into()).tooltip_text("Alpha");
});
}
fn color_picker_hsva(ui: &mut Ui, hsva: &mut Hsva) {
let id = ui.make_position_id().with("foo");
let button_response = color_button(ui, (*hsva).into()).tooltip_text("Click to edit color");
if button_response.clicked {
ui.memory().popup = Some(id);
}
// TODO: make it easier to show a temporary popup that closes when you click outside it
if ui.memory().popup == Some(id) {
let area_response = Area::new(id)
.order(Order::Foreground)
.default_pos(button_response.rect.max)
.show(ui.ctx(), |ui| {
Frame::popup(ui.style()).show(ui, |ui| {
color_picker_hsva_2d(ui, hsva);
})
});
if !button_response.clicked {
let clicked_outside = ui.input().mouse.click && !area_response.hovered;
if clicked_outside || ui.input().key_pressed(Key::Escape) {
ui.memory().popup = None;
}
}
}
}
// TODO: return Response so user can show a tooltip
/// Shows a button with the given color.
/// If the user clicks the button, a full color picker is shown.
pub fn color_edit_button(ui: &mut Ui, srgba: &mut Srgba) {
// To ensure we keep hue slider when `srgba` is grey we store the
// full `Hsva` in a cache:
let mut hsva = ui
.ctx()
.memory()
.color_cache
.get(srgba)
.cloned()
.unwrap_or_else(|| Hsva::from(*srgba));
color_picker_hsva(ui, &mut hsva);
*srgba = Srgba::from(hsva);
ui.ctx().memory().color_cache.set(*srgba, hsva);
}

View file

@ -235,13 +235,13 @@ impl<'a> Slider<'a> {
ui.memory().temp_edit_string = Some(value_text);
}
} else {
let mut response = ui.add(
let response = ui.add(
Label::new(value_text)
.multiline(false)
.text_color(text_color)
.text_style(TextStyle::Monospace),
);
response.tooltip_text("Click to enter a value");
let response = response.tooltip_text("Click to enter a value");
let response = ui.interact(response.rect, kb_edit_id, Sense::click());
if response.clicked {
ui.memory().request_kb_focus(kb_edit_id);