1 /++
2 	Displays a color-picker dialog box. On Windows, uses the standard system dialog you know from Paint. On X, uses a custom one with hsla and rgba support.
3 
4 	History:
5 		Written April 2017.
6 
7 		Added to dub on December 9, 2021.
8 +/
9 module arsd.minigui_addons.color_dialog;
10 
11 import arsd.minigui;
12 
13 static if(UsingWin32Widgets)
14 	pragma(lib, "comdlg32");
15 
16 /++
17 
18 +/
19 auto showColorDialog(Window owner, Color current, void delegate(Color choice) onOK, void delegate() onCancel = null) {
20 	static if(UsingWin32Widgets) {
21 		import core.sys.windows.windows;
22 		static COLORREF[16] customColors;
23 		CHOOSECOLOR cc;
24 		cc.lStructSize = cc.sizeof;
25 		cc.hwndOwner = owner ? owner.win.impl.hwnd : null;
26 		cc.lpCustColors = cast(LPDWORD) customColors.ptr;
27 		cc.rgbResult = RGB(current.r, current.g, current.b);
28 		cc.Flags = CC_FULLOPEN | CC_RGBINIT;
29 		if(ChooseColor(&cc)) {
30 			onOK(Color(GetRValue(cc.rgbResult), GetGValue(cc.rgbResult), GetBValue(cc.rgbResult)));
31 		} else {
32 			if(onCancel)
33 				onCancel();
34 		}
35 	} else static if(UsingCustomWidgets) {
36 		auto cpd = new ColorPickerDialog(current, onOK, owner);
37 		cpd.show();
38 		return cpd;
39 	} else static assert(0);
40 }
41 
42 /*
43 	Hue / Saturation picker
44 	Lightness Picker
45 
46 	Text selections
47 
48 	Graphical representation
49 
50 	Cancel OK
51 */
52 
53 static if(UsingCustomWidgets)
54 class ColorPickerDialog : Dialog {
55 	static arsd.simpledisplay.Sprite hslImage;
56 
57 	static bool canUseImage;
58 
59 	void delegate(Color) onOK;
60 
61 	this(Color current, void delegate(Color) onOK, Window owner) {
62 		super(owner, 360, 460, "Color picker");
63 
64 		this.onOK = onOK;
65 
66 
67 	/*
68 	statusBar.parts ~= new StatusBar.Part(140);
69 	statusBar.parts ~= new StatusBar.Part(140);
70 	statusBar.parts ~= new StatusBar.Part(140);
71 	statusBar.parts ~= new StatusBar.Part(140);
72         this.addEventListener("mouseover", (Event ev) {
73 		import std.conv;
74                 this.statusBar.parts[2].content = to!string(ev.target.minHeight) ~ " - " ~ to!string(ev.target.maxHeight);
75                 this.statusBar.parts[3].content = ev.target.toString();
76         });
77 	*/
78 
79 
80 		static if(UsingSimpledisplayX11)
81 			// it is brutally slow over the network if we don't
82 			// have xshm, so we've gotta do something else.
83 			canUseImage = Image.impl.xshmAvailable;
84 		else
85 			canUseImage = true;
86 
87 		if(hslImage is null && canUseImage) {
88 			auto img = new TrueColorImage(360, 255);
89 			double h = 0.0, s = 1.0, l = 0.5;
90 			foreach(y; 0 .. img.height) {
91 				foreach(x; 0 .. img.width) {
92 					img.imageData.colors[y * img.width + x] = Color.fromHsl(h,s,l);
93 					h += 360.0 / img.width;
94 				}
95 				h = 0.0;
96 				s -= 1.0 / img.height;
97 			}
98 
99 			hslImage = new arsd.simpledisplay.Sprite(this.win, Image.fromMemoryImage(img));
100 		}
101 
102 		auto t = this;
103 
104 		auto wid = new class Widget {
105 			this() { super(t); }
106 			override int minHeight() { return hslImage ? hslImage.height : 4; }
107 			override int maxHeight() { return hslImage ? hslImage.height : 4; }
108 			override int marginBottom() { return 4; }
109 			override void paint(WidgetPainter painter) {
110 				if(hslImage)
111 					hslImage.drawAt(painter, Point(0, 0));
112 			}
113 		};
114 
115 		auto hs = new HorizontalSlider(0, 1000, 50, t);
116 
117 		auto hr = new HorizontalLayout(t);
118 
119 		auto vlRgb = new VerticalLayout(180, hr);
120 		auto vlHsl = new VerticalLayout(180, hr);
121 
122 		h = new LabeledLineEdit("Hue:", TextAlignment.Right, vlHsl);
123 		s = new LabeledLineEdit("Saturation:", TextAlignment.Right, vlHsl);
124 		l = new LabeledLineEdit("Lightness:", TextAlignment.Right, vlHsl);
125 
126 		css = new LabeledLineEdit("CSS:", TextAlignment.Right, vlHsl);
127 
128 		r = new LabeledLineEdit("Red:", TextAlignment.Right, vlRgb);
129 		g = new LabeledLineEdit("Green:", TextAlignment.Right, vlRgb);
130 		b = new LabeledLineEdit("Blue:", TextAlignment.Right, vlRgb);
131 		a = new LabeledLineEdit("Alpha:", TextAlignment.Right, vlRgb);
132 
133 		import std.conv;
134 		import std.format;
135 
136 		double[3] lastHsl;
137 
138 		void updateCurrent() {
139 			r.content = to!string(current.r);
140 			g.content = to!string(current.g);
141 			b.content = to!string(current.b);
142 			a.content = to!string(current.a);
143 
144 			auto hsl = current.toHsl;
145 			if(hsl[2] == 0.0 || hsl[2] == 1.0) {
146 				hsl[0 .. 2] = lastHsl[0 .. 2];
147 			}
148 
149 			h.content = format("%0.3f", hsl[0]);
150 			s.content = format("%0.3f", hsl[1]);
151 			l.content = format("%0.3f", hsl[2]);
152 
153 			hs.setPosition(cast(int) (hsl[2] * 1000));
154 
155 			css.content = current.toCssString();
156 			lastHsl = hsl;
157 		}
158 
159 		updateCurrent();
160 
161 		r.addEventListener("focus", &r.selectAll);
162 		g.addEventListener("focus", &g.selectAll);
163 		b.addEventListener("focus", &b.selectAll);
164 		a.addEventListener("focus", &a.selectAll);
165 
166 		h.addEventListener("focus", &h.selectAll);
167 		s.addEventListener("focus", &s.selectAll);
168 		l.addEventListener("focus", &l.selectAll);
169 
170 		css.addEventListener("focus", &css.selectAll);
171 
172 		void convertFromHsl() {
173 			try {
174 				auto c = Color.fromHsl(h.content.to!double, s.content.to!double, l.content.to!double);
175 				c.a = a.content.to!ubyte;
176 				current = c;
177 				updateCurrent();
178 			} catch(Exception e) {
179 			}
180 		}
181 
182 		hs.addEventListener((ChangeEvent!int ce) {
183 			// this should only change l, not hs
184 			auto ch = h.content;
185 			auto cs = s.content;
186 			l.content = to!string(ce.value / 1000.0);
187 			convertFromHsl();
188 
189 			h.content = ch;
190 			s.content = cs;
191 		});
192 
193 
194 		h.addEventListener("change", &convertFromHsl);
195 		s.addEventListener("change", &convertFromHsl);
196 		l.addEventListener("change", &convertFromHsl);
197 
198 		css.addEventListener("change", () {
199 			current = Color.fromString(css.content);
200 			updateCurrent();
201 		});
202 
203 		void helper(MouseEventBase event) {
204 			try {
205 				// this should ONLY actually change hue and saturation
206 
207 				auto h = cast(double) event.clientX / hslImage.width * 360.0;
208 				auto s = 1.0 - (cast(double) event.clientY / hslImage.height * 1.0);
209 				auto oldl = this.l.content;
210 				auto oldhsp = hs.position;
211 				auto l = this.l.content.to!double;
212 
213 				current = Color.fromHsl(h, s, l);
214 				// import std.stdio; writeln(current.toHsl, " ", h, " ", s, " ", l);
215 				current.a = a.content.to!ubyte;
216 
217 				updateCurrent();
218 
219 				this.l.content = oldl;
220 				hs.setPosition(oldhsp);
221 
222 				auto e2 = new Event("change", this);
223 				e2.dispatch();
224 			} catch(Exception e) {
225 			}
226 		}
227 
228 		if(hslImage !is null)
229 			wid.addEventListener((MouseDownEvent ev) { helper(ev); });
230 
231 		if(hslImage !is null)
232 			wid.addEventListener((MouseMoveEvent event) {
233 				if(event.state & ModifierState.leftButtonDown)
234 					helper(event);
235 			});
236 
237 		this.addEventListener((KeyDownEvent event) {
238 			if(event.key == Key.Enter || event.key == Key.PadEnter)
239 				OK();
240 			if(event.key == Key.Escape)
241 				Cancel();
242 		});
243 
244 		this.addEventListener("change", {
245 			redraw();
246 		});
247 
248 		auto s = this;
249 		auto currentColorWidget = new class Widget {
250 			this() {
251 				super(s);
252 			}
253 
254 			override void paint(WidgetPainter painter) {
255 				auto c = currentColor();
256 
257 				auto c1 = alphaBlend(c, Color(64, 64, 64));
258 				auto c2 = alphaBlend(c, Color(192, 192, 192));
259 
260 				painter.outlineColor = c1;
261 				painter.fillColor = c1;
262 				painter.drawRectangle(Point(0, 0), this.width / 2, this.height / 2);
263 				painter.drawRectangle(Point(this.width / 2, this.height / 2), this.width / 2, this.height / 2);
264 
265 				painter.outlineColor = c2;
266 				painter.fillColor = c2;
267 				painter.drawRectangle(Point(this.width / 2, 0), this.width / 2, this.height / 2);
268 				painter.drawRectangle(Point(0, this.height / 2), this.width / 2, this.height / 2);
269 			}
270 		};
271 
272 		auto hl = new HorizontalLayout(this);
273 		auto cancelButton = new Button("Cancel", hl);
274 		auto okButton = new Button("OK", hl);
275 
276 		recomputeChildLayout(); // FIXME hack
277 
278 		cancelButton.addEventListener(EventType.triggered, &Cancel);
279 		okButton.addEventListener(EventType.triggered, &OK);
280 
281 		r.focus();
282 	}
283 
284 	LabeledLineEdit r;
285 	LabeledLineEdit g;
286 	LabeledLineEdit b;
287 	LabeledLineEdit a;
288 
289 	LabeledLineEdit h;
290 	LabeledLineEdit s;
291 	LabeledLineEdit l;
292 
293 	LabeledLineEdit css;
294 
295 	Color currentColor() {
296 		import std.conv;
297 		try {
298 			return Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content));
299 		} catch(Exception e) {
300 			return Color.transparent;
301 		}
302 	}
303 
304 
305 	override void OK() {
306 		import std.conv;
307 		try {
308 			onOK(Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content)));
309 			this.close();
310 		} catch(Exception e) {
311 			auto mb = new MessageBox("Bad value");
312 			mb.show();
313 		}
314 	}
315 }
316 
317