63c60ea79fc4f9f7a24b06cafacb4f61ea9ebc3aa527810429d251d04c45a5162b17e616a35c59dd90750e9da7a43f032aaea0a830e97cc760b900d43ab386 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { PixelRatio } from '../../../../base/browser/browser.js';
  6. import * as dom from '../../../../base/browser/dom.js';
  7. import { GlobalPointerMoveMonitor } from '../../../../base/browser/globalPointerMoveMonitor.js';
  8. import { Widget } from '../../../../base/browser/ui/widget.js';
  9. import { Color, HSVA, RGBA } from '../../../../base/common/color.js';
  10. import { Emitter } from '../../../../base/common/event.js';
  11. import { Disposable } from '../../../../base/common/lifecycle.js';
  12. import './colorPicker.css';
  13. import { localize } from '../../../../nls.js';
  14. import { editorHoverBackground } from '../../../../platform/theme/common/colorRegistry.js';
  15. import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
  16. const $ = dom.$;
  17. export class ColorPickerHeader extends Disposable {
  18. constructor(container, model, themeService) {
  19. super();
  20. this.model = model;
  21. this.domNode = $('.colorpicker-header');
  22. dom.append(container, this.domNode);
  23. this.pickedColorNode = dom.append(this.domNode, $('.picked-color'));
  24. const tooltip = localize('clickToToggleColorOptions', "Click to toggle color options (rgb/hsl/hex)");
  25. this.pickedColorNode.setAttribute('title', tooltip);
  26. const colorBox = dom.append(this.domNode, $('.original-color'));
  27. colorBox.style.backgroundColor = Color.Format.CSS.format(this.model.originalColor) || '';
  28. this.backgroundColor = themeService.getColorTheme().getColor(editorHoverBackground) || Color.white;
  29. this._register(registerThemingParticipant((theme, collector) => {
  30. this.backgroundColor = theme.getColor(editorHoverBackground) || Color.white;
  31. }));
  32. this._register(dom.addDisposableListener(this.pickedColorNode, dom.EventType.CLICK, () => this.model.selectNextColorPresentation()));
  33. this._register(dom.addDisposableListener(colorBox, dom.EventType.CLICK, () => {
  34. this.model.color = this.model.originalColor;
  35. this.model.flushColor();
  36. }));
  37. this._register(model.onDidChangeColor(this.onDidChangeColor, this));
  38. this._register(model.onDidChangePresentation(this.onDidChangePresentation, this));
  39. this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(model.color) || '';
  40. this.pickedColorNode.classList.toggle('light', model.color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : model.color.isLighter());
  41. this.onDidChangeColor(this.model.color);
  42. }
  43. onDidChangeColor(color) {
  44. this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(color) || '';
  45. this.pickedColorNode.classList.toggle('light', color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : color.isLighter());
  46. this.onDidChangePresentation();
  47. }
  48. onDidChangePresentation() {
  49. this.pickedColorNode.textContent = this.model.presentation ? this.model.presentation.label : '';
  50. this.pickedColorNode.prepend($('.codicon.codicon-color-mode'));
  51. }
  52. }
  53. export class ColorPickerBody extends Disposable {
  54. constructor(container, model, pixelRatio) {
  55. super();
  56. this.model = model;
  57. this.pixelRatio = pixelRatio;
  58. this.domNode = $('.colorpicker-body');
  59. dom.append(container, this.domNode);
  60. this.saturationBox = new SaturationBox(this.domNode, this.model, this.pixelRatio);
  61. this._register(this.saturationBox);
  62. this._register(this.saturationBox.onDidChange(this.onDidSaturationValueChange, this));
  63. this._register(this.saturationBox.onColorFlushed(this.flushColor, this));
  64. this.opacityStrip = new OpacityStrip(this.domNode, this.model);
  65. this._register(this.opacityStrip);
  66. this._register(this.opacityStrip.onDidChange(this.onDidOpacityChange, this));
  67. this._register(this.opacityStrip.onColorFlushed(this.flushColor, this));
  68. this.hueStrip = new HueStrip(this.domNode, this.model);
  69. this._register(this.hueStrip);
  70. this._register(this.hueStrip.onDidChange(this.onDidHueChange, this));
  71. this._register(this.hueStrip.onColorFlushed(this.flushColor, this));
  72. }
  73. flushColor() {
  74. this.model.flushColor();
  75. }
  76. onDidSaturationValueChange({ s, v }) {
  77. const hsva = this.model.color.hsva;
  78. this.model.color = new Color(new HSVA(hsva.h, s, v, hsva.a));
  79. }
  80. onDidOpacityChange(a) {
  81. const hsva = this.model.color.hsva;
  82. this.model.color = new Color(new HSVA(hsva.h, hsva.s, hsva.v, a));
  83. }
  84. onDidHueChange(value) {
  85. const hsva = this.model.color.hsva;
  86. const h = (1 - value) * 360;
  87. this.model.color = new Color(new HSVA(h === 360 ? 0 : h, hsva.s, hsva.v, hsva.a));
  88. }
  89. layout() {
  90. this.saturationBox.layout();
  91. this.opacityStrip.layout();
  92. this.hueStrip.layout();
  93. }
  94. }
  95. class SaturationBox extends Disposable {
  96. constructor(container, model, pixelRatio) {
  97. super();
  98. this.model = model;
  99. this.pixelRatio = pixelRatio;
  100. this._onDidChange = new Emitter();
  101. this.onDidChange = this._onDidChange.event;
  102. this._onColorFlushed = new Emitter();
  103. this.onColorFlushed = this._onColorFlushed.event;
  104. this.domNode = $('.saturation-wrap');
  105. dom.append(container, this.domNode);
  106. // Create canvas, draw selected color
  107. this.canvas = document.createElement('canvas');
  108. this.canvas.className = 'saturation-box';
  109. dom.append(this.domNode, this.canvas);
  110. // Add selection circle
  111. this.selection = $('.saturation-selection');
  112. dom.append(this.domNode, this.selection);
  113. this.layout();
  114. this._register(dom.addDisposableListener(this.domNode, dom.EventType.POINTER_DOWN, e => this.onPointerDown(e)));
  115. this._register(this.model.onDidChangeColor(this.onDidChangeColor, this));
  116. this.monitor = null;
  117. }
  118. onPointerDown(e) {
  119. if (!e.target || !(e.target instanceof Element)) {
  120. return;
  121. }
  122. this.monitor = this._register(new GlobalPointerMoveMonitor());
  123. const origin = dom.getDomNodePagePosition(this.domNode);
  124. if (e.target !== this.selection) {
  125. this.onDidChangePosition(e.offsetX, e.offsetY);
  126. }
  127. this.monitor.startMonitoring(e.target, e.pointerId, e.buttons, event => this.onDidChangePosition(event.pageX - origin.left, event.pageY - origin.top), () => null);
  128. const pointerUpListener = dom.addDisposableListener(document, dom.EventType.POINTER_UP, () => {
  129. this._onColorFlushed.fire();
  130. pointerUpListener.dispose();
  131. if (this.monitor) {
  132. this.monitor.stopMonitoring(true);
  133. this.monitor = null;
  134. }
  135. }, true);
  136. }
  137. onDidChangePosition(left, top) {
  138. const s = Math.max(0, Math.min(1, left / this.width));
  139. const v = Math.max(0, Math.min(1, 1 - (top / this.height)));
  140. this.paintSelection(s, v);
  141. this._onDidChange.fire({ s, v });
  142. }
  143. layout() {
  144. this.width = this.domNode.offsetWidth;
  145. this.height = this.domNode.offsetHeight;
  146. this.canvas.width = this.width * this.pixelRatio;
  147. this.canvas.height = this.height * this.pixelRatio;
  148. this.paint();
  149. const hsva = this.model.color.hsva;
  150. this.paintSelection(hsva.s, hsva.v);
  151. }
  152. paint() {
  153. const hsva = this.model.color.hsva;
  154. const saturatedColor = new Color(new HSVA(hsva.h, 1, 1, 1));
  155. const ctx = this.canvas.getContext('2d');
  156. const whiteGradient = ctx.createLinearGradient(0, 0, this.canvas.width, 0);
  157. whiteGradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
  158. whiteGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)');
  159. whiteGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
  160. const blackGradient = ctx.createLinearGradient(0, 0, 0, this.canvas.height);
  161. blackGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
  162. blackGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
  163. ctx.rect(0, 0, this.canvas.width, this.canvas.height);
  164. ctx.fillStyle = Color.Format.CSS.format(saturatedColor);
  165. ctx.fill();
  166. ctx.fillStyle = whiteGradient;
  167. ctx.fill();
  168. ctx.fillStyle = blackGradient;
  169. ctx.fill();
  170. }
  171. paintSelection(s, v) {
  172. this.selection.style.left = `${s * this.width}px`;
  173. this.selection.style.top = `${this.height - v * this.height}px`;
  174. }
  175. onDidChangeColor() {
  176. if (this.monitor && this.monitor.isMonitoring()) {
  177. return;
  178. }
  179. this.paint();
  180. }
  181. }
  182. class Strip extends Disposable {
  183. constructor(container, model) {
  184. super();
  185. this.model = model;
  186. this._onDidChange = new Emitter();
  187. this.onDidChange = this._onDidChange.event;
  188. this._onColorFlushed = new Emitter();
  189. this.onColorFlushed = this._onColorFlushed.event;
  190. this.domNode = dom.append(container, $('.strip'));
  191. this.overlay = dom.append(this.domNode, $('.overlay'));
  192. this.slider = dom.append(this.domNode, $('.slider'));
  193. this.slider.style.top = `0px`;
  194. this._register(dom.addDisposableListener(this.domNode, dom.EventType.POINTER_DOWN, e => this.onPointerDown(e)));
  195. this.layout();
  196. }
  197. layout() {
  198. this.height = this.domNode.offsetHeight - this.slider.offsetHeight;
  199. const value = this.getValue(this.model.color);
  200. this.updateSliderPosition(value);
  201. }
  202. onPointerDown(e) {
  203. if (!e.target || !(e.target instanceof Element)) {
  204. return;
  205. }
  206. const monitor = this._register(new GlobalPointerMoveMonitor());
  207. const origin = dom.getDomNodePagePosition(this.domNode);
  208. this.domNode.classList.add('grabbing');
  209. if (e.target !== this.slider) {
  210. this.onDidChangeTop(e.offsetY);
  211. }
  212. monitor.startMonitoring(e.target, e.pointerId, e.buttons, event => this.onDidChangeTop(event.pageY - origin.top), () => null);
  213. const pointerUpListener = dom.addDisposableListener(document, dom.EventType.POINTER_UP, () => {
  214. this._onColorFlushed.fire();
  215. pointerUpListener.dispose();
  216. monitor.stopMonitoring(true);
  217. this.domNode.classList.remove('grabbing');
  218. }, true);
  219. }
  220. onDidChangeTop(top) {
  221. const value = Math.max(0, Math.min(1, 1 - (top / this.height)));
  222. this.updateSliderPosition(value);
  223. this._onDidChange.fire(value);
  224. }
  225. updateSliderPosition(value) {
  226. this.slider.style.top = `${(1 - value) * this.height}px`;
  227. }
  228. }
  229. class OpacityStrip extends Strip {
  230. constructor(container, model) {
  231. super(container, model);
  232. this.domNode.classList.add('opacity-strip');
  233. this._register(model.onDidChangeColor(this.onDidChangeColor, this));
  234. this.onDidChangeColor(this.model.color);
  235. }
  236. onDidChangeColor(color) {
  237. const { r, g, b } = color.rgba;
  238. const opaque = new Color(new RGBA(r, g, b, 1));
  239. const transparent = new Color(new RGBA(r, g, b, 0));
  240. this.overlay.style.background = `linear-gradient(to bottom, ${opaque} 0%, ${transparent} 100%)`;
  241. }
  242. getValue(color) {
  243. return color.hsva.a;
  244. }
  245. }
  246. class HueStrip extends Strip {
  247. constructor(container, model) {
  248. super(container, model);
  249. this.domNode.classList.add('hue-strip');
  250. }
  251. getValue(color) {
  252. return 1 - (color.hsva.h / 360);
  253. }
  254. }
  255. export class ColorPickerWidget extends Widget {
  256. constructor(container, model, pixelRatio, themeService) {
  257. super();
  258. this.model = model;
  259. this.pixelRatio = pixelRatio;
  260. this._register(PixelRatio.onDidChange(() => this.layout()));
  261. const element = $('.colorpicker-widget');
  262. container.appendChild(element);
  263. const header = new ColorPickerHeader(element, this.model, themeService);
  264. this.body = new ColorPickerBody(element, this.model, this.pixelRatio);
  265. this._register(header);
  266. this._register(this.body);
  267. }
  268. layout() {
  269. this.body.layout();
  270. }
  271. }