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(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 hr = new HorizontalLayout(t); 116 117 auto vlRgb = new class VerticalLayout { 118 this() { 119 super(hr); 120 } 121 override int maxWidth() { return 150; }; 122 }; 123 124 auto vlHsl = new class VerticalLayout { 125 this() { 126 super(hr); 127 } 128 override int maxWidth() { return 150; }; 129 }; 130 131 h = new LabeledLineEdit("Hue:", TextAlignment.Right, vlHsl); 132 s = new LabeledLineEdit("Saturation:", TextAlignment.Right, vlHsl); 133 l = new LabeledLineEdit("Lightness:", TextAlignment.Right, vlHsl); 134 135 css = new LabeledLineEdit("CSS:", TextAlignment.Right, vlHsl); 136 137 r = new LabeledLineEdit("Red:", TextAlignment.Right, vlRgb); 138 g = new LabeledLineEdit("Green:", TextAlignment.Right, vlRgb); 139 b = new LabeledLineEdit("Blue:", TextAlignment.Right, vlRgb); 140 a = new LabeledLineEdit("Alpha:", TextAlignment.Right, vlRgb); 141 142 import std.conv; 143 import std.format; 144 145 void updateCurrent() { 146 r.content = to!string(current.r); 147 g.content = to!string(current.g); 148 b.content = to!string(current.b); 149 a.content = to!string(current.a); 150 151 auto hsl = current.toHsl; 152 153 h.content = format("%0.2f", hsl[0]); 154 s.content = format("%0.2f", hsl[1]); 155 l.content = format("%0.2f", hsl[2]); 156 157 css.content = current.toCssString(); 158 } 159 160 updateCurrent(); 161 162 r.addEventListener("focus", &r.selectAll); 163 g.addEventListener("focus", &g.selectAll); 164 b.addEventListener("focus", &b.selectAll); 165 a.addEventListener("focus", &a.selectAll); 166 167 h.addEventListener("focus", &h.selectAll); 168 s.addEventListener("focus", &s.selectAll); 169 l.addEventListener("focus", &l.selectAll); 170 171 css.addEventListener("focus", &css.selectAll); 172 173 void convertFromHsl() { 174 try { 175 auto c = Color.fromHsl(h.content.to!double, s.content.to!double, l.content.to!double); 176 c.a = a.content.to!ubyte; 177 current = c; 178 updateCurrent(); 179 } catch(Exception e) { 180 } 181 } 182 183 h.addEventListener("change", &convertFromHsl); 184 s.addEventListener("change", &convertFromHsl); 185 l.addEventListener("change", &convertFromHsl); 186 187 css.addEventListener("change", () { 188 current = Color.fromString(css.content); 189 updateCurrent(); 190 }); 191 192 void helper(MouseEventBase event) { 193 auto h = cast(double) event.clientX / hslImage.width * 360.0; 194 auto s = 1.0 - (cast(double) event.clientY / hslImage.height * 1.0); 195 auto l = this.l.content.to!double; 196 197 current = Color.fromHsl(h, s, l); 198 current.a = a.content.to!ubyte; 199 200 updateCurrent(); 201 202 auto e2 = new Event("change", this); 203 e2.dispatch(); 204 } 205 206 if(hslImage !is null) 207 wid.addEventListener((MouseDownEvent ev) { helper(ev); }); 208 if(hslImage !is null) 209 wid.addEventListener((MouseMoveEvent event) { 210 if(event.state & ModifierState.leftButtonDown) 211 helper(event); 212 }); 213 214 this.addEventListener((KeyDownEvent event) { 215 if(event.key == Key.Enter || event.key == Key.PadEnter) 216 OK(); 217 if(event.key == Key.Escape) 218 Cancel(); 219 }); 220 221 this.addEventListener("change", { 222 redraw(); 223 }); 224 225 auto s = this; 226 auto currentColorWidget = new class Widget { 227 this() { 228 super(s); 229 } 230 231 override void paint(WidgetPainter painter) { 232 auto c = currentColor(); 233 234 auto c1 = alphaBlend(c, Color(64, 64, 64)); 235 auto c2 = alphaBlend(c, Color(192, 192, 192)); 236 237 painter.outlineColor = c1; 238 painter.fillColor = c1; 239 painter.drawRectangle(Point(0, 0), this.width / 2, this.height / 2); 240 painter.drawRectangle(Point(this.width / 2, this.height / 2), this.width / 2, this.height / 2); 241 242 painter.outlineColor = c2; 243 painter.fillColor = c2; 244 painter.drawRectangle(Point(this.width / 2, 0), this.width / 2, this.height / 2); 245 painter.drawRectangle(Point(0, this.height / 2), this.width / 2, this.height / 2); 246 } 247 }; 248 249 auto hl = new HorizontalLayout(this); 250 auto cancelButton = new Button("Cancel", hl); 251 auto okButton = new Button("OK", hl); 252 253 recomputeChildLayout(); // FIXME hack 254 255 cancelButton.addEventListener(EventType.triggered, &Cancel); 256 okButton.addEventListener(EventType.triggered, &OK); 257 258 r.focus(); 259 } 260 261 LabeledLineEdit r; 262 LabeledLineEdit g; 263 LabeledLineEdit b; 264 LabeledLineEdit a; 265 266 LabeledLineEdit h; 267 LabeledLineEdit s; 268 LabeledLineEdit l; 269 270 LabeledLineEdit css; 271 272 Color currentColor() { 273 import std.conv; 274 try { 275 return Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content)); 276 } catch(Exception e) { 277 return Color.transparent; 278 } 279 } 280 281 282 override void OK() { 283 import std.conv; 284 try { 285 onOK(Color(to!int(r.content), to!int(g.content), to!int(b.content), to!int(a.content))); 286 this.close(); 287 } catch(Exception e) { 288 auto mb = new MessageBox("Bad value"); 289 mb.show(); 290 } 291 } 292 } 293 294