blob: cb45cf2406e8eee2e4d7bbb5cd8e7c4a259a4846 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2013, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ecommons.ui.components;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.statet.ecommons.collections.FastList;
import org.eclipse.statet.ecommons.graphics.core.ColorDef;
import org.eclipse.statet.ecommons.graphics.core.HSVColorDef;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
public class HSVSelector extends Canvas implements IObjValueWidget<ColorDef> {
/*
* s = saturation, v = value
* triangle: Psv = (x11, y11), (x01, y01), (x10, y10)
* equation of lines: a * x + b * y + c = 0
*/
private static float distance(final float x, final float y) {
return (float) Math.sqrt(x * x + y * y);
}
private static float save01(final float value) {
if (value <= 0) {
return 0;
}
if (value >= 1) {
return 1;
}
return value;
}
private static final Color G_BACKGROUND = SharedUIResources.getColors().getColor(SharedUIResources.GRAPHICS_BACKGROUND_COLOR_ID);
private static final HSVColorDef DEFAULT_VALUE = new HSVColorDef(0f, 1f, 1f);
private static Color createColor(final Device device, float hue, final float saturation, final float value) {
float r, g, b;
if (saturation == 0) {
r = g = b = value;
}
else {
if (hue == 1) {
hue = 0;
}
hue *= 6;
final int i = (int) hue;
final float f = hue - i;
final float p = value * (1 - saturation);
final float q = value * (1 - saturation * f);
final float t = value * (1 - saturation * (1 - f));
switch(i) {
case 0:
r = value;
g = t;
b = p;
break;
case 1:
r = q;
g = value;
b = p;
break;
case 2:
r = p;
g = value;
b = t;
break;
case 3:
r = p;
g = q;
b = value;
break;
case 4:
r = t;
g = p;
b = value;
break;
case 5:
default:
r = value;
g = p;
b = q;
break;
}
}
return new Color(device, (int) (r * 255 + 0.5), (int) (g * 255 + 0.5), (int) (b * 255 + 0.5));
}
private static void plotPoint(final GC gc, final int x, final int y, final Color color) {
if (color != null) {
gc.setForeground(color);
}
gc.drawLine(x - 1, y, x + 1, y);
gc.drawLine(x, y - 1, x, y + 1);
}
private static void plotLine(final GC gc, final int xMin, final int xMax, final float a, final float b, final float c, final Color color) {
if (color != null) {
gc.setForeground(color);
}
final float m = - a / b;
final float t = - c / b;
for (int x = xMin; x <= xMax; x++) {
final float y = m * x + t;
gc.drawPoint(x, Math.round(y));
}
}
private final static class Data {
private final static float TRIANGLE_RATIO = 0.78f;
private final static float TRIANGLE_ALPHA = -1f;
private final int size;
private final int center;
private final float outer;
private final float inner;
private final float h;
private final float xh1;
private final float yh1;
private final float x11;
private final float y11;
private final float x01;
private final float y01;
private final float x10;
private final float y10;
public Data(final int size, final float hue) {
this.size = size;
center = size / 2;
outer = center;
inner = (int) (outer * TRIANGLE_RATIO);
h = hue;
float[] xy1;
xy1 = hue_xy1(h);
xh1 = xy1[0];
yh1 = xy1[1];
x11 = center + inner * xh1;
y11 = center + inner * yh1;
xy1 = hue_xy1(h + 1.0/3.0);
x01 = center + inner * xy1[0];
y01 = center + inner * xy1[1];
xy1 = hue_xy1(h - 1.0/3.0);
x10 = center + inner * xy1[0];
y10 = center + inner * xy1[1];
}
public final int x0(final int x) {
return (x - center);
}
public final int y0(final int y) {
return (y - center);
}
public final float xy0_d(final int x0, final int y0) {
return (float) Math.sqrt((x0 * x0 + y0 * y0));
}
public final float xy0_hue(final int x0, final int y0) {
float hue = (float) (Math.atan2(y0, x0) / (2 * Math.PI));
hue += 0.25f;
if (hue < 0) {
hue += 1f;
}
return hue;
}
public final float[] hue_xy1(final double hue) {
final double a = (hue - 0.25) * (2 * Math.PI);
return new float[] { (float) Math.cos(a), (float) Math.sin(a) };
}
public HSVColorDef svColor(final int x, final int y) {
final float a_01_11 = (y11 - y01);
final float b_01_11 = -(x11 - x01);
final float c_01_11 = -(a_01_11 * x11 + b_01_11 * y11);
final float av = (y - y10);
final float bv = -(x - x10);
final float cv = -(av * x + bv * y);
final float s;
final float v;
if (a_01_11 < -4f || a_01_11 > 4f) {
final float yq = (av / a_01_11 * c_01_11 - cv) / (bv - (av / a_01_11 * b_01_11));
s = (yq - y01) / a_01_11;
v = (y - y10) / (yq - y10);
}
else {
final float yq = (a_01_11 / av * cv - c_01_11) / (b_01_11 - (a_01_11 / av * bv));
final float xq = - (bv * yq + cv) / av;
s = (xq - x01) / (x11 - x01);
v = (x - x10) / (xq - x10);
}
return new HSVColorDef(h, save01(s), save01(v));
}
public int[] sv_xy(final float s, final float v) {
final float xq = x01 + s * (x11 - x01);
final float yq = y01 + s * (y11 - y01);
final float x = x10 + v * (xq - x10);
final float y = y10 + v * (yq - y10);
return new int[] { Math.round(x), Math.round(y) };
}
private Image createBaseImage(final Display display) {
final Image image = new Image(display, size, size);
final GC gc = new GC(image);
gc.setBackground(G_BACKGROUND);
gc.fillRectangle(0, 0, size, size);
gc.setAdvanced(true);
gc.setAntialias(SWT.OFF);
int currentAlpha = 255;
final float in = inner + 1;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
final int x0 = x0(x);
final int y0 = y0(y);
final float d = xy0_d(x0, y0);
{ float a = outer - d;
if (a < 0) {
continue;
}
if (a > 1) {
a = d - in;
if (a < 0) {
continue;
}
if (a > 1) {
a = 1;
}
}
final int alpha = (int) (a * 255);
if (alpha != currentAlpha) {
gc.setAlpha(currentAlpha = alpha);
}
}
final Color color = createColor(display, xy0_hue(x0, y0), 1f, 1f);
gc.setForeground(color);
gc.drawPoint(x, y);
color.dispose();
}
}
gc.dispose();
return image;
}
private Image createFullImage(final Display display) {
final Image image = new Image(display, getBaseImage(this, display), SWT.IMAGE_COPY);
if (h < 0) {
return image;
}
final GC gc = new GC(image);
gc.setAdvanced(true);
gc.setAntialias(SWT.OFF);
int currentAlpha = 255;
final int xMin = Math.min((int) Math.ceil(x11), Math.min((int) Math.ceil(x01), (int) Math.ceil(x10))) - 1;
final int xMax = Math.max((int) Math.floor(x11), Math.max((int) Math.floor(x01), (int) Math.floor(x10))) + 1;
final int yMin = Math.min((int) Math.ceil(y11), Math.min((int) Math.ceil(y01), (int) Math.ceil(y10))) - 1;
final int yMax = Math.max((int) Math.floor(y11), Math.max((int) Math.floor(y01), (int) Math.floor(y10))) + 1;
final float a_01_11, b_01_11, c_01_11;
{ final float a = (y11 - y01);
final float b = -(x11 - x01);
final float d = distance(a, b);
a_01_11 = a / d;
b_01_11 = b / d;
c_01_11 = -(a * x11 + b * y11) / d;
}
final float a_10_01, b_10_01, c_10_01;
{ final float a = (y01 - y10);
final float b = -(x01 - x10);
final float d = distance(a, b);
a_10_01 = a / d;
b_10_01 = b / d;
c_10_01 = -(a * x01 + b * y01) / d;
}
final float a_11_10, b_11_10, c_11_10;
{ final float a = (y10 - y11);
final float b = -(x10 - x11);
final float d = distance(a, b);
a_11_10 = a / d;
b_11_10 = b / d;
c_11_10 = -(a * x10 + b * y10) / d;
}
for (int y = yMin; y <= yMax; y++) {
final float av = (y - y10);
final float av_as_cs = av / a_01_11 * c_01_11;
final float av_as_bs = av / a_01_11 * b_01_11;
for (int x = xMin; x <= xMax; x++) {
float min = 0f;
{ final float d = (a_01_11 * x + b_01_11 *y + c_01_11);
if (d < 0f) {
if (d < TRIANGLE_ALPHA) {
continue;
}
if (d < min) {
min = d;
}
}
}
{ final float d = (a_10_01 * x + b_10_01 *y + c_10_01);
if (d < 0f) {
if (d < TRIANGLE_ALPHA) {
continue;
}
if (d < min) {
min = d;
}
}
}
{ final float d = (a_11_10 * x + b_11_10 *y + c_11_10);
if (d < 0f) {
if (d < TRIANGLE_ALPHA) {
continue;
}
if (d < min) {
min = d;
}
}
}
final float s;
final float v;
{ final float bv = -(x - x10);
final float cv = -(av * x + bv * y);
if (a_01_11 < -4f || a_01_11 > 4f) {
final float yq = (av_as_cs - cv) / (bv - (av_as_bs));
s = (yq - y01) / a_01_11;
v = (y - y10) / (yq - y10);
}
else {
final float yq = (a_01_11 / av * cv - c_01_11) / (b_01_11 - (a_01_11 / av * bv));
final float xq = - (bv * yq + cv) / av;
s = (xq - x01) / (x11 - x01);
v = (x - x10) / (xq - x10);
}
}
// final float xq, yq;
// { final float bv = -(x - x10);
// final float cv = -(av * x + bv * y);
//
// yq = (av_as_cs - cv) / (bv - av_as_bs);
// xq = - (b_01_11 * yq + c_01_11) / a_01_11;
// }
//
// final float s = (xq - x01) / (x11 - x01);
// final float v = (x - x10) / (xq - x10);
{ final int alpha = (min >= 0f) ? 255 : (int) ((1f+min) * 255);
if (alpha != currentAlpha) {
gc.setAlpha(currentAlpha = alpha);
}
}
final Color color = createColor(display, h, save01(s), save01(v));
gc.setForeground(color);
gc.drawPoint(x, y);
color.dispose();
}
}
gc.dispose();
return image;
}
public void drawTriangle(final GC gc) {
final int[] xy = new int[6];
xy[0] = Math.round(x11);
xy[1] = Math.round(y11);
xy[2] = Math.round(x01);
xy[3] = Math.round(y01);
xy[4] = Math.round(x10);
xy[5] = Math.round(y10);
gc.drawPolygon(xy);
}
}
private static final Image[] BASE_CACHE = new Image[10];
private static Image getBaseImage(final Data data, final Display display) {
for (int i = 0; i < BASE_CACHE.length; i++) {
if (BASE_CACHE[i] == null) {
break;
}
if (BASE_CACHE[i].getImageData().width == data.size) {
return BASE_CACHE[i];
}
}
if (BASE_CACHE[BASE_CACHE.length - 1] != null) {
BASE_CACHE[BASE_CACHE.length - 1].dispose();
}
System.arraycopy(BASE_CACHE, 0, BASE_CACHE, 1, BASE_CACHE.length - 1);
BASE_CACHE[0] = data.createBaseImage(display);
return BASE_CACHE[0];
}
private class SWTListener implements PaintListener, Listener {
private static final int TRACK_HUE = 1;
private static final int TRACK_SV = 2;
private Data fData;
private Image fImage;
private int fMouseState;
@Override
public void handleEvent(final Event event) {
final Data data = fData;
switch (event.type) {
case SWT.MouseDown:
if (data != null) {
final int x0 = data.x0(event.x);
final int y0 = data.y0(event.y);
final float d = data.xy0_d(x0, y0);
if (d > data.inner) {
doSetValue(new HSVColorDef(data.xy0_hue(x0, y0),
fValue.getSaturation(), fValue.getValue()),
event.time, 0 );
fMouseState = TRACK_HUE;
}
else {
doSetValue(data.svColor(event.x, event.y), event.time, 0);
fMouseState = TRACK_SV;
}
}
return;
case SWT.MouseUp:
fMouseState = 0;
return;
case SWT.MouseMove:
if (data != null) {
switch (fMouseState) {
case TRACK_HUE:
doSetValue(new HSVColorDef(data.xy0_hue(data.x0(event.x), data.y0(event.y)),
fValue.getSaturation(), fValue.getValue()),
event.time, 0 );
break;
case TRACK_SV:
doSetValue(data.svColor(event.x, event.y), event.time, 0);
break;
}
}
return;
}
}
@Override
public void paintControl(final PaintEvent e) {
final Rectangle clientArea = getClientArea();
int size = Math.min(clientArea.width, clientArea.height);
if (fSize > 0 && fSize < size) {
size = fSize;
}
if (fData == null || fData.size != size || fData.h != fValue.getHue()) {
if (fImage != null) {
fImage.dispose();
fImage = null;
}
fData = new Data(size, fValue.getHue());
}
final GC gc = e.gc;
gc.setAdvanced(true);
gc.setAntialias(SWT.OFF);
final int currentAlpha = 255;
gc.setAlpha(currentAlpha);
gc.setBackground(G_BACKGROUND);
gc.fillRectangle(clientArea);
if (fImage == null) {
fImage = fData.createFullImage(e.display);
}
gc.drawImage(fImage, 0, 0);
gc.setLineWidth(1);
gc.setAntialias(SWT.ON);
gc.setForeground(e.display.getSystemColor(SWT.COLOR_BLACK));
gc.drawLine(Math.round(fData.center + fData.outer * fData.xh1),
Math.round(fData.center + fData.outer * fData.yh1),
Math.round(fData.x11),
Math.round(fData.y11) );
if (fValue.getValue() < 0.50) {
gc.setForeground(e.display.getSystemColor(SWT.COLOR_WHITE));
}
final int[] xy = fData.sv_xy(fValue.getSaturation(), fValue.getValue());
gc.drawOval(xy[0] - 1, xy[1] - 1, 3, 3);
}
}
private int fSize = 8 + LayoutUtils.defaultHSpacing() * 30;
private HSVColorDef fValue = DEFAULT_VALUE;
private final FastList<IObjValueListener<ColorDef>> fValueListeners= (FastList) new FastList<>(IObjValueListener.class);
public HSVSelector(final Composite parent) {
super(parent, SWT.DOUBLE_BUFFERED);
final SWTListener listener = new SWTListener();
addPaintListener(listener);
addListener(SWT.MouseDown, listener);
addListener(SWT.MouseUp, listener);
addListener(SWT.MouseMove, listener);
}
public void setSize(final int size) {
fSize = size;
}
private boolean doSetValue(final HSVColorDef newValue, final int time, final int flags) {
if (fValue.equals(newValue) && flags == 0 && fValue != DEFAULT_VALUE) {
return false;
}
final IObjValueListener<ColorDef>[] listeners = fValueListeners.toArray();
final ObjValueEvent<ColorDef> event= new ObjValueEvent<>(this, time, 0,
fValue, newValue, flags);
fValue = newValue;
for (int i = 0; i < listeners.length; i++) {
event.newValue = newValue;
listeners[i].valueChanged(event);
}
if (!isDisposed()) {
redraw();
}
return true;
}
@Override
public Point computeSize(final int wHint, final int hHint, final boolean changed) {
int width = fSize;
int height = fSize;
final int border = getBorderWidth();
width += border * 2;
height += border * 2;
return new Point(width, height);
}
@Override
public Control getControl() {
return this;
}
@Override
public Class<ColorDef> getValueType() {
return ColorDef.class;
}
@Override
public void addValueListener(final IObjValueListener<ColorDef> listener) {
fValueListeners.add(listener);
}
@Override
public void removeValueListener(final IObjValueListener<ColorDef> listener) {
fValueListeners.remove(listener);
}
@Override
public HSVColorDef getValue(final int idx) {
if (idx != 0) {
throw new IllegalArgumentException("idx: " + idx); //$NON-NLS-1$
}
return fValue;
}
@Override
public void setValue(final int idx, final ColorDef value) {
if (idx != 0) {
throw new IllegalArgumentException("idx: " + idx); //$NON-NLS-1$
}
if (value.getType() == "hsv") { //$NON-NLS-1$
doSetValue((HSVColorDef) value, 0, 0);
}
else {
doSetValue(new HSVColorDef(value), 0, 0);
}
}
}